From 2232985a9cd9fb86f345d14524be532cc94868a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Neto?= <886455+jpneto@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:46:39 +0100 Subject: [PATCH] Added Invector Small corrections/updates to narrows, Unane and Pippinzip --- locales/en/apgames.json | 31 +++ src/games/index.ts | 9 +- src/games/invector.ts | 460 ++++++++++++++++++++++++++++++++++++++++ src/games/narrows.ts | 2 +- src/games/pippinzip.ts | 69 +++--- src/games/unane.ts | 2 +- 6 files changed, 534 insertions(+), 39 deletions(-) create mode 100644 src/games/invector.ts diff --git a/locales/en/apgames.json b/locales/en/apgames.json index 0621e2f1..1a77f503 100644 --- a/locales/en/apgames.json +++ b/locales/en/apgames.json @@ -112,6 +112,7 @@ "hexy": "An adaptation of Y on a regular hexagon where players vye to control most of the perimeter of the board.", "homeworlds": "An Icehouse game for 2 to 4 players. Players are interstellar civilizations vying for dominance. Each of the four colours of pyramid gives access to different actions. Amass a fleet, explore the galaxy, and ultimately destroy your opponent.", "hula": "Two players aim to form a loop around the center of a regular hexagonal board, using their own stones and, possibly, some number of neutral stones.", + "invector": "Eliminate the adversary army while getting near the board center.", "iqishiqi": "Iqishiqi (pronounced EE-chee-shee-chee) is an abstract strategy game that is played on a hexagonal board composed of hexagonal cells. Players own designated edges of the board, and by clever placement of stones they push a neutral stone around the board. A player wins if he/she moves the neutral stone to one of his/her edges, or leaves his/her opponent unable to move.", "irensei": "Irensei is a mixture of Go and Gomoku. Get a seven in a row inside the middle 15x15 region of the Go board to win. To reduce the first player's advantage, the first player loses if they make an overline (even if it extends beyond the 15x15 region), but the second player may win by overline.", "jacynth": "A Decktet card game where opponents vye for control of the city of Jacynth. Place cards, exert influence, and control the most area to win.", @@ -285,6 +286,7 @@ "halma": "To prevent the [drawish nature](https://boardgamegeek.com/thread/3706389/unspoiling-halma-redux) of the game, the following rules apply: (a) a player wins if the opposite home-base is complete with at least one friendly stone (David Parlett's criteria); (b) Any piece in the player's home-base must make progress towards the enemy camp whenever this is possible by jumping over an enemy piece. (Zillions rule, to remove drawish strategies); (c) No stone can return to its home-base.\n\n[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).\n\nSuper Halma is a more dynamic, uncredited variant, presented in the 1992 book **New Rules for Classic Games** by Wayne Schmittberger. This variant proposes long jumps, where the number of empty cells before and after the jumped stone must be equal. The base's move restrictions still apply in this implementation of Super Halma.", "halmaclimbers": "Halma Climbers was designed by Alexander Brady in 2021.\n\nThis implementation allows players to move at will, rather than restricting them to moves that strictly increase the overall score (as mentioned in David Ploog's [ruleset](https://blackandwhite.develz.org/games/HalmaClimbers.pdf)). This design choice removes the burden of checking multiple move combinations just to find out that no more moves exist that increase the overall score. This also prevents the software from having to compute an excessive number of permutations, which can be quite high given the two-action turn structure and complex jump sequences. The game ends when both players pass their turns consecutively, or if the previous player evacuates their own area, or if the current player does not have forward jumps available.", "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.", + "invector": "Invector, designed by Mark Steere in 2026, is an elimination game inspired by the ancient Hawiian game of Kōnane. Invector begins with a checkerboard pattern of stones, has short range captures, and is extremely simple. Invector simply combines two basic, off-the-shelf ingredients: orthogonal, adjacent capturing moves, and orthogonal, adjacent moves closer to center.", "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.", "lasca": "Maneuverability is a measure of how close your pieces are to promoting. Your maximum maneuverability is the board size times the number of stacks you control.\n\nMaterial is calculated by giving you a point for every friendly piece in a stack you control, plus an extra point if you have an officer on top of that stack.", @@ -1616,6 +1618,26 @@ "name": "Toroidal 15x15 board" } }, + "invector": { + "size-6": { + "name": "5x6 board" + }, + "#board": { + "name": "7x8 board" + }, + "size-10": { + "name": "9x10 board" + }, + "size-12": { + "name": "11x12 board" + }, + "size-14": { + "name": "13x14 board" + }, + "size-16": { + "name": "15x16 board" + } + }, "jacynth": { "#deck": { "description": "The basic 36-card deck (aces through crowns)" @@ -5271,6 +5293,15 @@ }, "VALID_W_ACTIONS": "Looks like a valid move, but you still have actions to spend." }, + "invector": { + "INITIAL_INSTRUCTIONS": "Select a friendly piece.", + "INSTRUCTIONS": "Click on an orthogonally opposing piece to capture by replacement, or move closer to the board center.", + "INVALID_SELECTION": "Click on a friendly piece.", + "ONLY_PASS": "No move or captured available. Player must pass!", + "INVALID_PASS": "There are moves or captures available. Player cannot pass.", + "CANNOT_MOVE": "This piece cannot move!", + "INVALID_MOVE": "Need to capture an orthogonally adjacent opponent piece, or move to an orthogonally adjacent empty cell closer to the board center!" + }, "iqishiqi": { "BLOCKED": "The neutral stone cannot move {{spaces}} spaces away from the group of shared stones formed by placement at {{place}}.", "INITIAL_INSTRUCTIONS": "Drop a shared stone to push the neutral stone.", diff --git a/src/games/index.ts b/src/games/index.ts index 2aa08e4a..acac86a3 100644 --- a/src/games/index.ts +++ b/src/games/index.ts @@ -257,6 +257,7 @@ import { LinageGame, ILinageState } from "./linage"; import { PolluxGame, IPolluxState } from "./pollux"; import { PippinzipGame, IPippinzipState } from "./pippinzip"; import { NarrowsGame, INarrowsState } from "./narrows"; +import { InvectorGame, IInvectorState } from "./invector"; export { APGamesInformation, GameBase, GameBaseSimultaneous, IAPGameState, @@ -517,6 +518,7 @@ export { PolluxGame, IPolluxState, PippinzipGame, IPippinzipState, NarrowsGame, INarrowsState, + InvectorGame, IInvectorState, }; const games = new Map(); // Manually add each game to the following array [ @@ -645,7 +647,8 @@ 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."); @@ -1171,6 +1174,8 @@ export const GameFactory = (game: string, ...args: any[]): GameBase|GameBaseSimu return new PippinzipGame(...args); case "narrows": return new NarrowsGame(...args); + case "invector": + return new InvectorGame(...args); } return; } diff --git a/src/games/invector.ts b/src/games/invector.ts new file mode 100644 index 00000000..2a999568 --- /dev/null +++ b/src/games/invector.ts @@ -0,0 +1,460 @@ +import { GameBase, IAPGameState, IClickResult, IIndividualState, IValidationResult } from "./_base"; +import { APGamesInformation } from "../schemas/gameinfo"; +import { APRenderRep } from "@abstractplay/renderer/src/schemas/schema"; +import { APMoveResult } from "../schemas/moveresults"; +import { RectGrid, reviver, UserFacingError, SquareOrthGraph } from "../common"; +import i18next from "i18next"; + +type playerid = 1 | 2; // regarding pieces: 1 is the ball, 2 are the walls + +export interface IMoveState extends IIndividualState { + currplayer: playerid; + board: Map; + lastmove?: string; +}; + +export interface IInvectorState extends IAPGameState { + winner: playerid[]; + stack: Array; +}; + +export class InvectorGame extends GameBase { + public static readonly gameinfo: APGamesInformation = { + name: "Invector", + uid: "invector", + playercounts: [2], + version: "20260605", + dateAdded: "2026-06-05", + // i18next.t("apgames:descriptions.invector") + description: "apgames:descriptions.invector", + notes: "apgames:notes.invector", + urls: [ + "https://www.marksteeregames.com/Invector_rules.pdf", + ], + people: [ + { + type: "designer", + name: "Mark Steere", + urls: ["http://www.marksteeregames.com/"], + apid: "e7a3ebf6-5b05-4548-ae95-299f75527b3f", + }, + { + type: "coder", + name: "João Pedro Neto", + urls: ["https://boardgamegeek.com/boardgamedesigner/3829/joao-pedro-neto"], + apid: "9228bccd-a1bd-452b-b94f-d05380e6638f", + }, + ], + categories: ["goal>unify", "mechanic>move", "mechanic>capture", "board>shape>rect", "board>connect>rect", "components>simple>1per"], + variants: [ + { uid: "size-6", group: "board" }, // 5x6 + { uid: "#board", }, // 7 rows x 8 cols + { uid: "size-10", group: "board" }, // 9x10 + { uid: "size-12", group: "board" }, // 11x12 + { uid: "size-14", group: "board" }, // 13x14 + { uid: "size-16", group: "board" }, // 15x16 + ], + flags: ["automove", "pie", "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 boardSize = this.getBoardSize(); + private grid: RectGrid; + private dots: [number, number][] = []; // if there are points here, the renderer will show them + + constructor(state?: IInvectorState | string, variants?: string[]) { + super(); + if (state === undefined) { + if ( (variants !== undefined) && (variants.length > 0) ) { + this.variants = [...variants]; + } + const board = new Map(); + const sz = this.getBoardSize(); + const g = new SquareOrthGraph(sz, sz-1); + + for (let x=0; x= 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.boardSize = this.getBoardSize(); + this.results = [...state._results]; + return this; + } + + public getBoardSize(): number { + // Get board size from variants. + if (this.variants !== undefined && this.variants.length > 0 && + this.variants[0] !== undefined && this.variants[0].length > 0) { + const sizeVariants = this.variants.filter(v => v.includes("size")) + if (sizeVariants.length > 0) { + const size = sizeVariants[0].match(/\d+/); + return parseInt(size![0], 10); + } + if (isNaN(this.boardSize)) { + throw new Error(`Could not determine the board size from variant "${this.variants[0]}"`); + } + } + return 8; + } + + public get graph(): SquareOrthGraph { + return new SquareOrthGraph(this.boardSize, this.boardSize-1); + } + + // return the list of orthogonal neighbors of 'cell' + private orthNeighbours(cell: string): string[] { + const [x, y] = this.graph.algebraic2coords(cell); + const neighbours = this.grid.adjacencies(x, y, false); + return neighbours.map(n => this.graph.coords2algebraic(...n)); + } + + private manhattan(p1: [number, number], p2: [number, number]): number { + return Math.abs(p1[0] - p2[0]) + Math.abs(p1[1] - p2[1]); + } + + public moves(player?: playerid): string[] { + if (this.gameover) { return []; } + if (player === undefined) { player = this.currplayer; } + const g = this.graph; + const moves = []; + + for (const cell of g.graph.nodes()) { + if (this.board.has(cell) && this.board.get(cell) === player) { + // players can move their friendly stones to capture an enemy stone + for (const neigh of this.orthNeighbours(cell)) { + if (this.board.has(neigh) && this.board.get(neigh) !== player) { + moves.push(`${cell}-${neigh}`); + } + } + + // pieces can also move to empty cell if it's near to the board center of mass + const xc = (this.boardSize-1) / 2; // eg, if 6 cols, xc is 2.5 (0-indexed) + const yc = (this.boardSize-2) / 2; // eg, if 5 rows, ys is 2 (0-indexed) + + const [x, y] = g.algebraic2coords(cell); + const currentDistance = this.manhattan([x, y], [xc, yc] ); + for (const neigh of this.orthNeighbours(cell)) { + const [x1, y1] = g.algebraic2coords(neigh); + const newDistance = this.manhattan([x1, y1], [xc, yc] ); + if ( !this.board.has(neigh) && newDistance < currentDistance) { + moves.push(`${cell}-${neigh}`); + } + } + } + } + + if (moves.length === 0) { + moves.push("pass"); // if no legal move is available, pass turn + } + + return moves.sort((a,b) => a.localeCompare(b)); + } + + public handleClick(move: string, row: number, col: number, piece?: string): IClickResult { + try { + const cell = this.graph.coords2algebraic(col, row); + let newmove = ""; + + if (move === "") { + newmove = cell; + } else if (move === cell) { + newmove = ""; + } else { + newmove = `${move}-${cell}`; + } + + 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.canrender = true; + result.message = i18next.t("apgames:validation.invector.INITIAL_INSTRUCTIONS"); + return result; + } + + const moves = m.split('-'); + + const allMoves = this.moves(); + + if ( m === "pass" ) { + if (! allMoves.includes("pass") ) { + result.valid = false; + result.message = i18next.t("apgames:validation.invector.INVALID_PASS"); + return result; + } + result.valid = true; + result.complete = 1; + result.canrender = true; + result.message = i18next.t("apgames:validation._general.VALID_MOVE"); + return result; + } else { // if not a pass move, confirm the mentioned cells are all valid + try { + for (const cell of moves) { + this.graph.algebraic2coords(cell); + } + } catch { + result.valid = false; + result.message = i18next.t("apgames:validation._general.INVALID_MOVE", {move: m}); + return result; + } + // ok, the move is not a pass, but check if a pass move is mandatory + if ( allMoves.length === 1 && allMoves.includes("pass") ) { + result.valid = false; + result.message = i18next.t("apgames:validation.invector.ONLY_PASS"); + return result; + } + } + + if (moves.length === 1) { + if (!this.board.has(m) || this.board.get(m) !== this.currplayer) { + result.valid = false; + result.message = i18next.t("apgames:validation.invector.INVALID_SELECTION"); + return result; + } + if (! this.hasPrefix(allMoves, m) ) { + result.valid = false; + result.message = i18next.t("apgames:validation.narrows.CANNOT_MOVE"); + return result + } + result.valid = true; + result.complete = -1; // player still needs to decide to place or remove this stone + result.canrender = true; + result.message = i18next.t("apgames:validation.invector.INSTRUCTIONS"); + return result; + } + + if (! allMoves.includes(m) ) { + result.valid = false; + result.message = i18next.t("apgames:validation.invector.INVALID_MOVE"); + return result + } + + result.valid = true; + result.complete = 1; + result.canrender = true; + result.message = i18next.t("apgames:validation._general.VALID_MOVE"); + return result; + } + + private findPoints(cell: string): string[] { + return this.moves().filter(mv => mv.startsWith(cell)) + .map(mv => mv.split('-')[1]); + } + + public move(m: string, {partial = false, trusted = false} = {}): InvectorGame { + 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) + } + if ( (! partial) && (! this.moves().includes(m)) ) { + throw new UserFacingError("VALIDATION_FAILSAFE", i18next.t("apgames:validation._general.FAILSAFE", {move: m})) + } + } + + this.results = []; + this.dots = []; + if (m === "") { return this; } + + if (m !== "pass") { + if (partial) { + this.dots = this.findPoints(m).map(c => this.graph.algebraic2coords(c)); + return this; + } else { + this.dots = []; // otherwise delete the points and process the full move + } + + const moves = m.split('-'); + this.board.delete(moves[0]); + this.board.set(moves[1], this.currplayer); + this.results.push({ type: "move", from: moves[0], to: moves[1]}); + + if (partial) { return this; } + } + + this.lastmove = m; + this.currplayer = this.currplayer % 2 + 1 as playerid; + this.checkEOG(); + this.saveState(); + return this; + } + + protected checkEOG(): InvectorGame { + const p1Pieces = [...this.board.entries()].filter(([,owner]) => owner === 1); + const p2Pieces = [...this.board.entries()].filter(([,owner]) => owner === 2); + + if (p1Pieces.length === 0 || p2Pieces.length === 0) { + this.gameover = true; + this.winner = p1Pieces.length === 0 ? [2] : [1]; + } + + if ( this.gameover ) { + this.results.push( + {type: "eog"}, + {type: "winners", players: [...this.winner]} + ); + } + + return this; + } + + public state(): IInvectorState { + return { + game: InvectorGame.gameinfo.uid, + numplayers: this.numplayers, + variants: [...this.variants], + gameover: this.gameover, + winner: [...this.winner], + stack: [...this.stack] + }; + } + + public moveState(): IMoveState { + return { + _version: InvectorGame.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 (const row of g.listCells(true) as string[][]) { + if (pstr.length > 0) { + pstr += "\n"; + } + const pieces: string[] = []; + for (const cell of 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(""); + } + + // Build rep + const rep: APRenderRep = { + board: { + style: "vertex", + width: this.boardSize, + height: this.boardSize-1 + }, + legend: { + A: { name: "piece", colour: 1 }, + B: { name: "piece", colour: 2 }, + }, + pieces: pstr + }; + + rep.annotations = []; + for (const move of this.results) { + if (move.type === "place") { + const [toX, toY] = g.algebraic2coords(move.where!); + rep.annotations.push({type: "enter", targets: [{row: toY, col: toX}]}); + } else 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}]}); + } + } + + // show the dots where the selected piece can move to + if (this.dots.length > 0) { + const points = []; + for (const [x,y] of this.dots) { + points.push({row: y, col: x}); + } + rep.annotations.push({type: "dots", + targets: points as [{row: number; col: number;}, ...{row: number; col: number;}[]]}); + } + + return rep; + } + + public clone(): InvectorGame { + return new InvectorGame(this.serialize()); + } +} diff --git a/src/games/narrows.ts b/src/games/narrows.ts index 751c8ee3..68e94094 100644 --- a/src/games/narrows.ts +++ b/src/games/narrows.ts @@ -83,7 +83,7 @@ export class NarrowsGame extends GameBase { const g = new SquareOrthGraph(sz, sz-1); for (let x=0; xconnect", "mechanic>place", "mechanic>asymmetry", "board>shape>rect", "board>connect>rect", "components>simple>1per"], - flags: ["no-moves", "custom-buttons", "experimental"] + flags: ["no-moves", "custom-buttons", "custom-colours", "experimental"] }; public numplayers = 2; @@ -361,7 +361,7 @@ export class PippinzipGame extends GameBase { this.results = [{ type: "pass" }]; } else { this.results = []; - const p = this.inAuctionPhase() || this.isZipTurn() ? 3 : this.currplayer; + const p = this.getPlayerColour(this.currplayer) as playerid; for (const cell of m.split(',')) { this.board.set(cell, p); this.results.push( {type: "place", where:cell} ); @@ -401,8 +401,7 @@ export class PippinzipGame extends GameBase { // returns an orthogonal connection path between two opposite edges, // or [] if it does not exist private connectedPip(): string[] { - const pipPlayer: playerid = this.zipPlayer === 1 ? 2 : 1; - const graph = this.buildGraph(pipPlayer, false); // check orthogonal path for Pip pieces + const graph = this.buildGraph(2, false); // check orthogonal path for Pip pieces for (const [sources, targets] of this.lines) { for (const source of sources) { @@ -422,7 +421,7 @@ export class PippinzipGame extends GameBase { // returns an diagonal connection path between all four edges, // or [] if it does not exist private connectedZip(): string[] { - const graph = this.buildGraph(3, true); // check ortho+diag path for Zip pieces + const graph = this.buildGraph(1, true); // check ortho+diag path for Zip pieces const path: string[] = [] // check North/South @@ -467,27 +466,18 @@ export class PippinzipGame extends GameBase { let path = []; if ( this.inAuctionPhase() ) { - // if, strangely, the Zip pieces make a connection before the auction ends, + // if, strangely, the Zip pieces make a connection before the auction ends, // the game is a win for the player that made the connection path = this.connectedZip(); - if ( path.length > 0 ) { - this.gameover = true; - this.winner = [prevPlayer]; - this.connPath = [...path]; - this.results.push({ type: "eog" }); - } - } else { - if ( this.zipPlayer === prevPlayer ) { // check if Zip won - path = this.connectedZip(); - } else { // check if Pip won - path = this.connectedPip(); - } - if ( path.length > 0 ) { - this.gameover = true; - this.winner = [prevPlayer]; - this.connPath = [...path]; - this.results.push({ type: "eog" }); - } + } else { // check if Zip or Pip won + path = this.zipPlayer === prevPlayer ? this.connectedZip() : this.connectedPip(); + } + + if ( path.length > 0 ) { + this.gameover = true; + this.winner = [prevPlayer]; + this.connPath = [...path]; + this.results.push({ type: "eog" }); } if (this.gameover) { @@ -510,7 +500,6 @@ export class PippinzipGame extends GameBase { const contents = this.board.get(cell); if (contents === 1) { pstr += "A"; } if (contents === 2) { pstr += "B"; } - if (contents === 3) { pstr += "C"; } } else { pstr += "-"; } @@ -518,12 +507,6 @@ export class PippinzipGame extends GameBase { } pstr = pstr.replace(new RegExp(`-{${this.boardSize}}`, "g"), "_"); - const pipColour: Colourfuncs = { - func: "custom", - default: "#999", - palette: 3 - }; - // Build rep const rep: APRenderRep = { board: { @@ -534,9 +517,20 @@ export class PippinzipGame extends GameBase { legend: { A: { name: "piece", colour: 1 }, B: { name: "piece", colour: 2 }, - C: { name: "piece", colour: pipColour }, }, - pieces: pstr + pieces: pstr, + areas: [ + { + type: "key", + list: [ + { name: "Zip/Cross", piece: "A" }, + { name: "Pip/Line", piece: "B" } + ], + position: "left", + height: 0.5, + clickable: false, + } + ], }; // Add annotations @@ -580,6 +574,11 @@ export class PippinzipGame extends GameBase { return rep; } + public getPlayerColour(p: playerid): number | string { + // always return the color of the Zip/Line unless the auction is finished + return this.zipPlayer === undefined || p === this.zipPlayer ? 1 : 2; + } + public getButtons(): ICustomButton[] { if ( this.inAuctionPhase() ) { return [{ label: "pass", move: "pass" }]; diff --git a/src/games/unane.ts b/src/games/unane.ts index 38ffa24c..207965b2 100644 --- a/src/games/unane.ts +++ b/src/games/unane.ts @@ -82,7 +82,7 @@ export class UnaneGame extends GameBase { const g = new SquareOrthGraph(sz, sz-1); for (let x=0; x