Restructure into themed screens with flag card backs, real font, flip animation and sound
- Replace the bogus DroidSansJapanese.ttf (an HTML page) with M PLUS Rounded 1c, loaded with LoadFontEx and explicit kana codepoints - Theme struct per language with flag-based card backs (Hinomaru, Taegukgi, Trairong) - Card art rendered to textures so the flip squashes the whole card - Synthesized flip/match/mismatch/win/click sounds (new audio package) - Shared ui package for text and buttons; Screen enum replaces i32 ids - Kana <-> romaji pairs drawn from all 46 basic hiragana Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5557de62a3
commit
5806f0498c
11 files changed
+1046
-425
No files matched your search
+207
-188
@@ -2,213 +2,232 @@ package memory
|
||||
|
||||
import "core:math/rand"
|
||||
import raylib "vendor:raylib"
|
||||
import themes "../themes"
|
||||
import audio "../audio"
|
||||
import card "../card"
|
||||
import assets "../assets"
|
||||
import themes "../themes"
|
||||
import ui "../ui"
|
||||
|
||||
MemoryState :: enum {
|
||||
WAITING,
|
||||
FIRST,
|
||||
SECOND,
|
||||
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,
|
||||
}
|
||||
|
||||
memory_cards: [24]card.Card
|
||||
memory_state: MemoryState
|
||||
memory_first: i32
|
||||
memory_second: i32
|
||||
memory_matches: i32
|
||||
memory_moves: i32
|
||||
memory_time: f32
|
||||
memory_flip: f32
|
||||
|
||||
init_memory_game :: proc() {
|
||||
hiragana_chars: [12]cstring = {"あ", "い", "う", "え", "お", "か", "き", "く", "け", "こ", "さ", "し"}
|
||||
romanji_chars: [12]cstring = {"A", "I", "U", "E", "O", "KA", "KI", "KU", "KE", "KO", "SA", "SHI"}
|
||||
|
||||
for i := 0; i < 12; i += 1 {
|
||||
memory_cards[i*2] = card.Card{
|
||||
x = 0, y = 0,
|
||||
character = romanji_chars[i],
|
||||
is_face_up = false,
|
||||
is_matched = false,
|
||||
pair_id = i32(i),
|
||||
flip_progress = 0.0,
|
||||
is_flipping = false,
|
||||
flip_direction = false,
|
||||
}
|
||||
memory_cards[i*2+1] = card.Card{
|
||||
x = 0, y = 0,
|
||||
character = romanji_chars[i],
|
||||
is_face_up = false,
|
||||
is_matched = false,
|
||||
pair_id = i32(i),
|
||||
flip_progress = 0.0,
|
||||
is_flipping = false,
|
||||
flip_direction = false,
|
||||
}
|
||||
}
|
||||
|
||||
rand.shuffle(memory_cards[:])
|
||||
|
||||
total_w := themes.GRID_COLS * (themes.CARD_WIDTH + themes.CARD_SPACING) - themes.CARD_SPACING
|
||||
start_x := (themes.SCREEN_WIDTH - total_w) / 2
|
||||
start_y := themes.GRID_TOP
|
||||
|
||||
for i := 0; i < 24; i += 1 {
|
||||
row := i32(i) / themes.GRID_COLS
|
||||
col := i32(i) % themes.GRID_COLS
|
||||
memory_cards[i].x = start_x + col * (themes.CARD_WIDTH + themes.CARD_SPACING)
|
||||
memory_cards[i].y = start_y + row * (themes.CARD_HEIGHT + themes.CARD_SPACING)
|
||||
}
|
||||
|
||||
memory_state = MemoryState.WAITING
|
||||
memory_first = -1
|
||||
memory_second = -1
|
||||
memory_matches = 0
|
||||
memory_moves = 0
|
||||
memory_time = 0.0
|
||||
memory_flip = 0.0
|
||||
Kana :: struct {
|
||||
kana: cstring,
|
||||
romaji: cstring,
|
||||
}
|
||||
|
||||
update_memory_game :: proc(delta_time: f32, mouse_x, mouse_y: i32) {
|
||||
memory_time += delta_time
|
||||
|
||||
if memory_state == MemoryState.SECOND {
|
||||
memory_flip += delta_time
|
||||
if memory_flip >= themes.FLIP_DELAY {
|
||||
if memory_first >= 0 && memory_second >= 0 {
|
||||
if memory_cards[memory_first].pair_id == memory_cards[memory_second].pair_id {
|
||||
memory_cards[memory_first].is_matched = true
|
||||
memory_cards[memory_second].is_matched = true
|
||||
memory_matches += 1
|
||||
} else {
|
||||
memory_cards[memory_first].is_flipping = true
|
||||
memory_cards[memory_first].flip_direction = false
|
||||
memory_cards[memory_first].flip_progress = 0.0
|
||||
memory_cards[memory_second].is_flipping = true
|
||||
memory_cards[memory_second].flip_direction = false
|
||||
memory_cards[memory_second].flip_progress = 0.0
|
||||
}
|
||||
}
|
||||
memory_state = MemoryState.WAITING
|
||||
memory_flip = 0.0
|
||||
memory_first = -1
|
||||
memory_second = -1
|
||||
}
|
||||
// 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}
|
||||
}
|
||||
|
||||
// Update flip animations
|
||||
for i := 0; i < 24; i += 1 {
|
||||
if memory_cards[i].is_flipping {
|
||||
memory_cards[i].flip_progress += delta_time * 3.0 // Flip speed
|
||||
if memory_cards[i].flip_progress >= 1.0 {
|
||||
memory_cards[i].flip_progress = 1.0
|
||||
memory_cards[i].is_flipping = false
|
||||
memory_cards[i].is_face_up = memory_cards[i].flip_direction
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
if memory_state == MemoryState.WAITING || memory_state == MemoryState.FIRST {
|
||||
for i := 0; i < 24; i += 1 {
|
||||
if !memory_cards[i].is_matched && !memory_cards[i].is_flipping &&
|
||||
!memory_cards[i].is_face_up && i32(i) != memory_first {
|
||||
if card.is_card_clicked(&memory_cards[i], mouse_x, mouse_y) &&
|
||||
raylib.IsMouseButtonPressed(raylib.MouseButton.LEFT) {
|
||||
memory_cards[i].is_flipping = true
|
||||
memory_cards[i].flip_direction = true
|
||||
memory_cards[i].flip_progress = 0.0
|
||||
if memory_first == -1 {
|
||||
memory_first = i32(i)
|
||||
memory_state = MemoryState.FIRST
|
||||
} else {
|
||||
memory_second = i32(i)
|
||||
memory_moves += 1
|
||||
memory_state = MemoryState.SECOND
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if memory_matches >= 12 {
|
||||
memory_state = MemoryState.OVER
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
draw_memory_game :: proc(mouse_x, mouse_y: i32) -> i32 {
|
||||
raylib.ClearBackground(themes.JAPAN_BACKGROUND)
|
||||
|
||||
font := assets.get_font()
|
||||
title: cstring = "Memory Game - Japanese"
|
||||
title_size: i32 = 48
|
||||
title_w := raylib.MeasureText(title, title_size)
|
||||
title_x := (themes.SCREEN_WIDTH - title_w) / 2
|
||||
raylib.DrawTextEx(font, title, raylib.Vector2{f32(title_x + 3), 33}, f32(title_size), 0, themes.BLACK)
|
||||
raylib.DrawTextEx(font, title, raylib.Vector2{f32(title_x), 30}, f32(title_size), 0, themes.JAPAN_PRIMARY)
|
||||
|
||||
info_text := raylib.TextFormat("Moves: %d | Time: %.1fs", memory_moves, memory_time)
|
||||
info_w := raylib.MeasureText(info_text, themes.SUBTITLE_FONT_SIZE)
|
||||
info_x := (themes.SCREEN_WIDTH - info_w) / 2
|
||||
raylib.DrawTextEx(font, info_text, raylib.Vector2{f32(info_x), 92}, f32(themes.SUBTITLE_FONT_SIZE), 0, themes.DARK_GRAY)
|
||||
|
||||
for i := 0; i < 24; i += 1 {
|
||||
card.draw_card(&memory_cards[i], mouse_x, mouse_y, themes.JAPAN_PRIMARY)
|
||||
// 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
|
||||
}
|
||||
|
||||
back_w: i32 = 120
|
||||
back_h: i32 = 50
|
||||
back_x: i32 = 30
|
||||
back_y: i32 = 30
|
||||
back_hovered := mouse_x >= back_x && mouse_x <= back_x + back_w &&
|
||||
mouse_y >= back_y && mouse_y <= back_y + back_h
|
||||
back_color := themes.JAPAN_PRIMARY
|
||||
if back_hovered {
|
||||
back_color = themes.JAPAN_ACCENT
|
||||
|
||||
for &c, i in cards {
|
||||
card.draw(&c, theme, can_pick(i))
|
||||
}
|
||||
raylib.DrawRectangle(back_x, back_y, back_w, back_h, back_color)
|
||||
raylib.DrawRectangleLines(back_x, back_y, back_w, back_h, themes.BLACK)
|
||||
back_text: cstring = "Back"
|
||||
back_text_w := raylib.MeasureText(back_text, themes.BUTTON_FONT_SIZE)
|
||||
back_text_x := back_x + (back_w - back_text_w) / 2
|
||||
back_text_y := back_y + (back_h - themes.BUTTON_FONT_SIZE) / 2
|
||||
raylib.DrawTextEx(font, back_text, raylib.Vector2{f32(back_text_x), f32(back_text_y)}, f32(themes.BUTTON_FONT_SIZE), 0, themes.WHITE)
|
||||
|
||||
if back_hovered && raylib.IsMouseButtonPressed(raylib.MouseButton.LEFT) {
|
||||
return themes.JAPANESE_SCREEN
|
||||
}
|
||||
|
||||
if memory_state == MemoryState.OVER {
|
||||
raylib.DrawRectangle(0, 0, themes.SCREEN_WIDTH, themes.SCREEN_HEIGHT, raylib.Color{0, 0, 0, 128})
|
||||
|
||||
win_text: cstring = "Congratulations! You Win!"
|
||||
win_w := raylib.MeasureText(win_text, 48)
|
||||
win_x := (themes.SCREEN_WIDTH - win_w) / 2
|
||||
raylib.DrawTextEx(font, win_text, raylib.Vector2{f32(win_x), f32(themes.SCREEN_HEIGHT / 2 - 60)}, 48, 0, themes.WHITE)
|
||||
|
||||
stats_text := raylib.TextFormat("Moves: %d | Time: %.1fs", memory_moves, memory_time)
|
||||
stats_w := raylib.MeasureText(stats_text, 32)
|
||||
stats_x := (themes.SCREEN_WIDTH - stats_w) / 2
|
||||
raylib.DrawTextEx(font, stats_text, raylib.Vector2{f32(stats_x), f32(themes.SCREEN_HEIGHT / 2)}, 32, 0, themes.WHITE)
|
||||
|
||||
again_text: cstring = "Play Again"
|
||||
again_w := raylib.MeasureText(again_text, themes.BUTTON_FONT_SIZE)
|
||||
again_x := (themes.SCREEN_WIDTH - again_w) / 2
|
||||
again_y := themes.SCREEN_HEIGHT / 2 + 60
|
||||
again_hovered := mouse_x >= again_x && mouse_x <= again_x + again_w &&
|
||||
mouse_y >= again_y && mouse_y <= again_y + themes.BUTTON_FONT_SIZE
|
||||
|
||||
again_color := themes.JAPAN_PRIMARY
|
||||
if again_hovered {
|
||||
again_color = themes.JAPAN_ACCENT
|
||||
}
|
||||
raylib.DrawTextEx(font, again_text, raylib.Vector2{f32(again_x), f32(again_y)}, f32(themes.BUTTON_FONT_SIZE), 0, again_color)
|
||||
|
||||
if again_hovered && raylib.IsMouseButtonPressed(raylib.MouseButton.LEFT) {
|
||||
init_memory_game()
|
||||
|
||||
if state == .OVER {
|
||||
if action := draw_win_panel(); action != nil {
|
||||
next = action.?
|
||||
}
|
||||
}
|
||||
return themes.JAPANESE_MEMORY
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user