mancala/docs/specs/tutorial/design.md
Halit Aksoy 9522224bf9 docs(tutorial): add the spec
Requirements, design and tasks for a rules page, interactive lessons and a
practice game with hints against the Easy bot.
2026-10-16 14:46:50 +03:00

146 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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": "End your move in your store"
opponentReply?: number; // board index the opponent plays after a fulfilled move, if the lesson needs one
}
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[], player1ToMove: boolean, pit: number): MoveOutcome;
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" | "opponent" | "bot", t): string;
```
`getMoveOutcome` builds the position with `createGameFromBoard` (core history helpers), plays the pit and reads the
game steps, as `describeMove` does for the move list. `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: `Rule<Id>Title`, `Rule<Id>Text1..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. A scripted `opponentReply` is played after a short pause.
- `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.