From 30b1d74d414672fb5d439e5847c72da9d4ae3505 Mon Sep 17 00:00:00 2001 From: Jakub Dupak Date: Sun, 11 Oct 2026 00:45:26 +0200 Subject: [PATCH 01/12] Rewrite ShellGame in Rust Replace the Python (Click/Rich/Pydantic) implementation with a single Rust binary. Every player command and every hooked cd used to cold-start a PyInstaller bundle; the Rust binary answers a cd hook in about 2 ms and is about 2 MB. - Levels are Rust builder code, one function per level, with the assignment prose in Markdown files compiled in. Custom checks are plain functions, so all 94 levels are declarative or use a small escape hatch. - Drop-in compatible with the Python save file (state.json v2.1), level IDs, workspace and marker layout. Game shells started by the old binary keep working through the legacy cd-hook forms. - Shell integration keeps protocol v1. The cd hook no longer needs a per-level dispatch list, evidence is recorded after the move, and fish `cd -` is fixed. - Tests replay every level's solution through the real binary in bash and fish, fresh and after a re-prepare, plus strict-cd, CLI, shell, state compatibility and rendering suites (1307 tests). - CI builds static Linux x86_64/aarch64 and macOS binaries. macOS assets stay unpublished until the macOS content review. `shellgame update` replaces the binary with the latest release. Co-Authored-By: Claude Opus 5.5 --- .github/copilot-instructions.md | 565 ++-- .github/workflows/ci.yml | 243 +- .gitignore | 102 +- ARCHITECTURE.md | 347 ++- Cargo.lock | 773 ++++++ Cargo.toml | 53 + README.md | 198 +- docs/AUTHORING.md | 1089 +++++--- docs/protocols/SHELL_PROTOCOL.md | 286 ++- rust-toolchain.toml | 4 + scripts/install.sh | 66 +- src/authoring.rs | 3 + src/cli.rs | 1044 ++++++++ src/engine/answer.rs | 1015 ++++++++ src/engine/catalog.rs | 383 +++ src/engine/cd.rs | 1776 +++++++++++++ src/engine/check.rs | 73 + src/engine/ctx.rs | 129 + src/engine/fd.rs | 165 ++ src/engine/fixture.rs | 1528 +++++++++++ src/engine/id.rs | 138 + src/engine/level.rs | 1380 ++++++++++ src/engine/lint.rs | 907 +++++++ src/engine/mod.rs | 92 + src/engine/pytext.rs | 332 +++ src/engine/require.rs | 1785 +++++++++++++ src/engine/sample.rs | 517 ++++ src/engine/section.rs | 169 ++ src/env.rs | 305 +++ src/levels/mod.rs | 252 ++ src/levels/s00_uvod/00-uvod.md | 20 + src/levels/s00_uvod/01-zahrivaci-kolo.md | 8 + src/levels/s00_uvod/mod.rs | 29 + src/levels/s01_navigace/00-uvod.md | 52 + .../s01_navigace/01-aktualni-umisteni.md | 15 + .../s01_navigace/02-soubory-nebo-adresare.md | 18 + src/levels/s01_navigace/03-pripona.md | 10 + .../s01_navigace/04-navrat-na-zakladnu.md | 9 + src/levels/s01_navigace/05-hluboky-ponor.md | 12 + .../s01_navigace/06-viceurovnovy-vystup.md | 10 + src/levels/s01_navigace/07-bludiste.md | 17 + src/levels/s01_navigace/08-absolutni-cesta.md | 10 + src/levels/s01_navigace/09-cesta-domu.md | 14 + src/levels/s01_navigace/10-domov.md | 24 + src/levels/s01_navigace/12-souhrn.md | 23 + src/levels/s01_navigace/maze.rs | 443 ++++ src/levels/s01_navigace/mod.rs | 368 +++ src/levels/s01_navigace/walk.rs | 336 +++ src/levels/s02_soubory/00-uvod.md | 38 + src/levels/s02_soubory/01-sourozenci.md | 25 + src/levels/s02_soubory/02-rychly-navrat.md | 11 + src/levels/s02_soubory/03-hluboka-navigace.md | 14 + src/levels/s02_soubory/04-cteni-souboru.md | 18 + src/levels/s02_soubory/05-stopy.md | 10 + .../s02_soubory/06-vytvoreni-souboru.md | 12 + src/levels/s02_soubory/07-napoveda.md | 23 + src/levels/s02_soubory/08-integracni-vyzva.md | 12 + src/levels/s02_soubory/mod.rs | 256 ++ src/levels/s03_skryte/00-uvod.md | 38 + src/levels/s03_skryte/01-skryte-adresare.md | 17 + .../s03_skryte/02-cteni-skryteho-souboru.md | 11 + src/levels/s03_skryte/03-skryty-adresar.md | 11 + src/levels/s03_skryte/04-skryta-zaloha.md | 10 + src/levels/s03_skryte/05-souhrn-sekce-3.md | 22 + src/levels/s03_skryte/mod.rs | 188 ++ src/levels/s04_vytvareni/00-uvod.md | 44 + .../s04_vytvareni/01-vytvoreni-souboru.md | 12 + .../s04_vytvareni/02-vytvoreni-adresare.md | 12 + .../03-vytvareni-zanorenych-adresaru.md | 17 + src/levels/s04_vytvareni/04-mazani-souboru.md | 17 + .../s04_vytvareni/05-mazani-adresaru.md | 13 + .../s04_vytvareni/06-priprava-projektu.md | 16 + .../s04_vytvareni/07-uklid-neporadku.md | 13 + src/levels/s04_vytvareni/08-souhrn-sekce-4.md | 24 + src/levels/s04_vytvareni/mod.rs | 237 ++ src/levels/s05_zkoumani/00-uvod.md | 53 + .../s05_zkoumani/01-velikost-souboru.md | 16 + .../02-hledani-podle-velikosti.md | 16 + src/levels/s05_zkoumani/03-typ-souboru.md | 16 + src/levels/s05_zkoumani/04-velky-soubor.md | 34 + .../s05_zkoumani/05-zamaskovany-soubor.md | 19 + src/levels/s05_zkoumani/06-python-skript.md | 18 + src/levels/s05_zkoumani/07-souhrn-sekce-5.md | 26 + src/levels/s05_zkoumani/mod.rs | 338 +++ src/levels/s06_kopirovani/00-uvod.md | 44 + .../s06_kopirovani/01-kopirovani-souboru.md | 21 + .../s06_kopirovani/02-kopirovani-adresare.md | 12 + .../s06_kopirovani/03-prejmenovani-souboru.md | 13 + .../s06_kopirovani/04-presun-souboru.md | 17 + .../s06_kopirovani/05-presun-adresare.md | 11 + src/levels/s06_kopirovani/06-uklid-logu.md | 15 + src/levels/s06_kopirovani/07-alias.md | 17 + src/levels/s06_kopirovani/08-organizator.md | 20 + src/levels/s06_kopirovani/mod.rs | 269 ++ src/levels/s07_zoliky/00-uvod.md | 52 + src/levels/s07_zoliky/01-hvezdicka.md | 20 + src/levels/s07_zoliky/02-otaznik.md | 25 + src/levels/s07_zoliky/03-vyber-znaku.md | 25 + src/levels/s07_zoliky/04-tridy-znaku.md | 26 + src/levels/s07_zoliky/05-souhrn-sekce-7.md | 17 + src/levels/s07_zoliky/mod.rs | 206 ++ src/levels/s08_opravneni/00-uvod.md | 67 + .../s08_opravneni/01-spustitelny-soubor.md | 13 + .../02-nastaveni-spustitelnosti.md | 23 + .../s08_opravneni/03-odebrani-prava-zapisu.md | 13 + src/levels/s08_opravneni/04-ciselny-zapis.md | 18 + .../05-kontrolni-ukol-opravneni.md | 30 + .../s08_opravneni/06-zapis-do-adresare.md | 21 + src/levels/s08_opravneni/07-pravo-x.md | 19 + src/levels/s08_opravneni/mod.rs | 251 ++ src/levels/s09_vstup_vystup/00-uvod.md | 52 + .../s09_vstup_vystup/01-ulozeni-vystupu.md | 15 + .../s09_vstup_vystup/02-pridani-konec.md | 15 + .../s09_vstup_vystup/03-spojovani-souboru.md | 15 + src/levels/s09_vstup_vystup/04-uvozovky.md | 16 + src/levels/s09_vstup_vystup/05-pipes.md | 17 + src/levels/s09_vstup_vystup/06-head-tail.md | 10 + src/levels/s09_vstup_vystup/07-wc.md | 10 + src/levels/s09_vstup_vystup/08-sort-uniq.md | 18 + src/levels/s09_vstup_vystup/09-vyzva.md | 14 + src/levels/s09_vstup_vystup/10-eof.md | 20 + .../s09_vstup_vystup/11-navratovy-kod.md | 24 + src/levels/s09_vstup_vystup/mod.rs | 486 ++++ src/levels/s10_chyby/00-uvod.md | 37 + src/levels/s10_chyby/01-presmerovani-chyb.md | 18 + src/levels/s10_chyby/02-pridavani-chyb.md | 17 + src/levels/s10_chyby/03-vsechny-vystupy.md | 16 + src/levels/s10_chyby/04-dev-null.md | 16 + src/levels/s10_chyby/05-souhrn-sekce-10.md | 24 + src/levels/s10_chyby/mod.rs | 226 ++ src/levels/s11_vyhledavani/00-uvod.md | 40 + src/levels/s11_vyhledavani/01-grep.md | 14 + src/levels/s11_vyhledavani/02-grep-r.md | 11 + src/levels/s11_vyhledavani/03-grep-i.md | 12 + src/levels/s11_vyhledavani/04-find.md | 11 + .../05-hledani-podle-pripony.md | 17 + .../s11_vyhledavani/06-finalni-vyzva.md | 18 + src/levels/s11_vyhledavani/mod.rs | 295 +++ src/lib.rs | 18 + src/main.rs | 3 + src/markers.rs | 232 ++ src/messages.rs | 208 ++ src/paths.rs | 650 +++++ src/session.rs | 1778 +++++++++++++ src/shell/bash.sh | 217 ++ src/shell/detect.rs | 422 +++ src/shell/fish.fish | 199 ++ src/shell/launch.rs | 362 +++ src/shell/mod.rs | 137 + src/shell/protocol.rs | 271 ++ src/shell/render.rs | 207 ++ src/state.rs | 1741 +++++++++++++ src/ui/markdown.rs | 1039 ++++++++ src/ui/mod.rs | 343 +++ src/ui/panel.rs | 484 ++++ src/ui/prompt.rs | 336 +++ src/ui/screens.rs | 1659 ++++++++++++ src/ui/style.rs | 164 ++ src/ui/text.rs | 1126 ++++++++ tests/cli.rs | 2272 +++++++++++++++++ tests/cli_support/mod.rs | 539 ++++ tests/e2e_support/mod.rs | 967 +++++++ tests/shell.rs | 1318 ++++++++++ tests/shell_support/mock_shellgame.sh | 75 + tests/shell_support/mod.rs | 405 +++ tests/solutions.rs | 401 +++ tests/state/corrupt.json | 6 + tests/state/generate_golden.py | 300 +++ tests/state/migration_table.json | 592 +++++ tests/state/missing_workspace.json | 13 + tests/state/not_object.json | 5 + tests/state/python_v1_0_migrated.json | 75 + tests/state/python_v2_0_migrated.json | 81 + tests/state/python_v2_1.json | 119 + tests/state/python_v2_1_escapes.json | 24 + tests/state/python_v2_1_finished.json | 37 + tests/state/python_v2_1_fresh.json | 14 + tests/state/timestamp_table.json | 145 ++ tests/state/v1_0.json | 73 + tests/state/v2_0.json | 81 + tests/state/verify_python_load.py | 60 + tests/state/version_99.json | 14 + tests/state_compat.rs | 1312 ++++++++++ tests/strict_cd.rs | 850 ++++++ tests/ui_render.rs | 638 +++++ 185 files changed, 42456 insertions(+), 1099 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 rust-toolchain.toml create mode 100644 src/authoring.rs create mode 100644 src/cli.rs create mode 100644 src/engine/answer.rs create mode 100644 src/engine/catalog.rs create mode 100644 src/engine/cd.rs create mode 100644 src/engine/check.rs create mode 100644 src/engine/ctx.rs create mode 100644 src/engine/fd.rs create mode 100644 src/engine/fixture.rs create mode 100644 src/engine/id.rs create mode 100644 src/engine/level.rs create mode 100644 src/engine/lint.rs create mode 100644 src/engine/mod.rs create mode 100644 src/engine/pytext.rs create mode 100644 src/engine/require.rs create mode 100644 src/engine/sample.rs create mode 100644 src/engine/section.rs create mode 100644 src/env.rs create mode 100644 src/levels/mod.rs create mode 100644 src/levels/s00_uvod/00-uvod.md create mode 100644 src/levels/s00_uvod/01-zahrivaci-kolo.md create mode 100644 src/levels/s00_uvod/mod.rs create mode 100644 src/levels/s01_navigace/00-uvod.md create mode 100644 src/levels/s01_navigace/01-aktualni-umisteni.md create mode 100644 src/levels/s01_navigace/02-soubory-nebo-adresare.md create mode 100644 src/levels/s01_navigace/03-pripona.md create mode 100644 src/levels/s01_navigace/04-navrat-na-zakladnu.md create mode 100644 src/levels/s01_navigace/05-hluboky-ponor.md create mode 100644 src/levels/s01_navigace/06-viceurovnovy-vystup.md create mode 100644 src/levels/s01_navigace/07-bludiste.md create mode 100644 src/levels/s01_navigace/08-absolutni-cesta.md create mode 100644 src/levels/s01_navigace/09-cesta-domu.md create mode 100644 src/levels/s01_navigace/10-domov.md create mode 100644 src/levels/s01_navigace/12-souhrn.md create mode 100644 src/levels/s01_navigace/maze.rs create mode 100644 src/levels/s01_navigace/mod.rs create mode 100644 src/levels/s01_navigace/walk.rs create mode 100644 src/levels/s02_soubory/00-uvod.md create mode 100644 src/levels/s02_soubory/01-sourozenci.md create mode 100644 src/levels/s02_soubory/02-rychly-navrat.md create mode 100644 src/levels/s02_soubory/03-hluboka-navigace.md create mode 100644 src/levels/s02_soubory/04-cteni-souboru.md create mode 100644 src/levels/s02_soubory/05-stopy.md create mode 100644 src/levels/s02_soubory/06-vytvoreni-souboru.md create mode 100644 src/levels/s02_soubory/07-napoveda.md create mode 100644 src/levels/s02_soubory/08-integracni-vyzva.md create mode 100644 src/levels/s02_soubory/mod.rs create mode 100644 src/levels/s03_skryte/00-uvod.md create mode 100644 src/levels/s03_skryte/01-skryte-adresare.md create mode 100644 src/levels/s03_skryte/02-cteni-skryteho-souboru.md create mode 100644 src/levels/s03_skryte/03-skryty-adresar.md create mode 100644 src/levels/s03_skryte/04-skryta-zaloha.md create mode 100644 src/levels/s03_skryte/05-souhrn-sekce-3.md create mode 100644 src/levels/s03_skryte/mod.rs create mode 100644 src/levels/s04_vytvareni/00-uvod.md create mode 100644 src/levels/s04_vytvareni/01-vytvoreni-souboru.md create mode 100644 src/levels/s04_vytvareni/02-vytvoreni-adresare.md create mode 100644 src/levels/s04_vytvareni/03-vytvareni-zanorenych-adresaru.md create mode 100644 src/levels/s04_vytvareni/04-mazani-souboru.md create mode 100644 src/levels/s04_vytvareni/05-mazani-adresaru.md create mode 100644 src/levels/s04_vytvareni/06-priprava-projektu.md create mode 100644 src/levels/s04_vytvareni/07-uklid-neporadku.md create mode 100644 src/levels/s04_vytvareni/08-souhrn-sekce-4.md create mode 100644 src/levels/s04_vytvareni/mod.rs create mode 100644 src/levels/s05_zkoumani/00-uvod.md create mode 100644 src/levels/s05_zkoumani/01-velikost-souboru.md create mode 100644 src/levels/s05_zkoumani/02-hledani-podle-velikosti.md create mode 100644 src/levels/s05_zkoumani/03-typ-souboru.md create mode 100644 src/levels/s05_zkoumani/04-velky-soubor.md create mode 100644 src/levels/s05_zkoumani/05-zamaskovany-soubor.md create mode 100644 src/levels/s05_zkoumani/06-python-skript.md create mode 100644 src/levels/s05_zkoumani/07-souhrn-sekce-5.md create mode 100644 src/levels/s05_zkoumani/mod.rs create mode 100644 src/levels/s06_kopirovani/00-uvod.md create mode 100644 src/levels/s06_kopirovani/01-kopirovani-souboru.md create mode 100644 src/levels/s06_kopirovani/02-kopirovani-adresare.md create mode 100644 src/levels/s06_kopirovani/03-prejmenovani-souboru.md create mode 100644 src/levels/s06_kopirovani/04-presun-souboru.md create mode 100644 src/levels/s06_kopirovani/05-presun-adresare.md create mode 100644 src/levels/s06_kopirovani/06-uklid-logu.md create mode 100644 src/levels/s06_kopirovani/07-alias.md create mode 100644 src/levels/s06_kopirovani/08-organizator.md create mode 100644 src/levels/s06_kopirovani/mod.rs create mode 100644 src/levels/s07_zoliky/00-uvod.md create mode 100644 src/levels/s07_zoliky/01-hvezdicka.md create mode 100644 src/levels/s07_zoliky/02-otaznik.md create mode 100644 src/levels/s07_zoliky/03-vyber-znaku.md create mode 100644 src/levels/s07_zoliky/04-tridy-znaku.md create mode 100644 src/levels/s07_zoliky/05-souhrn-sekce-7.md create mode 100644 src/levels/s07_zoliky/mod.rs create mode 100644 src/levels/s08_opravneni/00-uvod.md create mode 100644 src/levels/s08_opravneni/01-spustitelny-soubor.md create mode 100644 src/levels/s08_opravneni/02-nastaveni-spustitelnosti.md create mode 100644 src/levels/s08_opravneni/03-odebrani-prava-zapisu.md create mode 100644 src/levels/s08_opravneni/04-ciselny-zapis.md create mode 100644 src/levels/s08_opravneni/05-kontrolni-ukol-opravneni.md create mode 100644 src/levels/s08_opravneni/06-zapis-do-adresare.md create mode 100644 src/levels/s08_opravneni/07-pravo-x.md create mode 100644 src/levels/s08_opravneni/mod.rs create mode 100644 src/levels/s09_vstup_vystup/00-uvod.md create mode 100644 src/levels/s09_vstup_vystup/01-ulozeni-vystupu.md create mode 100644 src/levels/s09_vstup_vystup/02-pridani-konec.md create mode 100644 src/levels/s09_vstup_vystup/03-spojovani-souboru.md create mode 100644 src/levels/s09_vstup_vystup/04-uvozovky.md create mode 100644 src/levels/s09_vstup_vystup/05-pipes.md create mode 100644 src/levels/s09_vstup_vystup/06-head-tail.md create mode 100644 src/levels/s09_vstup_vystup/07-wc.md create mode 100644 src/levels/s09_vstup_vystup/08-sort-uniq.md create mode 100644 src/levels/s09_vstup_vystup/09-vyzva.md create mode 100644 src/levels/s09_vstup_vystup/10-eof.md create mode 100644 src/levels/s09_vstup_vystup/11-navratovy-kod.md create mode 100644 src/levels/s09_vstup_vystup/mod.rs create mode 100644 src/levels/s10_chyby/00-uvod.md create mode 100644 src/levels/s10_chyby/01-presmerovani-chyb.md create mode 100644 src/levels/s10_chyby/02-pridavani-chyb.md create mode 100644 src/levels/s10_chyby/03-vsechny-vystupy.md create mode 100644 src/levels/s10_chyby/04-dev-null.md create mode 100644 src/levels/s10_chyby/05-souhrn-sekce-10.md create mode 100644 src/levels/s10_chyby/mod.rs create mode 100644 src/levels/s11_vyhledavani/00-uvod.md create mode 100644 src/levels/s11_vyhledavani/01-grep.md create mode 100644 src/levels/s11_vyhledavani/02-grep-r.md create mode 100644 src/levels/s11_vyhledavani/03-grep-i.md create mode 100644 src/levels/s11_vyhledavani/04-find.md create mode 100644 src/levels/s11_vyhledavani/05-hledani-podle-pripony.md create mode 100644 src/levels/s11_vyhledavani/06-finalni-vyzva.md create mode 100644 src/levels/s11_vyhledavani/mod.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/markers.rs create mode 100644 src/messages.rs create mode 100644 src/paths.rs create mode 100644 src/session.rs create mode 100644 src/shell/bash.sh create mode 100644 src/shell/detect.rs create mode 100644 src/shell/fish.fish create mode 100644 src/shell/launch.rs create mode 100644 src/shell/mod.rs create mode 100644 src/shell/protocol.rs create mode 100644 src/shell/render.rs create mode 100644 src/state.rs create mode 100644 src/ui/markdown.rs create mode 100644 src/ui/mod.rs create mode 100644 src/ui/panel.rs create mode 100644 src/ui/prompt.rs create mode 100644 src/ui/screens.rs create mode 100644 src/ui/style.rs create mode 100644 src/ui/text.rs create mode 100644 tests/cli.rs create mode 100644 tests/cli_support/mod.rs create mode 100644 tests/e2e_support/mod.rs create mode 100644 tests/shell.rs create mode 100644 tests/shell_support/mock_shellgame.sh create mode 100644 tests/shell_support/mod.rs create mode 100644 tests/solutions.rs create mode 100644 tests/state/corrupt.json create mode 100644 tests/state/generate_golden.py create mode 100644 tests/state/migration_table.json create mode 100644 tests/state/missing_workspace.json create mode 100644 tests/state/not_object.json create mode 100644 tests/state/python_v1_0_migrated.json create mode 100644 tests/state/python_v2_0_migrated.json create mode 100644 tests/state/python_v2_1.json create mode 100644 tests/state/python_v2_1_escapes.json create mode 100644 tests/state/python_v2_1_finished.json create mode 100644 tests/state/python_v2_1_fresh.json create mode 100644 tests/state/timestamp_table.json create mode 100644 tests/state/v1_0.json create mode 100644 tests/state/v2_0.json create mode 100644 tests/state/verify_python_load.py create mode 100644 tests/state/version_99.json create mode 100644 tests/state_compat.rs create mode 100644 tests/strict_cd.rs create mode 100644 tests/ui_render.rs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d8a7448..270d938 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,453 +1,248 @@ # Copilot Instructions — ShellGame -You are working in **ShellGame**, an interactive terminal learning game written in **Python** using **Click** (CLI) and **Rich** (UI). The game runs inside a wrapped **subshell** (bash or fish) to control navigation, observe commands, and persist progress. +You are working in **ShellGame**, an interactive terminal learning game written in **Rust**. The game runs inside a wrapped **subshell** (bash or fish) to control navigation, observe commands, and persist progress. -Your job is to make changes that preserve existing UX and gameplay rules. Prefer small, targeted edits and keep user-facing text concise (Czech). +Player-facing text is **Czech**. Code, identifiers, comments, docs and commit messages are English. -**Before making shell integration changes, read this entire section carefully.** +Your job is to make changes that preserve existing UX and gameplay rules. Prefer small, targeted edits and keep player-facing text concise. + +Read first: `ARCHITECTURE.md` (the whole picture), `docs/AUTHORING.md` (before adding or changing a level or section) and `docs/protocols/SHELL_PROTOCOL.md` (before any shell integration change). **Before touching the shell scripts, read section 4.2 below carefully.** --- -## 1) Tech stack (what to assume) +## 1) Tech stack and commands + +- **Rust**, edition 2024, Unix only (Linux and macOS). One crate at the repo root: library plus a thin `main`. The toolchain is pinned in `rust-toolchain.toml`; `Cargo.lock` is committed and CI builds with `--locked`. +- CLI: **clap** (derive), with Czech help. `cd-hook` and `fd-hook` are parsed by hand before clap, because a `cd` target can be `""`, `-`, `-P` or a directory named `pre`. +- State: serde, serde_json, indexmap, chrono. UI: anstyle and anstream, pulldown-cmark and our own text layout. rustix for termios and `fstat`. +- Shell integration: a wrapped **bash** or **fish** (see 4.2). +- Run before you finish: + +```bash +cargo fmt +cargo clippy --all-targets -- -D warnings +cargo test +``` -- Language: **Python 3.10+** -- Dependency Management: **uv** (use `uv sync`, `uv add`, etc.) -- Task Runner: **make** (use `make test`, `make lint`, etc.) -- CLI: **Click** -- UI: **Rich** panels + consistent helper notes -- Tests: **pytest** (run via `make test`) -- Shell integration: wrapped subshell (**bash**/**fish**) — see section 3.2 for critical details -- Persistence: state saved/loaded between sessions +The suite drives real shells: bash is required, and fish, shellcheck, python3 (pseudo-terminal tests), curl and tar are used when present. There is no Python, uv, PyInstaller or Makefile any more. --- ## 2) Where code lives (edit the right place) -### Common responsibilities +| You change | Edit | +| --- | --- | +| level content, hints, fixtures, solutions | `src/levels/sNN_/mod.rs` and the level's `NN-slug.md` | +| level logic that no builder method expresses | next to the level (`s01_navigace/maze.rs`, `walk.rs`) | +| the builder, answers, requirements, fixtures, `cd` policies | `src/engine/` (`level.rs`, `answer.rs`, `require.rs`, `fixture.rs`, `cd.rs`, `fd.rs`) | +| catalog lints | `src/engine/lint.rs` (`Catalog::validate`) | +| path containment | `src/paths.rs` (the only implementation) | +| evidence markers | `src/markers.rs` | +| shared Czech messages | `src/messages.rs` | +| commands: parsing, then behaviour | `src/cli.rs`, then `src/session.rs` | +| screens, panels, wording, paging, prompts | `src/ui/screens.rs`, `src/ui/panel.rs`, `src/ui/prompt.rs` | +| Markdown rendering of assignments | `src/ui/markdown.rs` | +| persistence | `src/state.rs` | +| shell scripts | `src/shell/bash.sh` **and** `src/shell/fish.fish` | +| the protocol, launching, shell detection | `src/shell/protocol.rs`, `launch.rs`, `render.rs`, `detect.rs` | +| the process environment | `src/env.rs` | -- CLI flow, teleport: - - `src/shellgame/cli/commands.py` +### Level IDs -- Subshell launch, shell detection: - - `src/shellgame/cli/subshell.py` +- IDs are explicit: `level(8, "...")` in section 2 is `2.8`. Never derive an ID from declaration order, never renumber, never reuse a retired number (section 1 has no `1.11`). IDs are persisted in player state. +- Section intros are `N.0` and are created with `intro(..)`. Sections are numbered 0, 1, 2, ... without gaps. Inserting a section into existing numbers needs a state migration. +- A new section is a module `sNN_` with `pub fn section()`, registered in `catalog()` in `src/levels/mod.rs`. -- Shell integration templates (bash/fish): - - `src/shellgame/cli/templates/bash_integration.template` - - `src/shellgame/cli/templates/fish_integration.template` +--- -- UI panels, wording consistency, indentation, paging: - - `src/shellgame/ui/display.py` +## 3) Architectural principles -- Level setup + validation rules + shell hooks (OOP): - - `src/shellgame/levels/sections/section*.py` - - Logic specific to a level (hooks, validation) must live in the `Level` class, not global handlers. +### 3.1 Declarative levels, plain Rust for the exceptions -- Declarative level infrastructure: - - `src/shellgame/levels/collector.py` — section registration and stable local IDs - - `src/shellgame/levels/fixture.py` — repeatable workspace setup/cleanup - - `src/shellgame/levels/completion.py` — answers and unconditional requirements - - `src/shellgame/levels/cdpolicy.py` — declarative `cd` restrictions - - `src/shellgame/levels/solution.py` — reference solutions that prove a level is solvable - - `src/shellgame/paths.py` — the single path-containment implementation +- A level is one builder chain: `level(n, title).text(..).start(..).hints([..]).file(..).answer(..).require(..).cd(..).solution(..)`. Where the vocabulary falls short, use an ordinary Rust function (`check`, `.feedback`, `.rule`, `.on_move`, `exact_with`, `.solution_with`). Do not reintroduce a data format for levels. +- Do not hardcode level IDs in `session.rs` or `cli.rs`. Behaviour specific to a level lives with the level. +- **Unconditional safeguards.** Filesystem, location, permission and evidence checks are `.require(..)`. They run before the answer, so a correct or empty answer never bypasses them. +- **Exact completion locations.** Navigation levels use `at(path)`, which compares canonical paths. Never validate only the last directory name: a same-named directory elsewhere must not satisfy the level. +- **One path vocabulary.** Every path a level declares (`.start`, fixtures, requirements, `cd` rules) is relative to the level's section root. `""` and `"."` are the root; `.start_at_workspace()` is for a level above its section. Never restate the section directory in a level. The lint enforces it. +- **Fresh evidence.** A level becomes active through `Level::prepare()`: it clears every marker the level references, repairs the section root, then applies the section fixture and the level fixture. Do not apply fixtures by hand from session code. +- **Safe fixtures.** Absolute paths, `..`, symlinked components and a fixture that targets its own root are rejected. All containment goes through `paths::resolve_within`; do not add a second implementation. Exact directory modes use `.dir_mode(..)`, applied after the files. +- **Declarative movement rules.** Strict navigation is a `policy()` on the level. The engine performs the anti-soft-lock contract. There is no hand-written shell `cd` logic. Stateful grading (the 1.9 walk) uses `.rule(..)`, `.on_move(..)` and `.manual_evidence()` inside that contract. +- **Level-local evidence.** The `cd` and `fd` markers derive from the level ID (`.cd-1.4`, `.fd-10.4`). Do not add global marker names or ad-hoc dotfiles; `Marker::Pwd` is the only shared one. +- **Prove solvability.** Every regular level has a solution, a shell script that the replay runs through the real game shell in bash and fish. Derive answers with the commands the level teaches. -- Authoring guide (read before adding a level or section): - - `docs/AUTHORING.md` +### 3.2 No ambient state -- Section intro markdown (content only; supports paging via `---`): - - `src/shellgame/levels/content/section*_intro.md` +Everything the game needs from the process is in `Env`, built once in `main`. Engine and session code never read `std::env`, the process working directory or `HOME`; they take a `Ctx` or an `Env`. Tests therefore run in parallel with no `chdir` and no `set_var` (`set_var` is `unsafe` in Rust 2024). -- Persistent progress model/load/save: - - `src/shellgame/state/manager.py` - - Missing state may initialize a game; corrupt state must raise an error and must not be overwritten. - - State-changing actions should perform one atomic save. +### 3.3 Feedback is data -### Level IDs -- Declare the persistent section number with `Section(number, ...)`. -- Declare each stable level suffix locally with `@section.level(number)`. -- Never derive IDs from declaration order; IDs are persisted in player state. -- Section intros end with `.0` (e.g., `1.0`, `2.0`) and support paging. -- Regular levels are `X.Y`. -- Sections have no `get_levels()` trailer; the loader reads the module-level - `section` object. Do not reintroduce one. +A wrong answer or an unmet requirement is `Check::Fail(text)`, not an error. Errors are for real failures (state, filesystem). Text for the player is printed as written; only a level's `.text(..)` goes through the Markdown renderer. ---- +### 3.4 Thin shell integration -## 3) Architectural Principles - -### 3.1 OOP over Special Casing -- **Encapsulate logic in Level classes**: Do not hardcode level IDs in `session.py` or `commands.py`. -- **Use overrides**: If a level needs special behavior (e.g., custom validation, hooks), override the corresponding method in its class. -- **Declarative configuration**: Regular levels should combine - `WorkspaceFixture`, `Completion`, typed answer rules, and typed requirements. - Override `setup()` or `validate()` only for genuinely custom behavior. -- **Unconditional safeguards**: Put filesystem, cwd, permissions, and evidence - checks in `Completion.requirements`. Correct or empty answers must never - bypass these checks. -- **Exact completion locations**: Navigation levels must use `AtDirectory` - with a section-root-relative path. Never validate only `Path.cwd().name`, - because a same-named directory outside the workspace must not satisfy the - level. -- **One path vocabulary**: every path a level declares — `start_directory`, - fixtures, requirements, solution steps — is relative to that level's section - root. `""` is the section root; use the `WORKSPACE_ROOT` sentinel for the - rare level that starts above its own section. Never restate the section - directory name inside a level, and never build a path with `state.workspace / - ...` in a section module. -- **Fresh evidence**: Runtime level activation goes through `Level.prepare()`, - which clears declarative evidence, applies the section fixture, applies the - level fixture, and then runs custom setup. Do not call `setup()` directly - from session orchestration. -- **Safe fixtures**: Fixture paths are relative to the section root. Absolute - paths, `..`, cleanup of the fixture root, and symlink escapes are rejected. - All containment goes through `paths.resolve_within()`; do not add a second - implementation. Declare exact directory modes with `DirectoryFixture` in - `WorkspaceFixture.directory_fixtures`; modes are applied after child files so - parent permission recovery cannot overwrite the requested final mode. -- **Declarative movement rules**: strict navigation levels declare a - `cd_policy` (`levels/cdpolicy.py`) instead of writing `_handle_cd`. The - engine then applies the anti-soft-lock contract structurally. Write a custom - `_handle_cd` only for stateful grading that a policy cannot express (level - 1.9 is the sole example). -- **Level-local evidence**: a `cd` policy derives its marker from the level ID - (`cd_marker("1.4")` → `cd-1.4`). Do not add global marker constants and do - not invent ad-hoc dotfile names. -- **Prove solvability**: a level with custom `validate()`/`setup()`, a - `cd_policy`, evidence, or a filesystem requirement must declare a `Solution` - (`levels/solution.py`). It is replayed against a throwaway workspace, both - fresh and after `reset()`. - -### 3.2 Thin Shell Integration -- **Minimal templates**: Keep bash/fish templates as small as possible. -- **Delegate to Python**: Shell scripts should only capture events (like `cd`) and forward them to Python via hooks or protocol directives. -- **No game logic in shell**: Do not implement validation or state management in shell scripts. +- Keep both scripts as small as possible. They capture events (`cd`, `pwd`) and forward them to the binary through hooks and protocol directives. +- No game logic, validation or state in shell scripts. The `cd` function is static: it never changes between levels. --- ## 4) Hard UX rules (do not break) ### 4.1 Teleport notices -ShellGame may auto-`cd` the user to a level start directory. - -Rules: -- Show a teleport notice **only if cwd actually changed**. -- Notice formatting: - - message in **yellow** - - destination path in **violet** - - **no** “from -> to” - - **no** “reason” text -- Helper must remain: `_teleport_notice(destination: Path)` in `commands.py` -- Do not change the signature; call sites must not pass `reason=...`. - -### 4.2 Subshell startup (CRITICAL — read carefully) - -Both bash and fish use a **single integration template** that includes: -1. Shell setup (disable interfering features) -2. Function definitions (`shellgame`, `pwd`, `cd` hooks, `__shellgame_eval`) -3. Autostart block **at the end** (calls `shellgame` function after it's defined) - -#### Bash launch flags -```bash -bash --rcfile -i -``` -**CRITICAL**: Do NOT use `--norc` — it disables `--rcfile` entirely! -**CRITICAL**: Do NOT use `--noprofile` — let users keep their PATH/env setup. +The game may `cd` the player to a level's start directory. -The integration script is used directly as the rcfile. There is NO separate rc template. +- Show the notice **only if the directory actually changed**. The session tracks the player's directory in memory and updates it when it emits a `cd`. +- Format (`screens::teleport_notice`): the message in **yellow**, the destination in **violet**, **no** "from -> to", **no** reason text. -#### Fish launch -```bash -fish --init-command "function fish_greeting; end; source " +### 4.2 Shell startup (CRITICAL) + +Both shells use **one integration script** that does, in order: setup, helper functions, `pwd`, `cd`, the `shellgame` function, completions, **autostart last**. + +```text +bash --rcfile