Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 76 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <this-repo-url>
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
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/commands/cleanup.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/commands/match.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
21 changes: 10 additions & 11 deletions apps/cli/src/commands/tournament.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
6 changes: 3 additions & 3 deletions apps/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/lib/match-execution.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/test/tournament.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/examples/counter-bot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<your-bot-id> <opponent-id>` 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 <your-bot-id> <opponent-id> [...]` 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).
2 changes: 1 addition & 1 deletion docs/guides/examples/counter-bot/smoke-test.mjs
Original file line number Diff line number Diff line change
@@ -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.
//
Expand Down
14 changes: 7 additions & 7 deletions docs/guides/rps-bot-author-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 <botId> <botId>` (Phase 6)
**Against a real second bot, end to end.** `yarn thunderdome match run <botId> <botId>`
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
Expand Down Expand Up @@ -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/<your-bot-id>/` — mechanically enforced by
`tools/boundary-check` (`docs/adr/0007-repository-enforcement.md`; bots are grouped under
`bots/<game-id>/`, so a Rock-Paper-Scissors bot's directory is `bots/rock-paper-scissors/<id>/`).
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 <your-bot-id> <opponent-id>` — no index
to update, nothing else to run.
Loading
Loading