# Tutorial: design See `requirements.md`; requirement ids (R1.1, …) are referenced below. Builds on `web-redesign` (board, pages, themes) and `game-bot` (the Easy bot for the practice game). ## Decisions | Decision | Reason | |---|---| | Rules, lessons and hints are defined once in `packages/core` | The web and mobile apps show the same rules (R5.1) and the texts stay in one place. | | The "after" board of every example and the outcome of every lesson move come from running the engine (`mancala.js`), never written by hand | The texts and pictures cannot drift from what the game does (R1.3). A rule change shows up in the tutorial at once, and a test fails if a lesson no longer works. | | Lessons run entirely in the client on the TypeScript engine | No server, no login, no waiting (R2.5). The engine is already in every client. | | A lesson's task is an outcome ("end in your store"), not a pit | Players who find another move with the same effect are right too; the check is the engine's game steps, the same ones the move list uses. | | The practice game is an ordinary Easy bot game with hints on; hints are a client feature of any bot game | No new server state (R3.1). Hints only show the immediate effect of a move, which the board's preview mostly shows already, so they give little away; still, they are limited to bot games (R3.5). | | Hints look one move ahead only | They explain the rules, not strategy. Looking further would need the bot's search on the client or a new server message (out of scope). | | Progress, "not now" and the hints setting are kept in the device's storage | Enough for learning the rules (R2.6, R3.4, R4.1); accounts are not needed for any of it. | | Mobile gets the rules screen only for now | The mobile app still has the old board and game screen; lessons and hints there would be built twice (R5.2). | | The end-of-game lesson teaches the current rule | `TODO.md` (Game rules) asks whether the remaining stones should always go to the player whose side is empty. The tutorial follows the engine; because its boards come from the engine, a rule change only needs the texts updated. | ## The rules as taught (R1.3) Checked against `packages/mancala.js` (`Board.move`, the four `GR…` rules): 1. **Board and goal**: two rows of 6 pits with 4 stones each, a store on each side. Stones in your store are yours; at the end, more stones in your store wins (48 stones: 25 always wins). A draw is possible. 2. **Sowing**: take all stones from one of your pits; put the first back into the same pit and one into each pit after it. A single stone moves to the next pit. Your own store gets a stone on the way, the opponent's is skipped. 3. **Extra turn**: if the last stone lands in your store, you play again. 4. **Even capture**: if the last stone lands in an opponent's pit and makes its count even, you take all stones in it. 5. **Empty pit capture**: if the last stone lands in an empty pit of yours and the opponent's pit opposite has stones, you take both. 6. **End of the game**: the game ends when one side's pits are empty; the player whose side is empty gets the stones left on the other side. Then the stores are counted. 7. **Clock**: 5 minutes per player for the whole game; the clock of the player to move runs; when it reaches zero, that player loses. ## Shared code (`packages/core/src/tutorial/`) ```ts type RuleId = "board" | "sowing" | "extraTurn" | "evenCapture" | "emptyPitCapture" | "endOfGame" | "clock"; interface RuleExample { board: number[]; // 14 stone counts, player 1 (bottom) to move pit: number; // board index played } type LessonTask = "anyMove" | "extraTurn" | "evenCapture" | "emptyPitCapture" | "endGame"; interface Lesson { board: number[]; // player 1 (bottom) to move task: LessonTask; taskKey: string; // e.g. "LessonTaskExtraTurn": "Find the pit whose last stone lands in your store" } interface Rule { id: RuleId; titleKey: string; textKeys: string[]; // at most three sentences (R1.2) example?: RuleExample; lesson?: Lesson; // every rule except the clock (R2.1) } export const RULES: Rule[]; // The immediate effect of playing a pit, by running the engine on a copy of the board. interface MoveOutcome { path: number[]; // where the stones went (getSowingPath) extraTurn: boolean; capture?: "even" | "emptyPit"; captured: number; // stones taken from the opponent's side storeGain: number; endsGame: boolean; board: number[]; // after the move } export function getMoveOutcome(board: number[], pitIndex: number): MoveOutcome; // the mover follows from the pit export function fulfilsTask(task: LessonTask, outcome: MoveOutcome): boolean; // "Why did that happen" for a wrong lesson move and for the practice game (R2.3, R3.3). export function explainMove(outcome: MoveOutcome, mover: "you" | "bot", t): string; ``` `getMoveOutcome` sets the stones on a `CommonMancalaGame`, plays the pit and reads the game steps, as `describeMove` does for the move list. None of the six lessons needs the opponent to move, so lessons have no scripted replies (R2.4 has nothing to do yet). `describeMove` gains `capture` kind and `captured`, so the move list, hints and explanations share one reading of the steps. Lesson positions (player 1, bottom, to move; pits 0–5, store 6), checked with the rules above. Task 2 tests that each has a move that fulfils its task and, where there is more than one legal move, one that does not: | Lesson | Board | Task | Moves that fulfil it | |---|---|---|---| | Board and goal | start position | `anyMove` | all | | Sowing | `[4,4,1,4,4,4, 1, 4,4,4,4,4,4, 2]` | `anyMove`; the text points at pit 3's single stone, which moves to pit 4 | all | | Extra turn | start position | `extraTurn` | pit 4 | | Even capture | `[4,4,4,4,4,4, 0, 4,3,4,4,4,4, 1]` | `evenCapture` | pit 6: its last stone makes the opponent's 3 a 4 | | Empty pit capture | `[2,0,4,4,4,4, 3, 4,4,4,4,5,4, 2]` | `emptyPitCapture` | pit 1: its last stone lands in the empty pit 2, opposite 5 stones | | End of the game | `[0,0,0,0,0,1, 20, 2,3,1,0,2,1, 18]` | `endGame` | pit 6 (the only move): your side is empty and the 9 stones opposite are yours, 30–18 | Texts: `RuleTitle`, `RuleText1..3`, `LessonTask…`, `MoveExplain…`, `HowToPlay`, `Learn`, `Hints`, `NewToMangala`, `StartTutorial`, `NotNow`, `PracticeGame`, in `en.ts` and `tr.ts`. Storage helpers next to the rules: `getCompletedLessons` / `completeLesson`, `isTutorialBannerHidden` / `hideTutorialBanner`, `getHintsEnabled` / `setHintsEnabled`, `markGamePlayed`. ## Web app Routes: - `/rules`: `RulesPage` with `RulesView`. - `/learn`: the lesson list with progress, then the practice game button. - `/learn/:ruleId`: `LessonPage`. Components: - `RulesView`: the rules in order; each with its texts, a `RuleBoard` and "Try it" linking to the lesson (R1.5). - `RuleBoard`: two `MiniBoard`s, before and after the example move, the played pit and the landing pit marked. - `RulesPanel`: `RulesView` in a side panel (full screen below 600 px) over the game page; opened from a "How to play" button in the game page's header; the game keeps running behind it (R1.4). - `AppHeader`: a help icon button "How to play" on every page, going to `/rules` (or opening `RulesPanel` on the game page). - `LessonPage`: the rule's text and task above a `BoardView` on a local game (`createGameFromBoard`), animated with `PitAnimator` like a live game. On a move: `getMoveOutcome`, animate, then praise and "Next lesson" or `explainMove` and "Try again", which resets the board. - `TutorialBanner` on the home page for newcomers (R4). - Hints (R3): in `GamePage`, when the opponent is a bot and hints are on, `BoardView` gets a `hints` map (pit → "Extra turn" / "Captures 5"), drawn as a chip on the pit like the move preview; below the board a line with `explainMove` of the last move. A "Hints" switch in the game page's toolbar, only in bot games. - Practice game: the lesson list's last button sets hints on and goes to `/loby?bot=easy`. ## Mobile app `RulesScreen`: the rules' texts and a simple stone-count drawing of each example (before/after), reachable from `HelpScreen` and from the game screen's header (R5.1). ## Testing - Core: every lesson position fulfils its task with some move and (except `anyMove`) fails it with another; `getMoveOutcome` against the engine on random positions; every rule example produces the effect its rule is about (the even capture example really captures, etc.); `explainMove` for each kind of move in both languages; every text key exists in `en` and `tr`. - Web: build and the temporary type check. - End to end in Chrome: a newcomer sees the banner and "Not now" hides it; all six lessons, each first with a wrong move (explanation, reset) and then a right one; the list shows them done; the rules page and the rules panel during a game (the clock goes on); a practice game shows hints and explanations, and a game between two people shows none. - Mobile: type check and bundle; the rules screen on a device when one is available.