With hints on, the player's pits show whether their move gives an extra turn or how many stones it takes, and a line under the board says what the last move did and why. A Hints switch appears only in games against the bot.
147 lines
9.1 KiB
Markdown
147 lines
9.1 KiB
Markdown
# 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: `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.
|
||
- `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" / "Takes 5"), drawn as a chip above the pit; pits whose move does neither get no chip. Below
|
||
the board a line with `explainMove` of the last move. A "Hints" switch next to the turn indicator, only in bot
|
||
games; hidden while looking back at earlier moves.
|
||
- 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.
|