# ymir-js > ymir-js is a TypeScript toolkit for board games. It ships complete, tested rule engines for checkers (Turkish and International), chess, Go and match-3, built on a small generic `Board`/`Item` core that you can use for your own games. Zero runtime dependencies, ES module only, Node 20.19+ / 22.12+ or any modern bundler. Install only the packages you need and import named exports from them: `@ymir-js/core` for the board (`import { Board, Item } from '@ymir-js/core'`), plus `@ymir-js/checkers` (`CheckersGame`), `@ymir-js/chess` (`ChessGame`), `@ymir-js/go` (`GoGame`) or `@ymir-js/match3` (`Match3Board`). Each game package depends on `@ymir-js/core`. Existing code may import from the older all-in-one `ymir-js` package, kept for backward compatibility with the same names (and the grouped `Core`, `Checkers`, `Chess`, `Go`, `Match3`, `Utils` exports); use the scoped packages for new code. Things to know before writing code with it: - Coordinates are strings `"row|col"`, zero-based, row 0 at the top (e.g. `"3|4"`). Chess also accepts algebraic squares (`"e4"`); convert with `toSquare` / `fromSquare`. Go converts to GTP vertices with `board.toGTP` / `board.fromGTP`. - `new Board({ rows, cols })` creates a board; `getItem`, `setItem`, `moveItem`, `removeItem`, `switchItem`, `isEmpty`, `isExistCoord`, `getBoardMatrix`, plus `getNeighbors(coord, { diagonal })`, `getReachable(from, { steps, canEnter, diagonal })` (a Map of square → steps; `from` may be several squares, counting from the nearest) and `findPath(from, to, options)` for movement ranges and routes. `squares()` lists every square as `{ coord, row, col, item }`; `findCoord(predicate)`, `findCoords(predicate?)` and `countItems(predicate?)` search the items; `isEdge(coord)` tells the outer ring. `ray(coord, direction, steps?)` is the line of squares in one direction up to the edge (for sliding pieces, blasts, Reversi flips); `loadRows(rows, toItem)` / `toRows(toChar)` read and write a position as text rows. `clone()` copies the whole board (same class, nothing shared); `snapshot()` / `restore(snapshot)` save and put back the items, for undo. Items are copied with `cloneItem`: same class, plain objects and arrays copied deeply. Coord helpers: `toCoord(row, col)`, `parseCoord(coord)` → `[row, col]`, `stepCoord(coord, direction, steps?)`, `manhattan(a, b)`, and `DIRECTIONS` / `LINEAR_DIRECTIONS` / `ANGULAR_DIRECTIONS` / `DIRECTION_STEPS`. Methods are bound, so they can be passed as callbacks. Off-board coords are safe: `getItem` returns `null`, `isEmpty` returns `false`. - Each game has a "game" class that owns turns, history, undo and the result, and a board you can read: `CheckersGame` (+ `TurkishBoard`, `InternationalBoard`), `ChessGame` (+ `ChessBoard`), `GoGame` (+ `GoBoard`), and `Match3Board` (single-player, no game class). - Moves are validated: `play` / `move` throw on an illegal move. Ask first with `getLegalMoves()` (all games), `checkPlay` (Go) or `canSwap` (match-3). - Checkers, chess and Go report their state with `getStatus()` (playing, won, draw and why) and save and restore with `toJSON()` / `fromJSON()`. Match-3 has no end state of its own; `swap` reports points and `getPossibleMoves()` is never empty because the board reshuffles itself. - Built-in computer players: `checkersBoard.autoPlay(color, { onMove })` and `chessGame.getBestMove({ depth })`. - Rules follow the official sources: FMJD for International checkers (incl. 25-move and 16/5-move endgame draws), Turkish draughts (pieces removed one by one, no 180° king turn, "one piece each" draw), FIDE chess (perft-verified move generation), and standard Go (simple or positional superko, area or territory scoring). ## Docs - [README](https://github.com/aykutkardas/ymir-js/blob/main/README.md): install, the core board API, and a section per game with examples and rule tables. - [Full reference](https://aykutkardas.github.io/ymir-js/llms-full.txt): the README plus every public type declaration, in one plain-text file. ## Games - [Checkers](https://github.com/aykutkardas/ymir-js/blob/main/packages/checkers/src/board.ts): `TurkishBoard` / `InternationalBoard` with `getLegalMoves(color)` (mandatory and maximum capture, full capture chains), `playMove(move)`, `autoPlay`; moves are `{ from, path, captured }`. - [Checkers game](https://github.com/aykutkardas/ymir-js/blob/main/packages/checkers/src/game.ts): `CheckersGame.create('turkish' | 'international')`, `play`, `undo`, `redo`, `getStatus()` (win, repetition, king-move and lone-king draws), `toJSON`. - [International notation](https://github.com/aykutkardas/ymir-js/blob/main/packages/checkers/src/international/notation.ts): PDN FEN (`toFEN`, `setFEN`) and moves (`32-28`, `28x19x8`). - [Chess](https://github.com/aykutkardas/ymir-js/blob/main/packages/chess/src/game.ts): `new ChessGame(fen?)`, `move('Nf3' | 'g1f3' | { from, to, promotion })`, `getLegalMoves(square?)`, `getStatus()`, `fen()`, `pgn()`, `undo()`, `getBestMove({ depth })`, `perft(depth)`. - [Go](https://github.com/aykutkardas/ymir-js/blob/main/packages/go/src/game.ts): `new GoGame({ size, rules, komi, ko, setup })`, `play(coord)`, `checkPlay`, `pass`, `resign`, `koPoint`, `captures`, after two passes `toggleDead` and `getScore()`. - [Go board](https://github.com/aykutkardas/ymir-js/blob/main/packages/go/src/board.ts): groups, liberties, captures, scoring, GTP coordinates. - [Match 3](https://github.com/aykutkardas/ymir-js/blob/main/packages/match3/src/board.ts): `new Match3Board({ rows, cols, kinds, random }).init()`, `swap(from, to)` returns every cascade step (`matches`, `cleared`, `fallen`, `spawned`, `points`), `getPossibleMoves()`, `shuffle()`. - [Core board](https://github.com/aykutkardas/ymir-js/blob/main/packages/core/src/board.ts): the generic `Board` every game builds on. ## Live examples - [Checkers](https://aykutkardas.github.io/ymir-js/examples/checkers/): Turkish and International against the computer, with undo. [Source](https://github.com/aykutkardas/ymir-js/tree/main/examples/checkers) - [Chess](https://aykutkardas.github.io/ymir-js/examples/chess/): against the built-in engine at three levels, or two players. [Source](https://github.com/aykutkardas/ymir-js/tree/main/examples/chess) - [Go](https://aykutkardas.github.io/ymir-js/examples/go/): 9×9, 13×13 and 19×19, with scoring. [Source](https://github.com/aykutkardas/ymir-js/tree/main/examples/go) - [Match 3](https://aykutkardas.github.io/ymir-js/examples/match3/): a 20-move puzzle with cascades and hints. [Source](https://github.com/aykutkardas/ymir-js/tree/main/examples/match3) ## Custom games (built on the core) These are not part of the library; each example writes its own rules on `Board` and `Item` and is meant to be read as a tutorial for building a new game. - [Hnefatafl rules](https://github.com/aykutkardas/ymir-js/blob/main/examples/hnefatafl/src/rules.ts): Copenhagen Hnefatafl (Viking chess) on the core: a `Piece extends Item` with `{ linear: true }` movement, `getColumnsByDirection` for moves, sandwich and shieldwall captures, king capture, exit fort and encirclement checks, and a `TaflGame` with undo. [Play](https://aykutkardas.github.io/ymir-js/examples/hnefatafl/) - [Sokoban rules](https://github.com/aykutkardas/ymir-js/blob/main/examples/sokoban/src/rules.ts): walls, boxes and the player as items; a push uses `getColumnsByDirection(player, { [direction]: true, stepCount: 2 })` for the next two squares and two `moveItem` calls; levels in the standard text format (`#@$.*+`); undo and a breadth-first solver. [Play](https://aykutkardas.github.io/ymir-js/examples/sokoban/) - [Tactics rules](https://github.com/aykutkardas/ymir-js/blob/main/examples/tactics/src/rules.ts): a turn-based battle; units are `Item` with typed stats in `data`; movement ranges from `getReachable` with a `canEnter` that walks through allies and around rocks; `findPath` for the walking animation; counter-attacks; a simple AI in `ai.ts`. [Play](https://aykutkardas.github.io/ymir-js/examples/tactics/) - [Reversi rules](https://github.com/aykutkardas/ymir-js/blob/main/examples/reversi/src/rules.ts): discs are items; the flips for a square come from `getColumnsByDirection(coord, { linear: true, angular: true, stepCount: 7 })` (all eight lines); passing, game end, undo; an alpha-beta player in `ai.ts`. [Play](https://aykutkardas.github.io/ymir-js/examples/reversi/) - [Invaders rules](https://github.com/aykutkardas/ymir-js/blob/main/examples/invaders/src/rules.ts): a real-time Invaders-style shooter on the core; invaders and shields are `Item`; the formation marches with `moveItem` (leading invaders first) and steps down at the edges; shooters are the invaders with nothing below them, from `getColumnsByDirection(coord, { bottom: true, stepCount })`; `tick(ms)`, seeded. [Play](https://aykutkardas.github.io/ymir-js/examples/invaders/) - [Bomberman rules](https://github.com/aykutkardas/ymir-js/blob/main/examples/bomberman/src/rules.ts): a real-time game on the core; walls, crates, bombs and power-ups are `Item`; a blast is `getColumnsByDirection(coord, { linear: true, stepCount: range })` cut at walls and at the first crate or bomb (chain reactions); enemies chase with `findPath`; the game advances with `tick(ms)` and is seeded, so it can be tested and replayed. [Play](https://aykutkardas.github.io/ymir-js/examples/bomberman/) ## Optional - [Tests](https://github.com/aykutkardas/ymir-js/tree/main/packages): `*.spec.ts` next to each module show exact expected behaviour, including perft counts for chess. - [npm package](https://www.npmjs.com/package/ymir-js) - [Source repository](https://github.com/aykutkardas/ymir-js) --- # README # ymir-js [![npm](https://img.shields.io/npm/v/@ymir-js/core?label=%40ymir-js%2Fcore)](https://www.npmjs.com/package/@ymir-js/core) [![Test](https://github.com/aykutkardas/ymir-js/actions/workflows/test.yml/badge.svg)](https://github.com/aykutkardas/ymir-js/actions/workflows/test.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE) A TypeScript toolkit for board games. Checkers, chess, Go and match-3 come with their full rules; underneath is a small board core you can use for your own games. **[Play the examples →](https://aykutkardas.github.io/ymir-js/)** · **[Learn the core in puzzles →](https://aykutkardas.github.io/ymir-js/tutorials/)** · **[For agents →](https://aykutkardas.github.io/ymir-js/#agents)**
Turkish checkers: a capture chain and a new king
Checkers
in the library
Chess: Morphy's Opera Game, ending in checkmate
Chess
in the library
Go on 9x9, ending with the score
Go
in the library
Match 3: swaps and cascades
Match 3
in the library
Hnefatafl: the attackers close in and the king escapes to a corner
Hnefatafl
custom game
Reversi: a whole game, discs flipping, ending on a full board
Reversi
custom game
Sokoban: the Storeroom level, solved
Sokoban
custom game
Tactics: blue and red units move, attack and strike back; blue wins
Tactics
custom game
Bomberman: bombs burning crates, power-ups, every enemy blown up
Bomberman
custom game
Invaders: the first wave shot down, then the next one marching in
Invaders
custom game
- **Rules you can trust.** Each game follows its official rules and is tested against them: chess move generation matches the standard perft counts, checkers follows FMJD and Turkish draughts rules. - **Plain classes, any UI.** No framework, no rendering. Read the state, call a method, draw it however you like. - **Typed, zero dependencies.** Written in TypeScript and published as an ES module with types. ## Install Install the core, and the game packages you want: ```sh npm install @ymir-js/core # the board, for your own games npm install @ymir-js/chess # or checkers, go, match3: each brings the core along ``` | Package | What's in it | | --- | --- | | [`@ymir-js/core`](packages/core) | `Board`, `Item`, movement, directions, pathfinding: the base for your own games | | [`@ymir-js/checkers`](packages/checkers) | Turkish and International checkers | | [`@ymir-js/chess`](packages/chess) | Chess and its engine | | [`@ymir-js/go`](packages/go) | Go | | [`@ymir-js/match3`](packages/match3) | Match-3 | Every package is an ES module with types and no other dependencies. Needs Node.js 20.19+ or 22.12+, or any modern bundler. ## Quick start ```js import { ChessGame } from '@ymir-js/chess'; const game = new ChessGame(); game.move('e4'); game.getLegalMoves().length; // 20 game.getBestMove({ depth: 3 }); // the built-in engine's reply game.getStatus(); // { state: 'playing', check: false } ``` Every game works the same way: a **game** object owns the turn, the history and the result; you ask it for legal moves, play one, and read the board to draw it. Illegal moves throw, so ask first. | Game | Main class | Highlights | Demo | | --- | --- | --- | --- | | [Checkers](#checkers) | `CheckersGame` | Turkish and International, capture chains, flying kings, undo, PDN | [Play](https://aykutkardas.github.io/ymir-js/examples/checkers/) | | [Chess](#chess) | `ChessGame` | Every rule, FEN / SAN / UCI / PGN, built-in engine | [Play](https://aykutkardas.github.io/ymir-js/examples/chess/) | | [Go](#go) | `GoGame` | Ko and superko, pass and resign, dead stones, area or territory scoring | [Play](https://aykutkardas.github.io/ymir-js/examples/go/) | | [Match 3](#match-3) | `Match3Board` | Swaps, cascades reported step by step, hints, auto-shuffle | [Play](https://aykutkardas.github.io/ymir-js/examples/match3/) | **Coordinates** are `"row|col"` strings, zero-based, row 0 at the top: `"0|0"` is the top-left square. Chess also takes squares like `"e4"`, and Go converts to GTP vertices like `"D4"`. --- ## Checkers Turkish and International checkers, each with its own rules: | Rule | Turkish | International | | --- | --- | --- | | Board | 8×8, 16 pieces each | 10×10, 20 pieces each | | Men move | forward and sideways | diagonally forward | | Men capture | forward and sideways | diagonally, also backward | | Kings | fly along rows and columns | fly along diagonals | | Captured pieces are removed | one by one, during the move | when the move ends | | Capturing | mandatory, and the chain that takes the most pieces | same | | A man reaching the far row mid-capture | keeps capturing as a man; crowned if the move ends there | same | ### Playing a game `CheckersGame` keeps the turn, the moves played and the result. ```js import { CheckersGame } from '@ymir-js/checkers'; const game = CheckersGame.create('international'); // or 'turkish'; white moves first game.getLegalMoves(); // [{ from: '3|0', path: ['4|1'], captured: [] }, ...] game.play({ from: '3|6', path: ['4|5'] }); // throws if the move is not legal game.turn; // 'black' game.undo(); game.redo(); game.getStatus(); // { state: 'playing' } // { state: 'won', winner: 'white', reason: 'no-moves' } // { state: 'draw', reason: 'repetition' | 'king-moves' | 'lone-king' | 'one-piece-each' } const saved = game.toJSON(); // start position + moves, plain JSON CheckersGame.fromJSON(saved); ``` A move is `{ from, path, captured }`: `path` lists every square the piece lands on, so a capture chain is one move. To start from your own position, set up a board and pass it in: ```js import { CheckersGame, TurkishBoard } from '@ymir-js/checkers'; const board = new TurkishBoard().setPosition({ '2|3': 'w', '5|3': 'B' }); // w/b men, W/B kings const game = new CheckersGame(board, { turn: 'black' }); ``` **Draw rules** follow each variant's rules by default, and can be changed with `new CheckersGame(board, { drawRules })`: | Rule | International (FMJD) | Turkish | | --- | --- | --- | | `repetition`: the same position, same side to move, this many times | 3 | 3 | | `kingMoves`: moves in a row with only kings moving and no capture | 50 (25 each) | off | | `loneKing`: a lone king against 3 pieces incl. a king draws after 16 moves each; against 2 or fewer incl. a king, after 5 each | on | off | | `onePieceEach`: a draw as soon as each side has one piece left ("gayyım") | off | on | ### Boards and the computer player `TurkishBoard` and `InternationalBoard` hold the rules, and can be used without a game: ```js import { TurkishBoard } from '@ymir-js/checkers'; const board = new TurkishBoard().init(); const [move] = board.getLegalMoves('black'); // mandatory and maximum capture applied board.playMove(move); // moves, removes captured pieces, promotes at the end board.getLegalMoves('white', '2|3'); // one piece only, e.g. to continue a chain board.getCaptureSequences('2|3'); // every capture chain of one piece // A simple computer player. onMove is called once per jump. board.autoPlay('white', { onSelect, onMove }); ``` ### International notation International boards read and write standard PDN, numbering the dark squares 1–50 with White at the bottom (ymir draws White at the top, so coords are rotated for you). ```js import { InternationalBoard } from '@ymir-js/checkers'; const board = new InternationalBoard().init(); board.toFEN('white'); // 'W:W31,32,...,50:B1,2,...,20' const turn = board.setFEN('W:W28:B23,13'); const move = board.findPDNMove('28x8', turn); // or '28x19x8' board.toPDNMove(move); // '28x19x8' ``` --- ## Chess `ChessGame` knows every rule: castling, en passant, promotion, check, checkmate, stalemate and the draw rules. Its move generator matches the standard [perft](https://www.chessprogramming.org/Perft_Results) counts. ```js import { ChessGame } from '@ymir-js/chess'; const game = new ChessGame(); // or new ChessGame(fen) game.move('e4'); // SAN game.move('e7e5'); // UCI game.move({ from: 'g1', to: 'f3' }); // squares; add promotion: 'q' | 'r' | 'b' | 'n' game.getLegalMoves('b8'); // [{ from: 'b8', to: 'c6', san: 'Nc6', uci: 'b8c6', ... }, ...] game.getStatus(); // { state: 'playing', check: false } // { state: 'checkmate', winner: 'white' } // { state: 'draw', reason: 'stalemate' | 'repetition' | 'fifty-move' | 'insufficient-material' } game.fen(); // the position as FEN game.pgn(); // '1. e4 e5 2. Nf3' game.undo(); ChessGame.fromJSON(game.toJSON()); game.board.getPiece('e4'); // { type: 'p', color: 'white' } ``` - **Built-in engine.** `game.getBestMove({ depth })` searches with alpha-beta (and keeps looking at captures until the position is quiet), scoring material and piece placement. Depth 3 answers in tens of milliseconds and plays a reasonable casual game; each extra level is several times slower. - **Draw rules.** By default a game is drawn at threefold repetition and after fifty moves without a pawn move or capture, as most engines do. Under FIDE rules a player may *claim* those, and they become automatic at fivefold repetition and seventy-five moves; use `new ChessGame(fen, { drawRules: { repetition: 5, halfMoves: 150 } })` for that. - **Squares.** `game.board` is a regular ymir board, row 0 being rank 8. `toSquare('6|4')` gives `'e2'`, `fromSquare('e2')` gives `'6|4'`. --- ## Go `GoGame` plays a full game: captures, suicide, ko, passing, resigning, dead stones and scoring. ```js import { GoGame } from '@ymir-js/go'; const game = new GoGame({ size: 19 }); // black moves first game.play('3|3'); // throws if not allowed game.checkPlay('3|3'); // { legal: false, reason: 'occupied' | 'suicide' | 'ko' | ... } game.getLegalMoves(); // every point the side to move may play game.koPoint; // the point simple ko forbids right now, or null game.captures; // { black: 0, white: 0 } game.pass(); game.pass(); game.getStatus(); // { state: 'scoring' } after two passes game.toggleDead('3|3'); // mark a group dead (or alive again) game.getScore(); // { black, white, territory, neutral, margin, winner } game.undo(); game.resign(); // getStatus() → { state: 'resigned', winner } GoGame.fromJSON(game.toJSON()); ``` | Option | Values | Default | | --- | --- | --- | | `size` | 2–25 | 19 | | `rules` | `'area'` (stones + territory) or `'territory'` (territory + prisoners) | `'area'` | | `komi` | points added to white | 7.5 for area, 6.5 for territory | | `ko` | `'simple'` (no immediate recapture) or `'positional'` (no earlier position may repeat) | `'simple'` | | `setup` | stones placed before the first move, e.g. handicap; white then moves first | none | `game.board` is a `GoBoard` with groups and liberties (`getGroup`, `getGroups`), and `toGTP('3|3')` / `fromGTP('D16')` for GTP vertices. --- ## Match 3 `Match3Board` handles swapping, matching, falling gems, refills and chain reactions. ```js import { Match3Board } from '@ymir-js/match3'; const board = new Match3Board({ rows: 8, cols: 8 }).init(); // no ready-made matches, at least one move const [hint] = board.getPossibleMoves(); // { from: '3|4', to: '3|5' } const result = board.swap(hint.from, hint.to); // { // valid: true, // points: 90, // steps: [{ matches, cleared, fallen, spawned, points }, ...], // one per cascade // shuffled: false, // true if no move was left and the board was reshuffled // } ``` - A swap that makes no match returns `valid: false` and changes nothing. - Each cascade step scores `cleared × pointsPerGem × step`, so chain reactions are worth 1×, 2×, 3×, … - `steps` has everything needed to animate: which runs matched, which gems fell where and what dropped in. - Pass `random` for repeatable boards and `kinds` for your own gem set; `setKinds(rows)` and `getKinds()` set up or read a board. --- ## Custom games The games above ship with the library. These don't: each one is an example app that writes its own rules on top of the core `Board` and `Item`, to show how far the core takes you. Read their `rules.ts` as a tutorial. | Game | What it shows | Code | | --- | --- | --- | | [Bomberman](https://aykutkardas.github.io/ymir-js/examples/bomberman/) | A real-time game: bombs, crates and power-ups as items, blasts along `getColumnsByDirection`, enemies chasing with `findPath`, time advanced by `tick(ms)` | [Source](examples/bomberman) | | [Hnefatafl](https://aykutkardas.github.io/ymir-js/examples/hnefatafl/) (Viking chess) | Rook-like movement with `getColumnsByDirection`, sandwich captures, special squares, an asymmetric goal, a computer player | [Source](examples/hnefatafl) | | [Invaders](https://aykutkardas.github.io/ymir-js/examples/invaders/) | A real-time shooter: a formation that marches with `moveItem`, shooters picked by looking down each column with `getColumnsByDirection`, shields that wear away | [Source](examples/invaders) | | [Reversi](https://aykutkardas.github.io/ymir-js/examples/reversi/) | Flipping along all eight lines with one `getColumnsByDirection` call, passing, a computer player | [Source](examples/reversi) | | [Sokoban](https://aykutkardas.github.io/ymir-js/examples/sokoban/) | Pushing boxes with `getColumnsByDirection`, levels in the classic text format, undo, a solver | [Source](examples/sokoban) | | [Tactics](https://aykutkardas.github.io/ymir-js/examples/tactics/) | Units with typed stats in `item.data`, movement ranges with `getReachable`, paths with `findPath`, a computer player | [Source](examples/tactics) | ### Hnefatafl Hnefatafl is the Vikings' board game, played in Scandinavia before chess arrived, and a fitting one for a library named after a Norse giant. The attackers lay siege; the king must reach a corner. The example follows the [Copenhagen rules](https://aagenielsen.dk/Copenhagen_Hnefatafl_11x11.pdf), including shieldwalls, exit forts and encirclement, in about 400 lines. The core does the board work. A piece is an `Item` with a movement pattern: ```ts class Piece extends Item { constructor(role: Role) { super({ name: role, movement: { linear: true, stepCount: 10 } }); // like a rook this.role = role; } } ``` and the board asks the core for the squares along each line, then applies the game's own rules: ```ts movesFrom(coord: string): string[] { const piece = this.getItem(coord); const lines = this.getColumnsByDirection(coord, piece.movement); return ['top', 'bottom', 'left', 'right'].flatMap((direction) => { const reachable = []; for (const square of lines[direction]) { if (!this.isEmpty(square)) break; // blocked by a piece if (piece.role === 'king' || !this.isRestricted(square)) reachable.push(square); } return reachable; }); } ``` Captures, the king's capture, forts and encirclement are a few more methods on the same board, and `TaflGame` adds turns, undo and the result. A small alpha-beta player (`ai.ts`) plays either side. The rules have their own tests (`rules.test.ts`, `ai.test.ts`), which CI runs. ### Sokoban Push every box onto a goal. Three small levels made for the demo, each checked by a solver that also powers the app's *Show solution* button and its "best" score. Walls, boxes and the player are all items on the board, so a push is two `moveItem` calls. The core finds the next square and the one after it: ```ts step(direction: Move) { const player = this.findPlayer(); const [next, beyond] = this.getColumnsByDirection(player, { [direction]: true, stepCount: 2, })[direction]; const blocker = this.getItem(next); if (!this.isExistCoord(next) || blocker?.kind === 'wall') return null; if (blocker?.kind === 'box') { if (!this.isEmpty(beyond)) return null; // a wall or another box this.moveItem(next, beyond); this.moveItem(player, next); return 'push'; } this.moveItem(player, next); return 'walk'; } ``` Levels use the usual Sokoban text format (`#` wall, `@` player, `$` box, `.` goal), so you can add your own in `levels.ts`. ### Tactics A small turn-based battle: knights, archers and scouts on a field with forests and rocks. Each unit moves, then attacks or waits; a unit that survives strikes back if it can reach. The red side is played by a simple AI. Units keep their stats in the item's typed `data`: ```ts class Unit extends Item { constructor(type: UnitType, side: Side) { super({ name: type, data: { type, side, hp: STATS[type].maxHp, ...STATS[type] } }); } } ``` and where a unit can go comes from the core's `getReachable`, with the game deciding what can be walked through: ```ts const reach = this.getReachable(coord, { steps: unit.data.move, canEnter: (square) => { if (this.terrain[square] === 'rock') return false; const there = this.getItem(square); return !there || there.data.side === unit.data.side; // through allies, not enemies }, }); ``` `findPath` gives the route a unit walks, for the animation. Both were added to the core for this game. ### Reversi Trap a line of the other colour between your new disc and one of yours, and every trapped disc flips. If you cannot move you pass; when neither side can, the most discs wins. Play against the computer (three levels) or a friend. The whole flipping rule is the eight lines out of a square, which one core call gives: ```ts flipsFor(coord: string, color: Color): string[] { if (!this.isEmpty(coord)) return []; const lines = this.getColumnsByDirection(coord, { linear: true, angular: true, stepCount: 7 }); const flips = []; for (const direction of DIRECTIONS) { const run = []; for (const square of lines[direction]) { const disc = this.getItem(square); if (!disc) break; // empty or off the board: the run is not closed if (disc.color === color) { flips.push(...run); break; } run.push(square); } } return flips; } ``` A legal move is any square where that list isn't empty. The computer player (`ai.ts`) searches a few moves ahead with the usual Reversi evaluation: corners are gold, the squares next to an empty corner are traps, and having more moves than the opponent helps. ### Bomberman Drop bombs, burn crates, pick up power-ups (more bombs, a longer blast, more speed) and blow up every enemy. Arrow keys or WASD and Space; buttons on a phone. The only real-time game here, and the board doesn't mind: walls, crates, bombs and power-ups are items, and the player and enemies are plain objects walking between squares. The game advances with `tick(ms)`, which the app calls in fixed 20 ms steps from `requestAnimationFrame` and the tests call directly. A blast is the bomb's four lines, cut at the first thing in the way: ```ts blastFrom(coord: string, range: number): string[] { const squares = [coord]; const lines = this.board.getColumnsByDirection(coord, { linear: true, stepCount: range }); for (const move of ['top', 'bottom', 'left', 'right']) { for (const square of lines[move]) { const kind = this.board.getItem(square)?.kind; if (kind === 'wall') break; squares.push(square); if (kind === 'crate' || kind === 'bomb') break; // it burns, and stops the fire } } return squares; } ``` A bomb caught in a blast goes off too, so chain reactions come free. Enemies wander, and when you are close they chase you with `findPath(enemy, you, { steps: 6, canEnter })`. Everything random comes from a seeded generator: add `?seed=42` to the URL to play the same map again. The GIF is a planned play-through, replayed on the browser's fake clock tick for tick. ### Invaders Five rows of invaders march across, step down at each edge and speed up as you thin them out. Shoot them before they land; shields soak up hits from both sides, and a mystery ship crosses the top now and then. ← → and Space; buttons on a phone. The invaders and the shields are items, so the formation's march is the arcade's own: every invader moves one square with `moveItem`, the leading ones first so nobody lands on a neighbour, and the whole group steps down and turns when one reaches an edge. Only an invader with nobody below it may drop a bomb, which is one look down its column: ```ts shooters(): string[] { return this.invaders().filter((coord) => { const { bottom } = this.board.getColumnsByDirection(coord, { bottom: true, stepCount: ROWS }); return !bottom.some((square) => this.board.getItem(square)?.type === 'invader'); }); } ``` Shots, bombs, the cannon and the ship are plain values; like Bomberman, the game advances with `tick(ms)` and is seeded, and the GIF is a planned game replayed tick for tick. ## Building your own game Every game here is built on the same `Board` and `Item`. Use them for a game of your own: ```js import { Board, Item } from '@ymir-js/core'; const board = new Board({ rows: 3, cols: 3 }); board.setItem('0|0', new Item({ name: 'knight', data: { hp: 3 } })); board.getItem('0|0'); // Item { name: 'knight', data: { hp: 3 }, ... } board.moveItem('0|0', '1|1'); board.switchItem('1|1', '2|2'); board.removeItem('2|2'); board.isEmpty('1|1'); // true board.isExistCoord('5|5'); // false board.getDirection('0|0', '2|2'); // 'bottomRight' board.getDistanceBetweenTwoCoords('0|0', '2|1'); // { x: 1, y: 2 } board.getColumnsByDirection('1|1', { linear: true }); // { top: ['0|1'], bottom: ['2|1'], ... } board.getNeighbors('1|1'); // ['0|1', '2|1', '1|0', '1|2']; { diagonal: true } for eight board.getReachable('0|0', { steps: 2 }); // Map { '0|0' => 0, '1|0' => 1, ... } — empty squares within 2 steps board.findPath('0|0', '2|2'); // ['1|0', '2|0', '2|1', '2|2'] — the shortest route board.getBoardMatrix(); // rows of { coord, item } board.squares(); // [{ coord: '0|0', row: 0, col: 0, item }, ...] — handy for rendering board.isEdge('0|1'); // true — on the outer ring board.ray('0|0', 'right'); // ['0|1', '0|2'] — a line up to the edge; ray('0|0', 'right', 1) for one step // Positions as text, e.g. for levels and tests. board.loadRows(['...', '.k.', 'p.p'], (char) => char === 'k' ? new Item({ name: 'king' }) : char === 'p' ? new Item({ name: 'pawn' }) : null ); board.toRows((item) => item?.name[0] ?? '.'); // ['...', '.k.', 'p.p'] // Find items without walking the board yourself. board.findCoord((item) => item.name === 'king'); // '1|1', or null board.findCoords((item) => item.name === 'pawn'); // ['2|0', '2|2']; every item without a predicate board.countItems((item) => item.name === 'pawn'); // 2 // Copies, for undo and for trying moves out. const saved = board.snapshot(); // copies of the items, by coord board.removeItem('1|1'); board.restore(saved); // the king is back; a snapshot can be restored again and again const next = board.clone(); // a whole new board of the same class, sharing nothing ``` Coords are plain strings, and a few helpers work on them: ```js import { DIRECTIONS, LINEAR_DIRECTIONS, manhattan, parseCoord, stepCoord, toCoord } from '@ymir-js/core'; toCoord(2, 3); // '2|3' parseCoord('2|3'); // [2, 3] stepCoord('2|3', 'top'); // '1|3'; stepCoord('2|3', 'right', 2) → '2|5' manhattan('0|0', '2|3'); // 5 LINEAR_DIRECTIONS; // ['top', 'bottom', 'left', 'right']; ANGULAR_DIRECTIONS for the diagonals, DIRECTIONS for all eight ``` - Off-board coords are safe: `getItem` returns `null`, `isEmpty` returns `false`, and `getDirection` / `getDistanceBetweenTwoCoords` return `null`. - Methods are bound to the board, so they can be passed as callbacks: `coords.forEach(board.removeItem)`. - `getReachable` and `findPath` step onto empty squares by default; pass `canEnter(square, from)` to decide yourself (walk through allies, avoid water…), and `diagonal: true` for eight directions. `getReachable` also takes several starts and counts from the nearest: `getReachable(enemies)` is a distance map to the closest enemy, and a flood fill from a group of pieces. - Subclass `Board` and override any method; `super` works. `CheckersBoard` is a full example. - `clone`, `snapshot` and `restore` copy items with `cloneItem`: the item keeps its class, and its plain objects and arrays (`data`, `movement`) are copied deeply; anything else (a `Date`, a class instance in `data`) is shared. `clone` doesn't run the constructor and copies a subclass's other fields shallowly; override it and call `super.clone()` if one must not be shared. --- ## For LLMs and coding agents [llms.txt](https://aykutkardas.github.io/ymir-js/llms.txt) sums up the library, its conventions and where each API lives. [llms-full.txt](https://aykutkardas.github.io/ymir-js/llms-full.txt) has this README and every public type declaration in one plain-text file, rebuilt on every release. ## Examples Each game, including the [custom games](#custom-games), has an example app in [`examples/`](examples), also [playable online](https://aykutkardas.github.io/ymir-js/). They import the library from the package sources in `packages/`, so they always run against the code in the repo: ```sh cd examples/chess # or checkers, go, match3, hnefatafl, sokoban, tactics, reversi, bomberman, invaders pnpm install --ignore-workspace pnpm dev ``` ## Upgrading Before the split into `@ymir-js/*` packages, everything came from one package, `ymir-js`. It is still published for backward compatibility, with the same names, so existing imports keep working; new code should use the scoped packages. Grouped imports from earlier versions (`Core.Board`, `Checkers.Turkish.Board`, `Utils.parseCoord`, …) also still work from `ymir-js`. These were deprecated in 0.11 and will be removed in 1.0: | Deprecated | Use instead | | --- | --- | | `new Board({ x, y })` (`x` = rows) | `new Board({ rows, cols })` | | `board.config.x`, `board.config.y` | `board.config.rows`, `board.config.cols` | | `getAvailableColumns(coord, movement, true)` | `getColumnsByDirection(coord, movement)` | | `selectItem`, `deselectItem`, `deselectAllItems`, `item.selected`, `item.lock` | keep selection in your app's state | | `AttactCoord` type | `AttackCoord` | ## Contributing This is a pnpm workspace: the packages are in [`packages/`](packages), the example apps in [`examples/`](examples). ```sh pnpm install pnpm test # vitest, against the sources pnpm typecheck pnpm build # every package, core first ``` The GIFs above are recorded from the example apps with `pnpm build && node scripts/build-site.mjs`, then `pnpm record` in [`scripts/media`](scripts/media) (uses an installed Edge or Chrome). Releases are published from CI. All packages share one version: bump it and push the tag, and GitHub Actions publishes every package that changed version to npm (with provenance) and creates a GitHub release. ```sh node scripts/version.mjs minor git push --follow-tags ``` ## License [MIT](LICENSE) © Aykut Kardaş --- # Public type declarations Generated from each package's published `dist/` folder; `ymir-js` re-exports all of them. Coordinates are "row|col" strings unless noted. ### @ymir-js/checkers: board.d.ts ```ts import { Board, BoardConfig, BoardType, Direction, ItemType, MovementType } from '@ymir-js/core'; import { CHECKERS_BLACK, CHECKERS_WHITE } from './constant.js'; export type CheckersColorType = typeof CHECKERS_BLACK | typeof CHECKERS_WHITE; export interface CheckersItemType extends ItemType { color: CheckersColorType; king: boolean; setKing(): void; } export type CheckersBoardType = BoardType; export type AttackCoord = { coord: string; destroyItemCoord: string; }; /** @deprecated Misspelled; use `AttackCoord`. */ export type AttactCoord = AttackCoord; export type DefendCoord = { coord: string; inDangerCoord: string; }; export type AutoPlayCallbacks = { onSelect?: (coord: string) => void; /** Called once per step; a capture chain calls it for every jump. */ onMove?: (fromCoord: string, toCoord: string) => void; }; /** A piece as one letter: `w`/`b` for men, `W`/`B` for kings. */ export type PieceCode = 'w' | 'b' | 'W' | 'B'; /** Every piece on the board, by coord. Empty squares are left out. */ export type CheckersPosition = Record; export type CheckersVariant = 'turkish' | 'international'; export type CheckersMove = { from: string; /** Squares the piece lands on, in order; the last one is where it ends. */ path: string[]; /** Captured pieces, in the order they are jumped. Empty for a plain move. */ captured: string[]; }; /** * Rules shared by every checkers variant. Variants provide the starting * position and how a piece is created; movement comes from the piece itself. * * All rules live in `getCaptureSequences` and `getPlainMoves`. Everything * else, including the older single-step methods, is built on top of them. */ declare abstract class CheckersBoard extends Board { abstract readonly variant: CheckersVariant; protected abstract readonly whiteItemCoords: string[]; protected abstract readonly blackItemCoords: string[]; protected abstract createItem(item: { color: CheckersColorType; king?: boolean; }): CheckersItemType; constructor(config: BoardConfig); /** Directions a piece may capture in. By default, the ones it moves in. */ protected getCaptureDirections(item: CheckersItemType): Direction[]; /** * Whether captured pieces leave the board as soon as they are jumped * (Turkish) or only once the whole move is over (International). When * they stay, they block the way and cannot be jumped twice. */ protected removesCapturedImmediately(): boolean; private getMoveDirections; /** Row a piece of this color is promoted on. */ getKingRowId(color: CheckersColorType): number; init(): this; reset(): void; /** The pieces on the board as `{ coord: 'w' | 'b' | 'W' | 'B' }`. */ getPosition(): CheckersPosition; /** Replaces every piece with the ones in `position`. */ setPosition(position: CheckersPosition): this; getItemsByColor(color: CheckersColorType): CheckersItemType[]; private getCoordsByColor; /** * Squares the piece on `coord` can move to without capturing: empty * squares along each of its directions, up to its step count. */ getPlainMoves(coord: string, movement?: MovementType | undefined): string[]; /** * Every complete capture chain the piece on `coord` can make. A chain * goes on while another capture is possible, so each result ends on a * square with nothing left to take. */ getCaptureSequences(coord: string): CheckersMove[]; /** * Legal moves for a color. Capturing is mandatory, and among captures * only the chains that take the most pieces are allowed. Pass * `fromCoord` to get the moves of a single piece under the same rules. */ getLegalMoves(color: CheckersColorType, fromCoord?: string): CheckersMove[]; /** * Plays a whole move: the piece ends on the last square of the path, * captured pieces are removed and a man that ends on its king row is * promoted. */ playMove(move: CheckersMove): this; /** * Next squares for the piece on `coord`: the first landing squares of * its captures if it has any, otherwise its plain moves. Unlike * `getLegalMoves`, this does not apply the maximum-capture rule. */ getAvailableColumns(coord: string, movement?: MovementType): string[]; /** @deprecated Use `getColumnsByDirection(coord, movement)`. */ getAvailableColumns(coord: string, movement: MovementType, columnsObj: true): Record; getAvailableCoordsByColor(color: CheckersColorType): Record; /** First jumps of every capture a color can make, by piece. */ getAttackCoordsByColor(color: CheckersColorType): Record; /** * Pieces of `color` that can move onto the landing square of an enemy * capture, blocking it. */ getDefendCoordsByColor(color: CheckersColorType): Record; /** * Squares between two coords on a straight or diagonal line that have a * piece on them, up to and including `toCoord`. */ getItemsBetweenTwoCoords(fromCoord: string, toCoord: string): string[]; /** * A simple computer player: capture as much as the rules require, then * block threats, promote, avoid danger and move forward. Calls `onMove` * once per jump. */ autoPlay(color: CheckersColorType, { onSelect, onMove }?: AutoPlayCallbacks): void; } export default CheckersBoard; ``` ### @ymir-js/checkers: constant.d.ts ```ts export declare const CHECKERS_INTERNATIONAL = "international"; export declare const CHECKERS_TURKISH = "turkish"; export declare const CHECKERS_WHITE = "white"; export declare const CHECKERS_BLACK = "black"; ``` ### @ymir-js/checkers: game.d.ts ```ts import type CheckersBoard from './board.js'; import type { CheckersColorType, CheckersMove, CheckersPosition, CheckersVariant } from './board.js'; export type DrawRules = { /** Draw when the same position, with the same side to move, occurs this many times. */ repetition: number | false; /** * Draw after this many moves in a row (counting both sides) in which only * kings moved and nothing was captured. */ kingMoves: number | false; /** * FMJD endgame rule. One side has a single king; the other has three * pieces including a king (draw after 16 moves each), or two pieces or * fewer including a king (draw after 5 moves each). Counting starts when * that material first appears and restarts when a piece is captured. */ loneKing: boolean; /** Turkish "gayyım": draw as soon as each side has a single piece. */ onePieceEach: boolean; }; export type DrawReason = 'repetition' | 'king-moves' | 'lone-king' | 'one-piece-each'; export type GameStatus = { state: 'playing'; } | { state: 'won'; winner: CheckersColorType; reason: 'no-moves'; } | { state: 'draw'; reason: DrawReason; }; export type GameOptions = { /** Side to move first. Both variants start with white. */ turn?: CheckersColorType; drawRules?: Partial; }; /** Everything needed to restore a game: start position and moves played. */ export type SavedGame = { variant: CheckersVariant; start: { position: CheckersPosition; turn: CheckersColorType; }; moves: CheckersMove[]; drawRules: DrawRules; }; /** * A game of checkers: whose turn it is, the moves played, undo/redo, and * whether the game is over. The board holds the position; the game is the * only thing that should change it while a game is in progress. */ declare class CheckersGame { readonly board: B; readonly drawRules: DrawRules; turn: CheckersColorType; private readonly start; private past; private future; private kingMoves; private loneKingMoves; private seen; /** Starts a game from the board's current position. */ constructor(board: B, options?: GameOptions); /** A new game in the starting position of `variant`. */ static create(variant: CheckersVariant, options?: GameOptions): CheckersGame; /** Restores a game saved with `toJSON`, replaying its moves. */ static fromJSON(saved: SavedGame): CheckersGame; get moves(): CheckersMove[]; get canUndo(): boolean; get canRedo(): boolean; /** Legal moves for the side to move; none once the game is over. */ getLegalMoves(fromCoord?: string): CheckersMove[]; getStatus(): GameStatus; /** * Plays a legal move for the side to move. `captured` can be left out; * the move is matched by `from` and `path`. Throws if the move is not * legal or the game is over. */ play(move: Pick): CheckersMove; /** Takes back the last move. Returns it, or null if there is none. */ undo(): CheckersMove | null; /** Plays the last undone move again. Returns it, or null if there is none. */ redo(): CheckersMove | null; /** Back to the starting position, with no history. */ reset(): void; toJSON(): SavedGame; private apply; private snapshot; private restore; private timesSeen; private countPosition; } export default CheckersGame; ``` ### @ymir-js/checkers: index.d.ts ```ts export { default as CheckersBoard } from './board.js'; export { default as TurkishBoard } from './turkish/board.js'; export { default as TurkishItem } from './turkish/item.js'; export { default as InternationalBoard } from './international/board.js'; export { default as InternationalItem } from './international/item.js'; export { default as CheckersGame } from './game.js'; export { fromFEN, fromSquareNumber, parsePDNMove, toFEN, toPDNMove, toSquareNumber, } from './international/notation.js'; export { CHECKERS_BLACK, CHECKERS_INTERNATIONAL, CHECKERS_TURKISH, CHECKERS_WHITE, } from './constant.js'; export type { AttackCoord, AttactCoord, AutoPlayCallbacks, CheckersBoardType, CheckersColorType, CheckersItemType, CheckersMove, CheckersPosition, CheckersVariant, DefendCoord, PieceCode, } from './board.js'; export type { DrawReason, DrawRules, GameOptions, GameStatus, SavedGame } from './game.js'; ``` ### @ymir-js/checkers: international/board.d.ts ```ts import { BoardConfig, Direction } from '@ymir-js/core'; import CheckersBoard, { CheckersColorType, CheckersMove } from '../board.js'; import Item from './item.js'; export type { AttackCoord, AttactCoord, CheckersBoardType, DefendCoord, } from '../board.js'; export type { BoardConfig }; declare class InternationalCheckersBoard extends CheckersBoard { readonly variant: "international"; protected readonly whiteItemCoords: string[]; protected readonly blackItemCoords: string[]; constructor(config?: BoardConfig); protected getCaptureDirections(): Direction[]; /** The position as a PDN FEN string, e.g. `W:W31-50:B1-20`. */ toFEN(turn: CheckersColorType): string; /** Sets up the position from a PDN FEN string and returns the side to move. */ setFEN(fen: string): CheckersColorType; /** A move in PDN notation, e.g. `32-28` or `28x19x10`. */ toPDNMove(move: CheckersMove): string; /** * The legal move written as `notation` (`32-28`, `28x19x10`, or a capture * by its start and end square, `28x10`), or null if there is none. */ findPDNMove(notation: string, turn: CheckersColorType): CheckersMove | null; protected createItem(item: { color: CheckersColorType; king?: boolean; }): Item; } export default InternationalCheckersBoard; ``` ### @ymir-js/checkers: international/item.d.ts ```ts import { Item, MovementType } from '@ymir-js/core'; import type { CheckersColorType, CheckersItemType } from '../board.js'; export type { CheckersColorType, CheckersItemType }; declare class CheckersItem extends Item implements CheckersItemType { color: CheckersColorType; king: boolean; movement: MovementType; constructor(item: Partial & { data?: any; }); setKing(): void; } export default CheckersItem; ``` ### @ymir-js/checkers: international/notation.d.ts ```ts import type { CheckersColorType, CheckersMove, CheckersPosition } from '../board.js'; /** The standard square number (1-50) of a dark-square coord. */ export declare const toSquareNumber: (coord: string) => number; /** The coord of a standard square number (1-50). */ export declare const fromSquareNumber: (square: number) => string; /** * A position as a PDN FEN string, e.g. `W:W31,32,K46:B1,2,3`. * `turn` is the side to move. */ export declare const toFEN: (position: CheckersPosition, turn: CheckersColorType) => string; /** Reads a PDN FEN string. Ranges like `B1-20` are accepted. */ export declare const fromFEN: (fen: string) => { position: CheckersPosition; turn: CheckersColorType; }; /** A move in PDN notation: `32-28` for a step, `28x19x10` for captures. */ export declare const toPDNMove: (move: CheckersMove) => string; /** * The square numbers of a PDN move (`32-28`, `28x19x10`). Match them * against `getLegalMoves` to find the move; a capture may be written with * only its start and end square. */ export declare const parsePDNMove: (notation: string) => string[]; ``` ### @ymir-js/checkers: turkish/board.d.ts ```ts import { BoardConfig } from '@ymir-js/core'; import CheckersBoard, { CheckersColorType } from '../board.js'; import Item from './item.js'; export type { AttackCoord, AttactCoord, CheckersBoardType, DefendCoord, } from '../board.js'; export type { BoardConfig }; declare class TurkishCheckersBoard extends CheckersBoard { readonly variant: "turkish"; protected readonly whiteItemCoords: string[]; protected readonly blackItemCoords: string[]; constructor(config?: BoardConfig); protected removesCapturedImmediately(): boolean; protected createItem(item: { color: CheckersColorType; king?: boolean; }): Item; } export default TurkishCheckersBoard; ``` ### @ymir-js/checkers: turkish/item.d.ts ```ts import { Item, MovementType } from '@ymir-js/core'; import type { CheckersColorType, CheckersItemType } from '../board.js'; export type { CheckersColorType, CheckersItemType }; declare class CheckersItem extends Item implements CheckersItemType { color: CheckersColorType; king: boolean; movement: MovementType; constructor(item: Partial & { data?: any; }); setKing(): void; } export default CheckersItem; ``` ### @ymir-js/chess: board.d.ts ```ts import { Board, Item, ItemType } from '@ymir-js/core'; export type ChessColor = 'white' | 'black'; /** Pawn, knight, bishop, rook, queen, king. */ export type ChessPieceType = 'p' | 'n' | 'b' | 'r' | 'q' | 'k'; export type ChessPieceItemType = ItemType & { type: ChessPieceType; color: ChessColor; }; export declare class ChessPiece extends Item implements ChessPieceItemType { type: ChessPieceType; color: ChessColor; constructor({ type, color }: { type: ChessPieceType; color: ChessColor; }); } /** `'e4'` for the coord `'4|4'`. Row 0 is rank 8, so white sits at the bottom. */ export declare const toSquare: (coord: string) => string; /** `'4|4'` for the square `'e4'`. */ export declare const fromSquare: (square: string) => string; /** * The pieces of a chess game as a board, so they can be read with the same * methods as every other ymir board. `ChessGame` keeps it up to date; change * the position through the game, not here. */ declare class ChessBoard extends Board { constructor(); /** The piece on a square in algebraic notation, e.g. `'e4'`. */ getPiece(square: string): ChessPiece | null; } export default ChessBoard; ``` ### @ymir-js/chess: game.d.ts ```ts import ChessBoard, { ChessColor, ChessPieceType } from './board.js'; export declare const START_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; export type ChessMove = { /** Squares in algebraic notation, e.g. `'e2'`. */ from: string; to: string; color: ChessColor; piece: ChessPieceType; captured?: ChessPieceType; promotion?: ChessPieceType; kind: 'normal' | 'double-push' | 'en-passant' | 'castle-king' | 'castle-queen'; /** Standard algebraic notation, e.g. `'Nf3'`, `'exd5'`, `'O-O'`, `'e8=Q#'`. */ san: string; /** UCI notation, e.g. `'e2e4'`, `'e7e8q'`. */ uci: string; }; export type ChessDrawReason = 'stalemate' | 'repetition' | 'fifty-move' | 'insufficient-material'; export type ChessStatus = { state: 'playing'; check: boolean; } | { state: 'checkmate'; winner: ChessColor; } | { state: 'draw'; reason: ChessDrawReason; }; export type ChessDrawRules = { /** Draw when the same position occurs this many times. FIDE: 3 to claim, 5 automatic. */ repetition: number | false; /** Draw after this many half-moves without a pawn move or capture. FIDE: 100 to claim, 150 automatic. */ halfMoves: number | false; /** Draw when neither side can possibly checkmate (e.g. king and bishop against king). */ insufficientMaterial: boolean; }; export type SavedChessGame = { start: string; /** Moves in UCI notation. */ moves: string[]; drawRules: ChessDrawRules; }; /** * A game of chess: every rule, including castling, en passant, promotion, * check, checkmate, stalemate and the draw rules. Moves can be given as * squares (`{ from: 'e2', to: 'e4' }`), SAN (`'Nf3'`) or UCI (`'g1f3'`). */ declare class ChessGame { /** The pieces, readable like any other ymir board. Kept in sync by the game. */ readonly board: ChessBoard; readonly drawRules: ChessDrawRules; private squares; private turnColor; private castling; private ep; private halfMoves; private fullMoves; private startFen; private stack; private played; private keys; constructor(fen?: string, options?: { drawRules?: Partial; }); static fromJSON(saved: SavedChessGame): ChessGame; get turn(): ChessColor; /** Moves played so far. */ get moves(): ChessMove[]; /** Sets up a position from FEN and clears the history. */ load(fen: string): this; /** The current position as FEN. */ fen(): string; /** Whether the side to move is in check. */ isCheck(): boolean; getStatus(): ChessStatus; /** Legal moves for the side to move, optionally only from one square. None once the game is over. */ getLegalMoves(from?: string): ChessMove[]; /** * Plays a move: `{ from, to, promotion? }`, SAN (`'Nf3'`, `'O-O'`, * `'e8=Q'`) or UCI (`'g1f3'`, `'e7e8q'`). Promotion defaults to a queen. * Throws if the move is not legal. */ move(input: string | { from: string; to: string; promotion?: ChessPieceType; }): ChessMove; /** Takes back the last move. Returns it, or null if there is none. */ undo(): ChessMove | null; /** The moves so far in SAN, numbered, e.g. `'1. e4 e5 2. Nf3'`. */ pgn(): string; /** * A move for the side to move from a small built-in engine: alpha-beta * search `depth` moves ahead (plus captures until the position is quiet), * scoring material and piece placement. Depth 3 plays a reasonable * casual game and answers quickly; each extra level is several times * slower. With `random`, equally good moves are picked at random. * Returns null when the game is over. */ getBestMove({ depth, random }?: { depth?: number; random?: () => number; }): ChessMove | null; /** Number of positions reachable in exactly `depth` moves. For testing move generation. */ perft(depth: number): number; toJSON(): SavedChessGame; private legal; private pseudoLegal; private addCastling; private isAttacked; private inCheck; private search; /** Keeps searching captures so the evaluation is not taken mid-exchange. */ private quiesce; /** The position's value for the side to move. */ private evaluate; /** Captures of valuable pieces by cheap ones first, then promotions; the search prunes more. */ private ordered; private make; private unmake; private find; private describe; /** Placement, side to move, castling and a capturable en passant square. */ private positionKey; private isInsufficientMaterial; /** Copies the position onto `board`. */ private sync; } export default ChessGame; ``` ### @ymir-js/chess: index.d.ts ```ts export { default as ChessBoard, ChessPiece, fromSquare, toSquare } from './board.js'; export { default as ChessGame, START_FEN } from './game.js'; export type { ChessColor, ChessPieceItemType, ChessPieceType } from './board.js'; export type { ChessDrawReason, ChessDrawRules, ChessMove, ChessStatus, SavedChessGame, } from './game.js'; ``` ### @ymir-js/core: board.bench.d.ts ```ts export {}; ``` ### @ymir-js/core: board.d.ts ```ts import { ItemType, MovementType } from './item.js'; export type DistanceType = { /** Columns between the two coords; positive to the right. */ x: number; /** Rows between the two coords; positive downward. */ y: number; }; export type BoardSize = { rows: number; cols: number; }; /** @deprecated Use `{ rows, cols }`. `x` is the row count, `y` the column count. */ export type LegacyBoardSize = { x: number; y: number; }; export type BoardConfig = BoardSize | LegacyBoardSize; export type ResolvedBoardConfig = BoardSize & { /** @deprecated Use `rows`. */ x: number; /** @deprecated Use `cols`. */ y: number; }; export type ColumnType = { item: T | null; }; export type BoardType = { [key: string]: ColumnType; }; export type Direction = 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topRight' | 'right' | 'left' | 'top' | 'bottom'; export type ColumnsByDirection = Record; export type BoardMatrixItem = { coord: string; item: T | null; }; declare class Board { config: ResolvedBoardConfig; board: BoardType; constructor(config: BoardConfig); static createBoard(config: BoardConfig): BoardType; updateBoard(board: BoardType): this; /** * A copy of the board, of the same class, that shares nothing with it: * every item is copied with `cloneItem`. The constructor is not run; the * subclass's other fields are copied shallowly, so override `clone` (and * call `super.clone()`) if one of them must not be shared. */ clone(): this; /** A copy of every item and where it stands, to put back with `restore`. */ snapshot(): BoardSnapshot; /** Puts the items back as they were in `snapshot`. A snapshot can be restored any number of times. */ restore(snapshot: BoardSnapshot): this; updateBoardWithMatrix(matrix: (T | null)[][]): this; getBoardMatrix(): BoardMatrixItem[][]; /** * Sets up the board from text rows, one character per square: `toItem` * turns each character into an item, or null for an empty square. * Squares the rows don't reach are emptied. */ loadRows(rows: string[], toItem: (char: string, coord: string) => T | null | undefined): this; /** The board as text rows, one character per square from `toChar`. */ toRows(toChar: (item: T | null, coord: string) => string): string[]; /** Every square with its row, col and item, row by row. */ squares(): Square[]; /** The coord of the first item that passes `predicate`, or null. */ findCoord(predicate: ItemPredicate): string | null; /** The coords of every item that passes `predicate`; of every item without one. */ findCoords(predicate?: ItemPredicate): string[]; /** How many items pass `predicate`; how many items there are without one. */ countItems(predicate?: ItemPredicate): number; getItem(coord: string): T | null; setItem(coord: string, item: T | null): void; removeItem(coord: string): void; moveItem(fromCoord: string, toCoord: string): void; switchItem(fromCoord: string, toCoord: string): void; /** @deprecated Selection is UI state; keep it in your app. Removed in 1.0. */ selectItem(coord: string): void; /** @deprecated Selection is UI state; keep it in your app. Removed in 1.0. */ deselectItem(coord: string): void; /** @deprecated Selection is UI state; keep it in your app. Removed in 1.0. */ deselectAllItems(): void; /** Whether the square is on the board and has no item on it. */ isEmpty(coord: string): boolean; isExistCoord(coord: string): boolean; /** Whether the square is on the board's outer ring. */ isEdge(coord: string): boolean; /** Distance from one coord to another, or null if either is off the board. */ getDistanceBetweenTwoCoords(fromCoord: string, toCoord: string): DistanceType | null; /** * Direction from one coord to another, or null when either coord is off * the board or both are the same square. */ getDirection(fromCoord: string, toCoord: string): Direction | null; /** * Squares reachable with `movement`, grouped by direction. Squares off * the board are included, as the movement describes them. */ getColumnsByDirection(coord: string, movement: MovementType): ColumnsByDirection; /** Squares on the board reachable with `movement`. */ getAvailableColumns(coord: string, movement: MovementType): string[]; /** @deprecated Use `getColumnsByDirection(coord, movement)`. */ getAvailableColumns(coord: string, movement: MovementType, columnsObj: true): ColumnsByDirection; /** * The squares from `coord` (not included) in `direction`, nearest first: * at most `steps` of them, and none past the edge of the board. */ ray(coord: string, direction: Direction, steps?: number): string[]; /** The squares next to `coord` on the board: four, or eight with `diagonal`. */ getNeighbors(coord: string, { diagonal }?: { diagonal?: boolean; }): string[]; /** * Every square reachable from `from` in at most `steps` steps, with the * number of steps it takes (the start is included, at 0). By default a * step may go to any empty square; pass `canEnter` to decide yourself, * e.g. to walk through allies or around water. With several starts, * each square gets the steps from the nearest one. */ getReachable(from: string | string[], options?: PathOptions): Map; /** * The shortest way from `from` to `to`, as the squares stepped on (not * including `from`), or null if there is none. Same options as * `getReachable`; `to` itself must pass `canEnter`. */ findPath(from: string, to: string, options?: PathOptions): string[] | null; } export type Square = { coord: string; row: number; col: number; item: T | null; }; /** Called with each item on the board (empty squares are skipped) and its coord. */ export type ItemPredicate = (item: T, coord: string) => boolean; /** Copies of the items on a board by coord, from `snapshot`, for `restore`. */ export type BoardSnapshot = ReadonlyMap; export type PathOptions = { /** The most steps to take. Unlimited by default. */ steps?: number; /** Whether a step may go onto `coord` (coming from `from`). Default: the square is empty. */ canEnter?: (coord: string, from: string) => boolean; /** Also step diagonally. */ diagonal?: boolean; }; export default Board; ``` ### @ymir-js/core: coords.d.ts ```ts import type { Direction } from './board.js'; /** The four straight directions, the ones `linear` movement uses. */ export declare const LINEAR_DIRECTIONS: readonly Direction[]; /** The four diagonal directions, the ones `angular` movement uses. */ export declare const ANGULAR_DIRECTIONS: readonly Direction[]; /** All eight directions, in the order `getColumnsByDirection` lists them. */ export declare const DIRECTIONS: readonly Direction[]; /** One step in each direction, as `[rows, cols]`. */ export declare const DIRECTION_STEPS: Readonly>; /** `row` and `col` as a `"row|col"` coord. */ export declare const toCoord: (row: number, col: number) => string; /** The coord `steps` squares from `coord` in `direction`, on the board or not. */ export declare const stepCoord: (coord: string, direction: Direction, steps?: number) => string; /** Steps from one coord to another along rows and columns only. */ export declare const manhattan: (from: string, to: string) => number; ``` ### @ymir-js/core: index.d.ts ```ts export { default as Board } from './board.js'; export { default as Item } from './item.js'; export { default as cloneItem } from './utils/cloneItem.js'; export { default as parseCoord } from './utils/parseCoord.js'; export { ANGULAR_DIRECTIONS, DIRECTION_STEPS, DIRECTIONS, LINEAR_DIRECTIONS, manhattan, stepCoord, toCoord, } from './coords.js'; export type { BoardConfig, BoardMatrixItem, BoardSnapshot, BoardSize, BoardType, ColumnsByDirection, ColumnType, Direction, DistanceType, ItemPredicate, PathOptions, ResolvedBoardConfig, Square, } from './board.js'; export type { ItemOptions, ItemType, MovementType } from './item.js'; ``` ### @ymir-js/core: item.d.ts ```ts export type MovementType = { top?: boolean; bottom?: boolean; left?: boolean; right?: boolean; topLeft?: boolean; topRight?: boolean; bottomLeft?: boolean; bottomRight?: boolean; angular?: boolean; linear?: boolean; stepCount?: number; }; export type ItemType = { name: string; movement: MovementType; /** @deprecated Not used by the library. Removed in 1.0. */ lock: boolean; /** @deprecated Selection is UI state; keep it in your app. Removed in 1.0. */ selected: boolean; [key: string]: any; }; export type ItemOptions = Partial> & { data?: TData; [key: string]: any; }; declare class Item implements ItemType { name: string; /** Anything your game wants to keep on the item. */ data: TData | undefined; /** @deprecated Not used by the library. Removed in 1.0. */ lock: boolean; /** @deprecated Selection is UI state; keep it in your app. Removed in 1.0. */ selected: boolean; movement: MovementType; constructor(item: ItemOptions); } export default Item; ``` ### @ymir-js/core: utils/cloneItem.d.ts ```ts /** * A copy of an item that keeps its class: its own fields are copied, with * plain objects and arrays (like `data` and `movement`) copied deeply, so * changing the copy never changes the original. The constructor is not run. */ declare const cloneItem: (item: T) => T; export default cloneItem; ``` ### @ymir-js/core: utils/getAvailableColumns.d.ts ```ts import { MovementType } from '../item.js'; declare const getAvailableColumns: (coord: string, movement: MovementType) => { [key: string]: string[]; }; export default getAvailableColumns; ``` ### @ymir-js/core: utils/parseCoord.d.ts ```ts /** * `"row|col"` to `[row, col]`. Plain coords take a fast path; anything * else is parsed as `coord.split('|').map((n) => parseInt(n, 10))`. */ declare const parseCoord: (coord: string) => [row: number, col: number]; export default parseCoord; ``` ### @ymir-js/go: board.d.ts ```ts import { Board, Item, ItemType } from '@ymir-js/core'; export type GoColor = 'black' | 'white'; export type GoStoneType = ItemType & { color: GoColor; }; export declare class GoStone extends Item implements GoStoneType { color: GoColor; constructor({ color }: { color: GoColor; }); } export type GoGroup = { color: GoColor; stones: string[]; liberties: string[]; }; /** Every stone on the board, by coord: `b` for black, `w` for white. */ export type GoPosition = Record; export type GoScoringRules = 'area' | 'territory'; export type GoScore = { black: number; white: number; /** Points of empty board each side surrounds. */ territory: { black: string[]; white: string[]; }; /** Empty points that touch both colours, or none. */ neutral: string[]; /** Positive when black is ahead, negative when white is. */ margin: number; winner: GoColor | null; }; export declare const otherColor: (color: GoColor) => GoColor; /** * A Go board: stones, groups, liberties and captures. Ko and turns belong to * the game (`GoGame`), since they depend on the moves played before. */ declare class GoBoard extends Board { readonly size: number; constructor(size?: number); getColor(coord: string): GoColor | null; /** The group of connected stones on `coord`, with its liberties. */ getGroup(coord: string): GoGroup | null; /** Every group of stones on the board. */ getGroups(): GoGroup[]; /** * What placing a stone would do, without placing it: the stones it * captures, or why it is not allowed. Ko is checked by the game. */ checkMove(color: GoColor, coord: string): { legal: true; captured: string[]; } | { legal: false; reason: 'occupied' | 'suicide' | 'off-board'; }; /** * Places a stone and removes the stones it captures. Returns the * captured coords, or null if the move is not allowed. */ placeStone(color: GoColor, coord: string): string[] | null; getPosition(): GoPosition; setPosition(position: GoPosition): this; /** * Scores the board. Stones in `dead` are taken off first and counted as * prisoners for the other side. * * - `area` (Chinese): stones on the board + surrounded empty points. * - `territory` (Japanese): surrounded empty points + prisoners * (`captures` made during the game, plus dead stones). * * `komi` is added to white. */ score({ rules, komi, dead, captures, }?: { rules?: GoScoringRules; komi?: number; dead?: string[]; captures?: Record; }): GoScore; /** A coord in GTP notation, e.g. `D4` (columns skip I; row 1 is the bottom). */ toGTP(coord: string): string; /** The coord of a GTP vertex such as `D4` or `q16`. */ fromGTP(vertex: string): string; } export default GoBoard; ``` ### @ymir-js/go: game.d.ts ```ts import GoBoard, { GoColor, GoPosition, GoScore, GoScoringRules } from './board.js'; export type GoMove = { type: 'play'; color: GoColor; coord: string; captured: string[]; } | { type: 'pass'; color: GoColor; }; export type GoKoRule = /** A move may not recreate the position from just before the opponent's last move. */ 'simple' /** A move may not recreate any earlier position. */ | 'positional'; export type GoGameOptions = { size?: number; rules?: GoScoringRules; komi?: number; ko?: GoKoRule; /** Stones placed before the first move (e.g. handicap). Black then gives white the first move. */ setup?: GoPosition; turn?: GoColor; }; export type GoStatus = { state: 'playing'; } /** Both players passed. Mark dead stones, then read `getScore()`. */ | { state: 'scoring'; } | { state: 'resigned'; winner: GoColor; }; export type GoIllegalReason = 'occupied' | 'suicide' | 'off-board' | 'ko' | 'game-over'; export type SavedGoGame = { size: number; rules: GoScoringRules; komi: number; ko: GoKoRule; setup: GoPosition; turn: GoColor; moves: ({ type: 'play'; coord: string; } | { type: 'pass'; } | { type: 'resign'; })[]; dead: string[]; }; /** * A game of Go: turns, captures, ko, passing, resigning, dead stones and * the score. */ declare class GoGame { readonly board: GoBoard; readonly rules: GoScoringRules; readonly komi: number; readonly ko: GoKoRule; turn: GoColor; /** Stones each colour has captured. */ captures: Record; private readonly setup; private readonly firstTurn; private history; private readonly setupKey; private resignedBy; private dead; constructor({ size, rules, komi, ko, setup, turn, }?: GoGameOptions); static fromJSON(saved: SavedGoGame): GoGame; get moves(): GoMove[]; /** The point a simple-ko rule forbids right now, if any. */ get koPoint(): string | null; getStatus(): GoStatus; /** Whether the side to move may play on `coord`, and why not if not. */ checkPlay(coord: string): { legal: true; captured: string[]; } | { legal: false; reason: GoIllegalReason; }; /** Every point the side to move may play on. */ getLegalMoves(): string[]; /** Plays a stone for the side to move. Throws if it is not allowed. */ play(coord: string): GoMove; pass(): GoMove; /** The side to move resigns. */ resign(): void; /** Takes back the last move (or a resignation). Dead-stone marks are cleared. */ undo(): GoMove | null; /** Marks or unmarks the whole group on `coord` as dead, for scoring. */ toggleDead(coord: string): void; getDeadStones(): string[]; /** The score with the current dead-stone marks. */ getScore(): GoScore; toJSON(): SavedGoGame; } export default GoGame; ``` ### @ymir-js/go: index.d.ts ```ts export { default as GoBoard, GoStone, otherColor } from './board.js'; export { default as GoGame } from './game.js'; export type { GoColor, GoGroup, GoPosition, GoScore, GoScoringRules, GoStoneType } from './board.js'; export type { GoGameOptions, GoIllegalReason, GoKoRule, GoMove, GoStatus, SavedGoGame, } from './game.js'; ``` ### @ymir-js/match3: board.d.ts ```ts import { Board, BoardSize, Item, ItemType } from '@ymir-js/core'; export type Match3ItemType = ItemType & { /** Which kind of gem this is, e.g. 'red'. Gems of the same kind match. */ kind: string; }; export declare class Match3Item extends Item implements Match3ItemType { kind: string; constructor({ kind }: { kind: string; }); } export type Match3Options = Partial & { /** Gem kinds to draw from. At least 3. */ kinds?: string[]; /** Random source in [0, 1). Pass a seeded one for repeatable boards. */ random?: () => number; /** Points per cleared gem. Each cascade step multiplies it: 1x, 2x, 3x, ... */ pointsPerGem?: number; }; export type Swap = { from: string; to: string; }; /** One round of clearing: matches found, gems fallen and new gems added. */ export type CascadeStep = { /** Each run of three or more of a kind, as coords. */ matches: string[][]; /** Coords cleared in this step (every match, without duplicates). */ cleared: string[]; /** Gems that fell down to fill the gaps. */ fallen: { from: string; to: string; }[]; /** New gems dropped in at the top. */ spawned: { coord: string; kind: string; }[]; points: number; }; export type SwapResult = { /** False when the swap made no match; the board is left unchanged. */ valid: boolean; steps: CascadeStep[]; points: number; /** True when no move was left after the cascade and the board was shuffled. */ shuffled: boolean; }; /** * A match-3 board: swap two neighbouring gems to line up three or more of a * kind. Matches clear, gems above fall, new ones drop in, and the cascade * repeats until nothing matches. */ declare class Match3Board extends Board { readonly kinds: string[]; readonly pointsPerGem: number; private readonly random; constructor({ rows, cols, kinds, random, pointsPerGem, }?: Match3Options); /** Fills the board with gems: no ready-made matches, at least one move. */ init(): this; /** Sets up gems from rows of kinds, e.g. `[['red', 'blue'], ...]`. */ setKinds(rows: (string | null)[][]): this; /** The kind on every square, row by row. */ getKinds(): (string | null)[][]; isAdjacent(from: string, to: string): boolean; /** Every run of three or more of the same kind, in rows and columns. */ findMatches(): string[][]; /** Whether swapping these two gems would make a match. */ canSwap(from: string, to: string): boolean; /** Every swap that makes a match. Empty means the board is stuck. */ getPossibleMoves(): Swap[]; /** * Swaps two neighbouring gems and resolves the cascade. If the swap makes * no match, nothing changes and `valid` is false. */ swap(from: string, to: string): SwapResult; /** * Rearranges the gems on the board until there is no match and at least * one move. Falls back to new gems if the current ones cannot get there. */ shuffle(): this; private resolveStep; /** Drops gems into the gaps below them and fills the top with new ones. */ private collapse; /** A kind for `coord` that does not complete a run with the two gems to its left or above. */ private pickKind; private forEachCoord; } export default Match3Board; ``` ### @ymir-js/match3: index.d.ts ```ts export { default as Match3Board, Match3Item } from './board.js'; export type { CascadeStep, Match3ItemType, Match3Options, Swap, SwapResult } from './board.js'; ``` ### ymir-js: index.d.ts ```ts import { Board, Item } from '@ymir-js/core'; import { CheckersGame, InternationalBoard, InternationalItem, TurkishBoard, TurkishItem } from '@ymir-js/checkers'; import { ChessBoard, ChessGame, ChessPiece } from '@ymir-js/chess'; import { GoBoard, GoGame, GoStone } from '@ymir-js/go'; import { Match3Board, Match3Item } from '@ymir-js/match3'; export * from '@ymir-js/core'; export * from '@ymir-js/checkers'; export * from '@ymir-js/chess'; export * from '@ymir-js/go'; export * from '@ymir-js/match3'; export declare const Core: { Board: typeof Board; Item: typeof Item; }; export declare const Checkers: { Turkish: { Board: typeof TurkishBoard; Item: typeof TurkishItem; }; International: { Board: typeof InternationalBoard; Item: typeof InternationalItem; }; Game: typeof CheckersGame; }; export declare const Chess: { Board: typeof ChessBoard; Piece: typeof ChessPiece; Game: typeof ChessGame; }; export declare const Go: { Board: typeof GoBoard; Stone: typeof GoStone; Game: typeof GoGame; }; export declare const Match3: { Board: typeof Match3Board; Item: typeof Match3Item; }; export declare const Utils: { parseCoord: (coord: string) => [row: number, col: number]; }; ```