Skip to content

Rewrite ShellGame in Rust - #4

Draft
jdupak wants to merge 13 commits into
mainfrom
rust-rewrite
Draft

jdupak wants to merge 13 commits into
mainfrom
rust-rewrite

Conversation

@jdupak

@jdupak jdupak commented Oct 23, 2026

Copy link
Copy Markdown
Owner

Replaces the Python implementation with a single Rust binary. Three commits:

  • Rewrite ShellGame in Rust: the port.
  • Remove the Python implementation.
  • Restructure the Rust engine and fix the review findings:
    • levels become const struct literals, one file per section;
    • positions ("S.L") replace ids, with state.json v3;
    • the game shell is confined by Landlock;
    • the docs move into AGENTS.md, module docs and skills;
    • a deep review's fixes.

What changes for students

  • Re-install: students on the Python build (build-13) have no shellgame update; they need to re-run the installer once. install.sh is now POSIX sh, and its overrides are renamed to SHELLGAME_INSTALL_DIR and SHELLGAME_REPO.
  • Old saves: a Python save is set aside as state-old.json and a new game starts. The notice names the command to continue, e.g. shellgame resume 5.3. A save from a newer version is refused, with a hint to run shellgame update.
  • Landlock or --unsafe: the game shell can change files only in /tmp, the state directory and /dev. Without Landlock (e.g. macOS, WSL 1, old kernels) the game refuses to start unless run with --unsafe.
  • Same course: the 12 sections are the same as in the Python version, with grading, wording and Czech fixes from the review. Answers that were only labels are now a bare shellgame submit. New CLI: hint -r, plus levels, resume, update and --version.

Review fixes in the last commit

  • reset rescue: it repairs a damaged workspace or section directory (lost permissions, a file or a symlink in its place, never followed). A parent directory without x no longer reads as "your directory was deleted".
  • Level bugs:
    • 2.7: the text gave the answer away.
    • 2.2: the cd - drill counted from anywhere.
    • 9.8: the mistake message for 3 was wrong.
    • 9.10: the Ctrl+C claims were false.
    • 11.4–11.6: find . paths, quoting and the format example.
    • Section 7: a move was accepted where the task says copy.
    • Section 6: the alias sg collided with /usr/bin/sg.
    • Also fixed: the Linux ~ keyboard help and both ls -l diagrams.
  • CI:
    • publishes only a commit that is still the tip of main, so an older build can't become latest;
    • the bash 3.2 shell tests on macOS now block a release;
    • the static Linux binary is smoke-tested in a confined game shell;
    • docs deploy only after CI succeeds.
  • Tests:
    • new cases for the reset damage, save versions, hint -r and a parent without x;
    • replays fail on a missing command;
    • Landlock ABI and over-restriction steps;
    • no pinned Czech wording;
    • the course validation also scans failed-check messages for leaked answers.

Before merging

  • Conflicts with main: it has a69c0a4 (feat: add levels and resume commands #2, levels and resume for the Python version). This branch replaces those files, so the conflicts should resolve in favour of this branch.
  • CI on GitHub: the workflow changes (publish guard, macOS blocking step, smoke test, workflow_run docs deploy) have only run locally. Check this PR's run. Whether the arm64 runner has Landlock is unknown; without it the smoke test falls back to --unsafe with a warning.
  • Maintainer decisions:
    • 4.1 duplicates 2.6; removing it would shift saved positions.
    • 5.2 is no longer optional.
    • 1.11 doesn't enforce running pwd before the final cd.
    • submit joins its words with spaces.
    • Content checks are exact, so a missing final newline is rejected; the messages now say so.
  • macOS content review (assets stay held back by PUBLISH_MACOS).

Test plan

Run locally on Linux with Landlock:

  • cargo fmt --check, cargo clippy --all-targets --locked -- -D warnings, RUSTDOCFLAGS="-D warnings" cargo doc --no-deps: clean.
  • SHELLGAME_TEST_LANDLOCK=required cargo test --locked:
    • unit and validation: 42
    • cli: 22
    • e2e: 195 (every solution in bash and fish, playthroughs, strict cd, regressions)
    • sandbox: 3
    • shell: 22
    • doctest: 1
  • shellcheck: every tracked *.sh passes.
  • By hand: reset after each damage case, old/new saves, hint -r, repeat, and 2.2, 3.4, 6.7 and 11.5 in tmux.

🤖 Generated with Claude Code

jdupak and others added 13 commits October 11, 2026 00:45
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 <noreply@anthropic.com>
The Rust version replaces it entirely: sources, Python tests, packaging
(pyproject, uv.lock, PyInstaller spec) and the Makefile. The golden-file
helper scripts in tests/state/ stay; regenerating the files needs a
checkout of the last Python release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rework this branch carried uncommitted:
- Levels are const struct literals, one file per section
  (src/levels/s00_welcome.rs ... s11_search.rs), each opening with its
  goals block; the engine moves from src/engine to src/game. Positions
  ("S.L") replace level ids and state.json v3 replaces the Python-era
  v2.1: an older save is set aside as state-old.json.
- The game shell is confined by Landlock (src/sandbox.rs); without it the
  game refuses to start unless run with --unsafe.
- ARCHITECTURE, AUTHORING, the specs and the shell protocol move into
  AGENTS.md, module docs and .claude/skills; cargo doc is published as
  the docs site. chrono, indexmap, terminal_size and assert_cmd are gone.
- The tests become the e2e (solution replays, playthroughs, strict cd),
  cli, shell and sandbox suites.

Fixes from the review:
- reset repairs a workspace or section directory the player damaged
  (lost permissions, a file or a symlink in its place, never followed).
- A parent directory without x no longer reads as a deleted one.
- A save of a newer version is an error; the notice about an older one
  comes last and names the position to resume.
- hint -r shows the unlocked hints again; repeat is a hidden alias of
  show; update has curl timeouts; setup errors are in Czech.
- Course: grading, leaked answers, wrong explanations and Czech wording
  fixed in every section; answers that were only labels become a bare
  submit. No level is added, removed or moved, so saved positions do not
  shift.
- Tests: reset damage cases, save versions, a replay that fails on a
  missing command, Landlock ABI and over-restriction steps, no pinned
  wording; the validation also scans failed-check messages for answers.
- CI publishes only the tip of main, blocks a release on the bash 3.2
  shell tests and smoke-tests the static binary in a confined game
  shell; docs deploy after CI. install.sh is POSIX sh with
  SHELLGAME_INSTALL_DIR and SHELLGAME_REPO.

Both parts changed the same uncommitted files, so they are one commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fish 3.3 (ubuntu-22.04) warns on every start when it cannot create
~/.config/fish and ~/.local/share/fish, and the sandbox forbids creating
them in the home. The warnings failed every fish replay that runs there.
A fish player's home has these directories: fish makes them on its first
start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sandbox test points XDG_*_HOME into the test directory, where fish
3.3 cannot create its config and data directories under Landlock and
warns about history, which the test rightly forbids. A fish player has
these directories, as the test home now does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main gained the `levels` and `resume` commands for the Python version
(#2). This branch removes the Python implementation and has its own
`levels` and `resume` (a bare `resume` also lists the levels), so the
merge keeps this branch's tree: the Python files and tests main changed
or added are dropped, and the README already documents both commands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It cut the answer out of `type shellgame`, which a Czech bash words as
"shellgame je funkce". `type -t` prints "function" in every locale, in
bash and fish. The level itself already accepts both words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Students play in cs_CZ.UTF-8, which changes the output of tools the
levels lean on (sort order, `ls --help`, bash's own messages), while the
tests ran in C.UTF-8. SHELLGAME_TEST_LOCALE overrides the harness locale,
and the lint leg replays every level in cs_CZ.UTF-8 as well.

`cargo test --no-fail-fast`: a failing suite no longer hides the
failures of the suites after it, which cost a CI round trip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fish makes ~/.config/fish and ~/.local/share/fish on its first start,
which the sandbox forbids; fish 3.3 (Ubuntu 22.04) then warns on every
game shell start that history and settings will not be saved. The
launcher, which is not confined, now makes them as fish would. Env gains
the XDG config and data homes for that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clap's usage errors are English ("error: unrecognized subcommand
'sumbit'", and for `--shell zsh` the tip "a similar value exists:
'fish'"), which first-semester students meet with every typo. An
unknown command, an unexpected argument and an invalid value are now
told in Czech with clap's suggestion (or the valid values) and a pointer
to `shellgame --help`; help, the version and anything else are printed
as clap writes them. The exit status stays 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`line="${line%$'\r'}"` puts a literal CR into the function body, so
`type shellgame` (which the alias level asks students to run) garbles a
line on screen. The CR is now made with `printf -v`, which `type` shows
as text. bash 3.1 has `printf -v`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plain texts wrap at spaces, which split a quoted command such as
'ls -l' or 'file *' across rows in the narrow centred panels. A span in
single quotes or backticks now stays whole when it fits a row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugs:
- 8.7: the mode message told a player standing in `parent` to run
  `chmod u+x ..`, which changed the wrong directory and looped. It now
  names the directory they stand in and `chmod go-x .`, and the check
  accepts any mode with x for the owner only.
- 5.6: `./calc.py` was rejected (suffix now) and `calc` gets a hint;
  the generic error no longer talks about the x permission.
- 8.6: the missing-copy message names recovered.txt.
- 1.3: `alpha`, submitted from inside alpha, no longer says to go there.

Wording:
- "není správně" agreement in sections 7, 9, 10 and 11;
- 9.6 calls the second word "slovo z posledního řádku";
- 7.5 count errors point at the folders to count;
- 9.11 explains a doubled line too;
- 11.4 explains a path to the directory only;
- 3.4 explains `.data`;
- the fish alias note in 6.7;
- the final screen is prose instead of a ragged checklist;
- sections 6 to 11 use `###` headings like the others;
- a too-wide line in the section 1 intro;
- "Aktuální level" in the status screen;
- smaller Czech fixes.

No level is added, removed or moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant