package memory import "core:fmt" import "core:math/rand" import "core:strings" import raylib "vendor:raylib" import audio "../audio" import card "../card" import decks "../decks" import profile "../profile" import themes "../themes" import ui "../ui" 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 RESOLVING, // two cards up, waiting before match/mismatch OVER, } 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 moves: int elapsed: f32 resolve_timer: f32 win_t: f32 // fades in the win panel restart_requested: bool last_result: profile.Result // filled in when a game is won focus: int // card selected with the keyboard keyboard_active: bool // show the focus ring (hidden again when the mouse moves) // Key for this deck and difficulty in the saved profile score_key :: proc(deck_id: decks.Deck_Id, game_difficulty: Difficulty) -> string { return fmt.tprintf("%v/%v", deck_id, game_difficulty) } // Deals a new game and renders the card faces. Needs the window to exist. 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(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 // 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[:card_count]) // 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 start_y := themes.GRID_TOP + (themes.SCREEN_HEIGHT - 16 - themes.GRID_TOP - total_h) / 2 for &c, i in active_cards() { card.start_deal(&c, i) c.rect = { start_x + f32(i % layout.cols) * step_x, start_y + f32(i / layout.cols) * step_y, themes.CARD_WIDTH, themes.CARD_HEIGHT, } } state = .WAITING first = -1 second = -1 matches = 0 moves = 0 elapsed = 0 resolve_timer = 0 win_t = 0 restart_requested = false focus = 0 } active_cards :: proc() -> []card.Card { return cards[:card_count] } unload :: proc() { for &c in cards { card.unload_face(&c) } } // Whether the player can click or press on card i right now (it must have finished being dealt) can_interact :: proc(i: int) -> bool { return can_pick(i) && cards[i].deal_t == 0 } 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 } // Turns card i face up if it can be picked. Returns false when the pick is not allowed. select_card :: proc(i: int) -> bool { if !can_pick(i) { return false } 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 } return true } // Moves the keyboard focus within the grid, clamped at the edges move_focus :: proc(dx, dy: int) { col := clamp(focus % layout.cols + dx, 0, layout.cols - 1) row := clamp(focus / layout.cols + dy, 0, layout.rows - 1) focus = row * layout.cols + col } @(private) key_pressed :: proc(keys: ..raylib.KeyboardKey) -> bool { for key in keys { if raylib.IsKeyPressed(key) || raylib.IsKeyPressedRepeat(key) { return true } } return false } // Arrows, WASD or vim keys move the focus; Enter or Space flips the focused card @(private) handle_keyboard :: proc() { if raylib.GetMouseDelta() != {} { keyboard_active = false } dx, dy := 0, 0 if key_pressed(.LEFT, .A, .H) { dx -= 1 } if key_pressed(.RIGHT, .D, .L) { dx += 1 } if key_pressed(.UP, .W, .K) { dy -= 1 } if key_pressed(.DOWN, .S, .J) { dy += 1 } if dx != 0 || dy != 0 { // The first key press only reveals the focus ring if keyboard_active { move_focus(dx, dy) } keyboard_active = true } if raylib.IsKeyPressed(.ENTER) || raylib.IsKeyPressed(.SPACE) { keyboard_active = true if can_interact(focus) { select_card(focus) } } } @(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 last_result = profile.record_game(score_key(deck, difficulty), moves, elapsed) 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(deck, difficulty) return } for &c in active_cards() { card.update(&c, dt) } if state != .OVER { handle_keyboard() } switch state { case .WAITING, .FIRST: elapsed += dt if raylib.IsMouseButtonPressed(.LEFT) { for &c, i in active_cards() { if card.is_hovered(&c) && can_interact(i) && select_card(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.MEMORY raylib.ClearBackground(theme.background) 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 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 = .LANGUAGE_MENU } for &c, i in active_cards() { card.draw(&c, theme, can_interact(i), focused = keyboard_active && i == focus && state != .OVER && c.deal_t == 0) } 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)) language := decks.language_of(deck) cx := panel.x + panel_w / 2 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)) stats := strings.clone_to_cstring(fmt.tprintf("%d moves · %s", moves, profile.format_time(elapsed)), context.temp_allocator) ui.draw_text_centered(stats, cx, panel.y + 156, 26, raylib.Fade(theme.ink, win_t), .BOLD) best := last_result.record best_text := strings.clone_to_cstring(fmt.tprintf("Best %d moves · %s", best.best_moves, profile.format_time(best.best_seconds)), context.temp_allocator) if last_result.new_best_moves || last_result.new_best_time { best_text = strings.clone_to_cstring(fmt.tprintf("New best! · %d games played", best.games), context.temp_allocator) } ui.draw_text_centered(best_text, cx, panel.y + 198, 22, raylib.Fade(theme.primary, win_t), .BOLD) 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, hotkey = .ENTER) { restart_requested = true } if ui.button({cx + 10, button_y, button_w, 60}, "Menu", theme.ink) { return .LANGUAGE_MENU } return nil }