package tests import "core:testing" import assets "../src/assets" import decks "../src/decks" import card "../src/card" import memory "../src/memory" import themes "../src/themes" // Runs the game logic forward in small steps, like frames @(private) advance :: proc(seconds: f32) { dt: f32 = 1.0 / 60 for t: f32 = 0; t < seconds; t += dt { memory.update(dt) } } // Finds the card index holding the other half of card i's pair @(private) partner_of :: proc(i: int) -> int { for c, j in memory.active_cards() { if j != i && c.pair_id == memory.cards[i].pair_id { return j } } return -1 } // Finds a card index that does not pair with card i @(private) non_partner_of :: proc(i: int) -> int { for c, j in memory.active_cards() { if j != i && c.pair_id != memory.cards[i].pair_id { return j } } return -1 } @(test) deck_sizes :: proc(t: ^testing.T) { testing.expect_value(t, len(decks.entries(.HIRAGANA)), 46) testing.expect_value(t, len(decks.entries(.KATAKANA)), 46) testing.expect_value(t, len(decks.entries(.HANGUL)), 24) testing.expect_value(t, len(decks.entries(.THAI_CONSONANTS)), 42) } @(test) decks_have_unique_symbols_and_readings :: proc(t: ^testing.T) { for deck in decks.Deck_Id { entries := decks.entries(deck) for a, i in entries { for b, j in entries { if i == j { continue } testing.expectf(t, a.symbol != b.symbol, "%v: duplicate symbol %s", deck, a.symbol) // Readings may repeat (e.g. Thai "kh") only when the hint tells them apart same_reading := a.reading == b.reading && a.hint == b.hint testing.expectf(t, !same_reading, "%v: %s and %s read the same", deck, a.symbol, b.symbol) } } } } @(private) covers :: proc(codepoints: []rune, text: cstring) -> (missing: rune, ok: bool) { outer: for r in string(text) { for c in codepoints { if c == r { continue outer } } return r, false } return 0, true } // raylib renders "?" for any glyph that wasn't baked into the font @(test) fonts_cover_every_deck_character :: proc(t: ^testing.T) { for deck in decks.Deck_Id { symbol_points := assets.codepoints_for(decks.language_of(deck).symbol_style) defer delete(symbol_points) ui_points := assets.codepoints_for(.BOLD) defer delete(ui_points) for entry in decks.entries(deck) { if r, ok := covers(symbol_points[:], entry.symbol); !ok { testing.expectf(t, false, "%v: %s needs %r in the symbol font", deck, entry.symbol, r) } if r, ok := covers(ui_points[:], entry.reading); !ok { testing.expectf(t, false, "%v: %s needs %r in the UI font", deck, entry.reading, r) } if entry.hint == nil { continue } if r, ok := covers(ui_points[:], entry.hint); !ok { testing.expectf(t, false, "%v: %s needs %r in the UI font", deck, entry.hint, r) } } info := decks.DECKS[deck] if r, ok := covers(symbol_points[:], info.native); !ok { testing.expectf(t, false, "%v: deck name %s needs %r", deck, info.native, r) } } for language in decks.Language { info := decks.LANGUAGES[language] points := assets.codepoints_for(info.congrats_style) defer delete(points) if r, ok := covers(points[:], info.congrats); !ok { testing.expectf(t, false, "%v: %s needs %r", language, info.congrats, r) } native_points := assets.codepoints_for(info.symbol_style) defer delete(native_points) if r, ok := covers(native_points[:], info.native); !ok { testing.expectf(t, false, "%v: %s needs %r", language, info.native, r) } } } @(test) deal_creates_symbol_reading_pairs :: proc(t: ^testing.T) { for deck in decks.Deck_Id { for difficulty in memory.Difficulty { memory.deal(deck, difficulty) layout := memory.LAYOUTS[difficulty] testing.expect_value(t, memory.card_count, layout.rows * layout.cols) testing.expect_value(t, memory.pair_count, memory.card_count / 2) testing.expect_value(t, memory.state, memory.State.WAITING) testing.expect_value(t, memory.moves, 0) testing.expect_value(t, memory.matches, 0) for pair in 0 ..< memory.pair_count { symbol_count, reading_count := 0, 0 symbol, reading, hint: cstring for c in memory.active_cards() { if c.pair_id != pair { continue } testing.expect(t, !c.face_up && !c.matched) if c.is_symbol { symbol_count += 1 symbol = c.text } else { reading_count += 1 reading = c.text hint = c.hint } } testing.expect_value(t, symbol_count, 1) testing.expect_value(t, reading_count, 1) // The two halves must belong to the same deck entry found := false for entry in decks.entries(deck) { if entry.symbol == symbol && entry.reading == reading && entry.hint == hint { found = true } } testing.expectf(t, found, "%v pair %d is %s/%s, which is not in the deck", deck, pair, symbol, reading) } } } } @(test) deal_lays_out_cards_on_screen_without_overlap :: proc(t: ^testing.T) { for difficulty in memory.Difficulty { memory.deal(.HIRAGANA, difficulty) for a, i in memory.active_cards() { testing.expect(t, a.rect.x >= 0 && a.rect.x + a.rect.width <= themes.SCREEN_WIDTH) testing.expect(t, a.rect.y >= themes.GRID_TOP && a.rect.y + a.rect.height <= themes.SCREEN_HEIGHT) for b, j in memory.active_cards() { if i == j { continue } overlap := a.rect.x < b.rect.x + b.rect.width && b.rect.x < a.rect.x + a.rect.width && a.rect.y < b.rect.y + b.rect.height && b.rect.y < a.rect.y + a.rect.height testing.expectf(t, !overlap, "cards %d and %d overlap", i, j) } } } } @(test) matching_pair_stays_up :: proc(t: ^testing.T) { memory.deal(.HIRAGANA, .NORMAL) a := 0 b := partner_of(a) testing.expect(t, memory.select_card(a)) testing.expect_value(t, memory.state, memory.State.FIRST) testing.expect(t, memory.select_card(b)) testing.expect_value(t, memory.state, memory.State.RESOLVING) testing.expect_value(t, memory.moves, 1) advance(2) testing.expect_value(t, memory.state, memory.State.WAITING) testing.expect_value(t, memory.matches, 1) testing.expect(t, memory.cards[a].matched && memory.cards[b].matched) testing.expect(t, memory.cards[a].face_up && memory.cards[b].face_up) } @(test) mismatched_pair_flips_back :: proc(t: ^testing.T) { memory.deal(.HIRAGANA, .NORMAL) a := 0 b := non_partner_of(a) memory.select_card(a) memory.select_card(b) advance(3) testing.expect_value(t, memory.state, memory.State.WAITING) testing.expect_value(t, memory.matches, 0) testing.expect_value(t, memory.moves, 1) testing.expect(t, !memory.cards[a].face_up && !memory.cards[b].face_up) testing.expect_value(t, memory.cards[a].flip_t, 0) } @(test) invalid_picks_are_rejected :: proc(t: ^testing.T) { memory.deal(.HIRAGANA, .NORMAL) a := 0 testing.expect(t, memory.select_card(a)) testing.expect(t, !memory.select_card(a)) // same card twice b := non_partner_of(a) memory.select_card(b) c := non_partner_of(b) for c == a { c += 1 } testing.expect(t, !memory.select_card(c)) // a third card while resolving testing.expect_value(t, memory.moves, 1) memory.deal(.HIRAGANA, .NORMAL) p := partner_of(0) memory.select_card(0) memory.select_card(p) advance(2) testing.expect(t, !memory.select_card(0)) // already matched } @(test) finding_all_pairs_wins :: proc(t: ^testing.T) { memory.deal(.HIRAGANA, .NORMAL) for i in 0 ..< memory.card_count { if memory.cards[i].matched { continue } memory.select_card(i) memory.select_card(partner_of(i)) advance(2) } testing.expect_value(t, memory.state, memory.State.OVER) testing.expect_value(t, memory.matches, memory.pair_count) testing.expect_value(t, memory.moves, memory.pair_count) for &c in memory.active_cards() { testing.expect(t, c.matched) } // The clock stops once the game is over elapsed := memory.elapsed advance(1) testing.expect_value(t, memory.elapsed, elapsed) } @(test) card_flip_animation_reaches_target :: proc(t: ^testing.T) { c := card.Card{} testing.expect(t, !card.is_animating(&c)) c.face_up = true testing.expect(t, card.is_animating(&c)) card.update(&c, 0.1) testing.expect(t, c.flip_t > 0 && c.flip_t < 1) for _ in 0 ..< 60 { card.update(&c, 1.0 / 60) } testing.expect_value(t, c.flip_t, 1) testing.expect(t, !card.is_animating(&c)) c.face_up = false for _ in 0 ..< 60 { card.update(&c, 1.0 / 60) } testing.expect_value(t, c.flip_t, 0) } @(test) card_pulse_counts_down :: proc(t: ^testing.T) { c := card.Card{} card.start_pulse(&c) testing.expect_value(t, c.pulse_t, card.PULSE_DURATION) card.update(&c, 1) testing.expect_value(t, c.pulse_t, 0) } @(test) keyboard_focus_stays_inside_grid :: proc(t: ^testing.T) { memory.deal(.HIRAGANA, .NORMAL) // 4 rows x 6 columns testing.expect_value(t, memory.focus, 0) memory.move_focus(-1, -1) // already top-left testing.expect_value(t, memory.focus, 0) memory.move_focus(1, 0) testing.expect_value(t, memory.focus, 1) memory.move_focus(0, 1) testing.expect_value(t, memory.focus, 7) memory.move_focus(100, 100) // clamps to bottom-right testing.expect_value(t, memory.focus, memory.card_count - 1) memory.deal(.HIRAGANA, .HARD) // dealing resets focus testing.expect_value(t, memory.focus, 0) memory.move_focus(0, 3) testing.expect_value(t, memory.focus, 24) // 4 rows x 8 columns }