Windows build links ole32; CI cross-builds Windows on main/develop; docs
Build and Test / build (push) Failing after 23s
Build and Test / windows (push) Successful in 11m6s

- os.user_data_dir needs ole32.lib on Windows
- Build workflow gets a Windows cross-build job with a cached SDK
- Drop the backtick from Jua's codepoints (the font lacks it)
- Update AGENTS.md and README for decks, controls, scores and layout

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
SansanandClaude Opus 5.5 committed 2026-10-01 21:38:07 +02:00
1 parent 3794a9e2e9
commit 26904b6852
5 files changed
+94 -36

No files matched your search

+31 -1
View File
@@ -1,7 +1,8 @@
# #
# .gitea/workflows/gitea-build.yaml # .gitea/workflows/gitea-build.yaml
# #
# Checks, tests and builds on every push and pull request to main and develop. # Checks, tests and builds (Linux, plus a Windows cross-build) on every push and
# pull request to main and develop.
# #
name: "Build and Test" name: "Build and Test"
@@ -31,3 +32,32 @@ jobs:
- name: Build - name: Build
run: just release run: just release
# Cross-compiles for Windows so link breakages show up before a release
windows:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Toolchain
uses: ./.gitea/actions/setup
with:
packages: lld zip
- name: Cache Windows SDK
uses: actions/cache@v4
with:
path: |
build/xwin
build/xwin-sdk
build/raylib-windows
key: windows-sdk-${{ hashFiles('justfile') }}
- name: Windows Build
run: |
# Debian/Ubuntu may only ship a versioned lld-link
LLD_LINK="$(command -v lld-link || ls /usr/lib/llvm-*/bin/lld-link | sort -V | tail -1)"
export LLD_LINK
just windows-deps
just windows
+35 -31
View File
@@ -3,66 +3,70 @@
## Overview ## Overview
Odin + raylib multilingual memory card game for learning Japanese (Hiragana/Katakana), Korean (Hangul), and Thai scripts. Odin + raylib multilingual memory card game for learning Japanese (Hiragana/Katakana), Korean (Hangul), and Thai scripts.
Commands (see `justfile`): `just run`, `just check`, `just test`, `just package <version>` Commands (see `justfile`): `just run`, `just check`, `just test`, `just package <version>`, `just package-windows <version>`
## Current Status ## Current Status
### Completed ### Completed
- [x] Code split into modules under `src/` (themes, assets, audio, ui, card, memory, screens) - [x] Code split into modules under `src/` (themes, assets, audio, ui, card, decks, memory, profile, screens)
- [x] `.odin` files tracked in git (`.gitignore` used to ignore `*.odin`) - [x] `.odin` files tracked in git (`.gitignore` used to ignore `*.odin`)
- [x] Real font: M PLUS Rounded 1c (Medium + ExtraBold, OFL) loaded via `LoadFontEx` with kana codepoints - [x] Fonts: M PLUS Rounded 1c (UI + kana), Jua (Hangul), Mali (Thai), all OFL, loaded via `LoadFontEx` with explicit codepoints
- [x] Per-language themes (`themes.Theme`) with flag-based card backs: Japan (Hinomaru), Korea (Taegukgi), Thailand (Trairong) - [x] Per-language themes (`themes.Theme`) with flag-based card backs: Japan (Hinomaru), Korea (Taegukgi), Thailand (Trairong)
- [x] Card flip animation: card art is rendered to textures and squashed horizontally, with lift, shadow and lighting; matched cards pulse - [x] Card flip animation (art rendered to textures and squashed), lift, shadow, lighting; matched cards pulse; cards are dealt in at game start
- [x] Sound effects synthesized in code (flip, match, mismatch, win, button click), no audio files needed - [x] Sound effects synthesized in code (flip, match, mismatch, win, button click), no audio files needed
- [x] Japanese memory game: kana ↔ romaji pairs, 12 pairs picked at random from the 46 basic hiragana - [x] Decks: Hiragana (46), Katakana (46), Hangul basic jamo (24), Thai consonants (42); symbol ↔ reading pairs
- [x] ESC goes back one screen (quits from the main menu); no longer closes the app everywhere - [x] Difficulty levels: Easy 3x4 (6 pairs), Normal 4x6 (12), Hard 4x8 (16)
- [x] Memory layout fixed (header and Back button no longer overlap cards), real shuffle, moves counted per pair - [x] Keyboard play: arrows / WASD / hjkl move focus, Enter/Space flips; number keys pick menu entries; Esc goes back (quits from main menu)
- [x] Win panel with Play Again / Menu - [x] High scores: best moves, best time and games played per deck + difficulty, plus last difficulty, saved as JSON in the user data folder
- [x] Screen fade-in transitions
- [x] Win panel with best score / "New best!", Play Again (Enter) / Menu
- [x] Tooling: justfile, ols/odinfmt config for neovim, tests, Gitea CI (build/test, Windows cross-build, tagged releases)
### Pending ### Ideas / Next
- [ ] Keyboard support for card selection (1-9, A-L, etc.) - [ ] Hangul syllable blocks deck (e.g. 가 ga, 나 na), Thai vowels (needs care: combining marks)
- [ ] Korean memory game (needs a Hangul font, e.g. Noto Sans KR; theme and card back already exist) - [ ] Hiragana ↔ katakana pairing mode, dakuten (が, ぱ…) deck
- [ ] Thai memory game (needs e.g. Noto Sans Thai; theme and card back already exist) - [ ] Settings: sound on/off, volume
- [ ] Katakana mode - [ ] Test the Windows build on a real machine
- [ ] Screen transitions - [ ] macOS release build (would need a macOS runner or osxcross)
- [ ] Score tracking and high scores
- [ ] Difficulty levels (number of pairs)
## Known Issues / Gotchas ## Known Issues / Gotchas
1. **Font glyphs must be listed**: raylib only bakes the codepoints passed to `LoadFontEx`. Any new non-ASCII text needs its characters added in `assets.build_codepoints`, otherwise it renders as `?`. 1. **Font glyphs must be listed**: raylib only bakes the codepoints passed to `LoadFontEx`. Any new non-ASCII text needs its characters added in `assets.codepoints_for` (or the `EXTRA_*` strings), otherwise it renders as `?`. The `fonts_cover_every_deck_character` test checks deck text.
2. **Old font failure root cause**: `DroidSansJapanese.ttf` was a GitHub HTML page saved from a blob URL, not a font. Always download fonts from raw URLs and check them with `file`. 2. **Old font failure root cause**: `DroidSansJapanese.ttf` was a GitHub HTML page saved from a blob URL, not a font. Always download fonts from raw URLs and check them with `file`.
3. **Thai shaping**: raylib does no text shaping, so Thai combining vowels/tone marks may be misplaced. Prefer standalone consonants on Thai cards. 3. **Thai shaping**: raylib does no text shaping, so Thai combining vowels/tone marks would be misplaced. Thai cards use standalone consonants; Thai UI text avoids marks (win message is English).
4. Tests share the memory game's global state, so they must run single-threaded (`just test` sets `ODIN_TEST_THREADS=1`). 4. Tests share the memory game's global state, so they must run single-threaded (`just test` sets `ODIN_TEST_THREADS=1`).
5. The game changes directory to the executable's folder at startup so `assets/` is found from anywhere. 5. The game changes directory to the executable's folder at startup so `assets/` is found from anywhere.
6. **Windows builds are cross-compiled**: Odin can't link Windows binaries on Linux, so `just windows` builds objects with `-build-mode:obj` and links with `lld-link` against an xwin SDK. The VC runtime is linked statically; only Windows system DLLs are needed. Not yet tested on a real Windows machine. 6. **Windows builds are cross-compiled**: Odin can't link Windows binaries on Linux, so `just windows` builds objects with `-build-mode:obj` and links with `lld-link` against an xwin SDK. The VC runtime is linked statically; only Windows system DLLs are needed. New Odin core usage can need new import libs (e.g. `ole32.lib` for `os.user_data_dir`); the CI `windows` job catches this. Not yet tested on a real Windows machine.
7. **Odin constant arrays can't be indexed at runtime**: lookup tables (`DECKS`, `LAYOUTS`, …) are `@(rodata)` globals instead of `::` constants.
8. **Profile memory**: `profile` always allocates from the heap allocator, because the test runner frees each test's allocator afterwards.
## File Structure ## File Structure
``` ```
HanjaKanaMun/ HanjaKanaMun/
├── AGENTS.md # Project plan (this file) ├── AGENTS.md # Project plan (this file)
├── README.md ├── README.md
├── justfile # build/run/check/test/package recipes ├── justfile # build/run/check/test/package/windows recipes
├── ols.json # ols language server settings (neovim) ├── ols.json # ols language server settings (neovim)
├── odinfmt.json # odinfmt formatter settings (4 spaces, LF) ├── odinfmt.json # odinfmt formatter settings (4 spaces, LF)
├── .gitea/ ├── .gitea/
│ ├── actions/setup/ # shared CI toolchain setup (apt, Odin, just) │ ├── actions/setup/ # shared CI toolchain setup (apt, Odin, just)
│ └── workflows/ │ └── workflows/
│ ├── gitea-build.yaml # check + test + build on main/develop │ ├── gitea-build.yaml # check + test + build, and Windows cross-build, on main/develop
│ └── gitea-release.yaml # on tags v*: Linux + Windows packages, package registry upload, Gitea release │ └── gitea-release.yaml # on tags v*: Linux + Windows packages, package registry upload, Gitea release
├── main.odin # Window/audio setup, screen state machine ├── main.odin # Window/audio setup, screen state machine, transitions
├── src/ ├── src/
│ ├── themes/themes.odin # Theme struct, flag colors, Screen/Flag enums, layout constants │ ├── themes/themes.odin # Theme struct, flag colors, Screen/Flag enums, layout constants
│ ├── assets/assets.odin # Font loading (codepoints), get_font │ ├── assets/assets.odin # Font styles and codepoint lists, font loading
│ ├── audio/audio.odin # Synthesized sound effects, play() │ ├── audio/audio.odin # Synthesized sound effects, play()
│ ├── ui/ui.odin # Text helpers, buttons, hover cursor │ ├── ui/ui.odin # Text helpers, buttons (with hotkeys), hover cursor
│ ├── card/card.odin # Card struct, flag card-back art, face textures, flip animation │ ├── card/card.odin # Card struct, flag card-back art, face textures, flip/deal animation
│ ├── memory/memory.odin # Memory game state, logic and drawing │ ├── decks/decks.odin # Languages, decks and their entries (symbol, reading, hint)
│ ├── memory/memory.odin # Memory game state, logic, keyboard focus, drawing
│ ├── profile/profile.odin # Best scores and settings, JSON in the user data folder
│ └── screens/screens.odin # Main menu and language menus │ └── screens/screens.odin # Main menu and language menus
├── tests/memory_test.odin # Game logic and card animation tests ├── tests/
└── assets/ │ ├── memory_test.odin # Decks, font coverage, game rules, layout, keyboard focus, card animation
├── MPLUSRounded1c-Medium.ttf │ └── profile_test.odin # Score records, JSON round trip, win recording
├── MPLUSRounded1c-ExtraBold.ttf └── assets/ # Fonts (+ OFL license files)
└── MPLUSRounded1c-OFL.txt
``` ```
## Notes ## Notes
+22 -2
View File
@@ -2,6 +2,24 @@
Odin + raylib memory card game for learning Japanese, Korean and Thai scripts. Odin + raylib memory card game for learning Japanese, Korean and Thai scripts.
Match each symbol with its reading: hiragana and katakana with romaji, Hangul letters with their
Revised Romanization, and Thai consonants with their sound and name word. Three difficulties
(6, 12 or 16 pairs), best scores per deck, and a card deck themed on each country's flag.
## Controls
| | |
|---|---|
| Mouse | Click cards and buttons |
| Arrows / WASD / hjkl | Move between cards |
| Enter / Space | Flip the selected card (Enter also plays again) |
| 1-3 | Pick a language or deck in menus |
| Left / Right | Change difficulty |
| Esc | Back (quits from the main menu) |
Scores are saved to `profile.json` in the user data folder (`~/Library/Application Support/HanjaKanaMun`
on macOS, `~/.local/share/HanjaKanaMun` on Linux, `%APPDATA%\HanjaKanaMun` on Windows).
## Development ## Development
Requires [Odin](https://odin-lang.org) (dev-2026-09) and [just](https://just.systems). Requires [Odin](https://odin-lang.org) (dev-2026-09) and [just](https://just.systems).
@@ -27,7 +45,8 @@ Formatting goes through ols (`vim.lsp.buf.format()`), or `just fmt` if `odinfmt`
## CI ## CI
- `.gitea/workflows/gitea-build.yaml`: check, test and build on pushes and PRs to `main` and `develop`. - `.gitea/workflows/gitea-build.yaml`: check, test and build on pushes and PRs to `main` and `develop`, plus a
Windows cross-build so link problems show up before a release.
- `.gitea/workflows/gitea-release.yaml`: on a version tag like `v1.0`, builds Linux and Windows packages, uploads them - `.gitea/workflows/gitea-release.yaml`: on a version tag like `v1.0`, builds Linux and Windows packages, uploads them
to the Gitea generic package registry (`HanjaKanaMun/1.0`) and creates a Gitea release with both attached. to the Gitea generic package registry (`HanjaKanaMun/1.0`) and creates a Gitea release with both attached.
Needs a `TOKEN` secret with package write access. Needs a `TOKEN` secret with package write access.
@@ -37,4 +56,5 @@ Release: `git tag v1.0 && git push origin v1.0`
## Credits ## Credits
Font: M PLUS Rounded 1c, SIL Open Font License (`assets/MPLUSRounded1c-OFL.txt`). Fonts, all under the SIL Open Font License (license files in `assets/`):
M PLUS Rounded 1c (UI and kana), Jua (Hangul), Mali (Thai).
+1 -1
View File
@@ -81,7 +81,7 @@ windows:
/nodefaultlib:msvcrt /nodefaultlib:vcruntime /nodefaultlib:libucrt \ /nodefaultlib:msvcrt /nodefaultlib:vcruntime /nodefaultlib:libucrt \
libcmt.lib libvcruntime.lib ucrt.lib \ libcmt.lib libvcruntime.lib ucrt.lib \
kernel32.lib user32.lib gdi32.lib winmm.lib shell32.lib opengl32.lib \ kernel32.lib user32.lib gdi32.lib winmm.lib shell32.lib opengl32.lib \
advapi32.lib bcrypt.lib ntdll.lib synchronization.lib advapi32.lib bcrypt.lib ntdll.lib synchronization.lib ole32.lib
cp -R assets "$out_dir/" cp -R assets "$out_dir/"
# Windows build packed as build/<name>_<version>_windows_x86_64.zip # Windows build packed as build/<name>_<version>_windows_x86_64.zip
+5 -1
View File
@@ -31,7 +31,11 @@ EXTRA_THAI :: "ไทย"
// Tests check that every deck's characters are covered. // Tests check that every deck's characters are covered.
codepoints_for :: proc(style: Font_Style, allocator := context.allocator) -> [dynamic]rune { codepoints_for :: proc(style: Font_Style, allocator := context.allocator) -> [dynamic]rune {
codepoints := make([dynamic]rune, allocator) codepoints := make([dynamic]rune, allocator)
for r in rune(0x20) ..= 0x7E { append(&codepoints, r) } // ASCII for r in rune(0x20) ..= 0x7E {
// Jua has no backtick; Korean text never uses it
if style == .KOREAN && r == '`' { continue }
append(&codepoints, r) // ASCII
}
switch style { switch style {
case .REGULAR, .BOLD: case .REGULAR, .BOLD:
for r in rune(0x3041) ..= 0x3096 { append(&codepoints, r) } // Hiragana for r in rune(0x3041) ..= 0x3096 { append(&codepoints, r) } // Hiragana