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
+85
-58
@@ -4,44 +4,50 @@ import "core:math/rand"
|
||||
import raylib "vendor:raylib"
|
||||
import audio "../audio"
|
||||
import card "../card"
|
||||
import decks "../decks"
|
||||
import themes "../themes"
|
||||
import ui "../ui"
|
||||
|
||||
PAIR_COUNT :: 12
|
||||
CARD_COUNT :: PAIR_COUNT * 2
|
||||
MAX_CARDS :: 32
|
||||
|
||||
MATCH_DELAY :: 0.35 // seconds both cards stay up before a match is confirmed
|
||||
MISMATCH_DELAY :: 0.9 // seconds before a wrong pair flips back
|
||||
|
||||
Difficulty :: enum {
|
||||
EASY,
|
||||
NORMAL,
|
||||
HARD,
|
||||
}
|
||||
|
||||
Layout :: struct {
|
||||
rows, cols: int,
|
||||
}
|
||||
|
||||
@(rodata)
|
||||
LAYOUTS := [Difficulty]Layout{
|
||||
.EASY = {3, 4},
|
||||
.NORMAL = {4, 6},
|
||||
.HARD = {4, 8},
|
||||
}
|
||||
|
||||
@(rodata)
|
||||
DIFFICULTY_NAMES := [Difficulty]cstring{.EASY = "Easy", .NORMAL = "Normal", .HARD = "Hard"}
|
||||
|
||||
State :: enum {
|
||||
WAITING, // no card selected
|
||||
FIRST, // one card selected
|
||||
WAITING, // no card selected
|
||||
FIRST, // one card selected
|
||||
RESOLVING, // two cards up, waiting before match/mismatch
|
||||
OVER,
|
||||
}
|
||||
|
||||
Kana :: struct {
|
||||
kana: cstring,
|
||||
romaji: cstring,
|
||||
}
|
||||
|
||||
// The 46 basic hiragana (gojūon)
|
||||
HIRAGANA :: [?]Kana{
|
||||
{"あ", "a"}, {"い", "i"}, {"う", "u"}, {"え", "e"}, {"お", "o"},
|
||||
{"か", "ka"}, {"き", "ki"}, {"く", "ku"}, {"け", "ke"}, {"こ", "ko"},
|
||||
{"さ", "sa"}, {"し", "shi"}, {"す", "su"}, {"せ", "se"}, {"そ", "so"},
|
||||
{"た", "ta"}, {"ち", "chi"}, {"つ", "tsu"}, {"て", "te"}, {"と", "to"},
|
||||
{"な", "na"}, {"に", "ni"}, {"ぬ", "nu"}, {"ね", "ne"}, {"の", "no"},
|
||||
{"は", "ha"}, {"ひ", "hi"}, {"ふ", "fu"}, {"へ", "he"}, {"ほ", "ho"},
|
||||
{"ま", "ma"}, {"み", "mi"}, {"む", "mu"}, {"め", "me"}, {"も", "mo"},
|
||||
{"や", "ya"}, {"ゆ", "yu"}, {"よ", "yo"},
|
||||
{"ら", "ra"}, {"り", "ri"}, {"る", "ru"}, {"れ", "re"}, {"ろ", "ro"},
|
||||
{"わ", "wa"}, {"を", "wo"}, {"ん", "n"},
|
||||
}
|
||||
|
||||
cards: [CARD_COUNT]card.Card
|
||||
state: State
|
||||
cards: [MAX_CARDS]card.Card
|
||||
card_count: int
|
||||
pair_count: int
|
||||
deck: decks.Deck_Id
|
||||
difficulty: Difficulty
|
||||
theme: themes.Theme
|
||||
layout: Layout
|
||||
state: State
|
||||
first: int = -1
|
||||
second: int = -1
|
||||
matches: int
|
||||
@@ -52,33 +58,46 @@ win_t: f32 // fades in the win panel
|
||||
restart_requested: bool
|
||||
|
||||
// Deals a new game and renders the card faces. Needs the window to exist.
|
||||
start :: proc(game_theme: themes.Theme) {
|
||||
deal(game_theme)
|
||||
for &c in cards { card.build_face(&c, theme) }
|
||||
start :: proc(deck_id: decks.Deck_Id, game_difficulty: Difficulty) {
|
||||
deal(deck_id, game_difficulty)
|
||||
style := decks.language_of(deck).symbol_style
|
||||
for &c in active_cards() { card.build_face(&c, theme, style) }
|
||||
}
|
||||
|
||||
// Shuffles, lays out and resets the game state without touching the GPU
|
||||
deal :: proc(game_theme: themes.Theme) {
|
||||
theme = game_theme
|
||||
deal :: proc(deck_id: decks.Deck_Id, game_difficulty: Difficulty) {
|
||||
unload()
|
||||
deck = deck_id
|
||||
difficulty = game_difficulty
|
||||
theme = decks.language_of(deck).theme
|
||||
layout = LAYOUTS[difficulty]
|
||||
card_count = layout.rows * layout.cols
|
||||
pair_count = card_count / 2
|
||||
|
||||
deck := HIRAGANA
|
||||
rand.shuffle(deck[:])
|
||||
for i in 0 ..< PAIR_COUNT {
|
||||
card.unload_face(&cards[i * 2])
|
||||
card.unload_face(&cards[i * 2 + 1])
|
||||
cards[i * 2] = card.Card{text = deck[i].kana, is_kana = true, pair_id = i}
|
||||
cards[i * 2 + 1] = card.Card{text = deck[i].romaji, is_kana = false, pair_id = i}
|
||||
// Pick random entries from the deck
|
||||
entries := decks.entries(deck)
|
||||
order := make([]int, len(entries), context.temp_allocator)
|
||||
for &o, i in order { o = i }
|
||||
rand.shuffle(order)
|
||||
|
||||
for pair in 0 ..< pair_count {
|
||||
entry := entries[order[pair]]
|
||||
cards[pair * 2] = card.Card{text = entry.symbol, is_symbol = true, pair_id = pair}
|
||||
cards[pair * 2 + 1] = card.Card{text = entry.reading, hint = entry.hint, pair_id = pair}
|
||||
}
|
||||
rand.shuffle(cards[:])
|
||||
rand.shuffle(cards[:card_count])
|
||||
|
||||
total_w: f32 = themes.GRID_COLS * (themes.CARD_WIDTH + themes.CARD_SPACING) - themes.CARD_SPACING
|
||||
// Center the grid horizontally and in the space below the header
|
||||
step_x: f32 = themes.CARD_WIDTH + themes.CARD_SPACING
|
||||
step_y: f32 = themes.CARD_HEIGHT + themes.CARD_SPACING
|
||||
total_w := f32(layout.cols) * step_x - themes.CARD_SPACING
|
||||
total_h := f32(layout.rows) * step_y - themes.CARD_SPACING
|
||||
start_x := (themes.SCREEN_WIDTH - total_w) / 2
|
||||
for &c, i in cards {
|
||||
row := f32(i / themes.GRID_COLS)
|
||||
col := f32(i % themes.GRID_COLS)
|
||||
start_y := themes.GRID_TOP + (themes.SCREEN_HEIGHT - 16 - themes.GRID_TOP - total_h) / 2
|
||||
for &c, i in active_cards() {
|
||||
c.rect = {
|
||||
start_x + col * (themes.CARD_WIDTH + themes.CARD_SPACING),
|
||||
themes.GRID_TOP + row * (themes.CARD_HEIGHT + themes.CARD_SPACING),
|
||||
start_x + f32(i % layout.cols) * step_x,
|
||||
start_y + f32(i / layout.cols) * step_y,
|
||||
themes.CARD_WIDTH,
|
||||
themes.CARD_HEIGHT,
|
||||
}
|
||||
@@ -95,11 +114,16 @@ deal :: proc(game_theme: themes.Theme) {
|
||||
restart_requested = false
|
||||
}
|
||||
|
||||
active_cards :: proc() -> []card.Card {
|
||||
return cards[:card_count]
|
||||
}
|
||||
|
||||
unload :: proc() {
|
||||
for &c in cards { card.unload_face(&c) }
|
||||
}
|
||||
|
||||
can_pick :: proc(i: int) -> bool {
|
||||
if i < 0 || i >= card_count { return false }
|
||||
c := &cards[i]
|
||||
return (state == .WAITING || state == .FIRST) && !c.face_up && !c.matched
|
||||
}
|
||||
@@ -131,7 +155,7 @@ resolve :: proc() {
|
||||
card.start_pulse(a)
|
||||
card.start_pulse(b)
|
||||
matches += 1
|
||||
if matches == PAIR_COUNT {
|
||||
if matches == pair_count {
|
||||
state = .OVER
|
||||
audio.play(.WIN)
|
||||
} else {
|
||||
@@ -151,17 +175,17 @@ resolve :: proc() {
|
||||
|
||||
update :: proc(dt: f32) {
|
||||
if restart_requested {
|
||||
start(theme)
|
||||
start(deck, difficulty)
|
||||
return
|
||||
}
|
||||
|
||||
for &c in cards { card.update(&c, dt) }
|
||||
for &c in active_cards() { card.update(&c, dt) }
|
||||
|
||||
switch state {
|
||||
case .WAITING, .FIRST:
|
||||
elapsed += dt
|
||||
if raylib.IsMouseButtonPressed(.LEFT) {
|
||||
for &c, i in cards {
|
||||
for &c, i in active_cards() {
|
||||
if card.is_hovered(&c) && select_card(i) {
|
||||
break
|
||||
}
|
||||
@@ -181,20 +205,22 @@ update :: proc(dt: f32) {
|
||||
|
||||
// Draws the game and returns the screen to show next frame
|
||||
draw :: proc() -> themes.Screen {
|
||||
next := themes.Screen.JAPANESE_MEMORY
|
||||
next := themes.Screen.MEMORY
|
||||
raylib.ClearBackground(theme.background)
|
||||
|
||||
ui.draw_title("Hiragana Memory", 24, theme.primary, 48)
|
||||
info := decks.DECKS[deck]
|
||||
ui.draw_title(raylib.TextFormat("%s · %s", theme.name, info.name), 24, theme.primary, 48)
|
||||
minutes := int(elapsed) / 60
|
||||
seconds := int(elapsed) % 60
|
||||
info := raylib.TextFormat("Moves %d · Pairs %d/%d · %d:%02d", moves, matches, PAIR_COUNT, minutes, seconds)
|
||||
ui.draw_text_centered(info, themes.SCREEN_WIDTH / 2, 88, 24, theme.ink)
|
||||
stats := raylib.TextFormat("%s · Moves %d · Pairs %d/%d · %d:%02d",
|
||||
DIFFICULTY_NAMES[difficulty], moves, matches, pair_count, minutes, seconds)
|
||||
ui.draw_text_centered(stats, themes.SCREEN_WIDTH / 2, 88, 24, theme.ink)
|
||||
|
||||
if state != .OVER && ui.button({30, 28, 120, 48}, "Back", theme.primary, font_size = 22) {
|
||||
next = .JAPANESE_MENU
|
||||
next = .LANGUAGE_MENU
|
||||
}
|
||||
|
||||
for &c, i in cards {
|
||||
for &c, i in active_cards() {
|
||||
card.draw(&c, theme, can_pick(i))
|
||||
}
|
||||
|
||||
@@ -218,13 +244,14 @@ draw_win_panel :: proc() -> Maybe(themes.Screen) {
|
||||
raylib.DrawRectangleRounded(panel, 0.12, 16, raylib.Fade(theme.card_face, win_t))
|
||||
raylib.DrawRectangleRoundedLinesEx(panel, 0.12, 16, 4, raylib.Fade(theme.primary, win_t))
|
||||
|
||||
language := decks.language_of(deck)
|
||||
cx := panel.x + panel_w / 2
|
||||
ui.draw_text_centered("おめでとう!", cx, panel.y + 34, 56, raylib.Fade(theme.primary, win_t), bold = true)
|
||||
ui.draw_text_centered("Congratulations, all pairs found!", cx, panel.y + 112, 26, raylib.Fade(theme.ink, win_t))
|
||||
ui.draw_text_centered(language.congrats, cx, panel.y + 34, 56, raylib.Fade(theme.primary, win_t), language.congrats_style)
|
||||
ui.draw_text_centered("All pairs found!", cx, panel.y + 112, 26, raylib.Fade(theme.ink, win_t))
|
||||
minutes := int(elapsed) / 60
|
||||
seconds := int(elapsed) % 60
|
||||
stats := raylib.TextFormat("%d moves · %d:%02d", moves, minutes, seconds)
|
||||
ui.draw_text_centered(stats, cx, panel.y + 156, 26, raylib.Fade(theme.ink, win_t), bold = true)
|
||||
ui.draw_text_centered(stats, cx, panel.y + 156, 26, raylib.Fade(theme.ink, win_t), .BOLD)
|
||||
|
||||
if win_t < 1 { return nil }
|
||||
button_w: f32 = 200
|
||||
@@ -233,7 +260,7 @@ draw_win_panel :: proc() -> Maybe(themes.Screen) {
|
||||
restart_requested = true
|
||||
}
|
||||
if ui.button({cx + 10, button_y, button_w, 60}, "Menu", theme.ink) {
|
||||
return .JAPANESE_MENU
|
||||
return .LANGUAGE_MENU
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user