Files
HanjaKanaMun/AGENTS.md
T
SansanandClaude Sonnet 5 ad7d19b85a
Build and Test / build (push) Successful in 22s
Build and Test / windows (push) Successful in 1m8s
Embed fonts into the executable, ship release artifacts as bare binaries
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>
2026-10-03 20:11:47 +02:00

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)
  • .odin files tracked in git (.gitignore used to ignore *.odin)
  • Fonts: M PLUS Rounded 1c (UI + kana), Jua (Hangul), Mali (Thai), all OFL, embedded via #load and loaded with LoadFontFromMemory + 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

  1. Font glyphs must be listed: raylib only bakes the codepoints passed to LoadFontFromMemory. 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.
  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).
  5. 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.
  6. Odin constant arrays can't be indexed at runtime: lookup tables (DECKS, LAYOUTS, …) are @(rodata) globals instead of :: constants.
  7. Profile memory: profile always allocates from the heap allocator, because the test runner frees each test's allocator afterwards.
  8. odin test segfaults if the stack rlimit is "unlimited": the CI runner's container sets ulimit -s unlimited, which makes getrlimit(RLIMIT_STACK).rlim_cur come back as RLIM_INFINITY (a huge unsigned value). Odin's core:thread passes that straight to pthread_attr_setstacksize, pthread_create then fails, and thread.create() returns nil - which core:testing's pool_init dereferences unconditionally, segfaulting the whole test binary (exit 139) before it prints anything. just test works around it with ulimit -s 8192 on the same line as odin test (just runs 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.
  9. CI jobs share one self-hosted runner: build and windows in gitea-build.yaml both clone actions/checkout/actions/cache into the same cache directory on the runner. Starting them at the same time can race on that clone and fail one job outright, so windows runs with needs: build.
  10. 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, then just run)
  • Keep existing functionality working
  • Commit after each working step