Narabe並べ

@johnmorrisdotca/narabe 1.1.0 · 3 entry points · 232 exports

@johnmorrisdotca/narabe

ALL_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

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.

function availableOpenings

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

type Blocked = "blocked";

An intersection the rules have taken out of play. See obstacles.ts.

const BLOCKED

BLOCKED: "blocked"

const BOARD_GRIDS

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.

const BOARD_SIZES

BOARD_SIZES: readonly [9, 13, 15, 19]

Board is size × size. The mini boards make for much shorter games.

type BoardGrid

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.

function boardSizesFor

boardSizesFor(variant: RuleVariant): readonly number[]

The board sizes a variant plays on, smallest first — the order they are drawn in.

const BRAZILIAN_DRAUGHTS_RULES

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.

function campOf

campOf(size: number, point: Point): Stone | null

Whose home camp point lies in, or null for the open board.

function campSize

campSize(size: number): number

Pieces a side has on size: the squares of its camp.

function campSquares

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.

const CANADIAN_CHECKERS_RULES

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.

function canBeDrawn

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.

function canChooseColour

canChooseColour(state: GameState): boolean

Whether the game is waiting on a seat to pick a colour.

function canExtendOpening

canExtendOpening(state: GameState): boolean

Swap2 only: the first chooser may add two stones and pass the choice back.

function canForfeit

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.

function canGrowBoard

canGrowBoard(state: GameState): boolean

function canPass

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.

function canShrinkBoard

canShrinkBoard(state: GameState): boolean

function canSkip

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.

function canSwapSeats

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.

function canTwist

canTwist(state: GameState): boolean

Whether the colour to move owes a quarter turn before the move is complete.

function canUndo

canUndo(state: GameState): boolean

const CAPTURE_CHOICES

CAPTURE_CHOICES: { readonly free: "free"; readonly maximum: "maximum"; }

type CaptureChoice

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.

function capturePaths

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

type Cell = Stone | Blocked | Hot | Worm | null;

One intersection of the board: a stone, an obstacle, a hotspot, a wormhole, or nothing.

function cellAt

cellAt(state: GameState, point: Point): Cell

function centreSquares

centreSquares(size: number): Point[]

The four centre squares, top-left first. Even boards only; the centre is between four cells.

function checkersHasCapture

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

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.

function chooseColour

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

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.

const COLUMN_LETTERS

COLUMN_LETTERS: "ABCDEFGHJKLMNOPQRSTUVWXYZ"

Column letters used in coordinate labels, left to right. "I" is skipped as in go.

function columnLetter

columnLetter(col: number): string

Column letter, left to right: A, B, C ... with I skipped as on a go board.

function createGame

createGame(overrides?: Partial<GameSettings>, roll?: number): GameState

const CROWN_MID_CAPTURE

CROWN_MID_CAPTURE: { readonly stops: "stops"; readonly continues: "continues"; readonly passes: "passes"; }

type CrownMidCapture

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.

const DEFAULT_BOARD_SIZE

DEFAULT_BOARD_SIZE: 15

const DEFAULT_CAPTURES_TO_WIN

DEFAULT_CAPTURES_TO_WIN: 10

Enemy stones to capture for a win in the capture game: five pairs.

const DEFAULT_SETTINGS

DEFAULT_SETTINGS: GameSettings

const DEFAULT_SWAPS_PER_SEAT

DEFAULT_SWAPS_PER_SEAT: 1

function defaultBoardFor

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.

const DIRECTIONS

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.

function discCount

discCount(board: Cell[]): Record<Stone, number>

const DRAW_LIMIT_LIST

DRAW_LIMIT_LIST: readonly DrawLimit[]

const DRAW_LIMIT_MIN_POINTS

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.

const DRAW_LIMIT_SHARE

DRAW_LIMIT_SHARE: Record<DrawLimit, number | null>

const DRAW_LIMITS

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

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.

function drawnByLength

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.

function dropTarget

dropTarget(board: Cell[], size: number, col: number): Point | null

Where a stone dropped into col lands, or null when the column is full.

function emptyPoints

emptyPoints(state: GameState): Point[]

Every intersection still open: no stone, no obstacle.

function endedWithNoMoves

endedWithNoMoves(state: GameState): boolean

Whether the game ended because neither side had a move: two forced passes, the last two moves on the record.

const ENDGAME_COUNT_KINDS

ENDGAME_COUNT_KINDS: { readonly endings: "endings"; readonly balance: "balance"; }

type EndgameCount

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

type EndgameCountKind = "endings" | "balance";

The two shapes an endgame count is written in — see EndgameCount.

function endsOnItsOwn

endsOnItsOwn(settings: GameSettings): boolean

Whether the rules alone guarantee this game ends.

const ENGLISH_CHECKERS_RULES

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.

function extendOpening

extendOpening(state: GameState): GameState

The chooser declines to choose and lays two more stones instead.

function findWinningLine

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.

const FIRST_PLAYERS

FIRST_PLAYERS: { readonly black: "black"; readonly white: "white"; readonly random: "random"; }

const FIRST_STONE

FIRST_STONE: Stone

Black opens unless the settings say otherwise.

type FirstPlayer

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.

function flipsAt

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.

function footprintAt

footprintAt(orientation: readonly PieceCell[], anchor: Point): PieceCell[]

The absolute cells of an orientation anchored with its origin at anchor.

function footprintFits

footprintFits(board: Cell[], size: number, cells: readonly PieceCell[]): boolean

Whether every cell of a footprint is on the board and empty.

const FORBIDDEN_PATTERNS

FORBIDDEN_PATTERNS: { readonly doubleThree: "doubleThree"; readonly doubleFour: "doubleFour"; readonly overline: "overline"; }

function forbiddenAt

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

type ForbiddenPattern = "doubleThree" | "doubleFour" | "overline";

Shapes a colour may be forbidden from making. See rules/forbidden.ts.

function forbiddenPoints

forbiddenPoints(state: GameState): Point[]

Every empty point the colour to move is forbidden from playing right now.

function forbiddenReason

forbiddenReason(state: GameState, point: Point): ForbiddenPattern | null

Why the colour to move may not play point, when a forbidden shape is why.

function forfeitOnRecord

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.

function forfeitTurn

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.

const GAME_STATUS

GAME_STATUS: { readonly playing: "playing"; readonly won: "won"; readonly draw: "draw"; }

type GameSettings

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

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

type GameStatus = "playing" | "won" | "draw";

function groupAt

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.

function growBoard

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

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.

const HANDICAP_RULES

HANDICAP_RULES: readonly ["doubleThree", "doubleFour", "overline", "exactLine", "openLine", "longerLine", "singleStone", "noCaptures"]

The handicap toggles, in the order the settings list them.

type HandicapRule

type HandicapRule = Exclude<keyof Handicap, "stone" | "secondStoneExclusion">;

The toggles of a handicap, without the colour that carries them.

type HandicapTerms

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.

function hasFlipMove

hasFlipMove(state: GameState, stone: Stone): boolean

function hasHandicap

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.

const HEAD_START_FREE_TURNS

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

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

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.

const HEX_LINE_DIRECTIONS

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

type Hot = "hot";

A hotspot: an intersection that counts as either colour's stone in a line.

const HOT

HOT: "hot"

function indexOf

indexOf(size: number, point: Point): number

function inLayingPhase

inLayingPhase(state: GameState): boolean

Classic reversi: the first four discs are laid by the players, in the centre, with no flipping.

function inMovePhase

inMovePhase(state: GameState): boolean

Whether the colour to move has all its pieces down and must now slide one.

const INTERNATIONAL_DRAUGHTS_RULES

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.

function isDarkSquare

isDarkSquare(point: Point): boolean

Checkers is played on one colour of square only: the board's own dark squares.

function isKingAt

isKingAt(kings: readonly Point[], at: Point): boolean

Whether the piece at at, if any, is a king.

function isLegalMove

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.

function isOnBoard

isOnBoard(size: number, point: Point): boolean

function isStone

isStone(cell: Cell): cell is Stone

Narrows a cell to a played stone, excluding empties and obstacles.

const KOMI

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.

function landingPoints

landingPoints(board: Cell[], size: number): Point[]

The one landing cell per column with room: the only cells a drop can fill.

function lastMove

lastMove(state: GameState): Point | null

function leavesNoStone

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.

function legalPoints

legalPoints(state: GameState): Point[]

Every intersection the colour to move may play right now.

const LINE_RULES

LINE_RULES: { readonly atLeast: "atLeast"; readonly exact: "exact"; readonly exactOpen: "exactOpen"; }

function lineDirectionsFor

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

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.

function longestPossibleGame

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

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. …

const MOVE_KINDS

MOVE_KINDS: { readonly place: "place"; readonly skip: "skip"; readonly move: "move"; readonly piece: "piece"; readonly pass: "pass"; readonly forfeit: "forfeit"; }

const MOVE_NARROWINGS

MOVE_NARROWINGS: { readonly capture: "capture"; readonly mostCaptured: "mostCaptured"; }

type MoveInput

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

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

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.

function movePiece

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.

function movesBeforeDraw

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.

function mustPass

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.

function nextBoardSize

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.

const NO_HANDICAP

NO_HANDICAP: Handicap

const NO_HEAD_START

NO_HEAD_START: HeadStart

Nobody given a start: the even game, which is most games.

const NO_POINT

NO_POINT: Point

A pass has no point on the board.

function noBoundReason

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

type NoBoundReason = "pieces-move" | "captures" | "line-clear";

Why a game has no bound, or null when the board is its bound.

function normaliseSettings

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.

const OBSTACLE_LAYOUTS

OBSTACLE_LAYOUTS: { readonly none: "none"; readonly hoshi: "hoshi"; }

type ObstacleLayout

type ObstacleLayout = "none" | "hoshi";

none: every intersection is playable. hoshi: the star points are blocked, except tengen at the centre.

const OPENING_CHOICE_EXTEND

OPENING_CHOICE_EXTEND: "extend"

The one opening choice that is not a colour.

const OPENING_RULE_LIST

OPENING_RULE_LIST: readonly ["free", "pro", "longPro", "swap", "swap2", "rif", "sakata", "tarannikov"]

const OPENING_RULES

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"; }

const OPENING_STAGES

OPENING_STAGES: { readonly placing: "placing"; readonly choosing: "choosing"; readonly extending: "extending"; readonly done: "done"; }

type OpeningChoice

type OpeningChoice = Stone | "extend";

A decision taken during the opening: a colour, or two more stones.

type OpeningRule

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

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

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[]; };

function orientations

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.

function orientCells

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.

function otherStone

otherStone(stone: Stone): Stone

function passesOwed

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.

function passTurn

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

type Piece = { cells: readonly PieceCell[]; };

A piece from the queue: its cells relative to the top-left of its bounding box.

const PIECE_PREVIEW

PIECE_PREVIEW: 3

How many queued pieces a player is shown ahead of the one in hand.

const PIECE_QUEUES

PIECE_QUEUES: { readonly domino: "domino"; readonly tetro: "tetro"; }

type PieceCell

type PieceCell = Point & { stone: Stone };

One cell of a multi-cell piece, with the colour it carries.

function pieceMoves

pieceMoves(state: GameState, from: Point): Point[]

Where a piece of the colour to move may step from from; empty if it may not move.

function piecePlacements

piecePlacements(state: GameState): PieceCell[][]

Every legal footprint of the piece in hand. Empty when nothing fits.

type PieceQueue

type PieceQueue = "domino" | "tetro";

Which queue of pieces a game draws from.

function piecesHome

piecesHome(board: Cell[], size: number, stone: Stone): number

How many of stone's pieces stand in the far camp.

type PieceTally

type PieceTally = { kings: number; men: number };

A side's pieces, as an endgame count names them.

type Placement

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.

const PLACEMENTS

PLACEMENTS: { readonly free: "free"; readonly drop: "drop"; readonly edge: "edge"; }

function placePiece

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.

function playMove

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

type Point = { row: number; col: number; };

Zero-based board coordinates. Row 0 is the top, column 0 is the left.

function pointName

pointName(size: number, point: Point): string

Renju-style name for an intersection, e.g. "H8" for the centre of a 15×15 board.

function pointOf

pointOf(size: number, index: number): Point

const POOL_CHECKERS_RULES

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.

function previousBoardSize

previousBoardSize(size: number, sizes?: readonly number[]): number | null

The next size down, or null when the board is already the smallest.

function quadrantCount

quadrantCount(size: number, quadrantSize: number): number

How many quadrants a board of this size holds.

function quadrantOrigin

quadrantOrigin(size: number, quadrantSize: number, quadrant: number): Point

The top-left corner of quadrant quadrant.

function queuedPiece

queuedPiece(state: GameState): Piece | null

The piece the colour to move must lay next, or null outside the piece games.

type ReplayFacts

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.

function replayGame

replayGame(game: StoredGame): GameState

The position as it stands now.

function replayMoves

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.

function replayTimeline

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.

function resign

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.

function resolveOpener

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.

function resolvePlacement

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.

function rowNumber

rowNumber(size: number, row: number): number

Row number counted from the bottom edge, so the bottom row is 1.

const RULE_VARIANT_LIST

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.

const RULE_VARIANTS

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…

function rulesFor

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

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.

const RUSSIAN_DRAUGHTS_RULES

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.

function scoreArea

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

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.

function seatOf

seatOf(state: GameState, stone: Stone): Seat

The seat holding stone right now. Swaps move seats between colours.

const SEATS

SEATS: { readonly one: "one"; readonly two: "two"; }

function seatToPlay

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.

const SECOND_STONE_EXCLUSIONS

SECOND_STONE_EXCLUSIONS: readonly [0, 2, 3]

Half-widths of the central square a handicapped second stone must leave.

const SEED_RANGE

SEED_RANGE: number

Seeds are 31-bit integers, small enough for every store and reproducible everywhere.

function seededRandom

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.

function shrinkBoard

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.

function singlesLeft

singlesLeft(state: GameState): number

Single stones the colour to move may still lay instead of a piece.

function sizeForVariant

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.

function skipMove

skipMove(state: GameState, roll?: number): GameState

Burns the turn on a corner stone. A no-op when skipping is not allowed.

function skipTarget

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

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.

function slideWord

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.

const STAR_POINTS

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.

const STAR_RADIUS

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.

function starCampOf

starCampOf(radius: number, point: Point): Stone | null

Whose home point point lies in, or null outside both.

function starCampSize

starCampSize(radius: number): number

Pieces a side has: the cells of one point, ten on the standard board.

function starPiecesHome

starPiecesHome(board: Cell[], size: number, radius: number, stone: Stone): number

How many of stone's pieces stand in the far point.

function starSize

starSize(radius?: number): number

The side of the square array a star of this radius is embedded in.

const STARTING_DISCS

STARTING_DISCS: { readonly none: "none"; readonly fixed: "fixed"; readonly laid: "laid"; }

type StartingDiscs

type StartingDiscs = "none" | "fixed" | "laid";

How a flipping game begins: nothing, the fixed four, or four the players lay themselves.

type Stone

type Stone = "black" | "white";

const STONELESS_WORDS

STONELESS_WORDS: { readonly pass: "pass"; readonly forfeit: "timed out"; }

How a written move list says the two moves that have no point.

function stonelessWord

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.

const STONES

STONES: { readonly black: "black"; readonly white: "white"; }

function stonesLeft

stonesLeft(state: GameState): number

Stones the colour to move still has to place before the turn passes.

type StoredGame

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.

function swapSeats

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.

const TRADITIONAL_HEAD_STARTS

TRADITIONAL_HEAD_STARTS: { readonly stones: "stones"; readonly corners: "corners"; readonly men: "men"; }

The traditional head starts, see TraditionalHeadStart.

type 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.

const TURN_CHOICE_KINDS

TURN_CHOICE_KINDS: { readonly move: "move"; readonly place: "place"; }

type TurnChoiceKind

type TurnChoiceKind = "move" | "place";

Whether a turn moves a piece already on the board, or places on a point.

function turnChoices

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

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.

function turnPassedBy

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

type Twist = { quadrant: number; clockwise: boolean; };

A quarter turn of one quadrant, which ends a move in the twist games.

function twistBoard

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.

function undoMove

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.

function upcomingPieces

upcomingPieces(state: GameState, count: number): Piece[]

The pieces after the one in hand, for the preview.

const VARIANT_SPECS

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

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.

const WIN_LENGTH

WIN_LENGTH: 5

Stones in a line needed to win, unless a variant pins it.

const WIN_LENGTHS

WIN_LENGTHS: readonly [4, 5, 6]

Line lengths a player may pick in the variants that leave it open.

const WIN_REASONS

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"; }

function winOnTime

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

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

type Worm = "worm";

A wormhole: a line entering it comes out of its partner and carries on.

const WORM

WORM: "worm"

const WRAP_MODES

WRAP_MODES: { readonly none: "none"; readonly columns: "columns"; readonly both: "both"; }

type WrapMode

type WrapMode = "none" | "columns" | "both";

Which edges of the board join up: a plane, a cylinder, or a torus.

@johnmorrisdotca/narabe/react

NarabeGame useNarabe

type NarabeGame

type 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; …

function useNarabe

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/draw

boardSvg DrawBoardOptions

function boardSvg

boardSvg(state: GameState, options?: DrawBoardOptions): string

The position as one <svg> element, drawn the way its game is traditionally drawn.

type DrawBoardOptions

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.