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
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>English</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.apple.xcode.dsym.HanjaKanaMun</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>dSYM</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2018 The Jua Project Authors
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2018 The Mali Project Authors (https://github.com/cadsondemak/Mali)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
@@ -4,6 +4,7 @@ import raylib "vendor:raylib"
|
|||||||
import assets "src/assets"
|
import assets "src/assets"
|
||||||
import audio "src/audio"
|
import audio "src/audio"
|
||||||
import card "src/card"
|
import card "src/card"
|
||||||
|
import decks "src/decks"
|
||||||
import memory "src/memory"
|
import memory "src/memory"
|
||||||
import screens "src/screens"
|
import screens "src/screens"
|
||||||
import themes "src/themes"
|
import themes "src/themes"
|
||||||
@@ -12,9 +13,9 @@ import ui "src/ui"
|
|||||||
// Where Escape takes you from each screen (nil = quit)
|
// Where Escape takes you from each screen (nil = quit)
|
||||||
parent_screen :: proc(screen: themes.Screen) -> Maybe(themes.Screen) {
|
parent_screen :: proc(screen: themes.Screen) -> Maybe(themes.Screen) {
|
||||||
switch screen {
|
switch screen {
|
||||||
case .MAIN_MENU: return nil
|
case .MAIN_MENU: return nil
|
||||||
case .JAPANESE_MEMORY: return .JAPANESE_MENU
|
case .LANGUAGE_MENU: return .MAIN_MENU
|
||||||
case .JAPANESE_MENU, .KOREAN_MENU, .THAI_MENU: return .MAIN_MENU
|
case .MEMORY: return .LANGUAGE_MENU
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -40,6 +41,8 @@ main :: proc() {
|
|||||||
defer memory.unload()
|
defer memory.unload()
|
||||||
|
|
||||||
screen := themes.Screen.MAIN_MENU
|
screen := themes.Screen.MAIN_MENU
|
||||||
|
language := decks.Language.JAPANESE
|
||||||
|
difficulty := memory.Difficulty.NORMAL
|
||||||
running := true
|
running := true
|
||||||
for running && !raylib.WindowShouldClose() {
|
for running && !raylib.WindowShouldClose() {
|
||||||
dt := raylib.GetFrameTime()
|
dt := raylib.GetFrameTime()
|
||||||
@@ -52,27 +55,37 @@ main :: proc() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if screen == .JAPANESE_MEMORY {
|
if screen == .MEMORY {
|
||||||
memory.update(dt)
|
memory.update(dt)
|
||||||
}
|
}
|
||||||
|
|
||||||
raylib.BeginDrawing()
|
raylib.BeginDrawing()
|
||||||
ui.begin_frame()
|
ui.begin_frame()
|
||||||
next := screen
|
next := screen
|
||||||
|
start_deck: Maybe(decks.Deck_Id)
|
||||||
switch screen {
|
switch screen {
|
||||||
case .MAIN_MENU: next = screens.main_menu()
|
case .MAIN_MENU:
|
||||||
case .JAPANESE_MENU: next = screens.japanese_menu()
|
if picked, ok := screens.main_menu().?; ok {
|
||||||
case .JAPANESE_MEMORY: next = memory.draw()
|
language = picked
|
||||||
case .KOREAN_MENU: next = screens.korean_menu()
|
next = .LANGUAGE_MENU
|
||||||
case .THAI_MENU: next = screens.thai_menu()
|
}
|
||||||
|
case .LANGUAGE_MENU:
|
||||||
|
switch result in screens.language_menu(language, &difficulty) {
|
||||||
|
case decks.Deck_Id:
|
||||||
|
start_deck = result
|
||||||
|
next = .MEMORY
|
||||||
|
case themes.Screen:
|
||||||
|
next = result
|
||||||
|
}
|
||||||
|
case .MEMORY:
|
||||||
|
next = memory.draw()
|
||||||
}
|
}
|
||||||
ui.end_frame()
|
ui.end_frame()
|
||||||
raylib.EndDrawing()
|
raylib.EndDrawing()
|
||||||
|
|
||||||
// Start a fresh game when entering the memory screen (outside of drawing,
|
// Building card faces renders into textures, so do it outside of drawing
|
||||||
// because building card faces renders into textures)
|
if deck, ok := start_deck.?; ok {
|
||||||
if next == .JAPANESE_MEMORY && screen != .JAPANESE_MEMORY {
|
memory.start(deck, difficulty)
|
||||||
memory.start(themes.JAPAN)
|
|
||||||
}
|
}
|
||||||
screen = next
|
screen = next
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-22
@@ -2,28 +2,57 @@ package assets
|
|||||||
|
|
||||||
import raylib "vendor:raylib"
|
import raylib "vendor:raylib"
|
||||||
|
|
||||||
REGULAR_FONT_PATH :: "assets/MPLUSRounded1c-Medium.ttf"
|
|
||||||
BOLD_FONT_PATH :: "assets/MPLUSRounded1c-ExtraBold.ttf"
|
|
||||||
|
|
||||||
// Glyphs are rasterized at this size and scaled down when drawn
|
// Glyphs are rasterized at this size and scaled down when drawn
|
||||||
FONT_BASE_SIZE :: 128
|
FONT_BASE_SIZE :: 128
|
||||||
|
|
||||||
regular_font: raylib.Font
|
Font_Style :: enum {
|
||||||
bold_font: raylib.Font
|
REGULAR, // UI text (M PLUS Rounded 1c Medium)
|
||||||
|
BOLD, // UI headings and Japanese symbols (M PLUS Rounded 1c ExtraBold)
|
||||||
|
KOREAN, // Hangul (Jua)
|
||||||
|
THAI, // Thai script (Mali SemiBold)
|
||||||
|
}
|
||||||
|
|
||||||
// raylib only bakes ASCII by default, so every non-ASCII glyph must be listed
|
@(rodata)
|
||||||
@(private)
|
FONT_PATHS := [Font_Style]cstring{
|
||||||
build_codepoints :: proc() -> [dynamic]rune {
|
.REGULAR = "assets/MPLUSRounded1c-Medium.ttf",
|
||||||
codepoints: [dynamic]rune
|
.BOLD = "assets/MPLUSRounded1c-ExtraBold.ttf",
|
||||||
for r in rune(0x20) ..= 0x7E { append(&codepoints, r) } // ASCII
|
.KOREAN = "assets/Jua-Regular.ttf",
|
||||||
for r in rune(0x3041) ..= 0x3096 { append(&codepoints, r) } // Hiragana
|
.THAI = "assets/Mali-SemiBold.ttf",
|
||||||
for r in rune(0x30A1) ..= 0x30FC { append(&codepoints, r) } // Katakana
|
}
|
||||||
for r in "日本語・·āō" { append(&codepoints, r) }
|
|
||||||
|
fonts: [Font_Style]raylib.Font
|
||||||
|
|
||||||
|
// Extra non-range characters used in UI text
|
||||||
|
EXTRA_JAPANESE :: "日本語・·āō"
|
||||||
|
EXTRA_KOREAN :: "한글국어축하해요"
|
||||||
|
EXTRA_THAI :: "ไทย"
|
||||||
|
|
||||||
|
// raylib only bakes the codepoints it is given, so every glyph a font must draw is listed here.
|
||||||
|
// Tests check that every deck's characters are covered.
|
||||||
|
codepoints_for :: proc(style: Font_Style, allocator := context.allocator) -> [dynamic]rune {
|
||||||
|
codepoints := make([dynamic]rune, allocator)
|
||||||
|
for r in rune(0x20) ..= 0x7E { append(&codepoints, r) } // ASCII
|
||||||
|
switch style {
|
||||||
|
case .REGULAR, .BOLD:
|
||||||
|
for r in rune(0x3041) ..= 0x3096 { append(&codepoints, r) } // Hiragana
|
||||||
|
for r in rune(0x30A1) ..= 0x30FC { append(&codepoints, r) } // Katakana
|
||||||
|
for r in EXTRA_JAPANESE { append(&codepoints, r) }
|
||||||
|
case .KOREAN:
|
||||||
|
for r in rune(0x3131) ..= 0x3163 { append(&codepoints, r) } // Modern Hangul compatibility jamo
|
||||||
|
for r in EXTRA_KOREAN { append(&codepoints, r) }
|
||||||
|
case .THAI:
|
||||||
|
for r in rune(0x0E01) ..= 0x0E3A { append(&codepoints, r) } // Thai (U+0E3B-0E3E are unassigned)
|
||||||
|
for r in rune(0x0E3F) ..= 0x0E5B { append(&codepoints, r) }
|
||||||
|
for r in EXTRA_THAI { append(&codepoints, r) }
|
||||||
|
}
|
||||||
return codepoints
|
return codepoints
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private)
|
@(private)
|
||||||
load_font :: proc(path: cstring, codepoints: []rune) -> raylib.Font {
|
load_font :: proc(style: Font_Style) -> raylib.Font {
|
||||||
|
codepoints := codepoints_for(style)
|
||||||
|
defer delete(codepoints)
|
||||||
|
path := FONT_PATHS[style]
|
||||||
font := raylib.LoadFontEx(path, FONT_BASE_SIZE, raw_data(codepoints), i32(len(codepoints)))
|
font := raylib.LoadFontEx(path, FONT_BASE_SIZE, raw_data(codepoints), i32(len(codepoints)))
|
||||||
if !raylib.IsFontValid(font) || font.texture.id == 0 {
|
if !raylib.IsFontValid(font) || font.texture.id == 0 {
|
||||||
raylib.TraceLog(.WARNING, "ASSETS: Falling back to default font, could not load %s", path)
|
raylib.TraceLog(.WARNING, "ASSETS: Falling back to default font, could not load %s", path)
|
||||||
@@ -36,18 +65,18 @@ load_font :: proc(path: cstring, codepoints: []rune) -> raylib.Font {
|
|||||||
}
|
}
|
||||||
|
|
||||||
load_fonts :: proc() {
|
load_fonts :: proc() {
|
||||||
codepoints := build_codepoints()
|
for style in Font_Style {
|
||||||
defer delete(codepoints)
|
fonts[style] = load_font(style)
|
||||||
regular_font = load_font(REGULAR_FONT_PATH, codepoints[:])
|
}
|
||||||
bold_font = load_font(BOLD_FONT_PATH, codepoints[:])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unload_fonts :: proc() {
|
unload_fonts :: proc() {
|
||||||
default_id := raylib.GetFontDefault().texture.id
|
default_id := raylib.GetFontDefault().texture.id
|
||||||
if regular_font.texture.id != default_id { raylib.UnloadFont(regular_font) }
|
for font in fonts {
|
||||||
if bold_font.texture.id != default_id { raylib.UnloadFont(bold_font) }
|
if font.texture.id != default_id { raylib.UnloadFont(font) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get_font :: proc(bold := false) -> raylib.Font {
|
get_font :: proc(style: Font_Style = .REGULAR) -> raylib.Font {
|
||||||
return bold ? bold_font : regular_font
|
return fonts[style]
|
||||||
}
|
}
|
||||||
+26
-11
@@ -2,6 +2,7 @@ package card
|
|||||||
|
|
||||||
import "core:math"
|
import "core:math"
|
||||||
import raylib "vendor:raylib"
|
import raylib "vendor:raylib"
|
||||||
|
import assets "../assets"
|
||||||
import themes "../themes"
|
import themes "../themes"
|
||||||
import ui "../ui"
|
import ui "../ui"
|
||||||
|
|
||||||
@@ -19,8 +20,9 @@ HOVER_LIFT :: 4
|
|||||||
|
|
||||||
Card :: struct {
|
Card :: struct {
|
||||||
rect: raylib.Rectangle, // resting position on screen
|
rect: raylib.Rectangle, // resting position on screen
|
||||||
text: cstring,
|
text: cstring,
|
||||||
is_kana: bool,
|
hint: cstring, // small second line on reading cards
|
||||||
|
is_symbol: bool, // script symbol (vs. its reading)
|
||||||
pair_id: int,
|
pair_id: int,
|
||||||
face_up: bool, // logical state; the animation follows it
|
face_up: bool, // logical state; the animation follows it
|
||||||
matched: bool,
|
matched: bool,
|
||||||
@@ -154,21 +156,34 @@ get_back_art :: proc(flag: themes.Flag) -> raylib.Texture2D {
|
|||||||
return back_art[flag].texture
|
return back_art[flag].texture
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renders the card's face into its own texture
|
// Font size that fits text inside the card width
|
||||||
build_face :: proc(card: ^Card, theme: themes.Theme) {
|
@(private)
|
||||||
|
fit_size :: proc(text: cstring, size: f32, style: assets.Font_Style) -> f32 {
|
||||||
|
max_width: f32 = ART_WIDTH * 0.78
|
||||||
|
width := ui.measure_text(text, size, style).x
|
||||||
|
return width > max_width ? size * max_width / width : size
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders the card's face into its own texture. Symbols use their script's font.
|
||||||
|
build_face :: proc(card: ^Card, theme: themes.Theme, symbol_style: assets.Font_Style) {
|
||||||
unload_face(card)
|
unload_face(card)
|
||||||
card.face_art = new_art_texture(theme.primary)
|
card.face_art = new_art_texture(theme.primary)
|
||||||
raylib.BeginTextureMode(card.face_art)
|
raylib.BeginTextureMode(card.face_art)
|
||||||
draw_frame(theme.card_face, theme.primary)
|
draw_frame(theme.card_face, theme.primary)
|
||||||
raylib.DrawRectangleRoundedLinesEx(art_rect(16), ROUNDNESS, SEGMENTS, 2, raylib.Fade(theme.primary, 0.35))
|
raylib.DrawRectangleRoundedLinesEx(art_rect(16), ROUNDNESS, SEGMENTS, 2, raylib.Fade(theme.primary, 0.35))
|
||||||
|
|
||||||
size: f32 = card.is_kana ? 132 : 96
|
if card.is_symbol {
|
||||||
color := card.is_kana ? theme.ink : theme.primary
|
ui.draw_text_in_rect(card.text, art_rect(), fit_size(card.text, 132, symbol_style), theme.ink, symbol_style)
|
||||||
// Shrink long romaji (e.g. "TSU") to fit inside the card
|
} else if card.hint == nil {
|
||||||
max_width: f32 = ART_WIDTH * 0.78
|
ui.draw_text_in_rect(card.text, art_rect(), fit_size(card.text, 96, .BOLD), theme.primary, .BOLD)
|
||||||
width := ui.measure_text(card.text, size, bold = true).x
|
} else {
|
||||||
if width > max_width { size *= max_width / width }
|
// Reading on top, hint underneath
|
||||||
ui.draw_text_in_rect(card.text, art_rect(), size, color, bold = true)
|
main_rect := art_rect()
|
||||||
|
main_rect.height -= 56
|
||||||
|
ui.draw_text_in_rect(card.text, main_rect, fit_size(card.text, 88, .BOLD), theme.primary, .BOLD)
|
||||||
|
hint_rect := raylib.Rectangle{0, ART_HEIGHT * 0.62, ART_WIDTH, 60}
|
||||||
|
ui.draw_text_in_rect(card.hint, hint_rect, fit_size(card.hint, 34, .REGULAR), theme.ink, .REGULAR)
|
||||||
|
}
|
||||||
raylib.EndTextureMode()
|
raylib.EndTextureMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package decks
|
||||||
|
|
||||||
|
import assets "../assets"
|
||||||
|
import themes "../themes"
|
||||||
|
|
||||||
|
Language :: enum {
|
||||||
|
JAPANESE,
|
||||||
|
KOREAN,
|
||||||
|
THAI,
|
||||||
|
}
|
||||||
|
|
||||||
|
Deck_Id :: enum {
|
||||||
|
HIRAGANA,
|
||||||
|
KATAKANA,
|
||||||
|
HANGUL,
|
||||||
|
THAI_CONSONANTS,
|
||||||
|
}
|
||||||
|
|
||||||
|
// One card pair: the script symbol and how it reads
|
||||||
|
Entry :: struct {
|
||||||
|
symbol: cstring,
|
||||||
|
reading: cstring, // romanization, shown large
|
||||||
|
hint: cstring, // optional small line under the reading
|
||||||
|
}
|
||||||
|
|
||||||
|
Deck_Info :: struct {
|
||||||
|
name: cstring, // English name, used on buttons
|
||||||
|
native: cstring, // name in its own script
|
||||||
|
language: Language,
|
||||||
|
}
|
||||||
|
|
||||||
|
Language_Info :: struct {
|
||||||
|
theme: themes.Theme,
|
||||||
|
native: cstring, // language name in its own script
|
||||||
|
symbol_style: assets.Font_Style, // font for the script's symbols
|
||||||
|
congrats: cstring, // win message
|
||||||
|
congrats_style: assets.Font_Style,
|
||||||
|
}
|
||||||
|
|
||||||
|
@(rodata)
|
||||||
|
DECKS := [Deck_Id]Deck_Info{
|
||||||
|
.HIRAGANA = {name = "Hiragana", native = "ひらがな", language = .JAPANESE},
|
||||||
|
.KATAKANA = {name = "Katakana", native = "カタカナ", language = .JAPANESE},
|
||||||
|
.HANGUL = {name = "Hangul", native = "한글", language = .KOREAN},
|
||||||
|
.THAI_CONSONANTS = {name = "Consonants", native = "ก ข ค", language = .THAI},
|
||||||
|
}
|
||||||
|
|
||||||
|
@(rodata)
|
||||||
|
LANGUAGES := [Language]Language_Info{
|
||||||
|
.JAPANESE = {theme = themes.JAPAN, native = "日本語", symbol_style = .BOLD, congrats = "おめでとう!", congrats_style = .BOLD},
|
||||||
|
.KOREAN = {theme = themes.KOREA, native = "한국어", symbol_style = .KOREAN, congrats = "축하해요!", congrats_style = .KOREAN},
|
||||||
|
// Thai words with vowel/tone marks need text shaping, which raylib doesn't do
|
||||||
|
.THAI = {theme = themes.THAI, native = "ไทย", symbol_style = .THAI, congrats = "Well done!", congrats_style = .BOLD},
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private, rodata)
|
||||||
|
japanese_decks := [?]Deck_Id{.HIRAGANA, .KATAKANA}
|
||||||
|
@(private, rodata)
|
||||||
|
korean_decks := [?]Deck_Id{.HANGUL}
|
||||||
|
@(private, rodata)
|
||||||
|
thai_decks := [?]Deck_Id{.THAI_CONSONANTS}
|
||||||
|
|
||||||
|
decks_for :: proc(language: Language) -> []Deck_Id {
|
||||||
|
switch language {
|
||||||
|
case .JAPANESE: return japanese_decks[:]
|
||||||
|
case .KOREAN: return korean_decks[:]
|
||||||
|
case .THAI: return thai_decks[:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
language_of :: proc(deck: Deck_Id) -> Language_Info {
|
||||||
|
return LANGUAGES[DECKS[deck].language]
|
||||||
|
}
|
||||||
|
|
||||||
|
entries :: proc(deck: Deck_Id) -> []Entry {
|
||||||
|
switch deck {
|
||||||
|
case .HIRAGANA: return hiragana[:]
|
||||||
|
case .KATAKANA: return katakana[:]
|
||||||
|
case .HANGUL: return hangul[:]
|
||||||
|
case .THAI_CONSONANTS: return thai_consonants[:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The 46 basic hiragana (gojūon)
|
||||||
|
@(private, rodata)
|
||||||
|
hiragana := [?]Entry{
|
||||||
|
{"あ", "a", nil}, {"い", "i", nil}, {"う", "u", nil}, {"え", "e", nil}, {"お", "o", nil},
|
||||||
|
{"か", "ka", nil}, {"き", "ki", nil}, {"く", "ku", nil}, {"け", "ke", nil}, {"こ", "ko", nil},
|
||||||
|
{"さ", "sa", nil}, {"し", "shi", nil}, {"す", "su", nil}, {"せ", "se", nil}, {"そ", "so", nil},
|
||||||
|
{"た", "ta", nil}, {"ち", "chi", nil}, {"つ", "tsu", nil}, {"て", "te", nil}, {"と", "to", nil},
|
||||||
|
{"な", "na", nil}, {"に", "ni", nil}, {"ぬ", "nu", nil}, {"ね", "ne", nil}, {"の", "no", nil},
|
||||||
|
{"は", "ha", nil}, {"ひ", "hi", nil}, {"ふ", "fu", nil}, {"へ", "he", nil}, {"ほ", "ho", nil},
|
||||||
|
{"ま", "ma", nil}, {"み", "mi", nil}, {"む", "mu", nil}, {"め", "me", nil}, {"も", "mo", nil},
|
||||||
|
{"や", "ya", nil}, {"ゆ", "yu", nil}, {"よ", "yo", nil},
|
||||||
|
{"ら", "ra", nil}, {"り", "ri", nil}, {"る", "ru", nil}, {"れ", "re", nil}, {"ろ", "ro", nil},
|
||||||
|
{"わ", "wa", nil}, {"を", "wo", nil}, {"ん", "n", nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
// The 46 basic katakana
|
||||||
|
@(private, rodata)
|
||||||
|
katakana := [?]Entry{
|
||||||
|
{"ア", "a", nil}, {"イ", "i", nil}, {"ウ", "u", nil}, {"エ", "e", nil}, {"オ", "o", nil},
|
||||||
|
{"カ", "ka", nil}, {"キ", "ki", nil}, {"ク", "ku", nil}, {"ケ", "ke", nil}, {"コ", "ko", nil},
|
||||||
|
{"サ", "sa", nil}, {"シ", "shi", nil}, {"ス", "su", nil}, {"セ", "se", nil}, {"ソ", "so", nil},
|
||||||
|
{"タ", "ta", nil}, {"チ", "chi", nil}, {"ツ", "tsu", nil}, {"テ", "te", nil}, {"ト", "to", nil},
|
||||||
|
{"ナ", "na", nil}, {"ニ", "ni", nil}, {"ヌ", "nu", nil}, {"ネ", "ne", nil}, {"ノ", "no", nil},
|
||||||
|
{"ハ", "ha", nil}, {"ヒ", "hi", nil}, {"フ", "fu", nil}, {"ヘ", "he", nil}, {"ホ", "ho", nil},
|
||||||
|
{"マ", "ma", nil}, {"ミ", "mi", nil}, {"ム", "mu", nil}, {"メ", "me", nil}, {"モ", "mo", nil},
|
||||||
|
{"ヤ", "ya", nil}, {"ユ", "yu", nil}, {"ヨ", "yo", nil},
|
||||||
|
{"ラ", "ra", nil}, {"リ", "ri", nil}, {"ル", "ru", nil}, {"レ", "re", nil}, {"ロ", "ro", nil},
|
||||||
|
{"ワ", "wa", nil}, {"ヲ", "wo", nil}, {"ン", "n", nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
// The 14 basic consonants and 10 basic vowels, Revised Romanization (initial position)
|
||||||
|
@(private, rodata)
|
||||||
|
hangul := [?]Entry{
|
||||||
|
{"ㄱ", "g", nil}, {"ㄴ", "n", nil}, {"ㄷ", "d", nil}, {"ㄹ", "r", nil}, {"ㅁ", "m", nil},
|
||||||
|
{"ㅂ", "b", nil}, {"ㅅ", "s", nil}, {"ㅇ", "ng", "silent first"}, {"ㅈ", "j", nil}, {"ㅊ", "ch", nil},
|
||||||
|
{"ㅋ", "k", nil}, {"ㅌ", "t", nil}, {"ㅍ", "p", nil}, {"ㅎ", "h", nil},
|
||||||
|
{"ㅏ", "a", nil}, {"ㅑ", "ya", nil}, {"ㅓ", "eo", nil}, {"ㅕ", "yeo", nil}, {"ㅗ", "o", nil},
|
||||||
|
{"ㅛ", "yo", nil}, {"ㅜ", "u", nil}, {"ㅠ", "yu", nil}, {"ㅡ", "eu", nil}, {"ㅣ", "i", nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
// The 42 Thai consonants in use (ฃ and ฅ are obsolete). Many share a sound, so the
|
||||||
|
// hint is each letter's acrophonic name word (RTGS), which is unique.
|
||||||
|
@(private, rodata)
|
||||||
|
thai_consonants := [?]Entry{
|
||||||
|
{"ก", "k", "kai"}, {"ข", "kh", "khai"}, {"ค", "kh", "khwai"}, {"ฆ", "kh", "rakhang"},
|
||||||
|
{"ง", "ng", "ngu"}, {"จ", "ch", "chan"}, {"ฉ", "ch", "ching"}, {"ช", "ch", "chang"},
|
||||||
|
{"ซ", "s", "so"}, {"ฌ", "ch", "choe"}, {"ญ", "y", "ying"}, {"ฎ", "d", "chada"},
|
||||||
|
{"ฏ", "t", "patak"}, {"ฐ", "th", "than"}, {"ฑ", "th", "montho"}, {"ฒ", "th", "phuthao"},
|
||||||
|
{"ณ", "n", "nen"}, {"ด", "d", "dek"}, {"ต", "t", "tao"}, {"ถ", "th", "thung"},
|
||||||
|
{"ท", "th", "thahan"}, {"ธ", "th", "thong"}, {"น", "n", "nu"}, {"บ", "b", "baimai"},
|
||||||
|
{"ป", "p", "pla"}, {"ผ", "ph", "phueng"}, {"ฝ", "f", "fa"}, {"พ", "ph", "phan"},
|
||||||
|
{"ฟ", "f", "fan"}, {"ภ", "ph", "samphao"}, {"ม", "m", "ma"}, {"ย", "y", "yak"},
|
||||||
|
{"ร", "r", "ruea"}, {"ล", "l", "ling"}, {"ว", "w", "waen"}, {"ศ", "s", "sala"},
|
||||||
|
{"ษ", "s", "ruesi"}, {"ส", "s", "suea"}, {"ห", "h", "hip"}, {"ฬ", "l", "chula"},
|
||||||
|
{"อ", "o", "ang"}, {"ฮ", "h", "nokhuk"},
|
||||||
|
}
|
||||||
+85
-58
@@ -4,44 +4,50 @@ import "core:math/rand"
|
|||||||
import raylib "vendor:raylib"
|
import raylib "vendor:raylib"
|
||||||
import audio "../audio"
|
import audio "../audio"
|
||||||
import card "../card"
|
import card "../card"
|
||||||
|
import decks "../decks"
|
||||||
import themes "../themes"
|
import themes "../themes"
|
||||||
import ui "../ui"
|
import ui "../ui"
|
||||||
|
|
||||||
PAIR_COUNT :: 12
|
MAX_CARDS :: 32
|
||||||
CARD_COUNT :: PAIR_COUNT * 2
|
|
||||||
|
|
||||||
MATCH_DELAY :: 0.35 // seconds both cards stay up before a match is confirmed
|
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
|
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 {
|
State :: enum {
|
||||||
WAITING, // no card selected
|
WAITING, // no card selected
|
||||||
FIRST, // one card selected
|
FIRST, // one card selected
|
||||||
RESOLVING, // two cards up, waiting before match/mismatch
|
RESOLVING, // two cards up, waiting before match/mismatch
|
||||||
OVER,
|
OVER,
|
||||||
}
|
}
|
||||||
|
|
||||||
Kana :: struct {
|
cards: [MAX_CARDS]card.Card
|
||||||
kana: cstring,
|
card_count: int
|
||||||
romaji: cstring,
|
pair_count: int
|
||||||
}
|
deck: decks.Deck_Id
|
||||||
|
difficulty: Difficulty
|
||||||
// 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
|
theme: themes.Theme
|
||||||
|
layout: Layout
|
||||||
|
state: State
|
||||||
first: int = -1
|
first: int = -1
|
||||||
second: int = -1
|
second: int = -1
|
||||||
matches: int
|
matches: int
|
||||||
@@ -52,33 +58,46 @@ win_t: f32 // fades in the win panel
|
|||||||
restart_requested: bool
|
restart_requested: bool
|
||||||
|
|
||||||
// Deals a new game and renders the card faces. Needs the window to exist.
|
// Deals a new game and renders the card faces. Needs the window to exist.
|
||||||
start :: proc(game_theme: themes.Theme) {
|
start :: proc(deck_id: decks.Deck_Id, game_difficulty: Difficulty) {
|
||||||
deal(game_theme)
|
deal(deck_id, game_difficulty)
|
||||||
for &c in cards { card.build_face(&c, theme) }
|
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
|
// Shuffles, lays out and resets the game state without touching the GPU
|
||||||
deal :: proc(game_theme: themes.Theme) {
|
deal :: proc(deck_id: decks.Deck_Id, game_difficulty: Difficulty) {
|
||||||
theme = game_theme
|
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
|
// Pick random entries from the deck
|
||||||
rand.shuffle(deck[:])
|
entries := decks.entries(deck)
|
||||||
for i in 0 ..< PAIR_COUNT {
|
order := make([]int, len(entries), context.temp_allocator)
|
||||||
card.unload_face(&cards[i * 2])
|
for &o, i in order { o = i }
|
||||||
card.unload_face(&cards[i * 2 + 1])
|
rand.shuffle(order)
|
||||||
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}
|
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
|
start_x := (themes.SCREEN_WIDTH - total_w) / 2
|
||||||
for &c, i in cards {
|
start_y := themes.GRID_TOP + (themes.SCREEN_HEIGHT - 16 - themes.GRID_TOP - total_h) / 2
|
||||||
row := f32(i / themes.GRID_COLS)
|
for &c, i in active_cards() {
|
||||||
col := f32(i % themes.GRID_COLS)
|
|
||||||
c.rect = {
|
c.rect = {
|
||||||
start_x + col * (themes.CARD_WIDTH + themes.CARD_SPACING),
|
start_x + f32(i % layout.cols) * step_x,
|
||||||
themes.GRID_TOP + row * (themes.CARD_HEIGHT + themes.CARD_SPACING),
|
start_y + f32(i / layout.cols) * step_y,
|
||||||
themes.CARD_WIDTH,
|
themes.CARD_WIDTH,
|
||||||
themes.CARD_HEIGHT,
|
themes.CARD_HEIGHT,
|
||||||
}
|
}
|
||||||
@@ -95,11 +114,16 @@ deal :: proc(game_theme: themes.Theme) {
|
|||||||
restart_requested = false
|
restart_requested = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
active_cards :: proc() -> []card.Card {
|
||||||
|
return cards[:card_count]
|
||||||
|
}
|
||||||
|
|
||||||
unload :: proc() {
|
unload :: proc() {
|
||||||
for &c in cards { card.unload_face(&c) }
|
for &c in cards { card.unload_face(&c) }
|
||||||
}
|
}
|
||||||
|
|
||||||
can_pick :: proc(i: int) -> bool {
|
can_pick :: proc(i: int) -> bool {
|
||||||
|
if i < 0 || i >= card_count { return false }
|
||||||
c := &cards[i]
|
c := &cards[i]
|
||||||
return (state == .WAITING || state == .FIRST) && !c.face_up && !c.matched
|
return (state == .WAITING || state == .FIRST) && !c.face_up && !c.matched
|
||||||
}
|
}
|
||||||
@@ -131,7 +155,7 @@ resolve :: proc() {
|
|||||||
card.start_pulse(a)
|
card.start_pulse(a)
|
||||||
card.start_pulse(b)
|
card.start_pulse(b)
|
||||||
matches += 1
|
matches += 1
|
||||||
if matches == PAIR_COUNT {
|
if matches == pair_count {
|
||||||
state = .OVER
|
state = .OVER
|
||||||
audio.play(.WIN)
|
audio.play(.WIN)
|
||||||
} else {
|
} else {
|
||||||
@@ -151,17 +175,17 @@ resolve :: proc() {
|
|||||||
|
|
||||||
update :: proc(dt: f32) {
|
update :: proc(dt: f32) {
|
||||||
if restart_requested {
|
if restart_requested {
|
||||||
start(theme)
|
start(deck, difficulty)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for &c in cards { card.update(&c, dt) }
|
for &c in active_cards() { card.update(&c, dt) }
|
||||||
|
|
||||||
switch state {
|
switch state {
|
||||||
case .WAITING, .FIRST:
|
case .WAITING, .FIRST:
|
||||||
elapsed += dt
|
elapsed += dt
|
||||||
if raylib.IsMouseButtonPressed(.LEFT) {
|
if raylib.IsMouseButtonPressed(.LEFT) {
|
||||||
for &c, i in cards {
|
for &c, i in active_cards() {
|
||||||
if card.is_hovered(&c) && select_card(i) {
|
if card.is_hovered(&c) && select_card(i) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -181,20 +205,22 @@ update :: proc(dt: f32) {
|
|||||||
|
|
||||||
// Draws the game and returns the screen to show next frame
|
// Draws the game and returns the screen to show next frame
|
||||||
draw :: proc() -> themes.Screen {
|
draw :: proc() -> themes.Screen {
|
||||||
next := themes.Screen.JAPANESE_MEMORY
|
next := themes.Screen.MEMORY
|
||||||
raylib.ClearBackground(theme.background)
|
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
|
minutes := int(elapsed) / 60
|
||||||
seconds := int(elapsed) % 60
|
seconds := int(elapsed) % 60
|
||||||
info := raylib.TextFormat("Moves %d · Pairs %d/%d · %d:%02d", moves, matches, PAIR_COUNT, minutes, seconds)
|
stats := raylib.TextFormat("%s · Moves %d · Pairs %d/%d · %d:%02d",
|
||||||
ui.draw_text_centered(info, themes.SCREEN_WIDTH / 2, 88, 24, theme.ink)
|
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) {
|
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))
|
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.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))
|
raylib.DrawRectangleRoundedLinesEx(panel, 0.12, 16, 4, raylib.Fade(theme.primary, win_t))
|
||||||
|
|
||||||
|
language := decks.language_of(deck)
|
||||||
cx := panel.x + panel_w / 2
|
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(language.congrats, cx, panel.y + 34, 56, raylib.Fade(theme.primary, win_t), language.congrats_style)
|
||||||
ui.draw_text_centered("Congratulations, all pairs found!", cx, panel.y + 112, 26, raylib.Fade(theme.ink, win_t))
|
ui.draw_text_centered("All pairs found!", cx, panel.y + 112, 26, raylib.Fade(theme.ink, win_t))
|
||||||
minutes := int(elapsed) / 60
|
minutes := int(elapsed) / 60
|
||||||
seconds := int(elapsed) % 60
|
seconds := int(elapsed) % 60
|
||||||
stats := raylib.TextFormat("%d moves · %d:%02d", moves, minutes, seconds)
|
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 }
|
if win_t < 1 { return nil }
|
||||||
button_w: f32 = 200
|
button_w: f32 = 200
|
||||||
@@ -233,7 +260,7 @@ draw_win_panel :: proc() -> Maybe(themes.Screen) {
|
|||||||
restart_requested = true
|
restart_requested = true
|
||||||
}
|
}
|
||||||
if ui.button({cx + 10, button_y, button_w, 60}, "Menu", theme.ink) {
|
if ui.button({cx + 10, button_y, button_w, 60}, "Menu", theme.ink) {
|
||||||
return .JAPANESE_MENU
|
return .LANGUAGE_MENU
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
+73
-50
@@ -3,15 +3,11 @@ package screens
|
|||||||
import "core:math"
|
import "core:math"
|
||||||
import raylib "vendor:raylib"
|
import raylib "vendor:raylib"
|
||||||
import card "../card"
|
import card "../card"
|
||||||
|
import decks "../decks"
|
||||||
|
import memory "../memory"
|
||||||
import themes "../themes"
|
import themes "../themes"
|
||||||
import ui "../ui"
|
import ui "../ui"
|
||||||
|
|
||||||
LANGUAGES :: [3]struct{ theme: themes.Theme, screen: themes.Screen }{
|
|
||||||
{themes.JAPAN, .JAPANESE_MENU},
|
|
||||||
{themes.KOREA, .KOREAN_MENU},
|
|
||||||
{themes.THAI, .THAI_MENU},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draws a card back like a playing card lying on the table
|
// Draws a card back like a playing card lying on the table
|
||||||
@(private)
|
@(private)
|
||||||
draw_card_art :: proc(flag: themes.Flag, rect: raylib.Rectangle, rotation: f32 = 0) {
|
draw_card_art :: proc(flag: themes.Flag, rect: raylib.Rectangle, rotation: f32 = 0) {
|
||||||
@@ -26,13 +22,18 @@ draw_card_art :: proc(flag: themes.Flag, rect: raylib.Rectangle, rotation: f32 =
|
|||||||
raylib.DrawTexturePro(texture, source, dest, origin, rotation, themes.WHITE)
|
raylib.DrawTexturePro(texture, source, dest, origin, rotation, themes.WHITE)
|
||||||
}
|
}
|
||||||
|
|
||||||
main_menu :: proc() -> themes.Screen {
|
@(private)
|
||||||
next := themes.Screen.MAIN_MENU
|
NUMBER_KEYS :: [?]raylib.KeyboardKey{.ONE, .TWO, .THREE, .FOUR, .FIVE}
|
||||||
|
|
||||||
|
// Returns the language picked this frame, if any
|
||||||
|
main_menu :: proc() -> Maybe(decks.Language) {
|
||||||
raylib.ClearBackground(themes.MENU_BACKGROUND)
|
raylib.ClearBackground(themes.MENU_BACKGROUND)
|
||||||
|
languages := decks.LANGUAGES
|
||||||
|
keys := NUMBER_KEYS
|
||||||
|
|
||||||
// Thin stripes along the top in each language's primary color
|
// Thin stripes along the top in each language's primary color
|
||||||
stripe_w: i32 = themes.SCREEN_WIDTH / 3
|
stripe_w: i32 = themes.SCREEN_WIDTH / len(decks.Language)
|
||||||
for lang, i in LANGUAGES {
|
for lang, i in languages {
|
||||||
raylib.DrawRectangle(i32(i) * stripe_w, 0, stripe_w, 8, lang.theme.primary)
|
raylib.DrawRectangle(i32(i) * stripe_w, 0, stripe_w, 8, lang.theme.primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,66 +47,88 @@ main_menu :: proc() -> themes.Screen {
|
|||||||
start_x := (themes.SCREEN_WIDTH - total) / 2
|
start_x := (themes.SCREEN_WIDTH - total) / 2
|
||||||
// Gentle bobbing so the menu feels alive
|
// Gentle bobbing so the menu feels alive
|
||||||
time := f32(raylib.GetTime())
|
time := f32(raylib.GetTime())
|
||||||
for lang, i in LANGUAGES {
|
picked: Maybe(decks.Language)
|
||||||
|
for lang, language in languages {
|
||||||
|
i := int(language)
|
||||||
x := start_x + f32(i) * (tile_w + gap)
|
x := start_x + f32(i) * (tile_w + gap)
|
||||||
bob := math.sin(time * 1.6 + f32(i) * 1.1) * 4
|
bob := math.sin(time * 1.6 + f32(i) * 1.1) * 4
|
||||||
rotation := f32(i - 1) * 4
|
rotation := f32(i - 1) * 4
|
||||||
draw_card_art(lang.theme.flag, {x, 250 + bob, tile_w, tile_h}, rotation)
|
draw_card_art(lang.theme.flag, {x, 250 + bob, tile_w, tile_h}, rotation)
|
||||||
button_rect := raylib.Rectangle{x + tile_w / 2 - themes.BUTTON_WIDTH / 2 + 30, 500, themes.BUTTON_WIDTH - 60, themes.BUTTON_HEIGHT}
|
button_rect := raylib.Rectangle{x + tile_w / 2 - (themes.BUTTON_WIDTH - 60) / 2, 500, themes.BUTTON_WIDTH - 60, themes.BUTTON_HEIGHT}
|
||||||
if ui.button(button_rect, lang.theme.name, lang.theme.primary) {
|
if ui.button(button_rect, lang.theme.name, lang.theme.primary, hotkey = keys[i]) {
|
||||||
next = lang.screen
|
picked = language
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.draw_text_centered("Esc to go back · Esc on this screen quits", themes.SCREEN_WIDTH / 2, 660, 18, raylib.Fade(themes.DARK_GRAY, 0.7))
|
ui.draw_text_centered("1-3 pick a language · Esc goes back, and quits from here", themes.SCREEN_WIDTH / 2, 660, 18, raylib.Fade(themes.DARK_GRAY, 0.8))
|
||||||
return next
|
return picked
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared layout for a language's menu screen. Returns the clicked option index, -1 for back, or nil.
|
Language_Menu_Result :: union {
|
||||||
@(private)
|
decks.Deck_Id, // start a game with this deck
|
||||||
language_menu :: proc(theme: themes.Theme, subtitle: cstring, options: []cstring) -> Maybe(int) {
|
themes.Screen, // go to another screen
|
||||||
|
}
|
||||||
|
|
||||||
|
// A language's menu: pick a deck and a difficulty
|
||||||
|
language_menu :: proc(language: decks.Language, difficulty: ^memory.Difficulty) -> Language_Menu_Result {
|
||||||
|
info := decks.LANGUAGES[language]
|
||||||
|
theme := info.theme
|
||||||
|
keys := NUMBER_KEYS
|
||||||
raylib.ClearBackground(theme.background)
|
raylib.ClearBackground(theme.background)
|
||||||
raylib.DrawRectangle(0, 0, themes.SCREEN_WIDTH, 8, theme.primary)
|
raylib.DrawRectangle(0, 0, themes.SCREEN_WIDTH, 8, theme.primary)
|
||||||
|
|
||||||
ui.draw_title(theme.name, 70, theme.primary)
|
ui.draw_title(theme.name, 50, theme.primary)
|
||||||
ui.draw_text_centered(subtitle, themes.SCREEN_WIDTH / 2, 170, themes.SUBTITLE_FONT_SIZE, theme.ink)
|
ui.draw_text_centered(info.native, themes.SCREEN_WIDTH / 2, 140, 30, theme.ink, info.symbol_style)
|
||||||
|
|
||||||
// A small fan of card backs
|
// A small fan of card backs
|
||||||
for i in 0 ..< 3 {
|
for i in 0 ..< 3 {
|
||||||
offset := f32(i - 1)
|
offset := f32(i - 1)
|
||||||
draw_card_art(theme.flag, {themes.SCREEN_WIDTH / 2 - 55 + offset * 70, 235 + abs(offset) * 10, 110, 146}, offset * 9)
|
draw_card_art(theme.flag, {themes.SCREEN_WIDTH / 2 - 50 + offset * 64, 195 + abs(offset) * 10, 100, 133}, offset * 9)
|
||||||
}
|
}
|
||||||
|
|
||||||
result: Maybe(int)
|
result: Language_Menu_Result
|
||||||
y: f32 = 440
|
|
||||||
for option, i in options {
|
// One button per deck
|
||||||
if ui.button({(themes.SCREEN_WIDTH - themes.BUTTON_WIDTH) / 2, y, themes.BUTTON_WIDTH, themes.BUTTON_HEIGHT}, option, theme.primary) {
|
deck_list := decks.decks_for(language)
|
||||||
result = i
|
button_w: f32 = 220
|
||||||
|
gap: f32 = 24
|
||||||
|
total := f32(len(deck_list)) * button_w + f32(len(deck_list) - 1) * gap
|
||||||
|
x := (themes.SCREEN_WIDTH - total) / 2
|
||||||
|
for deck, i in deck_list {
|
||||||
|
rect := raylib.Rectangle{x, 380, button_w, themes.BUTTON_HEIGHT}
|
||||||
|
if ui.button(rect, decks.DECKS[deck].name, theme.primary, hotkey = keys[i]) {
|
||||||
|
result = deck
|
||||||
}
|
}
|
||||||
y += themes.BUTTON_HEIGHT + 20
|
ui.draw_text_centered(decks.DECKS[deck].native, x + button_w / 2, 450, 22, theme.ink, info.symbol_style)
|
||||||
|
x += button_w + gap
|
||||||
}
|
}
|
||||||
if ui.button({(themes.SCREEN_WIDTH - themes.BUTTON_WIDTH) / 2, y, themes.BUTTON_WIDTH, themes.BUTTON_HEIGHT}, "Back", theme.ink) {
|
|
||||||
result = -1
|
// Difficulty selector
|
||||||
|
names := memory.DIFFICULTY_NAMES
|
||||||
|
layouts := memory.LAYOUTS
|
||||||
|
ui.draw_text_centered("Difficulty (Left / Right)", themes.SCREEN_WIDTH / 2, 500, 20, raylib.Fade(theme.ink, 0.7))
|
||||||
|
seg_w: f32 = 150
|
||||||
|
seg_x := (themes.SCREEN_WIDTH - 3 * seg_w - 2 * 12) / 2
|
||||||
|
for level in memory.Difficulty {
|
||||||
|
selected := difficulty^ == level
|
||||||
|
color := selected ? theme.primary : raylib.ColorLerp(theme.primary, themes.WHITE, 0.55)
|
||||||
|
label := raylib.TextFormat("%s %d", names[level], layouts[level].rows * layouts[level].cols / 2)
|
||||||
|
if ui.button({seg_x, 530, seg_w, 48}, label, color, font_size = 22, selected = selected) {
|
||||||
|
difficulty^ = level
|
||||||
|
}
|
||||||
|
seg_x += seg_w + 12
|
||||||
}
|
}
|
||||||
|
if raylib.IsKeyPressed(.LEFT) && difficulty^ > .EASY {
|
||||||
|
difficulty^ = memory.Difficulty(int(difficulty^) - 1)
|
||||||
|
}
|
||||||
|
if raylib.IsKeyPressed(.RIGHT) && difficulty^ < .HARD {
|
||||||
|
difficulty^ = memory.Difficulty(int(difficulty^) + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button({30, 28, 120, 48}, "Back", theme.ink, font_size = 22) {
|
||||||
|
result = themes.Screen.MAIN_MENU
|
||||||
|
}
|
||||||
|
hint := len(deck_list) > 1 ? cstring("1-2 pick a deck · Left / Right difficulty · Esc back") : cstring("1 start · Left / Right difficulty · Esc back")
|
||||||
|
ui.draw_text_centered(hint, themes.SCREEN_WIDTH / 2, 660, 18, raylib.Fade(theme.ink, 0.6))
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
japanese_menu :: proc() -> themes.Screen {
|
|
||||||
choice, clicked := language_menu(themes.JAPAN, "ひらがな · Hiragana", {"Memory Game"}).?
|
|
||||||
if !clicked { return .JAPANESE_MENU }
|
|
||||||
return choice == 0 ? .JAPANESE_MEMORY : .MAIN_MENU
|
|
||||||
}
|
|
||||||
|
|
||||||
korean_menu :: proc() -> themes.Screen {
|
|
||||||
if _, clicked := language_menu(themes.KOREA, "Hangul · coming soon", {}).?; clicked {
|
|
||||||
return .MAIN_MENU
|
|
||||||
}
|
|
||||||
return .KOREAN_MENU
|
|
||||||
}
|
|
||||||
|
|
||||||
thai_menu :: proc() -> themes.Screen {
|
|
||||||
if _, clicked := language_menu(themes.THAI, "Thai script · coming soon", {}).?; clicked {
|
|
||||||
return .MAIN_MENU
|
|
||||||
}
|
|
||||||
return .THAI_MENU
|
|
||||||
}
|
|
||||||
@@ -7,10 +7,8 @@ SCREEN_HEIGHT :: 720
|
|||||||
|
|
||||||
Screen :: enum {
|
Screen :: enum {
|
||||||
MAIN_MENU,
|
MAIN_MENU,
|
||||||
JAPANESE_MENU,
|
LANGUAGE_MENU,
|
||||||
JAPANESE_MEMORY,
|
MEMORY,
|
||||||
KOREAN_MENU,
|
|
||||||
THAI_MENU,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Which national flag the card backs are modelled on
|
// Which national flag the card backs are modelled on
|
||||||
@@ -88,5 +86,3 @@ CARD_WIDTH :: 96
|
|||||||
CARD_HEIGHT :: 128
|
CARD_HEIGHT :: 128
|
||||||
CARD_SPACING :: 14
|
CARD_SPACING :: 14
|
||||||
GRID_TOP :: 136
|
GRID_TOP :: 136
|
||||||
GRID_ROWS :: 4
|
|
||||||
GRID_COLS :: 6
|
|
||||||
+26
-15
@@ -5,6 +5,8 @@ import assets "../assets"
|
|||||||
import audio "../audio"
|
import audio "../audio"
|
||||||
import themes "../themes"
|
import themes "../themes"
|
||||||
|
|
||||||
|
Font_Style :: assets.Font_Style
|
||||||
|
|
||||||
TEXT_SPACING :: 1
|
TEXT_SPACING :: 1
|
||||||
|
|
||||||
@(private)
|
@(private)
|
||||||
@@ -25,38 +27,42 @@ set_hovered :: proc() {
|
|||||||
any_hovered = true
|
any_hovered = true
|
||||||
}
|
}
|
||||||
|
|
||||||
measure_text :: proc(text: cstring, size: f32, bold := false) -> raylib.Vector2 {
|
measure_text :: proc(text: cstring, size: f32, style: Font_Style = .REGULAR) -> raylib.Vector2 {
|
||||||
return raylib.MeasureTextEx(assets.get_font(bold), text, size, TEXT_SPACING)
|
return raylib.MeasureTextEx(assets.get_font(style), text, size, TEXT_SPACING)
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_text :: proc(text: cstring, pos: raylib.Vector2, size: f32, color: raylib.Color, bold := false) {
|
draw_text :: proc(text: cstring, pos: raylib.Vector2, size: f32, color: raylib.Color, style: Font_Style = .REGULAR) {
|
||||||
raylib.DrawTextEx(assets.get_font(bold), text, pos, size, TEXT_SPACING, color)
|
raylib.DrawTextEx(assets.get_font(style), text, pos, size, TEXT_SPACING, color)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draws text horizontally centered on center_x with its top at y
|
// Draws text horizontally centered on center_x with its top at y
|
||||||
draw_text_centered :: proc(text: cstring, center_x, y, size: f32, color: raylib.Color, bold := false, shadow := false) {
|
draw_text_centered :: proc(text: cstring, center_x, y, size: f32, color: raylib.Color,
|
||||||
w := measure_text(text, size, bold).x
|
style: Font_Style = .REGULAR, shadow := false) {
|
||||||
|
w := measure_text(text, size, style).x
|
||||||
pos := raylib.Vector2{center_x - w / 2, y}
|
pos := raylib.Vector2{center_x - w / 2, y}
|
||||||
if shadow {
|
if shadow {
|
||||||
draw_text(text, pos + {0, size * 0.05}, size, raylib.Fade(themes.BLACK, 0.18), bold)
|
draw_text(text, pos + {0, size * 0.05}, size, raylib.Fade(themes.BLACK, 0.18), style)
|
||||||
}
|
}
|
||||||
draw_text(text, pos, size, color, bold)
|
draw_text(text, pos, size, color, style)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draws text centered in a rectangle
|
// Draws text centered in a rectangle
|
||||||
draw_text_in_rect :: proc(text: cstring, rect: raylib.Rectangle, size: f32, color: raylib.Color, bold := false) {
|
draw_text_in_rect :: proc(text: cstring, rect: raylib.Rectangle, size: f32, color: raylib.Color,
|
||||||
dims := measure_text(text, size, bold)
|
style: Font_Style = .REGULAR) {
|
||||||
|
dims := measure_text(text, size, style)
|
||||||
pos := raylib.Vector2{rect.x + (rect.width - dims.x) / 2, rect.y + (rect.height - dims.y) / 2}
|
pos := raylib.Vector2{rect.x + (rect.width - dims.x) / 2, rect.y + (rect.height - dims.y) / 2}
|
||||||
draw_text(text, pos, size, color, bold)
|
draw_text(text, pos, size, color, style)
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_title :: proc(text: cstring, y: f32, color: raylib.Color, size: f32 = themes.TITLE_FONT_SIZE) {
|
draw_title :: proc(text: cstring, y: f32, color: raylib.Color, size: f32 = themes.TITLE_FONT_SIZE) {
|
||||||
draw_text_centered(text, themes.SCREEN_WIDTH / 2, y, size, color, bold = true, shadow = true)
|
draw_text_centered(text, themes.SCREEN_WIDTH / 2, y, size, color, style = .BOLD, shadow = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A rounded button with hover, press and shadow states. Returns true when clicked.
|
// A rounded button with hover, press and shadow states. Returns true when clicked
|
||||||
|
// or when its hotkey is pressed.
|
||||||
button :: proc(rect: raylib.Rectangle, text: cstring, color: raylib.Color,
|
button :: proc(rect: raylib.Rectangle, text: cstring, color: raylib.Color,
|
||||||
text_color := themes.WHITE, font_size: f32 = themes.BUTTON_FONT_SIZE) -> bool {
|
text_color := themes.WHITE, font_size: f32 = themes.BUTTON_FONT_SIZE,
|
||||||
|
hotkey := raylib.KeyboardKey.KEY_NULL, selected := false) -> bool {
|
||||||
hovered := raylib.CheckCollisionPointRec(raylib.GetMousePosition(), rect)
|
hovered := raylib.CheckCollisionPointRec(raylib.GetMousePosition(), rect)
|
||||||
pressed := hovered && raylib.IsMouseButtonDown(.LEFT)
|
pressed := hovered && raylib.IsMouseButtonDown(.LEFT)
|
||||||
if hovered { set_hovered() }
|
if hovered { set_hovered() }
|
||||||
@@ -76,9 +82,14 @@ button :: proc(rect: raylib.Rectangle, text: cstring, color: raylib.Color,
|
|||||||
raylib.DrawRectangleRounded(shadow, 0.4, 12, raylib.Fade(themes.BLACK, 0.18))
|
raylib.DrawRectangleRounded(shadow, 0.4, 12, raylib.Fade(themes.BLACK, 0.18))
|
||||||
body := raylib.Rectangle{rect.x, rect.y - lift, rect.width, rect.height}
|
body := raylib.Rectangle{rect.x, rect.y - lift, rect.width, rect.height}
|
||||||
raylib.DrawRectangleRounded(body, 0.4, 12, fill)
|
raylib.DrawRectangleRounded(body, 0.4, 12, fill)
|
||||||
draw_text_in_rect(text, body, font_size, text_color, bold = true)
|
if selected {
|
||||||
|
ring := raylib.Rectangle{body.x - 4, body.y - 4, body.width + 8, body.height + 8}
|
||||||
|
raylib.DrawRectangleRoundedLinesEx(ring, 0.4, 12, 3, color)
|
||||||
|
}
|
||||||
|
draw_text_in_rect(text, body, font_size, text_color, style = .BOLD)
|
||||||
|
|
||||||
clicked := hovered && raylib.IsMouseButtonPressed(.LEFT)
|
clicked := hovered && raylib.IsMouseButtonPressed(.LEFT)
|
||||||
|
if hotkey != .KEY_NULL && raylib.IsKeyPressed(hotkey) { clicked = true }
|
||||||
if clicked { audio.play(.CLICK) }
|
if clicked { audio.play(.CLICK) }
|
||||||
return clicked
|
return clicked
|
||||||
}
|
}
|
||||||
+128
-54
@@ -1,6 +1,8 @@
|
|||||||
package tests
|
package tests
|
||||||
|
|
||||||
import "core:testing"
|
import "core:testing"
|
||||||
|
import assets "../src/assets"
|
||||||
|
import decks "../src/decks"
|
||||||
import card "../src/card"
|
import card "../src/card"
|
||||||
import memory "../src/memory"
|
import memory "../src/memory"
|
||||||
import themes "../src/themes"
|
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
|
// Finds the card index holding the other half of card i's pair
|
||||||
@(private)
|
@(private)
|
||||||
partner_of :: proc(i: int) -> int {
|
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 }
|
if j != i && c.pair_id == memory.cards[i].pair_id { return j }
|
||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
@@ -26,76 +28,148 @@ partner_of :: proc(i: int) -> int {
|
|||||||
// Finds a card index that does not pair with card i
|
// Finds a card index that does not pair with card i
|
||||||
@(private)
|
@(private)
|
||||||
non_partner_of :: proc(i: int) -> int {
|
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 }
|
if j != i && c.pair_id != memory.cards[i].pair_id { return j }
|
||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
hiragana_table_is_complete_and_unique :: proc(t: ^testing.T) {
|
deck_sizes :: proc(t: ^testing.T) {
|
||||||
table := memory.HIRAGANA
|
testing.expect_value(t, len(decks.entries(.HIRAGANA)), 46)
|
||||||
testing.expect_value(t, len(table), 46)
|
testing.expect_value(t, len(decks.entries(.KATAKANA)), 46)
|
||||||
for a, i in table {
|
testing.expect_value(t, len(decks.entries(.HANGUL)), 24)
|
||||||
for b, j in table {
|
testing.expect_value(t, len(decks.entries(.THAI_CONSONANTS)), 42)
|
||||||
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)
|
@(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)
|
@(test)
|
||||||
deal_creates_kana_romaji_pairs :: proc(t: ^testing.T) {
|
deal_creates_symbol_reading_pairs :: proc(t: ^testing.T) {
|
||||||
memory.deal(themes.JAPAN)
|
for deck in decks.Deck_Id {
|
||||||
testing.expect_value(t, memory.state, memory.State.WAITING)
|
for difficulty in memory.Difficulty {
|
||||||
testing.expect_value(t, memory.moves, 0)
|
memory.deal(deck, difficulty)
|
||||||
testing.expect_value(t, memory.matches, 0)
|
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 {
|
for pair in 0 ..< memory.pair_count {
|
||||||
kana_count, romaji_count := 0, 0
|
symbol_count, reading_count := 0, 0
|
||||||
kana, romaji: cstring
|
symbol, reading, hint: cstring
|
||||||
for c in memory.cards {
|
for c in memory.active_cards() {
|
||||||
if c.pair_id != pair { continue }
|
if c.pair_id != pair { continue }
|
||||||
testing.expect(t, !c.face_up && !c.matched)
|
testing.expect(t, !c.face_up && !c.matched)
|
||||||
if c.is_kana {
|
if c.is_symbol {
|
||||||
kana_count += 1
|
symbol_count += 1
|
||||||
kana = c.text
|
symbol = c.text
|
||||||
} else {
|
} else {
|
||||||
romaji_count += 1
|
reading_count += 1
|
||||||
romaji = c.text
|
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)
|
@(test)
|
||||||
deal_lays_out_cards_on_screen_without_overlap :: proc(t: ^testing.T) {
|
deal_lays_out_cards_on_screen_without_overlap :: proc(t: ^testing.T) {
|
||||||
memory.deal(themes.JAPAN)
|
for difficulty in memory.Difficulty {
|
||||||
for a, i in memory.cards {
|
memory.deal(.HIRAGANA, difficulty)
|
||||||
testing.expect(t, a.rect.x >= 0 && a.rect.x + a.rect.width <= themes.SCREEN_WIDTH)
|
for a, i in memory.active_cards() {
|
||||||
testing.expect(t, a.rect.y >= themes.GRID_TOP && a.rect.y + a.rect.height <= themes.SCREEN_HEIGHT)
|
testing.expect(t, a.rect.x >= 0 && a.rect.x + a.rect.width <= themes.SCREEN_WIDTH)
|
||||||
for b, j in memory.cards {
|
testing.expect(t, a.rect.y >= themes.GRID_TOP && a.rect.y + a.rect.height <= themes.SCREEN_HEIGHT)
|
||||||
if i == j { continue }
|
for b, j in memory.active_cards() {
|
||||||
overlap := a.rect.x < b.rect.x + b.rect.width && b.rect.x < a.rect.x + a.rect.width &&
|
if i == j { continue }
|
||||||
a.rect.y < b.rect.y + b.rect.height && b.rect.y < a.rect.y + a.rect.height
|
overlap := a.rect.x < b.rect.x + b.rect.width && b.rect.x < a.rect.x + a.rect.width &&
|
||||||
testing.expectf(t, !overlap, "cards %d and %d overlap", i, j)
|
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)
|
@(test)
|
||||||
matching_pair_stays_up :: proc(t: ^testing.T) {
|
matching_pair_stays_up :: proc(t: ^testing.T) {
|
||||||
memory.deal(themes.JAPAN)
|
memory.deal(.HIRAGANA, .NORMAL)
|
||||||
a := 0
|
a := 0
|
||||||
b := partner_of(a)
|
b := partner_of(a)
|
||||||
|
|
||||||
@@ -114,7 +188,7 @@ matching_pair_stays_up :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
mismatched_pair_flips_back :: proc(t: ^testing.T) {
|
mismatched_pair_flips_back :: proc(t: ^testing.T) {
|
||||||
memory.deal(themes.JAPAN)
|
memory.deal(.HIRAGANA, .NORMAL)
|
||||||
a := 0
|
a := 0
|
||||||
b := non_partner_of(a)
|
b := non_partner_of(a)
|
||||||
|
|
||||||
@@ -130,7 +204,7 @@ mismatched_pair_flips_back :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
invalid_picks_are_rejected :: proc(t: ^testing.T) {
|
invalid_picks_are_rejected :: proc(t: ^testing.T) {
|
||||||
memory.deal(themes.JAPAN)
|
memory.deal(.HIRAGANA, .NORMAL)
|
||||||
a := 0
|
a := 0
|
||||||
testing.expect(t, memory.select_card(a))
|
testing.expect(t, memory.select_card(a))
|
||||||
testing.expect(t, !memory.select_card(a)) // same card twice
|
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(t, !memory.select_card(c)) // a third card while resolving
|
||||||
testing.expect_value(t, memory.moves, 1)
|
testing.expect_value(t, memory.moves, 1)
|
||||||
|
|
||||||
memory.deal(themes.JAPAN)
|
memory.deal(.HIRAGANA, .NORMAL)
|
||||||
p := partner_of(0)
|
p := partner_of(0)
|
||||||
memory.select_card(0)
|
memory.select_card(0)
|
||||||
memory.select_card(p)
|
memory.select_card(p)
|
||||||
@@ -152,17 +226,17 @@ invalid_picks_are_rejected :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
finding_all_pairs_wins :: proc(t: ^testing.T) {
|
finding_all_pairs_wins :: proc(t: ^testing.T) {
|
||||||
memory.deal(themes.JAPAN)
|
memory.deal(.HIRAGANA, .NORMAL)
|
||||||
for i in 0 ..< memory.CARD_COUNT {
|
for i in 0 ..< memory.card_count {
|
||||||
if memory.cards[i].matched { continue }
|
if memory.cards[i].matched { continue }
|
||||||
memory.select_card(i)
|
memory.select_card(i)
|
||||||
memory.select_card(partner_of(i))
|
memory.select_card(partner_of(i))
|
||||||
advance(2)
|
advance(2)
|
||||||
}
|
}
|
||||||
testing.expect_value(t, memory.state, memory.State.OVER)
|
testing.expect_value(t, memory.state, memory.State.OVER)
|
||||||
testing.expect_value(t, memory.matches, memory.PAIR_COUNT)
|
testing.expect_value(t, memory.matches, memory.pair_count)
|
||||||
testing.expect_value(t, memory.moves, memory.PAIR_COUNT)
|
testing.expect_value(t, memory.moves, memory.pair_count)
|
||||||
for &c in memory.cards { testing.expect(t, c.matched) }
|
for &c in memory.active_cards() { testing.expect(t, c.matched) }
|
||||||
|
|
||||||
// The clock stops once the game is over
|
// The clock stops once the game is over
|
||||||
elapsed := memory.elapsed
|
elapsed := memory.elapsed
|
||||||
|
|||||||
Reference in new issue
Block a user