Fonts are now baked in with #load and loaded via LoadFontFromMemory instead of LoadFontEx reading assets/*.ttf from disk, so a build no longer needs an assets/ folder next to it at runtime - the chdir to the executable's own directory in main.odin is dead code, removed. just package / just package-windows now produce a single executable (build/<name>_<version>_<os>_<arch>[.exe]) instead of a tar.gz/zip, and the release workflow's upload/release-asset globs and the lld/zip toolchain package list are updated to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
6.8 KiB
6.8 KiB
HanjaKanaMun - Project Plan
Overview
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>, just package-windows <version>
Current Status
Completed
- Code split into modules under
src/(themes, assets, audio, ui, card, decks, memory, profile, screens) .odinfiles tracked in git (.gitignoreused to ignore*.odin)- Fonts: M PLUS Rounded 1c (UI + kana), Jua (Hangul), Mali (Thai), all OFL, embedded via
#loadand loaded withLoadFontFromMemory+ explicit codepoints - release builds are a single self-contained executable - Per-language themes (
themes.Theme) with flag-based card backs: Japan (Hinomaru), Korea (Taegukgi), Thailand (Trairong) - Card flip animation (art rendered to textures and squashed), lift, shadow, lighting; matched cards pulse; cards are dealt in at game start
- Sound effects synthesized in code (flip, match, mismatch, win, button click), no audio files needed
- Decks: Hiragana (46), Katakana (46), Hangul basic jamo (24), Thai consonants (42); symbol ↔ reading pairs
- Difficulty levels: Easy 3x4 (6 pairs), Normal 4x6 (12), Hard 4x8 (16)
- Keyboard play: arrows / WASD / hjkl move focus, Enter/Space flips; number keys pick menu entries; Esc goes back (quits from main menu)
- High scores: best moves, best time and games played per deck + difficulty, plus last difficulty, saved as JSON in the user data folder
- Screen fade-in transitions
- Win panel with best score / "New best!", Play Again (Enter) / Menu
- Tooling: justfile, ols/odinfmt config for neovim, tests, Gitea CI (build/test, Windows cross-build, tagged releases)
Ideas / Next
- Hangul syllable blocks deck (e.g. 가 ga, 나 na), Thai vowels (needs care: combining marks)
- Hiragana ↔ katakana pairing mode, dakuten (が, ぱ…) deck
- Settings: sound on/off, volume
- Test the Windows build on a real machine
- macOS release build (would need a macOS runner or osxcross)
Known Issues / Gotchas
- Font glyphs must be listed: raylib only bakes the codepoints passed to
LoadFontFromMemory. Any new non-ASCII text needs its characters added inassets.codepoints_for(or theEXTRA_*strings), otherwise it renders as?. Thefonts_cover_every_deck_charactertest checks deck text. - Old font failure root cause:
DroidSansJapanese.ttfwas a GitHub HTML page saved from a blob URL, not a font. Always download fonts from raw URLs and check them withfile. - 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).
- Tests share the memory game's global state, so they must run single-threaded (
just testsetsODIN_TEST_THREADS=1). - Windows builds are cross-compiled: Odin can't link Windows binaries on Linux, so
just windowsbuilds objects with-build-mode:objand links withlld-linkagainst 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.libforos.user_data_dir); the CIwindowsjob catches this. Not yet tested on a real Windows machine. - Odin constant arrays can't be indexed at runtime: lookup tables (
DECKS,LAYOUTS, …) are@(rodata)globals instead of::constants. - Profile memory:
profilealways allocates from the heap allocator, because the test runner frees each test's allocator afterwards. odin testsegfaults if the stack rlimit is "unlimited": the CI runner's container setsulimit -s unlimited, which makesgetrlimit(RLIMIT_STACK).rlim_curcome back asRLIM_INFINITY(a huge unsigned value). Odin'score:threadpasses that straight topthread_attr_setstacksize,pthread_createthen fails, andthread.create()returnsnil- whichcore:testing'spool_initdereferences unconditionally, segfaulting the whole test binary (exit 139) before it prints anything.just testworks around it withulimit -s 8192on the same line asodin test(justruns each recipe line in its own shell, so a separate line wouldn't stick). Upstream Odin bug (core:thread/core:testing don't handle a failed thread creation); not our code.- CI jobs share one self-hosted runner:
buildandwindowsingitea-build.yamlboth cloneactions/checkout/actions/cacheinto the same cache directory on the runner. Starting them at the same time can race on that clone and fail one job outright, sowindowsruns withneeds: build. - Release artifacts are bare executables, not archives: fonts are embedded in the binary (see gotcha 1), so there's nothing else to ship alongside it.
File Structure
HanjaKanaMun/
├── AGENTS.md # Project plan (this file)
├── README.md
├── justfile # build/run/check/test/package/windows recipes
├── ols.json # ols language server settings (neovim)
├── odinfmt.json # odinfmt formatter settings (4 spaces, LF)
├── .gitea/
│ ├── actions/setup/ # shared CI toolchain setup (apt, Odin, just)
│ └── workflows/
│ ├── 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
├── main.odin # Window/audio setup, screen state machine, transitions
├── src/
│ ├── themes/themes.odin # Theme struct, flag colors, Screen/Flag enums, layout constants
│ ├── assets/assets.odin # Font styles and codepoint lists, font loading
│ ├── audio/audio.odin # Synthesized sound effects, play()
│ ├── ui/ui.odin # Text helpers, buttons (with hotkeys), hover cursor
│ ├── card/card.odin # Card struct, flag card-back art, face textures, flip/deal animation
│ ├── 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
├── tests/
│ ├── memory_test.odin # Decks, font coverage, game rules, layout, keyboard focus, card animation
│ └── profile_test.odin # Score records, JSON round trip, win recording
└── assets/ # Fonts (+ OFL license files)
Notes
- NO rm -rf or starting over
- Make small, incremental changes
- Test after each change (
just check,just test, thenjust run) - Keep existing functionality working
- Commit after each working step