diff --git a/course/harness.md b/course/harness.md new file mode 100644 index 0000000..1133efb --- /dev/null +++ b/course/harness.md @@ -0,0 +1,72 @@ +# Harness contract + +The harness is the thin adapter between the course tests and your engine. Tests only ever call harness functions. TypeScript signatures are canonical; Rust uses the same names in snake_case on `pub struct Game` / `pub struct Baba` / `pub struct SpellGrid` in `src/harness.rs`, with `pub type Id = u32;`, `pub type Player = u8;`, and events as `pub enum GameEvent`. Rust has no default arguments, so optional parameters (`player`, `target`) are explicit there. + +Rust tests: `use engine::harness::*;`. TS tests: `from "../src/harness"`. + +Each module adds functions; `start-module` prints the line for the module you start. + +## Part I (`Game`): additions per module + +- M1: `newGame(opts?: { decks?: [string[], string[]] }): Game`; `summon(card: string, player?: 0|1): Id` (to right end of board); `addToHand(card, player?)`, `addToDeck(card, player?)`; `play(handId: Id): void` (Expansion Drop); `zoneOf(id): "deck"|"hand"|"board"|"graveyard"`; `board(player): Id[]` (left to right); `hand(player): Id[]`; `orderOfPlay(id): number`; `nameOf(id): string`. +- M2: `hero(player): Id`; `dealDamage(target: Id, amount: number): void` (one Sequence); `cast(card: string, player?, target?: Id): void`; `health(id)`, `attack(id)`, `armor(player)`; `log(): GameEvent[]`; `clearLog()`. +- M3: `destroy(target: Id): void` (marks pending-destroy, one Sequence). +- M5: `registerCards(defs: CardDef[]): void`; `assemble(base: string, donor: string, half: "top"|"bottom"): string` (registers and returns new card name `" (Assembled)"`; throws `AssembleError` if text unchanged). + +### Side by side (Part I) + +| Module | TypeScript | Rust (`impl Game` unless noted) | +|---|---|---| +| M1 | `newGame(opts?)` | `new_game(opts: GameOpts) -> Game` (free function) | +| M1 | `summon(card, player?): Id` | `summon(&mut self, card: &str, player: Player) -> Id` | +| M1 | `addToHand(card, player?)` | `add_to_hand(&mut self, card: &str, player: Player) -> Id` | +| M1 | `addToDeck(card, player?)` | `add_to_deck(&mut self, card: &str, player: Player) -> Id` | +| M1 | `play(handId)` | `play(&mut self, hand_id: Id)` | +| M1 | `zoneOf(id)` | `zone_of(&self, id: Id) -> Zone` | +| M1 | `board(player)` / `hand(player)` | `board(&self, player) -> Vec` / `hand(...)` | +| M1 | `orderOfPlay(id)` | `order_of_play(&self, id: Id) -> u32` | +| M1 | `nameOf(id)` | `name_of(&self, id: Id) -> String` | +| M2 | `hero(player)` | `hero(&self, player) -> Id` | +| M2 | `dealDamage(target, amount)` | `deal_damage(&mut self, target: Id, amount: i32)` | +| M2 | `cast(card, player?, target?)` | `cast(&mut self, card: &str, player: Player, target: Option)` | +| M2 | `health(id)`, `attack(id)`, `armor(player)` | `health`, `attack`, `armor` | +| M2 | `log()`, `clearLog()` | `log(&self) -> Vec`, `clear_log(&mut self)` | +| M3 | `destroy(target)` | `destroy(&mut self, target: Id)` | +| M5 | `registerCards(defs)` | `register_cards(&mut self, defs: Vec)` | +| M5 | `assemble(base, donor, half)` (throws `AssembleError`) | `assemble(...) -> Result` | + +### `GameEvent` + +Tagged by `type`: `Summoned{id}`, `Damaged{target, amount}`, `Triggered{source, trigger}` (`trigger`: `"deathrattle"|"battlecry"|"damaged"|...`), `Destroyed{id}` (removed in a Death Creation Step), `DeathPhase{round}`, `Drew{player, id}`, `Fatigue{player}`, `ArmorGained{player, amount}`, `StatsChanged{id, attack, health}`, `SpellCast{card}`. + +Drawing from an empty deck logs `Fatigue{player}` and does nothing else; the engine must not panic. + +## Part II (`Baba`): additions per module + +- M6: `newBaba(level: LevelDef): Baba`; `place(kind: string, x, y): Id` (kind `"baba"` or `"text:BABA"`); `rules(): string[]` (sorted, normalised `"BABA IS YOU"`, includes `"TEXT IS PUSH"`). +- M7: `hasProperty(id, prop: string): boolean`; `activeRules(): string[]` (after NOT/identity post-pass). +- M8: `move(dir: "up"|"down"|"left"|"right"): void` (movement only, then reparse); `posOf(id): [number, number] | null` (null once destroyed); `kindsAt(x, y): string[]` (sorted). +- M9: `step(dir): void` (full simplified turn); `won(): boolean`; `lost(): boolean` (no YOU objects remain). + +Grid coordinates: `x` grows right, `y` grows down; vertical sentences read top-to-bottom. + +### Side by side (Part II) + +| Module | TypeScript | Rust (`impl Baba` unless noted) | +|---|---|---| +| M6 | `newBaba(level)` | `new_baba(level: LevelDef) -> Baba` (free function) | +| M6 | `place(kind, x, y): Id` | `place(&mut self, kind: &str, x: i32, y: i32) -> Id` | +| M6 | `rules()` | `rules(&self) -> Vec` | +| M7 | `hasProperty(id, prop)` | `has_property(&self, id: Id, prop: &str) -> bool` | +| M7 | `activeRules()` | `active_rules(&self) -> Vec` | +| M8 | `move(dir)` | `move_dir(&mut self, dir: Dir)` (`move` is a Rust keyword) | +| M8 | `posOf(id)` | `pos_of(&self, id: Id) -> Option<(i32, i32)>` | +| M8 | `kindsAt(x, y)` | `kinds_at(&self, x: i32, y: i32) -> Vec` | +| M9 | `step(dir)` | `step(&mut self, dir: Dir)` | +| M9 | `won()`, `lost()` | `won(&self) -> bool`, `lost(&self) -> bool` | + +## Part III (`SpellGrid`) + +- M10: `newSpellGrid(level: LevelDef, cards: CardDef[]): SpellGrid` exposing all of Part II plus `cast`, `health`, `log`, `hero`, `won`. Minions are grid objects whose noun is their card's `"noun"` field. + +Rust: `new_spell_grid(level: LevelDef, cards: Vec) -> SpellGrid`. diff --git a/workspace-template/rust/.course.json b/workspace-template/rust/.course.json new file mode 100644 index 0000000..20045fb --- /dev/null +++ b/workspace-template/rust/.course.json @@ -0,0 +1 @@ +{ "track": "rust", "engineDirs": ["src/engine"], "contentDirs": ["src/content"], "module": "01" } diff --git a/workspace-template/rust/Cargo.toml b/workspace-template/rust/Cargo.toml new file mode 100644 index 0000000..e992026 --- /dev/null +++ b/workspace-template/rust/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "engine" +version = "0.1.0" +edition = "2021" + +[dependencies] +# serde_json arrives at Module 5; nothing needed yet. diff --git a/workspace-template/rust/README.md b/workspace-template/rust/README.md new file mode 100644 index 0000000..b4141fa --- /dev/null +++ b/workspace-template/rust/README.md @@ -0,0 +1,11 @@ +# Systemic Design Course: Rust workspace + +Your engine lives in `src/engine/` (generic rules machinery) and `src/content/` (card definitions; only this directory may name specific cards). `src/harness.rs` is the thin adapter the course tests call; wire its `todo!()` stubs to your engine. + +Tests live in `tests/` and use `use engine::harness::*;`. Get a module's tests with `node tools/start-module.mjs NN ` from the course repo. + +``` +cargo test +``` + +The crate has no dependencies yet. `serde_json` is added at Module 5. diff --git a/workspace-template/rust/src/content/mod.rs b/workspace-template/rust/src/content/mod.rs new file mode 100644 index 0000000..15ff0a2 --- /dev/null +++ b/workspace-template/rust/src/content/mod.rs @@ -0,0 +1 @@ +// Content: card definitions. Only this directory may name specific cards. diff --git a/workspace-template/rust/src/engine/mod.rs b/workspace-template/rust/src/engine/mod.rs new file mode 100644 index 0000000..8d7b366 --- /dev/null +++ b/workspace-template/rust/src/engine/mod.rs @@ -0,0 +1 @@ +// Engine code: generic rules machinery. Never name a specific card here. diff --git a/workspace-template/rust/src/harness.rs b/workspace-template/rust/src/harness.rs new file mode 100644 index 0000000..2e4c432 --- /dev/null +++ b/workspace-template/rust/src/harness.rs @@ -0,0 +1,58 @@ +//! Test harness. Module 1 surface only; later modules add functions +//! (see course/harness.md). Replace each `todo!()` by delegating to your engine. +#![allow(unused_variables)] + +pub type Id = u32; +pub type Player = u8; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Zone { + Deck, + Hand, + Board, + Graveyard, +} + +#[derive(Debug, Default, Clone)] +pub struct GameOpts { + pub decks: Option<[Vec; 2]>, +} + +pub struct Game {} + +pub fn new_game(opts: GameOpts) -> Game { + todo!() +} + +impl Game { + /// Put `card` at the right end of `player`'s board. + pub fn summon(&mut self, card: &str, player: Player) -> Id { + todo!() + } + pub fn add_to_hand(&mut self, card: &str, player: Player) -> Id { + todo!() + } + pub fn add_to_deck(&mut self, card: &str, player: Player) -> Id { + todo!() + } + /// Play a card from hand (Expansion Drop). + pub fn play(&mut self, hand_id: Id) { + todo!() + } + pub fn zone_of(&self, id: Id) -> Zone { + todo!() + } + /// Board ids, left to right. + pub fn board(&self, player: Player) -> Vec { + todo!() + } + pub fn hand(&self, player: Player) -> Vec { + todo!() + } + pub fn order_of_play(&self, id: Id) -> u32 { + todo!() + } + pub fn name_of(&self, id: Id) -> String { + todo!() + } +} diff --git a/workspace-template/rust/src/lib.rs b/workspace-template/rust/src/lib.rs new file mode 100644 index 0000000..f5868fb --- /dev/null +++ b/workspace-template/rust/src/lib.rs @@ -0,0 +1,3 @@ +pub mod content; +pub mod engine; +pub mod harness; diff --git a/workspace-template/ts/.course.json b/workspace-template/ts/.course.json new file mode 100644 index 0000000..ab4fb30 --- /dev/null +++ b/workspace-template/ts/.course.json @@ -0,0 +1 @@ +{ "track": "ts", "engineDirs": ["src/engine"], "contentDirs": ["src/content"], "module": "01" } diff --git a/workspace-template/ts/README.md b/workspace-template/ts/README.md new file mode 100644 index 0000000..3375f62 --- /dev/null +++ b/workspace-template/ts/README.md @@ -0,0 +1,10 @@ +# Systemic Design Course: TypeScript workspace + +Your engine lives in `src/engine/` (generic rules machinery) and `src/content/` (card definitions; only this directory may name specific cards). `src/harness.ts` is the thin adapter the course tests call; replace its `throw new Error("not implemented")` stubs with calls into your engine. + +Tests live in `tests/` and import from `"../src/harness"`. Get a module's tests with `node tools/start-module.mjs NN ` from the course repo. + +``` +npm install +npm test +``` diff --git a/workspace-template/ts/package.json b/workspace-template/ts/package.json new file mode 100644 index 0000000..8d2da89 --- /dev/null +++ b/workspace-template/ts/package.json @@ -0,0 +1,14 @@ +{ + "name": "systemic-design-workspace", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "^5.6.0", + "vitest": "^2.1.0" + } +} diff --git a/workspace-template/ts/src/content/index.ts b/workspace-template/ts/src/content/index.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/workspace-template/ts/src/content/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/workspace-template/ts/src/engine/index.ts b/workspace-template/ts/src/engine/index.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/workspace-template/ts/src/engine/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/workspace-template/ts/src/harness.ts b/workspace-template/ts/src/harness.ts new file mode 100644 index 0000000..d7f7bb3 --- /dev/null +++ b/workspace-template/ts/src/harness.ts @@ -0,0 +1,22 @@ +// Test harness. Module 1 surface only; later modules add functions +// (see course/harness.md). Replace each stub by delegating to your engine. + +export type Id = number; +export type Player = 0 | 1; +export type Zone = "deck" | "hand" | "board" | "graveyard"; + +export interface Game { + summon(card: string, player?: Player): Id; // to right end of board + addToHand(card: string, player?: Player): Id; + addToDeck(card: string, player?: Player): Id; + play(handId: Id): void; // Expansion Drop + zoneOf(id: Id): Zone; + board(player: Player): Id[]; // left to right + hand(player: Player): Id[]; + orderOfPlay(id: Id): number; + nameOf(id: Id): string; +} + +export function newGame(opts?: { decks?: [string[], string[]] }): Game { + throw new Error("not implemented"); +} diff --git a/workspace-template/ts/tsconfig.json b/workspace-template/ts/tsconfig.json new file mode 100644 index 0000000..2099df7 --- /dev/null +++ b/workspace-template/ts/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true + }, + "include": ["src", "tests"] +} diff --git a/workspace-template/ts/vitest.config.ts b/workspace-template/ts/vitest.config.ts new file mode 100644 index 0000000..00988cf --- /dev/null +++ b/workspace-template/ts/vitest.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { include: ["tests/**/*.test.ts"] }, +});