Add Katakana, Korean and Thai games and difficulty levels
- decks package: Hiragana, Katakana, Hangul jamo and Thai consonants (Thai readings carry the letter's name word, since sounds repeat) - Per-script fonts: Jua for Hangul, Mali for Thai (both OFL) - Memory game works with any deck; Easy 3x4, Normal 4x6, Hard 4x8 - Language menus pick a deck and difficulty, with number/arrow key shortcuts - Tests cover every deck, difficulty, and font glyph coverage Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4cfe73c9eb
commit
8214fda8f6
15 files changed
+1920
-229
No files matched your search
+128
-54
@@ -1,6 +1,8 @@
|
||||
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"
|
||||
@@ -17,7 +19,7 @@ advance :: proc(seconds: f32) {
|
||||
// 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.cards {
|
||||
for c, j in memory.active_cards() {
|
||||
if j != i && c.pair_id == memory.cards[i].pair_id { return j }
|
||||
}
|
||||
return -1
|
||||
@@ -26,76 +28,148 @@ partner_of :: proc(i: int) -> int {
|
||||
// Finds a card index that does not pair with card i
|
||||
@(private)
|
||||
non_partner_of :: proc(i: int) -> int {
|
||||
for c, j in memory.cards {
|
||||
for c, j in memory.active_cards() {
|
||||
if j != i && c.pair_id != memory.cards[i].pair_id { return j }
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
@(test)
|
||||
hiragana_table_is_complete_and_unique :: proc(t: ^testing.T) {
|
||||
table := memory.HIRAGANA
|
||||
testing.expect_value(t, len(table), 46)
|
||||
for a, i in table {
|
||||
for b, j in table {
|
||||
if i == j { continue }
|
||||
testing.expectf(t, a.kana != b.kana, "duplicate kana %s", a.kana)
|
||||
testing.expectf(t, a.romaji != b.romaji, "duplicate romaji %s", a.romaji)
|
||||
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_kana_romaji_pairs :: proc(t: ^testing.T) {
|
||||
memory.deal(themes.JAPAN)
|
||||
testing.expect_value(t, memory.state, memory.State.WAITING)
|
||||
testing.expect_value(t, memory.moves, 0)
|
||||
testing.expect_value(t, memory.matches, 0)
|
||||
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 {
|
||||
kana_count, romaji_count := 0, 0
|
||||
kana, romaji: cstring
|
||||
for c in memory.cards {
|
||||
if c.pair_id != pair { continue }
|
||||
testing.expect(t, !c.face_up && !c.matched)
|
||||
if c.is_kana {
|
||||
kana_count += 1
|
||||
kana = c.text
|
||||
} else {
|
||||
romaji_count += 1
|
||||
romaji = c.text
|
||||
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)
|
||||
}
|
||||
}
|
||||
testing.expect_value(t, kana_count, 1)
|
||||
testing.expect_value(t, romaji_count, 1)
|
||||
|
||||
// The two halves must belong to the same table entry
|
||||
found := false
|
||||
for entry in memory.HIRAGANA {
|
||||
if entry.kana == kana && entry.romaji == romaji { found = true }
|
||||
}
|
||||
testing.expectf(t, found, "pair %d is %s/%s, which is not in the table", pair, kana, romaji)
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
deal_lays_out_cards_on_screen_without_overlap :: proc(t: ^testing.T) {
|
||||
memory.deal(themes.JAPAN)
|
||||
for a, i in memory.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.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)
|
||||
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(themes.JAPAN)
|
||||
memory.deal(.HIRAGANA, .NORMAL)
|
||||
a := 0
|
||||
b := partner_of(a)
|
||||
|
||||
@@ -114,7 +188,7 @@ matching_pair_stays_up :: proc(t: ^testing.T) {
|
||||
|
||||
@(test)
|
||||
mismatched_pair_flips_back :: proc(t: ^testing.T) {
|
||||
memory.deal(themes.JAPAN)
|
||||
memory.deal(.HIRAGANA, .NORMAL)
|
||||
a := 0
|
||||
b := non_partner_of(a)
|
||||
|
||||
@@ -130,7 +204,7 @@ mismatched_pair_flips_back :: proc(t: ^testing.T) {
|
||||
|
||||
@(test)
|
||||
invalid_picks_are_rejected :: proc(t: ^testing.T) {
|
||||
memory.deal(themes.JAPAN)
|
||||
memory.deal(.HIRAGANA, .NORMAL)
|
||||
a := 0
|
||||
testing.expect(t, memory.select_card(a))
|
||||
testing.expect(t, !memory.select_card(a)) // same card twice
|
||||
@@ -142,7 +216,7 @@ invalid_picks_are_rejected :: proc(t: ^testing.T) {
|
||||
testing.expect(t, !memory.select_card(c)) // a third card while resolving
|
||||
testing.expect_value(t, memory.moves, 1)
|
||||
|
||||
memory.deal(themes.JAPAN)
|
||||
memory.deal(.HIRAGANA, .NORMAL)
|
||||
p := partner_of(0)
|
||||
memory.select_card(0)
|
||||
memory.select_card(p)
|
||||
@@ -152,17 +226,17 @@ invalid_picks_are_rejected :: proc(t: ^testing.T) {
|
||||
|
||||
@(test)
|
||||
finding_all_pairs_wins :: proc(t: ^testing.T) {
|
||||
memory.deal(themes.JAPAN)
|
||||
for i in 0 ..< memory.CARD_COUNT {
|
||||
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.cards { testing.expect(t, c.matched) }
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user