package memory import "core:math/rand" import raylib "vendor:raylib" import audio "../audio" import card "../card" import themes "../themes" import ui "../ui" PAIR_COUNT :: 12 CARD_COUNT :: PAIR_COUNT * 2 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 State :: enum { 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 theme: themes.Theme first: int = -1 second: int = -1 matches: int moves: int elapsed: f32 resolve_timer: f32 win_t: f32 // fades in the win panel restart_requested: bool start :: proc(game_theme: themes.Theme) { theme = game_theme 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} } rand.shuffle(cards[:]) total_w: f32 = themes.GRID_COLS * (themes.CARD_WIDTH + themes.CARD_SPACING) - 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) c.rect = { start_x + col * (themes.CARD_WIDTH + themes.CARD_SPACING), themes.GRID_TOP + row * (themes.CARD_HEIGHT + themes.CARD_SPACING), themes.CARD_WIDTH, themes.CARD_HEIGHT, } card.build_face(&c, theme) } state = .WAITING first = -1 second = -1 matches = 0 moves = 0 elapsed = 0 resolve_timer = 0 win_t = 0 restart_requested = false } unload :: proc() { for &c in cards { card.unload_face(&c) } } @(private) can_pick :: proc(i: int) -> bool { c := &cards[i] return (state == .WAITING || state == .FIRST) && !c.face_up && !c.matched } @(private) pick :: proc(i: int) { cards[i].face_up = true audio.play(.FLIP) if state == .WAITING { first = i state = .FIRST } else { second = i moves += 1 is_match := cards[first].pair_id == cards[second].pair_id resolve_timer = is_match ? MATCH_DELAY : MISMATCH_DELAY state = .RESOLVING } } @(private) resolve :: proc() { a, b := &cards[first], &cards[second] if a.pair_id == b.pair_id { a.matched = true b.matched = true card.start_pulse(a) card.start_pulse(b) matches += 1 if matches == PAIR_COUNT { state = .OVER audio.play(.WIN) } else { audio.play(.MATCH) state = .WAITING } } else { a.face_up = false b.face_up = false audio.play(.MISMATCH) audio.play(.FLIP) state = .WAITING } first = -1 second = -1 } update :: proc(dt: f32) { if restart_requested { start(theme) return } for &c in cards { card.update(&c, dt) } switch state { case .WAITING, .FIRST: elapsed += dt if raylib.IsMouseButtonPressed(.LEFT) { for &c, i in cards { if can_pick(i) && card.is_hovered(&c) { pick(i) break } } } case .RESOLVING: elapsed += dt // Wait for the second card to finish turning before starting the delay if !card.is_animating(&cards[second]) { resolve_timer -= dt if resolve_timer <= 0 { resolve() } } case .OVER: win_t = min(win_t + dt * 2.5, 1) } } // Draws the game and returns the screen to show next frame draw :: proc() -> themes.Screen { next := themes.Screen.JAPANESE_MEMORY raylib.ClearBackground(theme.background) ui.draw_title("Hiragana Memory", 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) if state != .OVER && ui.button({30, 28, 120, 48}, "Back", theme.primary, font_size = 22) { next = .JAPANESE_MENU } for &c, i in cards { card.draw(&c, theme, can_pick(i)) } if state == .OVER { if action := draw_win_panel(); action != nil { next = action.? } } return next } @(private) draw_win_panel :: proc() -> Maybe(themes.Screen) { raylib.DrawRectangle(0, 0, themes.SCREEN_WIDTH, themes.SCREEN_HEIGHT, raylib.Fade(themes.BLACK, 0.45 * win_t)) panel_w: f32 = 560 panel_h: f32 = 340 slide := (1 - win_t) * 40 panel := raylib.Rectangle{(themes.SCREEN_WIDTH - panel_w) / 2, (themes.SCREEN_HEIGHT - panel_h) / 2 + slide, panel_w, panel_h} raylib.DrawRectangleRounded({panel.x, panel.y + 8, panel.width, panel.height}, 0.12, 16, raylib.Fade(themes.BLACK, 0.25 * win_t)) 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)) 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)) 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) if win_t < 1 { return nil } button_w: f32 = 200 button_y := panel.y + panel_h - 100 if ui.button({cx - button_w - 10, button_y, button_w, 60}, "Play Again", theme.primary) { restart_requested = true } if ui.button({cx + 10, button_y, button_w, 60}, "Menu", theme.ink) { return .JAPANESE_MENU } return nil }