From 8babd550372c31b535502eaf997ba29e893bd2ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Neto?= Date: Wed, 13 May 2026 15:31:11 +0100 Subject: [PATCH] Added Halma --- locales/en/apgames.json | 14 + src/games/halma.ts | 554 ++++++++++++++++++++++++++++++++++++++++ src/games/index.ts | 8 +- 3 files changed, 574 insertions(+), 2 deletions(-) create mode 100644 src/games/halma.ts diff --git a/locales/en/apgames.json b/locales/en/apgames.json index 4422fb52..f61036ce 100644 --- a/locales/en/apgames.json +++ b/locales/en/apgames.json @@ -102,6 +102,7 @@ "gorogo": "A Go variant played on a small board where the goal is to capture more pieces than your opponent, rather than to claim territory.", "gyges": "A breakthrough game where nobody owns any pieces, and pieces rebound off of each other.", "gyve": "Drawless unification game where you try to form fewer groups than your opponent. On each turn, you place two stones in sequence so that each one is adjacent to the same number of friendly groups at the moment of placement.", + "halma": "The original traversal game. Players need to be the first to move all their armies to the opponent's home base.", "havannah": "A connection game where you vye to form either a ring, a bridge, or a fork. A ring is a chain around at least one cell. A bridge is a chain linking two corners. A fork is a chain linking three sides. Corners do not belong to either side.", "hens": "Hens and Chicks is a draughts-style game with two types of pieces and a unique capturing mechanic. The winner is the first to land a piece in their opponent's home row or to eliminate all the opposing hens.", "hex": "In Hex, two players attempt to connect opposite sides of a rhombus-shaped board made of hexagonal cells", @@ -271,6 +272,7 @@ "frogger": "As in other Decktet games at Abstract Play, the deck is displayed at the bottom of the board and includes both cards in the deck and unknown cards in other players' hands. After the first hand, all cards are drawn from the open draw pool, so hands gradually become open. The discards pile is also displayed.\n\nDue to how randomization works at Abstract Play, forced passes are needed for a player to refill the draw pool in the middle of his turn. Passes are handled automatically by the server, but a there's also a draw pool variant that avoids forced passing if desired.\n\nThe Crocodiles variant is by Jorge Arroyo, the translator of the English rules. The Advanced rules and other minor variants are by P. D. Magnus; they appear in The Decktet Book, where the game is called Xing.", "garden": "To make it very clear what happened on a previous turn, each move is displayed over four separate boards. The first board shows the game after the piece was first placed. The second board shows the state after adjacent pieces were flipped. The third board shows any harvests. The fourth board is the final game state and is where you make your moves.\n\nIn our implementation, black is always the \"tome\" or tie-breaker colour. The last player to harvest black will have a `0.1` after their score.", "gyges": "The goal squares are adjacent to all the cells in the back row. The renderer cannot currently handle \"floating\" cells.", + "halma": "[Halma](https://en.wikipedia.org/wiki/Halma) was one of the first commercial successes for an abstract game. The game was designed by [George Howard Monks](https://en.wikipedia.org/wiki/George_Howard_Monks) in 1883/4. It is said that Halma was inspired by an older British game called *Hoppity*. However, this game has no documentation or surviving boards, turning this lineal statement into a historical mystery. Halma was later adapted (c.1892/3) into an even bigger success: [Chinese Checkers](https://boardgamegeek.com/boardgame/2386/chinese-checkers) (which could easily be played by three or six players).", "homeworlds": "The win condition is what's called \"Sinister Homeworlds.\" You only win by defeating the opponent to your left. If someone else does that, the game continues, but your left-hand opponent now shifts clockwise. For example, in a four-player game, if I'm South, then I win if I eliminate West. But if the North player ends up eliminating West, the game continues, but now my left-hand opponent is North.", "jacynth": "More information on the Decktet system can be found on the [official Decktet website](https://www.decktet.com). Cards in players' hands are hidden from observers and opponents.", "konane": "Several competing opening protocols exist, but the most common ruleset is the Naihe Ruleset, used by tournaments at the Bishop Museum in Hawaii and described in the BGG reference. This is what is implemented here.", @@ -1446,6 +1448,11 @@ "name": "Hexhex 12 (397 spaces)" } }, + "halma": { + "#board": { + "name": "16x16 board" + } + }, "havannah": { "#board": { "name": "Size-8 board" @@ -4975,6 +4982,13 @@ "NO_DUPES": "You must place stones on different cells.", "PARTIAL": "Select an empty cell to place a second stone. It must be adjacent to the same number of groups as the first stone." }, + "halma": { + "INITIAL_INSTRUCTIONS": "Move to an adjacent empty cell, or multiple short-jump a friendly piece.", + "NONEXISTENT": "Trying to interact with a friendly piece that doesn't exist at {{where}}.", + "ILLEGAL_MOVE": "No piece can return to his home-base, nor pieces inside the opponent's home-base can leave again.", + "FORCED_MOVES": "Pieces still at home-base are forced to forward-jump over enemy neighbors: pick {{forced}}.", + "BAD_MOVE": "This movement is illegal!" + }, "havannah": { "INITIAL_INSTRUCTIONS": "Select a point to place a piece." }, diff --git a/src/games/halma.ts b/src/games/halma.ts new file mode 100644 index 00000000..f125f805 --- /dev/null +++ b/src/games/halma.ts @@ -0,0 +1,554 @@ +import { GameBase, IAPGameState, IClickResult, IIndividualState, IValidationResult } from "./_base"; +import { APGamesInformation } from "../schemas/gameinfo"; +import { APRenderRep, RowCol, Colourfuncs } from "@abstractplay/renderer/src/schemas/schema"; +import { APMoveResult } from "../schemas/moveresults"; +import { allDirections, reviver, UserFacingError, SquareGraph, Direction } from "../common"; +import i18next from "i18next"; + +export type playerid = 1 |2; + +export interface IMoveState extends IIndividualState { + currplayer: playerid; + board: Map; + lastmove?: string; +}; + +export interface IHalmaState extends IAPGameState { + winner: playerid[]; + stack: Array; +}; + +export class HalmaGame extends GameBase { + public static readonly gameinfo: APGamesInformation = { + name: "Halma", + uid: "halma", + playercounts: [2], + version: "20260513", + dateAdded: "2026-05-13", + // i18next.t("apgames:descriptions.halma") + description: "apgames:descriptions.halma", + // i18next.t("apgames:notes.halma") + notes: "apgames:notes.halma", + urls: ["https://en.wikipedia.org/wiki/Halma"], + people: [ + { + type: "coder", + name: "João Pedro Neto", + urls: ["https://boardgamegeek.com/boardgamedesigner/3829/joao-pedro-neto"], + apid: "9228bccd-a1bd-452b-b94f-d05380e6638f", + }, + ], + categories: ["goal>evacuate", "other>traditional", "mechanic>move", "board>shape>rect", "components>simple>1per", "other>2+players"], + flags: ["no-moves", "experimental"] + }; + + public numplayers = 2; + public currplayer: playerid = 1; + public board!: Map; + public gameover = false; + public winner: playerid[] = []; + public variants: string[] = []; + public stack!: Array; + public results: Array = []; + private dots: string[] = []; + + constructor(state: IHalmaState | string, variants?: string[]) { + super(); + if (state === undefined) { + if ( (variants !== undefined) && (variants.length > 0) ) { + this.variants = [...variants]; + } + + const board = new Map([ + ["p1", 1], ["p2", 1], ["p3", 1], ["p4", 1], ["p5", 1], + ["o1", 1], ["o2", 1], ["o3", 1], ["o4", 1], ["o5", 1], + ["n1", 1], ["n2", 1], ["n3", 1], ["n4", 1], + ["m1", 1], ["m2", 1], ["m3", 1], + ["l1", 1], ["l2", 1], ["e15", 2], ["e16", 2], + ["d14", 2], ["d15", 2], ["d16", 2], + ["c13", 2], ["c14", 2], ["c15", 2], ["c16", 2], + ["b12", 2], ["b13", 2], ["b14", 2], ["b15", 2], ["b16", 2], + ["a12", 2], ["a13", 2], ["a14", 2], ["a15", 2], ["a16", 2], + ]); + + const fresh: IMoveState = { + _version: HalmaGame.gameinfo.version, + _results: [], + _timestamp: new Date(), + currplayer: 1, + board, + }; + this.stack = [fresh]; + } else { + if (typeof state === "string") { + state = JSON.parse(state, reviver) as IHalmaState; + } + if (state.game !== HalmaGame.gameinfo.uid) { + throw new Error(`The Halma engine cannot process a game of '${state.game}'.`); + } + this.gameover = state.gameover; + this.winner = [...state.winner]; + this.variants = state.variants; + this.stack = [...state.stack]; + } + this.load(); + } + + public load(idx = -1): HalmaGame { + if (idx < 0) { + idx += this.stack.length; + } + if ( (idx < 0) || (idx >= this.stack.length) ) { + throw new Error("Could not load the requested state from the stack."); + } + + const state = this.stack[idx]; + this.currplayer = state.currplayer; + this.board = new Map(state.board); + this.lastmove = state.lastmove; + this.results = [...state._results]; + return this; + } + + public get boardsize(): number { + return 16; + } + + public get graph(): SquareGraph { + return new SquareGraph(this.boardsize, this.boardsize); + } + + private homeBase(player?: playerid): string[] { + if (player === undefined) { player = this.currplayer; } + return player === 1 ? + ["p1", "p2", "p3", "p4", "p5", "o1", "o2", "o3", "o4", "o5", + "n1", "n2", "n3", "n4", "m1", "m2", "m3", "l1", "l2"] : + ["a12", "a13", "a14", "a15", "a16", "b12", "b13", "b14", "b15", "b16", + "c13", "c14", "c15", "c16", "d14", "d15", "d16", "e15", "e16"]; + } + + private enemyBase(player?: playerid): string[] { + if (player === undefined) { player = this.currplayer; } + return this.homeBase(player % 2 + 1 as playerid); + } + + // Check the base movement's requirements: + // * No stone inside the opponent's home-base can leave again + // * No stone can return to his home-base. + private respectBases(start: string, end: string): boolean { + if (!this.homeBase().includes(start) && this.homeBase().includes(end)) { + return false; + } + if (this.enemyBase().includes(start) && !this.enemyBase().includes(end)) { + return false; + } + return true + } + + private jumpNeighbors(cell: string): string[] { + const res: string[] = []; + const g = this.graph; + const [x, y] = g.algebraic2coords(cell); + + for (const dir of allDirections) { + const ray = g.ray(x, y, dir).map(c => g.coords2algebraic(...c)); + if (ray.length >= 2) { + if (this.board.has(ray[0]) && !this.board.has(ray[1])) { + if ( this.respectBases(cell, ray[1]) ) { + res.push(ray[1]); + } + } + } + } + return res; + } + + // Any piece in a player's home-base must make progress towards the enemy camp whenever + // this is possible by jumping over an enemy piece (Zillions' rule quoting Sid Sackson) + // This method returns all mandatory jumps (in any) + private mandatoryMoves(player?: playerid): string[] { + if (player === undefined) { player = this.currplayer; } + const g = this.graph; + const prevplayer = player % 2 + 1 as playerid; + const res = []; + + // select all friendly pieces from the home-base + const base = this.homeBase(player); + const homePieces = [...this.board.entries()].filter(e => e[1] === player) + .filter(e => base.includes(e[0])) + .map(e => e[0]); + + // check if there are neighbor opponent pieces in the forward directions + const dirs: Direction[] = player === 1 ? ["N", "NW", "W"] : ["S", "SE", "E"]; + for (const cell of homePieces) { + const [x, y] = g.algebraic2coords(cell); + for (const dir of dirs) { + const ray = g.ray(x, y, dir).map(c => g.coords2algebraic(...c)); + if (ray.length >= 2) { + if (this.board.has(ray[0]) && this.board.get(ray[0]) === prevplayer && !this.board.has(ray[1])) { + res.push(`${cell}-${ray[1]}`); + } + } + } + } + return res; + } + + private trimIfRepeated(moves: string[]): string[] { + if (moves.length === 0) { return []; } + const last = moves[moves.length - 1]; + const firstIndex = moves.indexOf(last); + + if (firstIndex !== moves.length - 1) { // if the last element appears earlier + return moves.slice(0, firstIndex+1); + } + return moves; + } + + public handleClick(move: string, row: number, col: number, piece?: string): IClickResult { + try { + const g = this.graph; + const cell = g.coords2algebraic(col, row); + let newmove:string; + + if (move === "" || this.board.has(cell)) { + newmove = cell; + } else { + // if a cell appears again, remove all jumps after its first occurrence + newmove = this.trimIfRepeated(`${move}-${cell}`.split("-")).join("-"); + } + + const result = this.validateMove(newmove) as IClickResult; + result.move = result.valid ? newmove : move; + return result; + } catch (e) { + return { + move, + valid: false, + message: i18next.t("apgames:validation._general.GENERIC", {move, row, col, piece, emessage: (e as Error).message}) + } + } + } + + private hasPrefix(moves: string[], partial: string): boolean { + return moves.some(str => str.startsWith(partial)); + } + + public validateMove(m: string): IValidationResult { + const result: IValidationResult = {valid: false, message: i18next.t("apgames:validation._general.DEFAULT_HANDLER")}; + + if (m.length === 0) { + result.valid = true; + result.complete = -1; + result.message = i18next.t("apgames:validation.halma.INITIAL_INSTRUCTIONS") + return result; + } + + const mandatory = this.mandatoryMoves(); + if ( mandatory.length > 0 ) { // mandatory moves take precedence! + if ( !this.hasPrefix(mandatory, m) && !mandatory.some(move => m.startsWith(move)) ) { + result.valid = false; + result.message = i18next.t("apgames:validation.halma.FORCED_MOVES", {forced: mandatory}); + return result; + } + } + + // drop or start of move + if (!m.includes("-")) { + + if (!this.board.has(m)) { // must be occupied + result.valid = false; + result.message = i18next.t("apgames:validation.halma.NONEXISTENT", {where: m}); + return result; + } + + if (this.board.get(m)! !== this.currplayer) { // must be a friendly stone + result.valid = false; + result.message = i18next.t("apgames:validation._general.UNCONTROLLED"); + return result; + } + // valid partial + result.valid = true; + result.complete = -1; + result.canrender = true; + result.message = i18next.t("apgames:validation._general.NEED_DESTINATION"); + return result; + + } else { + + const cells = m.split("-"); + let isJump = true; + if ( cells.length === 2 ) { + const g = this.graph; + const emptyNeighbors = g.graph.neighbors(cells[0]).filter(c => !this.board.has(c)); + if ( emptyNeighbors.includes(cells[1]) ) { + isJump = false; // it is a move + } + if (! this.respectBases(cells[0], cells[1]) ) { + result.valid = false; + result.message = i18next.t("apgames:validation.halma.ILLEGAL_MOVE"); + return result; + } + } + if ( isJump ) { + for (let i = 0; i < cells.length - 1; i++) { + const from = cells[i]; + const to = cells[i+1]; + if (! this.jumpNeighbors(from).includes(to) ) { + result.valid = false; + result.message = i18next.t("apgames:validation.halma.BAD_MOVE", {from, to}); + return result; + } + } + const last = cells.at(-1)!; + const penultimate = cells.at(-2)!; + const neighborsLast = this.jumpNeighbors(last); + if ( (neighborsLast.length === 0) || + (neighborsLast.length === 1 && neighborsLast.includes(penultimate)) ) { + isJump = false; // ie, this last jump is final; let's pretend it is a move to finish the sequence + } + } + + result.valid = true; + result.complete = isJump ? 0 : 1; // moves are final, jumps can be multiple + result.canrender = true; + result.message = i18next.t("apgames:validation._general.VALID_MOVE"); + return result; + } + } + + public move(m: string, {trusted = false, partial = false} = {}): HalmaGame { + if (this.gameover) { + throw new UserFacingError("MOVES_GAMEOVER", i18next.t("apgames:MOVES_GAMEOVER")); + } + + m = m.toLowerCase(); + m = m.replace(/\s+/g, ""); + if (! trusted) { + const result = this.validateMove(m); + if (! result.valid) { + throw new UserFacingError("VALIDATION_GENERAL", result.message) + } + } + + this.results = []; + this.dots = []; + + if (m === "") { return this;} + + if (partial) { // if partial, populate dots and get out + const mandatory = this.mandatoryMoves(); + if ( mandatory.length > 0 ) { // mandatory moves take precedence! + if ( this.hasPrefix(mandatory, m) ) { + this.dots.push(...mandatory.map(move => move.split('-')[1]) ); + } + if (! mandatory.some(move => m.startsWith(move)) ) { + return this; + } + } + + const cells = m.split("-"); + // if just starting, add simple moves + if (cells.length === 1) { + const g = this.graph; + this.dots.push(...g.graph.neighbors(cells[0]).filter(c => !this.board.has(c)) + .filter(c => this.respectBases(m, c))); + } + // now add jumps + this.dots.push(...this.jumpNeighbors(cells[cells.length - 1])); + + // go ahead and move the piece so the display updates + this.board.delete(cells[0]); + this.board.set(cells[cells.length - 1], this.currplayer); + return this; + } + + if (m.includes("-")) { + const steps = m.split("-"); + const from = steps[0]; + const to = steps[steps.length - 1]; + this.board.delete(from); + this.board.set(to, this.currplayer); + for (let i = 0; i < steps.length-1; i++) { + this.results.push({type: "move", from: steps[i], to: steps[i+1]}); + } + } else { + this.board.set(m, this.currplayer); + this.results.push({type: "place", where: m}); + } + + this.lastmove = m; + this.currplayer = this.currplayer % 2 + 1 as playerid; + this.checkEOG(); + this.saveState(); + return this; + } + + protected checkEOG(): HalmaGame { + const prevplayer = this.currplayer % 2 + 1 as playerid; + // a player wins if opponent's base is full and he has at least one stone there (Parlett's win condition) + const isEnemyFull = this.enemyBase(prevplayer).every(c => this.board.has(c)); + const haveOneThere = this.enemyBase(prevplayer).some(c => this.board.has(c) && this.board.get(c) === prevplayer); + + if ( isEnemyFull && haveOneThere ) { + this.gameover = true; + this.winner = [prevplayer]; + } + + if (this.gameover) { + this.results.push( + {type: "eog"}, + {type: "winners", players: [...this.winner]} + ); + } + return this; + } + + public state(): IHalmaState { + return { + game: HalmaGame.gameinfo.uid, + numplayers: this.numplayers, + variants: [...this.variants], + gameover: this.gameover, + winner: [...this.winner], + stack: [...this.stack] + }; + } + + public moveState(): IMoveState { + return { + _version: HalmaGame.gameinfo.version, + _results: [...this.results], + _timestamp: new Date(), + currplayer: this.currplayer, + lastmove: this.lastmove, + board: new Map(this.board), + }; + } + + public render(): APRenderRep { + const g = this.graph; + // Build piece string + let pstr = ""; + for (let row = 0; row < this.boardsize; row++) { + if (pstr.length > 0) { pstr += "\n"; } + const pieces: string[] = []; + for (let col = 0; col < this.boardsize; col++) { + const cell = g.coords2algebraic(col, row); + if (this.board.has(cell)) { + const contents = this.board.get(cell)!; + if (contents === 1) { + pieces.push("A"); + } else { + pieces.push("B"); + } + } else { + pieces.push("-"); + } + } + pstr += pieces.join(""); + } + + const base: Colourfuncs = { + func: "custom", + default: "#FFDF00", // gold yellow + palette: 3 + }; + + const size = this.boardsize; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const markers: Array = [ // shade home-bases + { + type: "shading", + colour: this.getPlayerColour(1), + points: [ {row:size , col:size-5}, {row:size , col:size }, {row:size-5, col:size }, + {row:size-5, col:size-2}, {row:size-4, col:size-2}, {row:size-4, col:size-3}, + {row:size-3, col:size-3}, {row:size-3, col:size-4}, {row:size-2, col:size-4}, + {row:size-2, col:size-5}, {row:size , col:size-5}] + }, + { + type: "shading", + colour: this.getPlayerColour(2), + points: [ {row:0, col:0}, {row:0, col:5}, {row:2, col:5}, {row:2, col:4}, {row:3, col:4}, + {row:3, col:3}, {row:4, col:3}, {row:4, col:2}, {row:5, col:2}, {row:5, col:0}, + {row:0, col:0} + ] + }, + // draw home-base frontier of player 1 + { type: "line", colour: base, points: [ {row:size , col:size-5}, {row:size-2, col:size-5} ], width: 3, opacity: 0.5 }, + { type: "line", colour: base, points: [ {row:size-2, col:size-5}, {row:size-2, col:size-4} ], width: 3, opacity: 0.5 }, + { type: "line", colour: base, points: [ {row:size-2, col:size-4}, {row:size-3, col:size-4} ], width: 3, opacity: 0.5 }, + { type: "line", colour: base, points: [ {row:size-3, col:size-4}, {row:size-3, col:size-3} ], width: 3, opacity: 0.5 }, + { type: "line", colour: base, points: [ {row:size-3, col:size-3}, {row:size-4, col:size-3} ], width: 3, opacity: 0.5 }, + { type: "line", colour: base, points: [ {row:size-4, col:size-3}, {row:size-4, col:size-2} ], width: 3, opacity: 0.5 }, + { type: "line", colour: base, points: [ {row:size-4, col:size-2}, {row:size-5, col:size-2} ], width: 3, opacity: 0.5 }, + { type: "line", colour: base, points: [ {row:size-5, col:size-2}, {row:size-5, col:size } ], width: 3, opacity: 0.5 }, + // draw home-base frontier of player 2 + { type: "line", colour: base, points: [ {row:0, col:5}, {row:2, col:5} ], width: 3, opacity: 0.5 }, + { type: "line", colour: base, points: [ {row:2, col:5}, {row:2, col:4} ], width: 3, opacity: 0.5 }, + { type: "line", colour: base, points: [ {row:2, col:4}, {row:3, col:4} ], width: 3, opacity: 0.5 }, + { type: "line", colour: base, points: [ {row:3, col:4}, {row:3, col:3} ], width: 3, opacity: 0.5 }, + { type: "line", colour: base, points: [ {row:3, col:3}, {row:4, col:3} ], width: 3, opacity: 0.5 }, + { type: "line", colour: base, points: [ {row:4, col:3}, {row:4, col:2} ], width: 3, opacity: 0.5 }, + { type: "line", colour: base, points: [ {row:4, col:2}, {row:5, col:2} ], width: 3, opacity: 0.5 }, + { type: "line", colour: base, points: [ {row:5, col:2}, {row:5, col:0} ], width: 3, opacity: 0.5 }, + ]; + + // Build rep + const rep: APRenderRep = { + board: { + style: "squares-checkered", + width: this.boardsize, + height: this.boardsize, + markers, + }, + legend: { + A: { name: "piece", colour: this.getPlayerColour(1) }, + B: { name: "piece", colour: this.getPlayerColour(2) }, + }, + pieces: pstr + }; + + // Add annotations + if (this.stack[this.stack.length - 1]._results.length > 0) { + rep.annotations = []; + for (const move of this.stack[this.stack.length - 1]._results) { + if (move.type === "move") { + const [fromX, fromY] = g.algebraic2coords(move.from); + const [toX, toY] = g.algebraic2coords(move.to); + rep.annotations.push({type: "move", targets: [{row: fromY, col: fromX}, {row: toY, col: toX}]}); + } else if (move.type === "place") { + const [x, y] = g.algebraic2coords(move.where!); + rep.annotations.push({type: "enter", targets: [{row: y, col: x}]}); + } + } + } + + if (this.dots.length > 0) { + if (!("annotations" in rep) || rep.annotations === undefined) { + rep.annotations = []; + } + rep.annotations.push({ + type: "dots", + targets: this.dots.map(cell => { + const [x, y] = g.algebraic2coords(cell); + return {row: y, col: x}; + }) as [RowCol, ...RowCol[]], + }); + } + + return rep; + } + + public getPlayerColour(p: playerid): Colourfuncs { + if (p === 1) { + return { func: "custom", default: 1, palette: 1 }; + } else { + return { func: "custom", default: 2, palette: 2 }; + } + } + + public clone(): HalmaGame { + return new HalmaGame(this.serialize()); + } +} diff --git a/src/games/index.ts b/src/games/index.ts index 95d13ea7..c01d981f 100644 --- a/src/games/index.ts +++ b/src/games/index.ts @@ -246,6 +246,7 @@ import { SlimetrailGame, ISlimetrailState } from "./slimetrail"; import { CatsDogsGame, ICatsDogsState } from "./catsdogs"; import { SoccolotGame, ISoccolotState } from "./soccolot"; import { CourtGame, ICourtState } from "./court"; +import { HalmaGame, IHalmaState } from "./halma"; export { APGamesInformation, GameBase, GameBaseSimultaneous, IAPGameState, @@ -495,6 +496,7 @@ export { CatsDogsGame, ICatsDogsState, SoccolotGame, ISoccolotState, CourtGame, ICourtState, + HalmaGame, IHalmaState, }; const games = new Map(); // Manually add each game to the following array [ @@ -618,7 +620,7 @@ const games = new Map { if (games.has(g.gameinfo.uid)) { throw new Error("Another game with the UID '" + g.gameinfo.uid + "' has already been used. Duplicates are not allowed."); @@ -1122,6 +1124,8 @@ export const GameFactory = (game: string, ...args: any[]): GameBase|GameBaseSimu return new SoccolotGame(...args); case "court": return new CourtGame(...args); + case "halma": + return new HalmaGame(args[0], ...args.slice(1)); } return; }