const ALL_BOARD_SIZES
ALL_BOARD_SIZES: readonly [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 19]
Every size any game here is played on, for the schemas at the API edge.
@johnmorrisdotca/narabe 1.1.0 · 3 entry points · 232 exports
@johnmorrisdotca/narabeALL_BOARD_SIZES availableOpenings Blocked BLOCKED BOARD_GRIDS BOARD_SIZES BoardGrid boardSizesFor BRAZILIAN_DRAUGHTS_RULES campOf campSize campSquares CANADIAN_CHECKERS_RULES canBeDrawn canChooseColour canExtendOpening canForfeit canGrowBoard canPass canShrinkBoard canSkip canSwapSeats canTwist canUndo CAPTURE_CHOICES CaptureChoice capturePaths Cell cellAt centreSquares checkersHasCapture CheckersRules chooseColour ColourRules COLUMN_LETTERS columnLetter createGame CROWN_MID_CAPTURE CrownMidCapture DEFAULT_BOARD_SIZE DEFAULT_CAPTURES_TO_WIN DEFAULT_SETTINGS DEFAULT_SWAPS_PER_SEAT defaultBoardFor DIRECTIONS discCount DRAW_LIMIT_LIST DRAW_LIMIT_MIN_POINTS DRAW_LIMIT_SHARE DRAW_LIMITS DrawLimit drawnByLength dropTarget emptyPoints endedWithNoMoves ENDGAME_COUNT_KINDS EndgameCount EndgameCountKind endsOnItsOwn ENGLISH_CHECKERS_RULES extendOpening findWinningLine FIRST_PLAYERS FIRST_STONE FirstPlayer flipsAt footprintAt footprintFits FORBIDDEN_PATTERNS forbiddenAt ForbiddenPattern forbiddenPoints forbiddenReason forfeitOnRecord forfeitTurn GAME_STATUS GameSettings GameState GameStatus groupAt growBoard Handicap HANDICAP_RULES HandicapRule HandicapTerms hasFlipMove hasHandicap HEAD_START_FREE_TURNS HeadStart HeadStartTurns HEX_LINE_DIRECTIONS Hot HOT indexOf inLayingPhase inMovePhase INTERNATIONAL_DRAUGHTS_RULES isDarkSquare isKingAt isLegalMove isOnBoard isStone KOMI landingPoints lastMove leavesNoStone legalPoints LINE_RULES lineDirectionsFor LineRule longestPossibleGame Move MOVE_KINDS MOVE_NARROWINGS MoveInput MoveKind MoveNarrowing movePiece movesBeforeDraw mustPass nextBoardSize NO_HANDICAP NO_HEAD_START NO_POINT noBoundReason NoBoundReason normaliseSettings OBSTACLE_LAYOUTS ObstacleLayout OPENING_CHOICE_EXTEND OPENING_RULE_LIST OPENING_RULES OPENING_STAGES OpeningChoice OpeningRule OpeningStage OpeningState orientations orientCells otherStone passesOwed passTurn Piece PIECE_PREVIEW PIECE_QUEUES PieceCell pieceMoves piecePlacements PieceQueue piecesHome PieceTally Placement PLACEMENTS placePiece playMove Point pointName pointOf POOL_CHECKERS_RULES previousBoardSize quadrantCount quadrantOrigin queuedPiece ReplayFacts replayGame replayMoves replayTimeline resign resolveOpener resolvePlacement rowNumber RULE_VARIANT_LIST RULE_VARIANTS rulesFor RuleVariant RUSSIAN_DRAUGHTS_RULES scoreArea Seat seatOf SEATS seatToPlay SECOND_STONE_EXCLUSIONS SEED_RANGE seededRandom shrinkBoard singlesLeft sizeForVariant skipMove skipTarget SlideMove slideWord STAR_POINTS STAR_RADIUS starCampOf starCampSize starPiecesHome starSize STARTING_DISCS StartingDiscs Stone STONELESS_WORDS stonelessWord STONES stonesLeft StoredGame swapSeats TRADITIONAL_HEAD_STARTS TraditionalHeadStart TURN_CHOICE_KINDS TurnChoiceKind turnChoices TurnChoices turnPassedBy Twist twistBoard undoMove upcomingPieces VARIANT_SPECS VariantSpec WIN_LENGTH WIN_LENGTHS WIN_REASONS winOnTime WinReason Worm WORM WRAP_MODES WrapMode
ALL_BOARD_SIZES: readonly [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 19]
Every size any game here is played on, for the schemas at the API edge.
availableOpenings(settings: GameSettings): OpeningRule[]
The openings these settings may use: what the variant offers, less the colour-swapping ones when a handicap is bound to a colour — and only the free opening under a head start. Pro and the renju protocols count stones from the first, and a head start's free turns and starting pieces are exactly what that count does not expect.
type Blocked = "blocked";
An intersection the rules have taken out of play. See obstacles.ts.
BLOCKED: "blocked"
BOARD_GRIDS: { readonly lines: "lines"; readonly cells: "cells"; }
Where a game's stones sit when it is drawn its own way: on the crossings, or in the squares.
BOARD_SIZES: readonly [9, 13, 15, 19]
Board is size × size. The mini boards make for much shorter games.
type BoardGrid = "lines" | "cells";
Where a game's stones sit when it is drawn the way it is traditionally played: on the crossings of the lines, as in go and gomoku, or inside the squares, as in tic-tac-toe, Othello and checkers.
A fact about the game's custom, not its rules: the same points exist either way, and the engine never reads it. It is on the spec all the same, because it is a fact about the GAME, and the one thing a reader's own preference cannot supply. Nothing infers it from the rules — tic-tac-toe and gomoku have the same mechanics and are drawn differently by everybody who has ever played them — so every row declares it, and a row that does not will not compile.
boardSizesFor(variant: RuleVariant): readonly number[]
The board sizes a variant plays on, smallest first — the order they are drawn in.
BRAZILIAN_DRAUGHTS_RULES: CheckersRules
Brazilian draughts: the international rules of capture and crowning on 8×8, with twelve men, and the draws of the Brazilian confederation's own rules (CBJD, Regras Oficiais) rather than the FMJD's 8×8 set, which differs. A third repetition (art. 98), and five moves each for the small endings of art. 99: two kings against two, two kings against one, two kings against a king and a man, a king against a king, a king against a king and a man. Its twenty-move kings-only count is in rules/noProgress.ts.
NOT APPLIED: art. 100, five moves for three pieces against a lone king on the long diagonal, for the same reason as the FMJD's version of it above. With it left out, those endings are bounded by the kings-only count instead.
campOf(size: number, point: Point): Stone | null
Whose home camp point lies in, or null for the open board.
campSize(size: number): number
Pieces a side has on size: the squares of its camp.
campSquares(size: number, stone: Stone): Point[]
The squares of a colour's home camp. Black starts top-left and white bottom-right, each camp the mirror of the other through the centre.
CANADIAN_CHECKERS_RULES: CheckersRules
Canadian checkers: the international rules on 12×12, thirty men a side in five rows. No federation's draw rules for it could be found — the Quebec association's own site did not answer — so its draws are the FMJD's, borrowed, and its rules page says so.
canBeDrawn(settings: GameSettings): boolean
Whether this game can be drawn at all.
Hex cannot. A full Hex board always holds exactly one chain from side to side, so there is no position in which neither player has won — and the rules page says so as a fact about the shape of the board rather than as a rule anybody wrote. A length that could produce a drawn Hex game would make that sentence false, so the length simply does not apply there, and the setting is refused rather than quietly ignored.
Read from the spec, never from a variant's name: any game that is won by joining two sides has the same theorem behind it.
canChooseColour(state: GameState): boolean
Whether the game is waiting on a seat to pick a colour.
canExtendOpening(state: GameState): boolean
Swap2 only: the first chooser may add two stones and pass the choice back.
canForfeit(state: GameState): boolean
Whether a recorded forfeit could have been written here: only where the clock had no pass to write instead. The replay's half of forfeitOnRecord, and the reason a forfeit where a pass was on offer is refused rather than read — it is a record the claim could not have made.
canGrowBoard(state: GameState): boolean
canPass(state: GameState): boolean
Whether passing is on offer: forced in a piece game with nothing to lay, free at any point in Go.
This, and not mustPass, is the question anything accepting a pass asks. A live game once asked only whether the pass was forced, which refused every chosen pass in Go — a person's click and a computer's choice alike — so a Go game on the server could never end by two passes, and a programs' game sat stuck with the engine offering a pass the server would not take.
canShrinkBoard(state: GameState): boolean
canSkip(state: GameState): boolean
What can be done to a game's record rather than to its position: burning a turn, lifting the last move back off, and reading a record forwards.
Lifted out of engine.ts, which had reached the File Size Gate's limit and been answered twice by trimming its comments — a file doing too many jobs does not do fewer of them because it is described in fewer words. These are a coherent one: none of them decides whether a move is legal, and nothing in the engine calls any of them. The dependency runs one way, from here into the engine, which is why this can import from it without a cycle.
canSwapSeats(state: GameState): boolean
Whether the seat to play may trade seats right now. This covers the mechanical limits only; analysis.ts adds the rule that you cannot swap into a position the opponent has already won. Not while an opening protocol is still settling who holds which colour, and never under a handicap, which belongs to a colour and would otherwise change hands with it.
canTwist(state: GameState): boolean
Whether the colour to move owes a quarter turn before the move is complete.
canUndo(state: GameState): boolean
CAPTURE_CHOICES: { readonly free: "free"; readonly maximum: "maximum"; }
type CaptureChoice = "free" | "maximum";
Which capture a checkers-family player may choose, when more than one is on offer.
free: any of them. Capturing is still forced, and a sequence once begun is still carried on while the same piece has another piece to take. maximum: only a sequence that takes the most pieces available anywhere on the board, men and kings counted alike — the majority rule of international draughts and the games built on it.
capturePaths(moves: readonly SlideMove[]): (Point[] | null)[]
The squares each capture has passed through so far, move by move, or null for a move that took nothing. A draughts multi-jump is kept as one move a hop, so its second hop reads the whole chain: g5, e3, then c1. John, 2026-09-24, from vint.ee's replays: "I found out how they do their moves".
Read from the ENGINE's moves (captured, continuedChain), which a replay of the record rebuilds; a stored move keeps neither.
type Cell = Stone | Blocked | Hot | Worm | null;
One intersection of the board: a stone, an obstacle, a hotspot, a wormhole, or nothing.
cellAt(state: GameState, point: Point): Cell
centreSquares(size: number): Point[]
The four centre squares, top-left first. Even boards only; the centre is between four cells.
checkersHasCapture(board: Cell[], kings: readonly Point[], size: number, stone: Stone, rules?: CheckersRules): boolean
Whether any of stone's pieces on the board has a capture available right now.
type CheckersRules = { /* * The draw rules below are the game's OWN — the ones its federation writes * down — and sit beside the site-wide no-progress backstop in * rules/noProgress.ts rather than replacing it, which is where the "only * kings have moved" count already lives for every game of the family. */ /** Rows of men each side starts with, on the dark squares of its own side of the board. */ menRows: number; /…
How one game of the checkers family is played, as data: the rows of men each side sets out, which way a man may take, how far a king may travel, which capture must be chosen, and what crowning does to a capture under way.
Every one of these is a rule some published game differs on, and the engine reads them here rather than asking which game it is playing.
chooseColour(state: GameState, stone: Stone): GameState
The deciding seat takes stone. Seats are exchanged if that colour is not already theirs; the board and the colour to move are untouched.
type ColourRules = { lineRule: LineRule; forbidden: readonly ForbiddenPattern[]; captures: boolean; stonesPerTurn: number; winLength: number; secondStoneExclusion: number; };
The rules one colour actually plays under: the variant's spec for that colour with the handicap laid over it. Everything in the engine that asks "may this colour…" reads one of these, never the spec directly.
COLUMN_LETTERS: "ABCDEFGHJKLMNOPQRSTUVWXYZ"
Column letters used in coordinate labels, left to right. "I" is skipped as in go.
columnLetter(col: number): string
Column letter, left to right: A, B, C ... with I skipped as on a go board.
createGame(overrides?: Partial<GameSettings>, roll?: number): GameState
CROWN_MID_CAPTURE: { readonly stops: "stops"; readonly continues: "continues"; readonly passes: "passes"; }
type CrownMidCapture = "stops" | "continues" | "passes";
What becomes of a man that reaches the far row in the middle of a capture.
stops: it is crowned, and the move ends there, whatever else it could take. continues: it is crowned at once, and goes on capturing as a king. passes: it is not crowned while it has more to take. It carries on as a man, and is crowned only if the move ends on the far row.
DEFAULT_BOARD_SIZE: 15
DEFAULT_CAPTURES_TO_WIN: 10
Enemy stones to capture for a win in the capture game: five pairs.
DEFAULT_SETTINGS: GameSettings
DEFAULT_SWAPS_PER_SEAT: 1
defaultBoardFor(variant: RuleVariant): number
The board a game OPENS on, where nothing else has said: its own defaultBoard if it has one, else the first of its boards.
Asked for by name rather than read off the front of the list, because the list is in numerical order and the two facts had been the same number by accident. Sorting the lists moved Halma's default from its own sixteen to the quick eight and Honeycomb's from the 91-cell board to the 37, and nothing would have said so.
DIRECTIONS: readonly Point[]
The four line orientations through a point. Each is checked in both its forward and reverse sense, so four entries cover all eight neighbours.
discCount(board: Cell[]): Record<Stone, number>
DRAW_LIMIT_LIST: readonly DrawLimit[]
DRAW_LIMIT_MIN_POINTS: 81
The smallest board a length means anything on, in points.
Nine by nine. Below it a game is over long before any share of the board could matter — a 3×3 has nine points and is finished in nine moves, so "half the board" is four, and cutting a game of noughts and crosses short at four moves is not a rule, it is a bug with a setting in front of it. The whole reason for a length is a board big enough that two careful players can fail to resolve it, and that starts here.
DRAW_LIMIT_SHARE: Record<DrawLimit, number | null>
DRAW_LIMITS: { readonly none: "none"; readonly half: "half"; readonly threeQuarters: "threeQuarters"; }
The shares of the board a game may be called a draw at.
A fraction rather than a number of moves, so one setting means the same thing on every board: half of a 9x9 is forty moves and half of a 19x19 is a hundred and eighty, and neither needs anybody to work it out. none is the default and is how every game here behaved before this existed.
type DrawLimit = "none" | "half" | "threeQuarters";
When a game nobody has won is called a draw.
Some of these games can run for ever between two careful players, and a board that never fills is a game neither side can leave. The limit is a share of the board's points rather than a number of moves, so it needs no arithmetic per size: the same setting means something sensible on 9x9 and on 19x19.
drawnByLength(state: GameState): boolean
Whether a finished game was drawn because it ran to its length, rather than because the board filled or both sides made a line at once.
The status line has to say which: "both made a line at once" is a different thing from "we agreed to stop here", and telling a player the wrong one is worse than telling them nothing.
dropTarget(board: Cell[], size: number, col: number): Point | null
Where a stone dropped into col lands, or null when the column is full.
emptyPoints(state: GameState): Point[]
Every intersection still open: no stone, no obstacle.
endedWithNoMoves(state: GameState): boolean
Whether the game ended because neither side had a move: two forced passes, the last two moves on the record.
ENDGAME_COUNT_KINDS: { readonly endings: "endings"; readonly balance: "balance"; }
type EndgameCount = | { kind: "endings"; endings: readonly (readonly [PieceTally, PieceTally])[]; restartsOnChange: boolean; movesEach: number; } | { kind: "balance"; pieces: readonly number[]; movesEach: number; };
An ending that must be won within so many moves or it is a draw, once each player has made movesEach more moves inside it.
endings: named pairings, either side holding either half — three kings against one king, a king and a man against a king. restartsOnChange says what a capture or a crowning that keeps the position inside the set does: lets the count run on (FMJD 6.3, the Brazilian confederation's art. 99), or starts it again, as a count "from when that balance arose" does.
balance: any ending of one of these numbers of pieces in which both sides have a king, counted while the pieces stay exactly as they are — a capture or a crowning always starts it again. The Russian federation's rule, which no list of pairings could state without naming dozens of them.
type EndgameCountKind = "endings" | "balance";
The two shapes an endgame count is written in — see EndgameCount.
endsOnItsOwn(settings: GameSettings): boolean
Whether the rules alone guarantee this game ends.
ENGLISH_CHECKERS_RULES: CheckersRules
English draughts, American checkers: three rows of men, a man takes forward only, a king moves one square, any capture may be chosen, and a man crowned by a capture ends the move there. Exactly what rules/checkers.ts did before any of this was data, and the Checkers tests that predate it still say so.
extendOpening(state: GameState): GameState
The chooser declines to choose and lays two more stones instead.
findWinningLine(board: Cell[], settings: GameSettings, point: Point): Point[]
The winning line through point, if the stone there completes one under the rule that applies to its colour. Only lines through the given point are examined, which is all that can change after a single move. Returns an empty array when there is no win.
FIRST_PLAYERS: { readonly black: "black"; readonly white: "white"; readonly random: "random"; }
FIRST_STONE: Stone
Black opens unless the settings say otherwise.
type FirstPlayer = Stone | "random";
Who opens. random is resolved once when the game is created — the engine stays pure by taking the roll as an argument, see resolveOpener.
flipsAt(board: Cell[], size: number, stone: Stone, point: Point, directions?: readonly Point[]): Point[]
The discs a stone of stone at point would turn. Empty means the move is illegal. directions is the square board's eight unless a caller says otherwise; everything that has a state to hand reads flipDirections.
footprintAt(orientation: readonly PieceCell[], anchor: Point): PieceCell[]
The absolute cells of an orientation anchored with its origin at anchor.
footprintFits(board: Cell[], size: number, cells: readonly PieceCell[]): boolean
Whether every cell of a footprint is on the board and empty.
FORBIDDEN_PATTERNS: { readonly doubleThree: "doubleThree"; readonly doubleFour: "doubleFour"; readonly overline: "overline"; }
forbiddenAt(board: Cell[], settings: GameSettings, stone: Stone, point: Point, depth?: number): ForbiddenPattern | null
Why stone may not play point, or null when it may. Only the patterns the variant forbids that colour are looked for, so this is cheap outside renju and omok.
type ForbiddenPattern = "doubleThree" | "doubleFour" | "overline";
Shapes a colour may be forbidden from making. See rules/forbidden.ts.
forbiddenPoints(state: GameState): Point[]
Every empty point the colour to move is forbidden from playing right now.
forbiddenReason(state: GameState, point: Point): ForbiddenPattern | null
Why the colour to move may not play point, when a forbidden shape is why.
forfeitOnRecord(state: GameState): GameState
A missed turn, as the position the record will replay it to.
Wherever the rules offer a pass, the missed turn IS that pass, with everything a pass does: in Go the second in a row ends the game by count, whether the first was chosen or missed. Where no pass is on offer it is forfeitTurn, the turn taken away and nothing decided — written as its own kind, because a pass there is one the rules refuse and a replay stops at. The claim writes whichever kind this settled, so the row and the replay cannot disagree.
forfeitTurn(state: GameState): GameState
Takes the turn away from the colour to move without a stone: the graceful penalty for a missed deadline. It is a FORFEIT on the record, not a pass, so a replay changes hands at the same point the game did — a pass the rules do not offer would stop the replay there instead. Unlike a pass in the piece games, two of these in a row do not end anything; the forfeit count does.
Where the rules DO offer a pass, a missed turn is that pass and not this: forfeitOnRecord in engine.ts decides, since only the engine knows.
GAME_STATUS: { readonly playing: "playing"; readonly won: "won"; readonly draw: "draw"; }
type GameSettings = { /** Board is `size` × `size` intersections. */ size: number; /** Stones in a line needed to win. */ winLength: number; variant: RuleVariant; opening: OpeningRule; handicap: Handicap; /** A start for one colour, see HeadStart. `NO_HEAD_START` for an even game. */ headStart: HeadStart; /** * The random seed the game was created with: it places dead and hot * squares and draws the piece queues, so…
type GameState = { settings: GameSettings; /** Row-major, `size * size` entries. See `indexOf` / `pointOf`. */ board: Cell[]; /** Every move played so far, in order. */ moves: Move[]; /** The colour that opened, kept so the game can be replayed from move zero. */ opener: Stone; /** Which seat currently holds each colour. Exchanged by `swapSeats`. */ seats: Record<Stone, Seat>; /** Swaps each seat has spent, counted …
type GameStatus = "playing" | "won" | "draw";
groupAt(board: Cell[], size: number, from: Point): { stones: Point[]; liberties: Set<number>; }
The connected group of stones sharing the colour at from, and the empty points touching any of them — its liberties. A group with none is what capturing takes off the board.
growBoard(state: GameState): GameState
Returns the game on a larger board, or the state unchanged when it cannot grow. Every stone, every recorded move and the winning line all move together, so the record still replays to the position on the screen.
type Handicap = { stone: Stone | null; doubleThree: boolean; doubleFour: boolean; overline: boolean; exactLine: boolean; openLine: boolean; longerLine: boolean; singleStone: boolean; noCaptures: boolean; secondStoneExclusion: number; };
Extra restrictions one colour plays under, so a stronger player can give a weaker one a fair game. Every item is a rule some variant already imposes on a colour, applied here on top of whatever the variant says. A handicap belongs to a colour, not a seat, so seat swaps are off while one is set.
doubleThree / doubleFour / overline: shapes this colour may not make. exactLine: this colour's overline is not a win. openLine: this colour's winning line must not be shut in at both ends. longerLine: this colour needs one more stone in a row. singleStone: one stone a turn where the variant gives two. noCaptures: this colour does not capture, in the capture variants. secondStoneExclusion: this colour's second stone must land outside the central square of this half-width (2 for 5×5, 3 for 7×7); 0 for none.
HANDICAP_RULES: readonly ["doubleThree", "doubleFour", "overline", "exactLine", "openLine", "longerLine", "singleStone", "noCaptures"]
The handicap toggles, in the order the settings list them.
type HandicapRule = Exclude<keyof Handicap, "stone" | "secondStoneExclusion">;
The toggles of a handicap, without the colour that carries them.
type HandicapTerms = Pick<GameSettings, "handicap" | "headStart">;
EVERYTHING THAT MAKES A GAME UNEVEN ON PURPOSE, as hasHandicap reads it: the per-colour handicap and the head start. Every question that takes it — whether a rating may move, on the pages, the set-up screen and the writers — has to be handed both, so none of them can go on rating a game with either.
hasFlipMove(state: GameState, stone: Stone): boolean
hasHandicap(settings: HandicapTerms): boolean
Whether any handicap is in force for anyone: harder rules for one colour, or a head start for one.
THE ONE PLACE THAT QUESTION IS ANSWERED, and a rating depends on it: a game this is true of moves nobody's rating (handicapRefusal). The head start joined here and in HandicapTerms, and was refused a rating without anybody having to find the rating code.
HEAD_START_FREE_TURNS: readonly [0, 1, 2, 3]
The free turns a head start may give, none included. John chose one to three.
type HeadStart = { stone: Stone | null; freeTurns: number; traditional: number; };
A start given to one colour — the weaker player's — before the game is even.
freeTurns: turns this colour takes at the start with nothing played between them, 0 to 3. Each is recorded as the other colour's pass, so a replay reaches the same position from the move list alone. traditional: how much of the game's own traditional head start this colour is given — handicap stones, corners, or the other side's men taken off — as its spec's headStart names; 0 for none. See rules/headStart.ts.
The opposite colour from a handicap's, by nature: a handicap makes the stronger side's game harder, a head start makes the weaker side's easier.
type HeadStartTurns = 0 | 1 | 2 | 3;
The most free turns a game offers as a head start: 0 where it offers none. Declared per game and measured, never inferred — see headStartTurns on the spec and simulation.headStartDecides.ts.
HEX_LINE_DIRECTIONS: readonly Point[]
The three line orientations on the hexagon lattice — see rules/hexagon.ts for the embedding. Each is checked both ways, covering the six neighbours a hexagon cell has. Three of the square board's own four: horizontal, vertical and the slanted diagonal that follows the lattice's shear. The fourth, {row: 1, col: 1}, is a diagonal of the SQUARE embedding only — two cells that far apart along it are not lattice neighbours, and no unbroken run of hexagon cells ever lies along it — so it is left out here on purpose, never scanned as a line.
type Hot = "hot";
A hotspot: an intersection that counts as either colour's stone in a line.
HOT: "hot"
indexOf(size: number, point: Point): number
inLayingPhase(state: GameState): boolean
Classic reversi: the first four discs are laid by the players, in the centre, with no flipping.
inMovePhase(state: GameState): boolean
Whether the colour to move has all its pieces down and must now slide one.
INTERNATIONAL_DRAUGHTS_RULES: CheckersRules
International draughts, from the FMJD's official rules (Annex 1, 2018, and the 2024 Annexes). Men take both ways (4.1), kings fly (3.9, 4.3), the capture taking the most pieces is compulsory with a king counting as one piece (4.13), a man crossing the far row mid-capture stays a man (4.15), and taken pieces come off only once the capture is over and may not be jumped twice (4.8, 4.11).
The draws of article 6: a third repetition with the same side to move (6.1); three pieces, one at least a king, against a lone king, sixteen more moves each (6.3); two kings, a king and a man, or a king against a lone king, five more moves each (6.4). The twenty-five-move kings-only count (6.2) is the no-progress rule in rules/noProgress.ts.
NOT APPLIED: the 2024 clause that cuts 6.3's sixteen moves to five when the lone king "solely occupies" the long diagonal. The text does not say whether the king must hold the diagonal from the start of the count or at its end, nor what leaving it does, and a rule this site cannot read exactly must not fire. Without it those endings run to sixteen moves each, which is the older rule and the generous side of the new one.
isDarkSquare(point: Point): boolean
Checkers is played on one colour of square only: the board's own dark squares.
isKingAt(kings: readonly Point[], at: Point): boolean
Whether the piece at at, if any, is a king.
isLegalMove(state: GameState, point: Point): boolean
Whether the colour to move may play point: on the board, empty, allowed by the opening, not a shape the variant forbids that colour, the landing cell of its column in a drop game, and not while a twist or a slide is owed.
isOnBoard(size: number, point: Point): boolean
isStone(cell: Cell): cell is Stone
Narrows a cell to a played stone, excluding empties and obstacles.
KOMI: 6.5
Go: stones never move once played, and nothing about a line ever decides anything. The whole of the game is in four things no other variant here needs — a group's liberties, capturing by taking the last one, the ko rule that stops an instant recapture undoing the position it just made, and, at the end, counting the board rather than reading a line on it.
The standard bonus for playing second — komi — is fixed rather than a setting, at the usual 6.5: a half point so the count can never tie.
landingPoints(board: Cell[], size: number): Point[]
The one landing cell per column with room: the only cells a drop can fill.
lastMove(state: GameState): Point | null
leavesNoStone(kind: string | undefined): boolean
Whether a recorded move put nothing on the board: a pass, or a turn the clock took away. Neither has a point — both sit at -1, -1 — so anything reading a move's row and column asks this first.
Not the question "was this a pass". Two passes in a row end a game, and a forfeit before a pass is not the first of two: losing a turn on time says nothing about whether anybody could move. Code asking THAT compares with MOVE_KINDS.pass itself.
legalPoints(state: GameState): Point[]
Every intersection the colour to move may play right now.
LINE_RULES: { readonly atLeast: "atLeast"; readonly exact: "exact"; readonly exactOpen: "exactOpen"; }
lineDirectionsFor(hexagon: boolean): readonly Point[]
The directions a winning line may run: the hexagon's three axes on a hexagon board, the square's four otherwise.
type LineRule = "atLeast" | "exact" | "exactOpen";
What a completed line has to look like to win.
atLeast: winLength or longer. exact: precisely winLength; an overline is not a win. exactOpen: precisely winLength, and not shut in at both ends.
longestPossibleGame(settings: GameSettings): number | null
The most moves a game of these settings can contain, or null when nothing in the rules bounds it.
Read from the spec rather than from a list of variant names, so a game added tomorrow is classified the day it lands.
type Move = Point & { /** The colour of the stone placed. */ stone: Stone; /** The colour that moved, when the game lets a mover place the other colour. */ by?: Stone; kind: MoveKind; /** Opponent stones this move took off the board, in the capture variants. */ captured?: Point[]; /** Where a moving piece came from. Only on `move` kinds. */ from?: Point; /** The twist that finished this move, once it has been made. …
MOVE_KINDS: { readonly place: "place"; readonly skip: "skip"; readonly move: "move"; readonly piece: "piece"; readonly pass: "pass"; readonly forfeit: "forfeit"; }
MOVE_NARROWINGS: { readonly capture: "capture"; readonly mostCaptured: "mostCaptured"; }
type MoveInput = Point & { /** As a record stores it: a string, checked against MOVE_KINDS where it matters. */ kind?: string; /** The colour placed, where the mover chose it. */ stone?: string; from?: Point; twist?: Twist; cells?: PieceCell[]; };
The shape of a move as a record or a request carries it, without the colour.
type MoveKind = "place" | "skip" | "move" | "piece" | "pass" | "forfeit";
place: an ordinary stone. skip: a deliberately wasted move, dropped on the emptiest corner. move: a piece stepping from from to the move's point, in the games where a fixed handful of pieces move once they are all down. piece: a multi-cell piece from the queue, laid as cells. pass: a turn taken without a stone that the rules offered — nothing fit, or Go, where passing is always a choice. A missed deadline is a pass too wherever a pass is on offer, since there it IS one. forfeit: a turn taken away by the clock where the rules offer no pass. Only a claimed timeout writes one, and a replay applies it only to a record that ran a clock. Kept apart from pass because the two replay differently: a pass the rules refuse stops a replay, and a forfeit read as a pass would stop every game with a timeout in it at the turn that was lost. Neither has a point; their row and column are -1.
type MoveNarrowing = "capture" | "mostCaptured";
A rule that has narrowed the moves on offer below what the pieces could make: a capture that must be made, or carried on, instead of a step; or — where the game takes the most — the capture taking the most pieces, over shorter ones the pieces also have.
movePiece(state: GameState, from: Point, to: Point): GameState
Slides a piece one step, or in checkers a step or a capture. Illegal moves return the state unchanged.
movesBeforeDraw(settings: GameSettings): number | null
How many moves may be played before a game nobody has won is a draw, or null when it is to be played out.
Rounded down, so "half the board" on an odd board is the smaller half: a limit that arrives a move early is easier to defend than one that arrives a move late.
mustPass(state: GameState): boolean
Whether the colour to move has nothing it may play. Then the turn passes, on the record, rather than the game stopping where it stands.
Gated to piece games once, so a stone game reaching the same condition fell through it: a handicap forbids shapes to one colour, and the last point on a board can be a shape that colour may not make. The board then never fills, the draw never comes, and neither can move. Passing decides nothing.
nextBoardSize(size: number, sizes?: readonly number[]): number | null
The next size up, or null when the board is already the largest. A game played on boards of its own — the flipping games, on 4, 6 and 8 — grows through its own list rather than the go sizes.
NO_HANDICAP: Handicap
NO_HEAD_START: HeadStart
Nobody given a start: the even game, which is most games.
NO_POINT: Point
A pass has no point on the board.
noBoundReason(settings: GameSettings): NoBoundReason | null
What stops this game being bounded by its board, or null when nothing does.
Order matters only for the answer given to a reader: a game that both moves pieces and captures is unbounded for the first reason, which is the stronger one — captures at least run material down, and sliding does not.
type NoBoundReason = "pieces-move" | "captures" | "line-clear";
Why a game has no bound, or null when the board is its bound.
normaliseSettings(settings: GameSettings): GameSettings
Settings that agree with their variant: a pinned line length wins over the player's choice, an opening the variant does not offer falls back to free, and a handicap rules out the openings that swap colours. Applied on creation so a state can never carry a contradiction.
OBSTACLE_LAYOUTS: { readonly none: "none"; readonly hoshi: "hoshi"; }
type ObstacleLayout = "none" | "hoshi";
none: every intersection is playable. hoshi: the star points are blocked, except tengen at the centre.
OPENING_CHOICE_EXTEND: "extend"
The one opening choice that is not a colour.
OPENING_RULE_LIST: readonly ["free", "pro", "longPro", "swap", "swap2", "rif", "sakata", "tarannikov"]
OPENING_RULES: { readonly free: "free"; readonly pro: "pro"; readonly longPro: "longPro"; readonly swap: "swap"; readonly swap2: "swap2"; readonly rif: "rif"; readonly sakata: "sakata"; readonly tarannikov: "tarannikov"; }
OPENING_STAGES: { readonly placing: "placing"; readonly choosing: "choosing"; readonly extending: "extending"; readonly done: "done"; }
type OpeningChoice = Stone | "extend";
A decision taken during the opening: a colour, or two more stones.
type OpeningRule = | "free" | "pro" | "longPro" | "swap" | "swap2" | "rif" | "sakata" | "tarannikov";
How the first stones go down. Everything after the opening is the variant's business; these only shape the start, to blunt black's first-move advantage.
free: anywhere, any order. pro / longPro: black opens at tengen and black's second stone must leave the central 5×5 (7×7 for long pro). swap: seat one places three stones, seat two picks a colour. swap2: as swap, but seat two may instead add two stones and hand the choice back. rif: the classic renju opening — centre, then inside the 3×3, then inside the 5×5, after which white may swap colours. sakata: the RIF start and swap, and then the fifth stone must land inside the central 7×7. tarannikov: the first five stones must land inside the central 1×1, 3×3, 5×5, 7×7 and 9×9 in turn, and after each of them the other seat may swap.
type OpeningStage = "placing" | "choosing" | "extending" | "done";
Where a swap-style opening stands. placing and extending are stretches where one seat lays every stone regardless of colour; choosing is a pause where no stone is legal until the deciding seat has picked a colour.
type OpeningState = { stage: OpeningStage; /** The seat acting outside the normal turn order, if any. */ actor: Seat | null; /** Every decision so far, so a stored game can be replayed through them. */ choices: OpeningChoice[]; };
orientations(piece: Piece): PieceCell[][]
Every distinct way the piece can lie: four rotations, each also mirrored. Colours travel with their cells, so a black-white domino turned round is a different orientation from the one it started as.
orientCells(piece: Piece, turns: number, flipped: boolean): PieceCell[]
The piece turned turns quarter turns and, if asked, mirrored — the shape a player has in hand.
otherStone(stone: Stone): Stone
passesOwed(state: GameState): GameState
The position after every pass the rules force: none when the colour to move has something to play, one when only it is stuck, and two — ending the game — when neither side can move. Never a pass for a colour with a move, and never Go's, which is a choice.
passTurn(state: GameState): GameState
Takes a turn without a stone. Two passes end a piece game as a draw; in Go they end it by area count instead, since passing there is a real choice, not a sign nobody can move. A replay passes at the same point either way.
type Piece = { cells: readonly PieceCell[]; };
A piece from the queue: its cells relative to the top-left of its bounding box.
PIECE_PREVIEW: 3
How many queued pieces a player is shown ahead of the one in hand.
PIECE_QUEUES: { readonly domino: "domino"; readonly tetro: "tetro"; }
type PieceCell = Point & { stone: Stone };
One cell of a multi-cell piece, with the colour it carries.
pieceMoves(state: GameState, from: Point): Point[]
Where a piece of the colour to move may step from from; empty if it may not move.
piecePlacements(state: GameState): PieceCell[][]
Every legal footprint of the piece in hand. Empty when nothing fits.
type PieceQueue = "domino" | "tetro";
Which queue of pieces a game draws from.
piecesHome(board: Cell[], size: number, stone: Stone): number
How many of stone's pieces stand in the far camp.
type PieceTally = { kings: number; men: number };
A side's pieces, as an endgame count names them.
type Placement = "free" | "drop" | "edge";
Where a stone goes when played. free: where it was put. drop: it slides to the lowest empty cell of its column, as if the board were upright and the stones were magnetic.
PLACEMENTS: { readonly free: "free"; readonly drop: "drop"; readonly edge: "edge"; }
placePiece(state: GameState, cells: readonly PieceCell[]): GameState
Lays the piece in hand on cells. The cells must be that piece in some orientation, on empty points. A piece carries both colours, so it can finish a line for either side: one line wins for its owner, whoever laid it; a line for each is a draw.
playMove(state: GameState, where: Point, kind?: Move["kind"], chosen?: Stone | null): GameState
Plays the stone to move at point. Illegal moves (occupied intersection, obstacle, off the board, forbidden shape, outside the opening, or game already over) return the state unchanged.
type Point = { row: number; col: number; };
Zero-based board coordinates. Row 0 is the top, column 0 is the left.
pointName(size: number, point: Point): string
Renju-style name for an intersection, e.g. "H8" for the centre of a 15×15 board.
pointOf(size: number, index: number): Point
POOL_CHECKERS_RULES: CheckersRules
Pool checkers, from the American Pool Checker Association's Tournament Rules of Play (2016): men take both ways (rule 14), kings fly (15, 18), any capture may be chosen — "not compelled to take the greater or lesser number" (20) — and a capture once begun is completed (21). A man that must jump on out of the king row stays a man, and one whose move ends there is crowned (22, 23). Black moves first (7).
The one count of the APCA's this site can read is the thirteen count (27): three kings against a lone king, all four kings, drawn once the lone king has made thirteen moves. Counted here as thirteen moves each, which is that exactly when the lone king moves second in the ending and one move later for the stronger side when it moves first — the generous side. There is no repetition rule: the APCA has none outside its thirty-move rule.
NOT APPLIED: the thirty-move rule (26), which the weaker side announces and counts, in endgames the players themselves identify. Nobody announces anything here, and a count that fired unasked would be a different rule. Nor the five-move count for a lone king on the long line (28), as above. A game going nowhere is ended instead by the site's own forty-move count in rules/noProgress.ts, the same as Checkers', and the rules page says so.
previousBoardSize(size: number, sizes?: readonly number[]): number | null
The next size down, or null when the board is already the smallest.
quadrantCount(size: number, quadrantSize: number): number
How many quadrants a board of this size holds.
quadrantOrigin(size: number, quadrantSize: number, quadrant: number): Point
The top-left corner of quadrant quadrant.
queuedPiece(state: GameState): Piece | null
The piece the colour to move must lay next, or null outside the piece games.
type ReplayFacts = { clocked: boolean; };
What a replay needs to know about a record that its moves cannot say.
clocked: the game ran a clock, so a turn lost to it may be on the record. False for anything that never had one — a board at one screen, a filed game from a browser — and there a forfeit is refused like any other move the rules could not have produced.
replayGame(game: StoredGame): GameState
The position as it stands now.
replayMoves(start: GameState, moves: readonly MoveInput[], choices?: readonly OpeningChoice[], facts?: ReplayFacts): GameState[]
Replays a record through the engine: every position it passed through, including the ones a swap-opening decision produced. When the record runs out of decisions while a choice is pending, the chooser is assumed to have kept their colour, which is all a store without seat data can say.
Stops at the first move that will not replay, since the record no longer fits the rules from there. facts says what the moves cannot: a record read without them is taken to have had no clock, and a forfeit on it stops the replay like any other move the rules refuse.
replayTimeline(game: StoredGame): GameState[]
Rebuilds every position a game passed through by replaying its moves.
A stored game is a move list, never a board, so this is the only way to read one back — and because it runs the same engine, a replayed game and a live one can never disagree about what is legal or who has won.
Swap-opening decisions are not stored, so a game with one replays as though the chooser kept their colour: the stones are the same either way.
resign(state: GameState, loser: Stone): GameState
Gives the game up. The other colour wins at once, with no line to show; the record keeps the moves as they were and says why it ended. A finished game cannot be resigned — there is nothing left to give.
resolveOpener(settings: GameSettings, roll?: number): Stone
The colour that opens. random is decided by roll, a number in [0, 1), which the caller supplies so this stays pure and testable. Variants that constrain black, and every opening protocol, put black on move one.
resolvePlacement(state: GameState, point: Point): Point
Where a stone played at point actually goes: the same point, or in a drop game the bottom of its column. A click anywhere in a column is a play in that column.
rowNumber(size: number, row: number): number
Row number counted from the bottom edge, so the bottom row is 1.
RULE_VARIANT_LIST: readonly ["freestyle", "standard", "renju", "omok", "caro", "ninuki", "sannuki", "connect6", "misereFive", "toroidalFive", "obstacleFive", "scatteredRocks", "rockfall", "makerBreaker", "dominoFive", "blockFive", "dropFour", "ringDrop", "holeDrop", "hotDrop", "clearDrop", "giveawayDrop", "edgeDrop", "wormDrop", "twistFive", "twistFour", "trapThree", "squareFour", "tictactoe", "wildTicTacToe", "nota…
The variants in the order the browser and the filters list them.
RULE_VARIANTS: { readonly freestyle: "freestyle"; readonly standard: "standard"; readonly renju: "renju"; readonly omok: "omok"; readonly caro: "caro"; readonly ninuki: "ninuki"; readonly connect6: "connect6"; readonly tictactoe: "tictactoe"; readonly trapThree: "trapThree"; readonly dropFour: "dropFour"; readonly twistFive: "twistFive"; readonly twistFour: "twistFour"; readonly squareFour: "squareFour"; readonly ri…
rulesFor(settings: GameSettings, stone: Stone): ColourRules
The rules a colour plays under: the variant's spec for that colour, with the handicap laid over it when the handicap belongs to that colour.
A handicap can only make a colour's game harder. It adds forbidden shapes, tightens the line rule, lengthens the line, cuts the stones per turn and removes captures; it never loosens anything the variant already imposes.
type RuleVariant = | "freestyle" | "standard" | "renju" | "omok" | "caro" | "ninuki" | "connect6" | "tictactoe" | "trapThree" | "dropFour" | "twistFive" | "twistFour" | "squareFour" | "ringDrop" | "holeDrop" | "hotDrop" | "clearDrop" | "giveawayDrop" | "edgeDrop" | "dominoFive" | "blockFive" | "sannuki" | "wormDrop" | "misereFive" | "makerBreaker" | "wildTicTacToe" | "notakto" | "toroidalFive" | "reversi" | "classic…
The named rule sets. Each is described as data in VARIANT_SPECS, so the engine reads a spec rather than switching on the name.
freestyle: five or more in a row wins. standard: exactly five wins; an overline (six or more) does not. renju: black is forbidden the double three, double four and overline. omok: the double three is forbidden for both sides; overlines win. caro: exactly five wins, and not when blocked at both ends. ninuki: five in a row wins, and so does capturing five pairs. connect6: two stones a turn, six in a row wins.
RUSSIAN_DRAUGHTS_RULES: CheckersRules
Russian draughts (shashki), from the Russian Draughts Federation's rules (ФШР, shashki.ru) and the FMJD/IDF rules for 8×8 draughts: men take both ways, kings fly, any capture may be chosen whatever it takes (FMJD-64 4.13), and a man that reaches the far row in the middle of a capture is crowned there and carries on capturing as a king (4.14).
Draws, as the federation writes them: a third repetition with the same side to move; three kings or more that have not taken a lone king by their fifteenth move, counted from when that balance arose; and any ending in which both sides have a king and nothing is taken or crowned for five moves (two or three pieces on the board), thirty (four or five) or sixty (six or seven). Fifteen moves of kings alone is rules/noProgress.ts.
NOT APPLIED: the five-move count for three pieces against a lone king on the main road, for the reason given at INTERNATIONAL_DRAUGHTS_RULES; the "clearly drawn position", which is an arbiter's judgement and not a count; and the three-kings rule's "or kings and men", which the federation's own text leaves unclear. It is read as kings alone, the narrower reading, so it never draws a game it might not apply to.
scoreArea(board: Cell[], size: number): { black: number; white: number; }
The area score: every stone on the board, plus every empty region whose only neighbours are one colour. An empty region touching both colours, or touching neither (an empty board), counts for nobody — dame, not territory. Dead stones are not marked or removed here: a stone left on the board still counts as a stone, so a player who wants credit for territory a stray stone sits in has to capture it before passing, the same as playing it out at the real table.
type Seat = "one" | "two";
The two people at the board. Seats are distinct from stone colours because swapSeats exchanges them mid-game — see GameState.seats.
seatOf(state: GameState, stone: Stone): Seat
The seat holding stone right now. Swaps move seats between colours.
SEATS: { readonly one: "one"; readonly two: "two"; }
seatToPlay(state: GameState): Seat
The seat whose turn it is. During a swap opening one seat lays every stone and then the other decides, whatever colour those stones are.
SECOND_STONE_EXCLUSIONS: readonly [0, 2, 3]
Half-widths of the central square a handicapped second stone must leave.
SEED_RANGE: number
Seeds are 31-bit integers, small enough for every store and reproducible everywhere.
seededRandom(seed: number): () => number
A small seeded random number generator, so anything a game decides by chance — where the dead squares fall, which pieces come next — is fixed by the seed stored with the game and comes out the same on every replay.
Mulberry32: fast, tiny, and good enough for a board game. The seed is a 31-bit integer.
shrinkBoard(state: GameState): GameState
Returns the game on a smaller board, or the state unchanged when the ring that would be removed is in use. Everything shifts inwards by the same offset growing shifts out by, so the stones keep their positions relative to each other and the centre stays the centre.
singlesLeft(state: GameState): number
Single stones the colour to move may still lay instead of a piece.
sizeForVariant(variant: RuleVariant, size: number): number
The board this variant will actually be played on, given a size somebody asked for. A game with a board of its own gets that board.
normaliseSettings has always done this when it builds a state, so the board a player sees was never wrong. What could be wrong was the row: a Reversi game could be stored at 19×19, shown as 19×19 on its page and in its record, and played on the 8×8 board Reversi actually has. Anything writing a size to the database asks here first, so the row and the board cannot disagree.
skipMove(state: GameState, roll?: number): GameState
Burns the turn on a corner stone. A no-op when skipping is not allowed.
skipTarget(state: GameState, roll?: number): Point | null
Where a skipped turn puts its stone: the open intersection furthest from the action, picked from the corner chosen by roll. A skip is still a stone on the board — it just spends the turn somewhere that should not matter.
type SlideMove = { kind: string; row: number; col: number; from?: Point; /** The pieces this move took, where the engine replayed it (a draughts jump takes one). */ captured?: readonly unknown[]; /** A later hop of one piece's multi-jump: it starts where the last capture landed. */ continuedChain?: boolean; };
What a record needs of a move to say whether it slid, jumped, or went on jumping.
slideWord(squares: readonly string[], capture: boolean): string
A slide as the draughts records write it: square to square with an arrow, a capture with a colon (g5:e3), and a multi-jump as every square it landed on (g5:e3:c1). The colon is the draughts convention vint.ee and Russian notation use; pdn.ts writes a file's own separators.
STAR_POINTS: Record<number, readonly Point[]>
Hoshi (star point) positions drawn on the board, by board size: the four corner points, plus tengen at the centre, and for 19×19 the side points too.
STAR_RADIUS: 4
How many rows deep each of the star's six points is. The board's centre hexagon has the same radius, and the standard 121-hole set is radius 4: a 61-cell hexagon plus six 10-cell points.
starCampOf(radius: number, point: Point): Stone | null
Whose home point point lies in, or null outside both.
starCampSize(radius: number): number
Pieces a side has: the cells of one point, ten on the standard board.
starPiecesHome(board: Cell[], size: number, radius: number, stone: Stone): number
How many of stone's pieces stand in the far point.
starSize(radius?: number): number
The side of the square array a star of this radius is embedded in.
STARTING_DISCS: { readonly none: "none"; readonly fixed: "fixed"; readonly laid: "laid"; }
type StartingDiscs = "none" | "fixed" | "laid";
How a flipping game begins: nothing, the fixed four, or four the players lay themselves.
type Stone = "black" | "white";
STONELESS_WORDS: { readonly pass: "pass"; readonly forfeit: "timed out"; }
How a written move list says the two moves that have no point.
stonelessWord(kind: string | undefined): string | null
How a written record says a move with no point, or null for a move that has one. The words a move list and a copied notation print, so the two agree.
STONES: { readonly black: "black"; readonly white: "white"; }
stonesLeft(state: GameState): number
Stones the colour to move still has to place before the turn passes.
type StoredGame = { size: number; winLength: number; variant: string; obstacles: string; opener: string; opening?: string; handicap?: Handicap | null; /** The head start, parsed — see `parseHeadStart`. Games stored before head starts had none. */ headStart?: HeadStart | null; seed?: number; /** See DrawLimit. Games recorded before it existed carry "none", as they were played. */ drawLimit?: string; /** * The clock, …
The stored shape of a game, as both the API and the pages see it.
swapSeats(state: GameState): GameState
Trades seats: the player to move hands over their colour and takes the opponent's stones instead. The board is untouched and the turn passes, so a swap costs you the move you were about to make.
TRADITIONAL_HEAD_STARTS: { readonly stones: "stones"; readonly corners: "corners"; readonly men: "men"; }
The traditional head starts, see TraditionalHeadStart.
type TraditionalHeadStart = "stones" | "corners" | "men";
The head start a game's own tradition gives a weaker player, where it has one: Go's handicap stones on the star points, Othello's corners, and draughts' piece odds, the men taken off the stronger side before the start. Null for a game with no such custom. Declared per game, never inferred from the mechanics — anti-Othello flips discs as Othello does, and a corner there is a burden rather than a gift.
TURN_CHOICE_KINDS: { readonly move: "move"; readonly place: "place"; }
type TurnChoiceKind = "move" | "place";
Whether a turn moves a piece already on the board, or places on a point.
turnChoices(state: GameState): TurnChoices | null
What the colour to move may do this turn — every piece that has a move, or every point that may be played — and whether a rule narrowed it.
A question put to the engine's own answers, adding no rule of its own: pieceMoves and legalPoints already decide what is legal, and this gathers them so a board can show a player the few things they may do without working any of it out itself. The board's marking reads this and nothing else.
The dependency runs one way, from here into the engine, as record.ts does; nothing in the engine calls this.
Null where a turn has no set of moves to show: a finished game, a quarter turn owed, a game whose piece is laid by its footprint from a queue, and Go, where a pass is always on offer beside the points.
type TurnChoices = | { kind: "move"; pieces: Point[]; count: number; narrowedBy: MoveNarrowing | null } | { kind: "place"; points: Point[]; count: number };
What the colour to move may do this turn, as the engine answers it — see turnChoices.
turnPassedBy(state: GameState): Stone | null
The colour whose turn passed because it had no move, as the latest turn left the board — or null when nobody's did.
Two ways a turn passes, and the record says both: a forced pass on the record, and the flipping games' pass with no row at all, where the colour that just moved is to move again. A pass somebody chose — Go's — is not one.
type Twist = { quadrant: number; clockwise: boolean; };
A quarter turn of one quadrant, which ends a move in the twist games.
twistBoard(state: GameState, quadrant: number, clockwise: boolean): GameState
Turns one quadrant to finish the move. The whole board is read afterwards, because a turn can complete a line for either colour anywhere: one line wins for its owner, a line for each is a draw, and a full board with no line is a draw too.
undoMove(state: GameState): GameState
Removes the last move, putting back anything it captured, a piece where it came from, and a twisted quadrant the way it was. Also reopens a finished game. The opening is left as it stands: a colour choice is a decision, not a stone, and is not undone by lifting one.
upcomingPieces(state: GameState, count: number): Piece[]
The pieces after the one in hand, for the preview.
VARIANT_SPECS: Record<RuleVariant, VariantSpec>
Every rule set, as data. The engine reads these and never the variant name, so a new variant is a new row here plus its copy in variants.constants.ts.
type VariantSpec = { /** Per colour, because renju lets white win with an overline and not black. */ lineRule: Record<Stone, LineRule>; forbidden: Record<Stone, readonly ForbiddenPattern[]>; /** Flanking a pair of enemy stones removes them. */ captures: boolean; stonesPerTurn: number; /** Connect6 opens with a single stone before the two-a-turn rhythm starts. */ firstTurnStones: number; /** A pinned line length, or …
One rule set, as data. The engine consults this and never the variant's name, so adding a variant is a matter of adding a row.
WIN_LENGTH: 5
Stones in a line needed to win, unless a variant pins it.
WIN_LENGTHS: readonly [4, 5, 6]
Line lengths a player may pick in the variants that leave it open.
WIN_REASONS: { readonly line: "line"; readonly captures: "captures"; readonly time: "time"; readonly resign: "resign"; readonly trap: "trap"; readonly square: "square"; readonly full: "full"; readonly count: "count"; readonly camp: "camp"; readonly connection: "connection"; readonly blocked: "blocked"; readonly territory: "territory"; }
winOnTime(state: GameState, loser: Stone): GameState
Ends the game against a player who has run out of time.
A clock is not a rule of gomoku, so the engine does not run one — but the result still has to be a proper game state rather than something the UI paints over the top, or the record and the board would disagree.
type WinReason = "line" | "captures" | "time" | "resign" | "trap" | "square" | "full" | "count" | "camp" | "connection" | "blocked" | "territory";
How a won game was won. Null while nobody has. trap is the loser's doing: they made the line the rules forbid. square is four in a 2×2. blocked is the checkers family: the colour to move has no legal move left, whether because it has no pieces or because every one of them is shut in.
type Worm = "worm";
A wormhole: a line entering it comes out of its partner and carries on.
WORM: "worm"
WRAP_MODES: { readonly none: "none"; readonly columns: "columns"; readonly both: "both"; }
type WrapMode = "none" | "columns" | "both";
Which edges of the board join up: a plane, a cylinder, or a torus.
@johnmorrisdotca/narabe/reacttype NarabeGame = { /** The game as it stands. A new object after every move that changed it. */ state: GameState; /** What the player to move may do, or null when there is no set of moves to show. See `turnChoices`. */ choices: TurnChoices | null; /** Places a stone at `point`: `playMove`. `colour` is for the games where the mover chooses it. */ play: (point: Point, kind?: MoveKind, colour?: Stone | null) => void; …
useNarabe(settings?: Partial<GameSettings>): NarabeGame
One game, kept in React state.
settings is read when the hook first runs and again on reset; give it a seed for a game that starts the same way every time.
@johnmorrisdotca/narabe/drawboardSvg(state: GameState, options?: DrawBoardOptions): string
The position as one <svg> element, drawn the way its game is traditionally drawn.
type DrawBoardOptions = { /** The width in pixels. Unless said, the SVG has a `viewBox` and no size, and fills what holds it. */ width?: number; /** What a screen reader says for the picture. Unless said, "Board". An empty string makes it decoration. */ title?: string; /** Mark the last move with a dot. Unless said, on. */ lastMove?: boolean; /** Ring the stones of a winning line. Unless said, on. */ winningLine?: b…
What to draw besides the board and the stones. Every field is optional.
この日本語は、まだ日本語を母語とする方の確認を受けていません。訂正を歓迎します。