Add justfile, ols/odinfmt config, tests and Gitea workflows; drop odin.json
- justfile with build/run/check/test/release/package/fmt/clean recipes - ols.json and odinfmt.json for neovim (ols) - Tests for memory game rules and card animation; memory.deal and memory.select_card split out so logic is testable without a window - Gitea workflows: build+test on main/develop, package and upload to the generic package registry on version branches - Game changes to the executable's directory so packaged builds find assets Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
148570a4e6
commit
9ee2eabe9a
12 files changed
+428
-23
No files matched your search
@@ -0,0 +1,53 @@
|
|||||||
|
#
|
||||||
|
# .gitea/workflows/gitea-build.yaml
|
||||||
|
#
|
||||||
|
# Builds and tests on every push and pull request to main and develop.
|
||||||
|
#
|
||||||
|
|
||||||
|
name: "Build and Test"
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [main, develop]
|
||||||
|
|
||||||
|
env:
|
||||||
|
ODIN_VERSION: dev-2026-09
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install System Dependencies
|
||||||
|
run: |
|
||||||
|
SUDO=""; command -v sudo >/dev/null && SUDO=sudo
|
||||||
|
$SUDO apt-get update
|
||||||
|
# clang links Odin programs; raylib needs X11 on Linux
|
||||||
|
$SUDO apt-get install -y --no-install-recommends clang libx11-dev curl ca-certificates
|
||||||
|
|
||||||
|
- name: Install Odin
|
||||||
|
run: |
|
||||||
|
curl -fsSL -o odin.tar.gz https://github.com/odin-lang/Odin/releases/download/$ODIN_VERSION/odin-linux-amd64-$ODIN_VERSION.tar.gz
|
||||||
|
mkdir -p "$HOME/odin"
|
||||||
|
tar -xzf odin.tar.gz -C "$HOME/odin" --strip-components=1
|
||||||
|
echo "$HOME/odin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Install just
|
||||||
|
run: |
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to "$HOME/.local/bin"
|
||||||
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Check
|
||||||
|
run: |
|
||||||
|
odin version
|
||||||
|
just check
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: just test
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: just release
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#
|
||||||
|
# .gitea/workflows/gitea-release.yaml
|
||||||
|
#
|
||||||
|
# Builds a release package on version branches (e.g. 1.0) and uploads it to
|
||||||
|
# the Gitea generic package registry.
|
||||||
|
#
|
||||||
|
|
||||||
|
name: "Publish"
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '*.*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
ODIN_VERSION: dev-2026-09
|
||||||
|
PROJECT_NAME: HanjaKanaMun
|
||||||
|
GAME_VERSION: ${{gitea.ref_name}}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install System Dependencies
|
||||||
|
run: |
|
||||||
|
SUDO=""; command -v sudo >/dev/null && SUDO=sudo
|
||||||
|
$SUDO apt-get update
|
||||||
|
# clang links Odin programs; raylib needs X11 on Linux
|
||||||
|
$SUDO apt-get install -y --no-install-recommends clang libx11-dev curl ca-certificates
|
||||||
|
|
||||||
|
- name: Install Odin
|
||||||
|
run: |
|
||||||
|
curl -fsSL -o odin.tar.gz https://github.com/odin-lang/Odin/releases/download/$ODIN_VERSION/odin-linux-amd64-$ODIN_VERSION.tar.gz
|
||||||
|
mkdir -p "$HOME/odin"
|
||||||
|
tar -xzf odin.tar.gz -C "$HOME/odin" --strip-components=1
|
||||||
|
echo "$HOME/odin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Install just
|
||||||
|
run: |
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to "$HOME/.local/bin"
|
||||||
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: just test
|
||||||
|
|
||||||
|
- name: Linux Build
|
||||||
|
run: just package "$GAME_VERSION"
|
||||||
|
|
||||||
|
- name: Upload Artifact Linux
|
||||||
|
run: |
|
||||||
|
ls -la build
|
||||||
|
FILE="${PROJECT_NAME}_${GAME_VERSION}_linux_x86_64.tar.gz"
|
||||||
|
curl --fail --user ${{ github.repository_owner }}:${{ secrets.TOKEN }} \
|
||||||
|
--upload-file "build/$FILE" \
|
||||||
|
https://gitea.sanplex.xyz/api/packages/sansan/generic/$PROJECT_NAME/$GAME_VERSION/$FILE
|
||||||
@@ -16,3 +16,6 @@ HanjaKanaMun
|
|||||||
.vscode/
|
.vscode/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
build/
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
## 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.
|
||||||
|
|
||||||
Build and run: `odin run .` (check with `odin check . -vet`)
|
Commands (see `justfile`): `just run`, `just check`, `just test`, `just package <version>`
|
||||||
|
|
||||||
## Current Status
|
## Current Status
|
||||||
|
|
||||||
@@ -32,14 +32,20 @@ Build and run: `odin run .` (check with `odin check . -vet`)
|
|||||||
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.build_codepoints`, otherwise it renders as `?`.
|
||||||
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 may be misplaced. Prefer standalone consonants on Thai cards.
|
||||||
4. `odin.json` is not read by the Odin compiler (imports use relative paths); it is only informational.
|
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.
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
```
|
```
|
||||||
HanjaKanaMun/
|
HanjaKanaMun/
|
||||||
├── AGENTS.md # Project plan (this file)
|
├── AGENTS.md # Project plan (this file)
|
||||||
├── README.md
|
├── README.md
|
||||||
├── odin.json # Informational only, not used by the compiler
|
├── justfile # build/run/check/test/package recipes
|
||||||
|
├── ols.json # ols language server settings (neovim)
|
||||||
|
├── odinfmt.json # odinfmt formatter settings (4 spaces, LF)
|
||||||
|
├── .gitea/workflows/
|
||||||
|
│ ├── gitea-build.yaml # check + test + build on main/develop
|
||||||
|
│ └── gitea-release.yaml # package on version branches (*.*), upload to Gitea packages
|
||||||
├── main.odin # Window/audio setup, screen state machine
|
├── main.odin # Window/audio setup, screen state machine
|
||||||
├── 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
|
||||||
@@ -49,6 +55,7 @@ HanjaKanaMun/
|
|||||||
│ ├── 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 animation
|
||||||
│ ├── memory/memory.odin # Memory game state, logic and drawing
|
│ ├── memory/memory.odin # Memory game state, logic and drawing
|
||||||
│ └── 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
|
||||||
└── assets/
|
└── assets/
|
||||||
├── MPLUSRounded1c-Medium.ttf
|
├── MPLUSRounded1c-Medium.ttf
|
||||||
├── MPLUSRounded1c-ExtraBold.ttf
|
├── MPLUSRounded1c-ExtraBold.ttf
|
||||||
@@ -58,6 +65,6 @@ HanjaKanaMun/
|
|||||||
## Notes
|
## Notes
|
||||||
- NO rm -rf or starting over
|
- NO rm -rf or starting over
|
||||||
- Make small, incremental changes
|
- Make small, incremental changes
|
||||||
- Test after each change (`odin check . -vet`, then run)
|
- Test after each change (`just check`, `just test`, then `just run`)
|
||||||
- Keep existing functionality working
|
- Keep existing functionality working
|
||||||
- Commit after each working step
|
- Commit after each working step
|
||||||
@@ -1,3 +1,30 @@
|
|||||||
# HanjaKanaMun
|
# HanjaKanaMun
|
||||||
|
|
||||||
Odin + raylib multilingual application (Thai, Korean, Japanese)
|
Odin + raylib memory card game for learning Japanese, Korean and Thai scripts.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Requires [Odin](https://odin-lang.org) (dev-2026-09) and [just](https://just.systems).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
just run # build and play (debug)
|
||||||
|
just check # type-check with -vet
|
||||||
|
just test # run tests
|
||||||
|
just package 1.0 # release build -> build/HanjaKanaMun_1.0_<os>_<arch>.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
### Neovim
|
||||||
|
|
||||||
|
`ols.json` and `odinfmt.json` configure the [ols](https://github.com/DanielGavin/ols) language server and formatter.
|
||||||
|
Install ols (e.g. `:MasonInstall ols`) and enable it with nvim-lspconfig: `vim.lsp.enable('ols')`.
|
||||||
|
Formatting goes through ols (`vim.lsp.buf.format()`), or `just fmt` if `odinfmt` is on your PATH.
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
- `.gitea/workflows/gitea-build.yaml`: check, test and build on pushes and PRs to `main` and `develop`.
|
||||||
|
- `.gitea/workflows/gitea-release.yaml`: on version branches like `1.0`, packages the Linux build and uploads it to
|
||||||
|
the Gitea generic package registry (`HanjaKanaMun/<version>`). Needs a `TOKEN` secret with package write access.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
Font: M PLUS Rounded 1c, SIL Open Font License (`assets/MPLUSRounded1c-OFL.txt`).
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
name := "HanjaKanaMun"
|
||||||
|
build_dir := "build"
|
||||||
|
|
||||||
|
# List available recipes
|
||||||
|
default:
|
||||||
|
@just --list
|
||||||
|
|
||||||
|
# Debug build
|
||||||
|
build:
|
||||||
|
odin build . -vet -debug -out:{{name}}
|
||||||
|
|
||||||
|
# Build and run (debug)
|
||||||
|
run:
|
||||||
|
odin run . -vet -debug -out:{{name}}
|
||||||
|
|
||||||
|
# Type-check with vet, no binary
|
||||||
|
check:
|
||||||
|
odin check . -vet
|
||||||
|
|
||||||
|
# Run the test suite (single-threaded: the tests share the memory game's global state)
|
||||||
|
test:
|
||||||
|
mkdir -p {{build_dir}}
|
||||||
|
odin test tests -vet -define:ODIN_TEST_THREADS=1 -out:{{build_dir}}/tests
|
||||||
|
|
||||||
|
# Optimized build into build/, with the assets it needs at runtime
|
||||||
|
release:
|
||||||
|
mkdir -p {{build_dir}}/{{name}}
|
||||||
|
odin build . -vet -o:speed -out:{{build_dir}}/{{name}}/{{name}}
|
||||||
|
cp -R assets {{build_dir}}/{{name}}/
|
||||||
|
|
||||||
|
# Release build packed as build/<name>_<version>_<os>_<arch>.tar.gz
|
||||||
|
package version="dev": release
|
||||||
|
tar -czf {{build_dir}}/{{name}}_{{version}}_{{os()}}_{{arch()}}.tar.gz -C {{build_dir}} {{name}}
|
||||||
|
@echo "{{build_dir}}/{{name}}_{{version}}_{{os()}}_{{arch()}}.tar.gz"
|
||||||
|
|
||||||
|
# Format all Odin files with odinfmt (settings in odinfmt.json)
|
||||||
|
fmt:
|
||||||
|
odinfmt -w .
|
||||||
|
|
||||||
|
# Remove build output
|
||||||
|
clean:
|
||||||
|
if [ -d {{build_dir}} ]; then rm -r -- {{build_dir}}; fi
|
||||||
|
rm -f -- {{name}}
|
||||||
@@ -24,6 +24,8 @@ main :: proc() {
|
|||||||
raylib.InitWindow(themes.SCREEN_WIDTH, themes.SCREEN_HEIGHT, "HanjaKanaMun")
|
raylib.InitWindow(themes.SCREEN_WIDTH, themes.SCREEN_HEIGHT, "HanjaKanaMun")
|
||||||
defer raylib.CloseWindow()
|
defer raylib.CloseWindow()
|
||||||
raylib.SetExitKey(.KEY_NULL)
|
raylib.SetExitKey(.KEY_NULL)
|
||||||
|
// Assets are loaded relative to the executable, so it can be started from anywhere
|
||||||
|
raylib.ChangeDirectory(raylib.GetApplicationDirectory())
|
||||||
raylib.SetTargetFPS(60)
|
raylib.SetTargetFPS(60)
|
||||||
|
|
||||||
raylib.InitAudioDevice()
|
raylib.InitAudioDevice()
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "HanjaKanaMun",
|
|
||||||
"path": ".",
|
|
||||||
"source_dir": ".",
|
|
||||||
"include_dirs": ["vendor", "src"],
|
|
||||||
"rays": [
|
|
||||||
{
|
|
||||||
"name": "raylib",
|
|
||||||
"path": "vendor:raylib"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/odinfmt.schema.json",
|
||||||
|
"character_width": 120,
|
||||||
|
"tabs": false,
|
||||||
|
"spaces": 4,
|
||||||
|
"newline_style": "LF"
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/ols.schema.json",
|
||||||
|
"enable_semantic_tokens": true,
|
||||||
|
"enable_document_symbols": true,
|
||||||
|
"enable_hover": true,
|
||||||
|
"enable_snippets": true,
|
||||||
|
"enable_references": true,
|
||||||
|
"enable_format": true,
|
||||||
|
"enable_inlay_hints_params": true,
|
||||||
|
"enable_inlay_hints_default_params": true,
|
||||||
|
"enable_unused_imports_reporting": true,
|
||||||
|
"checker_args": "-vet"
|
||||||
|
}
|
||||||
+12
-6
@@ -51,7 +51,14 @@ resolve_timer: f32
|
|||||||
win_t: f32 // fades in the win panel
|
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.
|
||||||
start :: proc(game_theme: themes.Theme) {
|
start :: proc(game_theme: themes.Theme) {
|
||||||
|
deal(game_theme)
|
||||||
|
for &c in cards { card.build_face(&c, theme) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffles, lays out and resets the game state without touching the GPU
|
||||||
|
deal :: proc(game_theme: themes.Theme) {
|
||||||
theme = game_theme
|
theme = game_theme
|
||||||
|
|
||||||
deck := HIRAGANA
|
deck := HIRAGANA
|
||||||
@@ -75,7 +82,6 @@ start :: proc(game_theme: themes.Theme) {
|
|||||||
themes.CARD_WIDTH,
|
themes.CARD_WIDTH,
|
||||||
themes.CARD_HEIGHT,
|
themes.CARD_HEIGHT,
|
||||||
}
|
}
|
||||||
card.build_face(&c, theme)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state = .WAITING
|
state = .WAITING
|
||||||
@@ -93,14 +99,14 @@ unload :: proc() {
|
|||||||
for &c in cards { card.unload_face(&c) }
|
for &c in cards { card.unload_face(&c) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private)
|
|
||||||
can_pick :: proc(i: int) -> bool {
|
can_pick :: proc(i: int) -> bool {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private)
|
// Turns card i face up if it can be picked. Returns false when the pick is not allowed.
|
||||||
pick :: proc(i: int) {
|
select_card :: proc(i: int) -> bool {
|
||||||
|
if !can_pick(i) { return false }
|
||||||
cards[i].face_up = true
|
cards[i].face_up = true
|
||||||
audio.play(.FLIP)
|
audio.play(.FLIP)
|
||||||
if state == .WAITING {
|
if state == .WAITING {
|
||||||
@@ -113,6 +119,7 @@ pick :: proc(i: int) {
|
|||||||
resolve_timer = is_match ? MATCH_DELAY : MISMATCH_DELAY
|
resolve_timer = is_match ? MATCH_DELAY : MISMATCH_DELAY
|
||||||
state = .RESOLVING
|
state = .RESOLVING
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private)
|
@(private)
|
||||||
@@ -155,8 +162,7 @@ update :: proc(dt: f32) {
|
|||||||
elapsed += dt
|
elapsed += dt
|
||||||
if raylib.IsMouseButtonPressed(.LEFT) {
|
if raylib.IsMouseButtonPressed(.LEFT) {
|
||||||
for &c, i in cards {
|
for &c, i in cards {
|
||||||
if can_pick(i) && card.is_hovered(&c) {
|
if card.is_hovered(&c) && select_card(i) {
|
||||||
pick(i)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import "core:testing"
|
||||||
|
import card "../src/card"
|
||||||
|
import memory "../src/memory"
|
||||||
|
import themes "../src/themes"
|
||||||
|
|
||||||
|
// Runs the game logic forward in small steps, like frames
|
||||||
|
@(private)
|
||||||
|
advance :: proc(seconds: f32) {
|
||||||
|
dt: f32 = 1.0 / 60
|
||||||
|
for t: f32 = 0; t < seconds; t += dt {
|
||||||
|
memory.update(dt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds the card index holding the other half of card i's pair
|
||||||
|
@(private)
|
||||||
|
partner_of :: proc(i: int) -> int {
|
||||||
|
for c, j in memory.cards {
|
||||||
|
if j != i && c.pair_id == memory.cards[i].pair_id { return j }
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds a card index that does not pair with card i
|
||||||
|
@(private)
|
||||||
|
non_partner_of :: proc(i: int) -> int {
|
||||||
|
for c, j in memory.cards {
|
||||||
|
if j != i && c.pair_id != memory.cards[i].pair_id { return j }
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
hiragana_table_is_complete_and_unique :: proc(t: ^testing.T) {
|
||||||
|
table := memory.HIRAGANA
|
||||||
|
testing.expect_value(t, len(table), 46)
|
||||||
|
for a, i in table {
|
||||||
|
for b, j in table {
|
||||||
|
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)
|
||||||
|
deal_creates_kana_romaji_pairs :: proc(t: ^testing.T) {
|
||||||
|
memory.deal(themes.JAPAN)
|
||||||
|
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 {
|
||||||
|
kana_count, romaji_count := 0, 0
|
||||||
|
kana, romaji: cstring
|
||||||
|
for c in memory.cards {
|
||||||
|
if c.pair_id != pair { continue }
|
||||||
|
testing.expect(t, !c.face_up && !c.matched)
|
||||||
|
if c.is_kana {
|
||||||
|
kana_count += 1
|
||||||
|
kana = c.text
|
||||||
|
} else {
|
||||||
|
romaji_count += 1
|
||||||
|
romaji = c.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
deal_lays_out_cards_on_screen_without_overlap :: proc(t: ^testing.T) {
|
||||||
|
memory.deal(themes.JAPAN)
|
||||||
|
for a, i in memory.cards {
|
||||||
|
testing.expect(t, a.rect.x >= 0 && a.rect.x + a.rect.width <= themes.SCREEN_WIDTH)
|
||||||
|
testing.expect(t, a.rect.y >= themes.GRID_TOP && a.rect.y + a.rect.height <= themes.SCREEN_HEIGHT)
|
||||||
|
for b, j in memory.cards {
|
||||||
|
if i == j { continue }
|
||||||
|
overlap := a.rect.x < b.rect.x + b.rect.width && b.rect.x < a.rect.x + a.rect.width &&
|
||||||
|
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)
|
||||||
|
matching_pair_stays_up :: proc(t: ^testing.T) {
|
||||||
|
memory.deal(themes.JAPAN)
|
||||||
|
a := 0
|
||||||
|
b := partner_of(a)
|
||||||
|
|
||||||
|
testing.expect(t, memory.select_card(a))
|
||||||
|
testing.expect_value(t, memory.state, memory.State.FIRST)
|
||||||
|
testing.expect(t, memory.select_card(b))
|
||||||
|
testing.expect_value(t, memory.state, memory.State.RESOLVING)
|
||||||
|
testing.expect_value(t, memory.moves, 1)
|
||||||
|
|
||||||
|
advance(2)
|
||||||
|
testing.expect_value(t, memory.state, memory.State.WAITING)
|
||||||
|
testing.expect_value(t, memory.matches, 1)
|
||||||
|
testing.expect(t, memory.cards[a].matched && memory.cards[b].matched)
|
||||||
|
testing.expect(t, memory.cards[a].face_up && memory.cards[b].face_up)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
mismatched_pair_flips_back :: proc(t: ^testing.T) {
|
||||||
|
memory.deal(themes.JAPAN)
|
||||||
|
a := 0
|
||||||
|
b := non_partner_of(a)
|
||||||
|
|
||||||
|
memory.select_card(a)
|
||||||
|
memory.select_card(b)
|
||||||
|
advance(3)
|
||||||
|
testing.expect_value(t, memory.state, memory.State.WAITING)
|
||||||
|
testing.expect_value(t, memory.matches, 0)
|
||||||
|
testing.expect_value(t, memory.moves, 1)
|
||||||
|
testing.expect(t, !memory.cards[a].face_up && !memory.cards[b].face_up)
|
||||||
|
testing.expect_value(t, memory.cards[a].flip_t, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
invalid_picks_are_rejected :: proc(t: ^testing.T) {
|
||||||
|
memory.deal(themes.JAPAN)
|
||||||
|
a := 0
|
||||||
|
testing.expect(t, memory.select_card(a))
|
||||||
|
testing.expect(t, !memory.select_card(a)) // same card twice
|
||||||
|
|
||||||
|
b := non_partner_of(a)
|
||||||
|
memory.select_card(b)
|
||||||
|
c := non_partner_of(b)
|
||||||
|
for c == a { c += 1 }
|
||||||
|
testing.expect(t, !memory.select_card(c)) // a third card while resolving
|
||||||
|
testing.expect_value(t, memory.moves, 1)
|
||||||
|
|
||||||
|
memory.deal(themes.JAPAN)
|
||||||
|
p := partner_of(0)
|
||||||
|
memory.select_card(0)
|
||||||
|
memory.select_card(p)
|
||||||
|
advance(2)
|
||||||
|
testing.expect(t, !memory.select_card(0)) // already matched
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
finding_all_pairs_wins :: proc(t: ^testing.T) {
|
||||||
|
memory.deal(themes.JAPAN)
|
||||||
|
for i in 0 ..< memory.CARD_COUNT {
|
||||||
|
if memory.cards[i].matched { continue }
|
||||||
|
memory.select_card(i)
|
||||||
|
memory.select_card(partner_of(i))
|
||||||
|
advance(2)
|
||||||
|
}
|
||||||
|
testing.expect_value(t, memory.state, memory.State.OVER)
|
||||||
|
testing.expect_value(t, memory.matches, memory.PAIR_COUNT)
|
||||||
|
testing.expect_value(t, memory.moves, memory.PAIR_COUNT)
|
||||||
|
for &c in memory.cards { testing.expect(t, c.matched) }
|
||||||
|
|
||||||
|
// The clock stops once the game is over
|
||||||
|
elapsed := memory.elapsed
|
||||||
|
advance(1)
|
||||||
|
testing.expect_value(t, memory.elapsed, elapsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
card_flip_animation_reaches_target :: proc(t: ^testing.T) {
|
||||||
|
c := card.Card{}
|
||||||
|
testing.expect(t, !card.is_animating(&c))
|
||||||
|
|
||||||
|
c.face_up = true
|
||||||
|
testing.expect(t, card.is_animating(&c))
|
||||||
|
card.update(&c, 0.1)
|
||||||
|
testing.expect(t, c.flip_t > 0 && c.flip_t < 1)
|
||||||
|
for _ in 0 ..< 60 { card.update(&c, 1.0 / 60) }
|
||||||
|
testing.expect_value(t, c.flip_t, 1)
|
||||||
|
testing.expect(t, !card.is_animating(&c))
|
||||||
|
|
||||||
|
c.face_up = false
|
||||||
|
for _ in 0 ..< 60 { card.update(&c, 1.0 / 60) }
|
||||||
|
testing.expect_value(t, c.flip_t, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
card_pulse_counts_down :: proc(t: ^testing.T) {
|
||||||
|
c := card.Card{}
|
||||||
|
card.start_pulse(&c)
|
||||||
|
testing.expect_value(t, c.pulse_t, card.PULSE_DURATION)
|
||||||
|
card.update(&c, 1)
|
||||||
|
testing.expect_value(t, c.pulse_t, 0)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user