diff --git a/README.md b/README.md index d20c7d1..798154b 100644 --- a/README.md +++ b/README.md @@ -74,12 +74,85 @@ plot of scorched earth. ## Forging your arms (local development) -Requires Node.js 25.x and Yarn 1.22.22 (Classic) — see +Requires Node.js 25.x, Yarn 1.22.22 (Classic), and Docker — see [`docs/adr/0008-toolchain-simplification.md`](docs/adr/0008-toolchain-simplification.md) for the -full decree — plus Docker, for there is no combat permitted outside the cage. +full decree on the first two. There is no combat permitted outside the cage: Docker is what +actually runs every bot, and the real thing (`yarn thunderdome match run`, the runtime's own +integration tests) needs it running, not just installed. + +### 1. Node.js 25.x + +The exact version in use is pinned in [`.node-version`](.node-version) (`25`) and enforced by +`package.json`'s `"engines"` field (`>=25 <26`). Installing it through a version manager — rather +than your OS's package manager directly — is what keeps you from clobbering whatever Node version +some other project on your machine expects. + +**[fnm](https://github.com/Schniz/fnm)** is the one recommendation here that behaves identically +on macOS, Linux, and Windows, and reads this repo's `.node-version` file natively (no `.nvmrc` +translation needed): + +| Platform | Install fnm | +| -------- | ---------------------------------------------------- | +| macOS | `brew install fnm` | +| Linux | `curl -fsSL https://fnm.vercel.app/install \| bash` | +| Windows | `winget install Schniz.fnm` (or `scoop install fnm`) | + +Then, from anywhere inside this repo, on any platform: + +```bash +fnm use --install-if-missing # reads .node-version, installs 25.x if you don't have it, and switches to it +node -v # sanity check — should print a v25.x.x +``` + +`fnm`'s install instructions end with a one-time shell hook (`eval "$(fnm env --use-on-cd)"` in +your `.bashrc`/`.zshrc`, or the PowerShell equivalent on Windows) that makes it auto-switch +versions when you `cd` into a directory with a `.node-version` file — worth doing once so you +don't have to remember to run `fnm use` by hand every time. + +Already using a different version manager? Any of these work too, as long as you land on a real +Node 25.x — none of them read `.node-version` automatically the way `fnm` does, so you'll need to +name the version explicitly: + +- **nvm** (macOS/Linux): `nvm install 25 && nvm use 25` +- **nvm-windows**: `nvm install 25.8.0 && nvm use 25.8.0` (needs an exact version, not a range) +- **Volta** (macOS/Linux/Windows): `volta install node@25` + +### 2. Yarn 1.22.22 (Classic) + +There's no version-manager pinning for Yarn here (a deliberate simplification — +`docs/adr/0008-toolchain-simplification.md`), so install this exact version globally with npm +(which you already have once Node is installed), on any platform: + +```bash +npm install -g yarn@1.22.22 +yarn -v # should print exactly 1.22.22 +``` + +### 3. Docker + +Any bot's match — including this repo's own integration tests — runs inside a real Docker +container (`docs/adr/0003-docker-bot-isolation.md`), so this isn't optional. + +| Platform | Install | +| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| macOS | [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/), or `brew install --cask docker` | +| Windows | [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/) (uses the WSL2 backend — Docker's installer walks you through enabling WSL2 if it isn't already) | +| Linux | [Docker Engine](https://docs.docker.com/engine/install/) via your distro's package manager, or the official convenience script: `curl -fsSL https://get.docker.com \| sh`. Afterwards, add yourself to the `docker` group (`sudo usermod -aG docker $USER`, then log out/in) so you don't need `sudo` for every command. | + +Verify it's actually running (Docker Desktop needs to be launched, not just installed, on +macOS/Windows) before moving on: + +```bash +docker ps +``` + +### 4. Clone and bootstrap ```bash -npm install -g yarn@1.22.22 # if you don't already have this exact Yarn version +git clone +cd thunderdome +fnm use --install-if-missing # or your version manager's equivalent — see step 1 +npm install -g yarn@1.22.22 # if you don't already have this exact Yarn version yarn install yarn build yarn lint diff --git a/apps/cli/src/commands/cleanup.ts b/apps/cli/src/commands/cleanup.ts index 05d34ff..b9d3212 100644 --- a/apps/cli/src/commands/cleanup.ts +++ b/apps/cli/src/commands/cleanup.ts @@ -1,4 +1,4 @@ -// `thunderdome cleanup` — Phase 12's backstop for leftover bot containers. `match run`/ +// `thunderdome cleanup` — a backstop for leftover bot containers. `match run`/ // `tournament run` (via apps/cli/src/lib/match-execution.ts) and the CLI's own SIGINT/SIGTERM // handler (apps/cli/src/index.ts) are the primary defense; this exists for whatever slips past // both — most notably the process being SIGKILLed (or the host crashing) before either gets a diff --git a/apps/cli/src/commands/match.ts b/apps/cli/src/commands/match.ts index 0403af2..05f76ee 100644 --- a/apps/cli/src/commands/match.ts +++ b/apps/cli/src/commands/match.ts @@ -1,4 +1,4 @@ -// `thunderdome match run` — Phase 6's registry-backed, real end-to-end match runner. Resolves +// `thunderdome match run` — the registry-backed, real end-to-end match runner. Resolves // bot/game ids through @thunderdome/registry, builds each bot's Docker image on demand from its // own manifest (no manual pre-build step), then drives a real match through @thunderdome/engine // and @thunderdome/runtime — the registry-driven successor to an earlier ad hoc scrimmage script diff --git a/apps/cli/src/commands/tournament.ts b/apps/cli/src/commands/tournament.ts index 0e21f3c..7bf1fbb 100644 --- a/apps/cli/src/commands/tournament.ts +++ b/apps/cli/src/commands/tournament.ts @@ -1,15 +1,14 @@ -// `thunderdome tournament run` — Phase 7's tournament orchestrator (round robin), joined in -// Phase 9 by single elimination — wired to real bots. Reuses exactly the same -// registry-resolution, on-demand image build, and single-match execution `match run` (Phase 6) -// already established (apps/cli/src/lib/match-execution.ts) — a tournament is just that same -// wiring, called once per `MatchDescriptor` the format unlocks via `@thunderdome/engine`'s -// `runTournament()`. +// `thunderdome tournament run` — the tournament orchestrator, supporting both round robin and +// single elimination — wired to real bots. Reuses exactly the same registry-resolution, +// on-demand image build, and single-match execution `match run` already established +// (apps/cli/src/lib/match-execution.ts) — a tournament is just that same wiring, called once per +// `MatchDescriptor` the format unlocks via `@thunderdome/engine`'s `runTournament()`. // -// Phase 11 added persistence: every `run` writes a `TournamentRecord` -// (`@thunderdome/tournament-store`) to disk as it goes, and `inspect`/`replay`/`list` read it -// back. The tournament-author-guide's original three-piece design (`tournament create`, then a -// separate run-by-id step) was simplified into one — `run` always creates *and* drives its own -// record — since nothing in this platform needs a record to exist before it's actually played. +// Persistence: every `run` writes a `TournamentRecord` (`@thunderdome/tournament-store`) to disk +// as it goes, and `inspect`/`replay`/`list` read it back. The tournament-author-guide's original +// three-piece design (`tournament create`, then a separate run-by-id step) was simplified into +// one — `run` always creates *and* drives its own record — since nothing in this platform needs +// a record to exist before it's actually played. import { randomUUID } from 'node:crypto'; import path from 'node:path'; import { parseArgs } from 'node:util'; diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 615b5a9..53cdae0 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -12,9 +12,9 @@ import { import { abortActiveMatch } from './lib/match-execution.js'; // Presentation layer only — no engine logic lives here (docs/architecture.md §2), with three -// exceptions: `match run` (Phase 6), `tournament run` (Phase 7), and `play` are real, -// registry-backed behavior — see commands/match.ts, commands/tournament.ts, and commands/play.ts. -// Every other subcommand group below is still a stub. +// exceptions: `match run`, `tournament run`, and `play` are real, registry-backed behavior — see +// commands/match.ts, commands/tournament.ts, and commands/play.ts. Every other subcommand group +// below is still a stub. const SUBCOMMANDS = ['games', 'bots', 'tournament', 'match', 'play', 'cleanup'] as const; type Subcommand = (typeof SUBCOMMANDS)[number]; diff --git a/apps/cli/src/lib/match-execution.ts b/apps/cli/src/lib/match-execution.ts index 5341831..79ebfd4 100644 --- a/apps/cli/src/lib/match-execution.ts +++ b/apps/cli/src/lib/match-execution.ts @@ -1,4 +1,4 @@ -// Shared by `match run` (Phase 6) and `tournament run` (Phase 7): resolving bot/game ids through +// Shared by `match run` and `tournament run`: resolving bot/game ids through // the registry, building bot images on demand, and driving one real match through the engine and // runtime. Neither command duplicates this — a tournament is just this same single-match wiring, // called once per `MatchDescriptor` a `TournamentFormat` unlocks. diff --git a/apps/cli/test/tournament.integration.test.ts b/apps/cli/test/tournament.integration.test.ts index 7590f9c..3930120 100644 --- a/apps/cli/test/tournament.integration.test.ts +++ b/apps/cli/test/tournament.integration.test.ts @@ -32,8 +32,8 @@ const dockerAvailable = await checkDockerAvailable(); describe.runIf(dockerAvailable)('runTournamentCommand (real Docker, real registry)', () => { // Every test gets its own store directory, outside the real repo tree — `tournament run` - // always persists a record now (Phase 11), and this keeps that from littering the actual - // working tree (or one test's records from leaking into another's) on every test run. + // always persists a record, and this keeps that from littering the actual working tree (or + // one test's records from leaking into another's) on every test run. let storeDir: string; beforeEach(async () => { diff --git a/docs/guides/README.md b/docs/guides/README.md index 4997aec..6aed87b 100644 --- a/docs/guides/README.md +++ b/docs/guides/README.md @@ -6,7 +6,7 @@ architecture these guides use, see `docs/architecture.md` and `docs/adr/`. | Guide | For | Status | | ---------------------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | [`rps-bot-author-guide.md`](rps-bot-author-guide.md) | Writing a Rock-Paper-Scissors bot | Real and testable today — protocol, runtime, and the game itself are all implemented | -| [`tournament-author-guide.md`](tournament-author-guide.md) | Configuring and persisting a tournament | Real for round robin (Phase 7), single elimination (Phase 9), and persistence (Phase 11); Swiss/pool-then-elimination not built | +| [`tournament-author-guide.md`](tournament-author-guide.md) | Configuring and persisting a tournament | Real for round robin, single elimination, and persistence; Swiss/pool-then-elimination not built | | [`game-authoring-guide.md`](game-authoring-guide.md) | Implementing a new `GameDefinition` | Real — describes the two real games (Rock-Paper-Scissors, Connect Four) and the interface both implement | | [`protocol-reference.md`](protocol-reference.md) | The wire protocol, precisely | Real — every message type, with real examples from `packages/protocol/fixtures` | | [`security-model.md`](security-model.md) | Isolation, timeouts, and cleanup guarantees | Real — summarizes ADR-0003 for bot authors and operators, plus resource-cleanup detail ADR-0003 itself doesn't spell out in one place | diff --git a/docs/guides/examples/counter-bot/README.md b/docs/guides/examples/counter-bot/README.md index 58f4a14..2395d2d 100644 --- a/docs/guides/examples/counter-bot/README.md +++ b/docs/guides/examples/counter-bot/README.md @@ -75,8 +75,8 @@ registry entry (it lives under `docs/guides/examples/`, not `bots/`) — see the [`/bots/rock-paper-scissors/`](../../../../bots/rock-paper-scissors/) for that. Once your own bot is submitted there (`docs/guides/rps-bot-author-guide.md` §8), `yarn thunderdome match run ` resolves both through the real bot registry and drives an actual -match via `@thunderdome/engine` and `@thunderdome/runtime` (Phase 6) — or +match via `@thunderdome/engine` and `@thunderdome/runtime` — or `yarn thunderdome tournament run [...]` for a real round-robin -tournament among more than two bots (Phase 7). See +tournament among more than two bots. See [`docs/guides/tournament-author-guide.md`](../../tournament-author-guide.md) for what's still missing beyond round robin (other formats, persistence). diff --git a/docs/guides/examples/counter-bot/smoke-test.mjs b/docs/guides/examples/counter-bot/smoke-test.mjs index 05c5903..c2a0d2e 100644 --- a/docs/guides/examples/counter-bot/smoke-test.mjs +++ b/docs/guides/examples/counter-bot/smoke-test.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node // Drives this bot's real Docker container through a scripted exchange using the platform's own // runtime primitives (docs/guides/rps-bot-author-guide.md, "Testing your bot locally"). There's -// no CLI wrapper for this specific kind of single-bot, scripted-opponent smoke test — Phase 6's +// no CLI wrapper for this specific kind of single-bot, scripted-opponent smoke test — // `yarn thunderdome match run` is for real two-bot matches through the registry instead — so // this script exercises the same DockerBotProcess + BotLifecycle pieces the platform itself uses. // diff --git a/docs/guides/rps-bot-author-guide.md b/docs/guides/rps-bot-author-guide.md index 7686440..ebc8719 100644 --- a/docs/guides/rps-bot-author-guide.md +++ b/docs/guides/rps-bot-author-guide.md @@ -11,11 +11,11 @@ how to test it before submitting it. exists today, providing both the manifest schema (§5 below uses it directly) and a developer-facing protocol client (`runBot()`) that TypeScript/JavaScript bots can use instead of implementing the wire protocol by hand — §4 below covers both paths. The bot registry and a -`match run` CLI command (Phase 6) are also real now — §7 covers running your bot against a real -second one. What's **not** built yet is a whole-tournament CLI (`TournamentFormat` and the -orchestrator are Phase 7 — see `docs/guides/tournament-author-guide.md`). Where this guide -describes something that doesn't exist yet, it says so explicitly rather than pretending -otherwise. +`match run` CLI command are also real now — §7 covers running your bot against a real +second one. A whole-tournament CLI is real too (`TournamentFormat` and the orchestrator — see +`docs/guides/tournament-author-guide.md` for round robin, single elimination, and persistence). +Where this guide describes something that doesn't exist yet, it says so explicitly rather than +pretending otherwise. ## 1. What a bot is @@ -371,7 +371,7 @@ expected — the same mechanics (timeouts, forfeit reasons, graceful shutdown) a will use. It's a good template for testing your own bot: swap in your image tag and whatever observation sequence exercises your strategy. -**Against a real second bot, end to end.** `yarn thunderdome match run ` (Phase 6) +**Against a real second bot, end to end.** `yarn thunderdome match run ` resolves both bot ids and their shared game through the real bot/game registry (`@thunderdome/registry`), builds each bot's Docker image on demand from its own manifest — no manual `docker build` step first — and drives a real match through the generic engine @@ -404,7 +404,7 @@ rather than only ever reading it off a final tally. Type "quit" any time to stop Open a PR that touches only `bots/rock-paper-scissors//` — mechanically enforced by `tools/boundary-check` (`docs/adr/0007-repository-enforcement.md`; bots are grouped under `bots//`, so a Rock-Paper-Scissors bot's directory is `bots/rock-paper-scissors//`). -There's no separate registration step: the bot registry (Phase 6, `@thunderdome/registry`) is a +There's no separate registration step: the bot registry (`@thunderdome/registry`) is a pure filesystem scan of `manifest.json` files, so the moment your PR merges, your bot is discoverable and playable via `yarn thunderdome match run ` — no index to update, nothing else to run. diff --git a/docs/guides/tournament-author-guide.md b/docs/guides/tournament-author-guide.md index f8b9aa6..ecd2986 100644 --- a/docs/guides/tournament-author-guide.md +++ b/docs/guides/tournament-author-guide.md @@ -20,13 +20,13 @@ example here yet. A tournament is the combination of: -| Piece | What it is | Status | -| ------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| **Game** | Which game, which version, and that game's own config | Real — `rock-paper-scissors`, `{ totalRounds, onMissingAction }` | -| **Roster** | Which bots (from the bot registry) are competing | Real (Phase 6) — `@thunderdome/registry`'s `scanBots`/`scanGames` | -| **Format** | How matchups are generated and standings computed | Real for round robin (Phase 7) and single elimination (Phase 9) — `@thunderdome/tournament-formats`; Swiss and pool-then-elimination not built | -| **Seed** | The one entropy boundary this tournament's reproducibility traces back to | Real — `@thunderdome/rng` | -| **Resource limits** | Per-game CPU/memory/timeout ceilings | Partially real — `GameDefinition.resourceLimits` exists; runtime enforcement is per-match, not tournament-level yet | +| Piece | What it is | Status | +| ------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| **Game** | Which game, which version, and that game's own config | Real — `rock-paper-scissors`, `{ totalRounds, onMissingAction }` | +| **Roster** | Which bots (from the bot registry) are competing | Real — `@thunderdome/registry`'s `scanBots`/`scanGames` | +| **Format** | How matchups are generated and standings computed | Real for round robin and single elimination — `@thunderdome/tournament-formats`; Swiss and pool-then-elimination not built | +| **Seed** | The one entropy boundary this tournament's reproducibility traces back to | Real — `@thunderdome/rng` | +| **Resource limits** | Per-game CPU/memory/timeout ceilings | Partially real — `GameDefinition.resourceLimits` exists; runtime enforcement is per-match, not tournament-level yet | None of these are new concepts invented for this guide — they're exactly `docs/architecture.md`'s mental model: **Games + Tournaments + Bots → Matches → Match @@ -258,28 +258,27 @@ into a table; `getPublicStandings` projects that into whatever a spectator or CL ## 5. What's done, and what's still missing -Done, as of Phase 6, 7, 9, and 11: +Done: -1. **Bot registry** (Phase 6) — `@thunderdome/registry`'s `scanBots`/`scanGames`, so a roster can +1. **Bot registry** — `@thunderdome/registry`'s `scanBots`/`scanGames`, so a roster can reference real bot ids instead of a made-up list. -2. **Runtime ↔ engine adapter** (Phase 6) — `@thunderdome/runtime`'s `DockerActionCollector`, a +2. **Runtime ↔ engine adapter** — `@thunderdome/runtime`'s `DockerActionCollector`, a real `ActionCollector` (`@thunderdome/engine`) backed by real `BotLifecycle` instances, so `runMatch()` can actually drive Docker containers instead of a test's scripted collector. -3. **A CLI command for a single match** (Phase 6) — `yarn thunderdome match run ` +3. **A CLI command for a single match** — `yarn thunderdome match run ` (`apps/cli/src/commands/match.ts`) — see [the bot guide's §7](rps-bot-author-guide.md#7-testing-your-bot-locally). -4. **Two `TournamentFormat` implementations** — round robin (Phase 7) and single elimination - (Phase 9), both in `packages/tournament-formats`, both concretely implementing the interface in - §3. -5. **Tournament orchestrator** (Phase 7) — `@thunderdome/engine`'s `runTournament()` +4. **Two `TournamentFormat` implementations** — round robin and single elimination, both in + `packages/tournament-formats`, both concretely implementing the interface in §3. +5. **Tournament orchestrator** — `@thunderdome/engine`'s `runTournament()` (`src/tournament-runner.ts`), the generic pull-loop that calls a match executor for each `readyMatches` entry and feeds results back into the format. Game/runtime-agnostic by construction: it takes a plain `(match) => Promise` callback rather than knowing anything about Docker or the registry itself. -6. **A CLI command for a whole tournament** (Phase 7) — `yarn thunderdome tournament run +6. **A CLI command for a whole tournament** — `yarn thunderdome tournament run [...]` (`apps/cli/src/commands/tournament.ts`), with `--tournament-config`'s `format` field choosing between the two implemented formats — see §2. -7. **Tournament persistence** (Phase 11) — every `run` writes a `TournamentRecord` +7. **Tournament persistence** — every `run` writes a `TournamentRecord` (`@thunderdome/tournament-store`) as it goes; `tournament list`/`inspect`/`replay` read it back. See §6. diff --git a/packages/engine/src/match-runner.ts b/packages/engine/src/match-runner.ts index ecf332d..789fdce 100644 --- a/packages/engine/src/match-runner.ts +++ b/packages/engine/src/match-runner.ts @@ -9,7 +9,7 @@ import type { /** * The generic per-match loop. Contains no game-specific logic and no knowledge of Docker, bot * processes, or any wire protocol — `ActionCollector` is the seam a real adapter fills in with - * real bot processes (`@thunderdome/runtime`'s `DockerActionCollector`, Phase 6); here it's just + * real bot processes (`@thunderdome/runtime`'s `DockerActionCollector`); here it's just * an interface, so this whole module is testable with a synthetic collector and a synthetic game * (see test/match-runner.test.ts). */ diff --git a/packages/game-sdk/src/index.ts b/packages/game-sdk/src/index.ts index dab47e2..bfbd71c 100644 --- a/packages/game-sdk/src/index.ts +++ b/packages/game-sdk/src/index.ts @@ -1,3 +1,4 @@ -// Phase 5 scope (see docs/adr/0005-observation-vs-game-state.md): GameDefinition author helpers -// and test utilities land here. The manifest schema below is real — everything else is Phase 5+. +// See docs/adr/0005-observation-vs-game-state.md: GameDefinition author helpers and test +// utilities are intended to land here eventually. The manifest schema below is real; nothing +// else in that scope has been built yet. export * from './manifest.js'; diff --git a/packages/runtime/src/lifecycle.ts b/packages/runtime/src/lifecycle.ts index 86f9846..090227e 100644 --- a/packages/runtime/src/lifecycle.ts +++ b/packages/runtime/src/lifecycle.ts @@ -20,8 +20,8 @@ import type { BotProcess, ExitInfo } from './bot-process.js'; * -> MATCH_END_SENT -> GRACE_PERIOD -> TERMINATED * * Deliberately game-agnostic and Docker-agnostic: it only knows about one `BotProcess` and the - * wire protocol. A future match-runner (Phase 5) drives N of these per match; this class never - * knows how many participants exist or what game is being played. + * wire protocol. `@thunderdome/engine`'s match-runner drives N of these per match; this class + * never knows how many participants exist or what game is being played. */ export type BotLifecycleState = 'spawning' | 'awaiting-ready' | 'running' | 'match-end-sent' | 'grace-period' | 'terminated'; diff --git a/packages/runtime/test/fixtures/minimal-bot/index.mjs b/packages/runtime/test/fixtures/minimal-bot/index.mjs index 62f2caa..4a42e1d 100644 --- a/packages/runtime/test/fixtures/minimal-bot/index.mjs +++ b/packages/runtime/test/fixtures/minimal-bot/index.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -// Minimal test bot for validating the runtime's Docker lifecycle end-to-end (Phase 4). This is +// Minimal test bot for validating the runtime's Docker lifecycle end-to-end. This is // a test fixture, not a competitive bot or an example for bot authors — it just speaks the // protocol correctly: acknowledge init, echo back an action for any awaited observation, and // exit cleanly on match-end. diff --git a/scripts/README.md b/scripts/README.md index b9c0df0..8c8d496 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -8,7 +8,7 @@ Dev tooling that isn't a workspace package in its own right. | `pack-bot-sdk.sh` | Builds `@thunderdome/bot-sdk` and vendors it into every bot that depends on it. See below. | Running a real match between bots from `/bots` is now `yarn thunderdome match run -` (`apps/cli/src/commands/match.ts`) — the registry-backed, Phase 6 successor to what used +` (`apps/cli/src/commands/match.ts`) — the registry-backed successor to what used to be a hand-rolled `run-scrimmage.mjs` script here. See [`docs/guides/rps-bot-author-guide.md`](../docs/guides/rps-bot-author-guide.md) for usage.