From 0bebce139891782915a3c9a8a04bd620aaa8a754 Mon Sep 17 00:00:00 2001 From: samtcifihi <43021399+samtcifihi@users.noreply.github.com> Date: Sun, 13 Sep 2026 06:55:36 -0400 Subject: [PATCH 1/3] Add 40x40 Mirador board variant --- locales/en/apgames.json | 10 +++- locales/eo/apgames.json | 10 +++- src/games/mirador.ts | 94 +++++++++++++++++++++++++------------- test/games/mirador.test.ts | 71 ++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 33 deletions(-) create mode 100644 test/games/mirador.test.ts diff --git a/locales/en/apgames.json b/locales/en/apgames.json index 8bf62709..c77adea5 100644 --- a/locales/en/apgames.json +++ b/locales/en/apgames.json @@ -156,7 +156,7 @@ "mimic": "A game where enemy pieces mimic your moves while you race to be the first player to reach the back row.", "minefield": "Connect opposite sides of the board, but certain piece configurations are forbidden.", "minimize": "The player with the largest smallest group wins the game.", - "mirador": "Mirador is a fast and exhilarating connection game played on a 27 x 27 square grid by placing 2 x 2 squares to represent watchtowers. It uses line-of-sight connections and equivalent goals (making it more of a race than a pure connection game).", + "mirador": "Mirador is a fast and exhilarating connection game played on a square grid (27 x 27 by default) by placing 2 x 2 squares to represent watchtowers. It uses line-of-sight connections and equivalent goals (making it more of a race than a pure connection game).", "mixtour": "Two players create towers with the goal of creating a tower at least five high with your colour at the top. The twist here is two-fold: (1) you can move opponent's pieces and (2) movement range is determined by the *target* stack height and not the moving stack height.", "monkey": "You start with a single queen, composed of a stack of checkers. When you make noncapturing moves, the queen leaves a singleton behind. Queens and singletons move and capture like chess queens. To win, capture the opposing queen or leave them with no legal moves.", "morphos": "Orthogonal-only square connection game. On your turn, place a friendly stone or flip an enemy stone in one of three predetermined patterns. Win by connecting your two opposite sides of the board with a chain of stones of your color.", @@ -2614,6 +2614,14 @@ "name": "Hexagonal board (base-8)" } }, + "mirador": { + "#board": { + "name": "28x28 board" + }, + "size-40": { + "name": "40x40 board" + } + }, "mixtour": { "five": { "description": "The winner is the first to score three points.", diff --git a/locales/eo/apgames.json b/locales/eo/apgames.json index 6cbd5a87..e2e55a16 100644 --- a/locales/eo/apgames.json +++ b/locales/eo/apgames.json @@ -2056,6 +2056,14 @@ "name": "Sesangula tabulo (bazo 8)" } }, + "mirador": { + "#board": { + "name": "Tabulo 28×28" + }, + "size-40": { + "name": "Tabulo 40×40" + } + }, "mixtour": { "five": { "description": "La gajninto estas la unua, kiu gajnas tri poentojn.", @@ -4481,7 +4489,7 @@ "mimic": "Imitanto (angle Mimic): Ludo, en kiu malamikaj pecoj imitas viajn movojn dum vi konkuras por esti la unua, kiu atingas la malantaŭan vicon.", "minefield": "Minkampo (angle Minefield): Konektu kontraŭajn flankojn de la tabulo, sed iuj pecaranĝoj estas malpermesitaj.", "minimize": "Minimumigo (angle Minimize): Gajnas la ludanto, kies plej malgranda grupo estas la plej granda.", - "mirador": "Mirador estas rapida kaj ekscita konektludo sur kvadrata krado 27×27, kie kvadratoj 2×2 reprezentas gardoturojn. Ĝi uzas vidliniajn konektojn kaj ekvivalentajn celojn, do ĝi estas pli vetkuro ol pura konektludo.", + "mirador": "Mirador estas rapida kaj ekscita konektludo sur kvadrata krado (defaŭlte 27×27), kie kvadratoj 2×2 reprezentas gardoturojn. Ĝi uzas vidliniajn konektojn kaj ekvivalentajn celojn, do ĝi estas pli vetkuro ol pura konektludo.", "mixtour": "Du ludantoj konstruas turojn kun la celo fari turon almenaŭ kvin pecojn altan, kun sia koloro supre. Estas du apartaĵoj: (1) vi povas movi kontraŭulajn pecojn, kaj (2) la movdistanco dependas de la alto de la cela stako, ne de tiu de la movata stako.", "monkey": "Simia Damo (angle Monkey Queen): Vi komencas kun unu damo, kiu estas stako de dampecoj. Dum senkapta movo, la damo postlasas unuopan pecon. Damoj kaj unuopaj pecoj moviĝas kaj kaptas kiel ŝakaj damoj. Por venki, kaptu la kontraŭulan damon aŭ lasu la kontraŭulon sen validaj movoj.", "morphos": "Nur-orta kvadrata konektludo. Dum via vico, metu amikan ŝtonon aŭ renversu malamikan ŝtonon laŭ unu el tri antaŭdifinitaj ŝablonoj. Gajnu konektante viajn du kontraŭajn flankojn per ĉeno de samkoloraj ŝtonoj.", diff --git a/src/games/mirador.ts b/src/games/mirador.ts index e1465437..b4d7339e 100644 --- a/src/games/mirador.ts +++ b/src/games/mirador.ts @@ -9,7 +9,7 @@ import { import type { APGamesInformation } from "../schemas/gameinfo.js"; import { APRenderRep, AnnotationBasic } from "@abstractplay/renderer/build/schemas/schema"; import type { APMoveResult } from "../schemas/moveresults.js"; -import { reviver, UserFacingError } from "../common/index.js"; +import { generateColumnLabel, reviver, UserFacingError } from "../common/index.js"; import i18next from "i18next"; export type playerid = 1 | 2; @@ -18,8 +18,8 @@ export type Stage = "play" | "challenge"; export interface IMoveState extends IIndividualState { currplayer: playerid; - // board is 27 x 27. A1 corresponds to board[0][0], board[0][1], board[1][0], and board[1][1], - // Z26 corresponds to board[25][25], board[25][26], board[26][25], board[26][26] + // The stored grid is one row and column smaller than the rendered board. + // A placement at (x, y) occupies a 2x2 block from board[y][x] through board[y + 1][x + 1]. board: Array>; stage: Stage; lastmove?: string; @@ -56,6 +56,12 @@ export class MiradorGame extends GameBase { apid: "a96b36a2-2c9d-4597-8c4a-f926062a45b6", }, ], + variants: [ + { + uid: "size-40", + group: "board", + }, + ], categories: ["goal>connect", "mechanic>block", "mechanic>place", "board>shape>rect", "board>connect>rect", "components>simple>pnp"], flags: ["pie", "custom-buttons"], }; @@ -70,13 +76,28 @@ export class MiradorGame extends GameBase { public stack!: Array; public results: Array = []; - constructor(state?: IMiradorState | string) { + private get boardSize(): number { + return this.variants.includes("size-40") ? 40 : 28; + } + + private get gridSize(): number { + return this.boardSize - 1; + } + + private get placementSize(): number { + return this.boardSize - 2; + } + + constructor(state?: IMiradorState | string, variants?: string[]) { super(); if (state === undefined) { + if (variants !== undefined) { + this.variants = [...variants]; + } const board = []; - for (let row = 0; row < 27; row++) { + for (let row = 0; row < this.gridSize; row++) { const node: Array = []; - for (let col = 0; col < 27; col++) { + for (let col = 0; col < this.gridSize; col++) { node.push(null); } board.push(node); @@ -132,10 +153,10 @@ export class MiradorGame extends GameBase { } const moves: string[] = []; // placements - for (let x = 0; x < 26; x++) { - for (let y = 0; y < 26; y++) { + for (let x = 0; x < this.placementSize; x++) { + for (let y = 0; y < this.placementSize; y++) { if (this.canPlace(this.board, this.currplayer, x, y)) { - moves.push(GameBase.coords2algebraic(x, y, 26)); + moves.push(GameBase.coords2algebraic(x, y, this.placementSize)); } } } @@ -159,14 +180,14 @@ export class MiradorGame extends GameBase { piece?: string ): IClickResult { try { - if (col === 0 || col === 27 || row === 0 || row === 27) { + if (col === 0 || col === this.boardSize - 1 || row === 0 || row === this.boardSize - 1) { return { move, valid: false, message: i18next.t("apgames:validation.mirador.NO_EDGE_PLAY") }; } - const cell = GameBase.coords2algebraic(col - 1, row - 1, 26); + const cell = GameBase.coords2algebraic(col - 1, row - 1, this.placementSize); const newmove = move ? `${move}-${cell}` : cell; const result = this.validateMove(newmove) as IClickResult; if (!result.valid) { @@ -194,10 +215,10 @@ export class MiradorGame extends GameBase { const localBoard = []; const work: [number, number][] = []; if (horizontal) { - for (let row = 0; row < 27; row++) { + for (let row = 0; row < this.gridSize; row++) { const node: Array = []; let seeleft = true; - for (let col = 0; col < 27; col++) { + for (let col = 0; col < this.gridSize; col++) { if (seeleft && this.board[row][col] === player) { node.push(3); // 3 means connected to the left work.push([row, col]); @@ -212,10 +233,10 @@ export class MiradorGame extends GameBase { localBoard.push(node); } } else { - for (let col = 0; col < 27; col++) { + for (let col = 0; col < this.gridSize; col++) { const node: Array = []; let seeleft = true; - for (let row = 0; row < 27; row++) { + for (let row = 0; row < this.gridSize; row++) { if (seeleft && this.board[row][col] === player) { node.push(3); work.push([col, row]); // transposing the board @@ -242,10 +263,10 @@ export class MiradorGame extends GameBase { let dist = 1; while (true) { const next: [number, number] = [start[0] + dir[0] * dist, start[1] + dir[1] * dist]; - if (next[1] >= 27) { + if (next[1] >= this.gridSize) { return true; } - if (next[0] < 0 || next[0] >= 27 || next[1] < 0 || localBoard[next[0]][next[1]] === 3 - player || localBoard[next[0]][next[1]] === 3) { + if (next[0] < 0 || next[0] >= this.gridSize || next[1] < 0 || localBoard[next[0]][next[1]] === 3 - player || localBoard[next[0]][next[1]] === 3) { break; } if (localBoard[next[0]][next[1]] === 3 - player) { @@ -267,7 +288,7 @@ export class MiradorGame extends GameBase { ]; for (const dir of diags) { const next: [number, number] = [start[0] + dir[0], start[1] + dir[1]]; - if (! (next[0] < 0 || next[0] >= 27 || next[1] < 0 || next[1] >= 27) && localBoard[next[0]][next[1]] === player) { + if (! (next[0] < 0 || next[0] >= this.gridSize || next[1] < 0 || next[1] >= this.gridSize) && localBoard[next[0]][next[1]] === player) { localBoard[next[0]][next[1]] = 3; work.push(next); } @@ -279,7 +300,7 @@ export class MiradorGame extends GameBase { private canPlace(board: Array>, player: playerid, x: number, y: number): boolean { for (let dx = -1; dx <= 2; dx++) { for (let dy = -1; dy <= 2; dy++) { - if (x + dx >= 0 && x + dx < 27 && y + dy >= 0 && y + dy < 27 && !(board[y + dy][x + dx] === null || ((dx === -1 || dx === 2) && (dy === -1 || dy === 2) && board[y + dy][x + dx] === player))) { + if (x + dx >= 0 && x + dx < this.gridSize && y + dy >= 0 && y + dy < this.gridSize && !(board[y + dy][x + dx] === null || ((dx === -1 || dx === 2) && (dy === -1 || dy === 2) && board[y + dy][x + dx] === player))) { return false; } } @@ -339,7 +360,7 @@ export class MiradorGame extends GameBase { for (const placement of placements) { let coords; try { - coords = GameBase.algebraic2coords(placement, 26); + coords = GameBase.algebraic2coords(placement, this.placementSize); } catch { result.valid = false; result.message = i18next.t("apgames:validation._general.INVALIDCELL", { @@ -347,7 +368,7 @@ export class MiradorGame extends GameBase { }); return result; } - if (coords[0] < 0 || coords[0] >= 26 || coords[1] < 0 || coords[1] >= 26) { + if (coords[0] < 0 || coords[0] >= this.placementSize || coords[1] < 0 || coords[1] >= this.placementSize) { result.valid = false; result.message = i18next.t("apgames:validation._general.INVALIDCELL", { cell: placement, @@ -418,7 +439,7 @@ export class MiradorGame extends GameBase { } else { const placements = m.split("-"); for (const placement of placements) { - const coords = GameBase.algebraic2coords(placement, 26); + const coords = GameBase.algebraic2coords(placement, this.placementSize); this.board[coords[1]][coords[0]] = this.currplayer; this.board[coords[1]][coords[0] + 1] = this.currplayer; this.board[coords[1] + 1][coords[0]] = this.currplayer; @@ -489,11 +510,12 @@ export class MiradorGame extends GameBase { public render(): APRenderRep { // Build piece string - let pstr = "----------------------------"; - for (let row = 0; row < 26; row++) { + const border = "-".repeat(this.boardSize); + let pstr = border; + for (let row = 0; row < this.placementSize; row++) { pstr += "\n"; const pieces: string[] = []; - for (let col = 0; col < 26; col++) { + for (let col = 0; col < this.placementSize; col++) { if (this.board[row][col] !== null && this.board[row][col] === this.board[row][col + 1] && this.board[row][col] === this.board[row + 1][col] && this.board[row][col] === this.board[row + 1][col + 1]) { pieces.push(this.board[row][col] === 1 ? "A" : "B"); } else { @@ -502,17 +524,27 @@ export class MiradorGame extends GameBase { } pstr += "-" + pieces.join("") + "-"; } - pstr += "\n----------------------------"; + pstr += `\n${border}`; + + const labelIterator = generateColumnLabel("abcdefghijklmnopqrstuvwxyz"); + const columnLabels = Array.from( + { length: this.placementSize }, + () => labelIterator.next().value as string, + ); + const rowLabels = Array.from( + { length: this.placementSize }, + (_, index) => (index + 1).toString(), + ); // Build rep const rep: APRenderRep = { options: ["hide-star-points"], board: { style: "vertex", - width: 28, - height: 28, - columnLabels: ["", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", ""], - rowLabels: ["", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", ""] + width: this.boardSize, + height: this.boardSize, + columnLabels: ["", ...columnLabels, ""], + rowLabels: ["", ...rowLabels, ""], }, legend: { A: { @@ -535,7 +567,7 @@ export class MiradorGame extends GameBase { for (const move of this.results) { if (move.type === "place") { for (const placement of move.where!.split("-")) { - const [x, y] = GameBase.algebraic2coords(placement, 26); + const [x, y] = GameBase.algebraic2coords(placement, this.placementSize); rep.annotations.push({type: "dots", targets: [{row: y + 1, col: x + 1}], size: 0.3, colour: "#f4ea56"}); } } diff --git a/test/games/mirador.test.ts b/test/games/mirador.test.ts new file mode 100644 index 00000000..2eb24035 --- /dev/null +++ b/test/games/mirador.test.ts @@ -0,0 +1,71 @@ +import "mocha"; +import { expect } from "chai"; +import { MiradorGame } from "../../src/games/mirador"; + +describe("Mirador", () => { + it("keeps the standard 28x28 board as the default", () => { + const game = new MiradorGame(); + const rep = game.render(); + + expect(game.board).to.have.length(27); + expect(Object.keys(game.board[0])).to.have.length(27); + expect(rep.board).to.include({ width: 28, height: 28 }); + expect(game.moves()).to.have.length(26 * 26 + 1); + expect(game.moves()).to.include.members(["a26", "z1", "declare"]); + }); + + it("supports legal placements and multi-letter notation on the 40x40 board", () => { + const game = new MiradorGame(undefined, ["size-40"]); + const rep = game.render(); + const board = rep.board as { columnLabels: string[]; height: number; rowLabels: string[]; width: number }; + + expect(game.board).to.have.length(39); + expect(Object.keys(game.board[0])).to.have.length(39); + expect(board).to.include({ width: 40, height: 40 }); + expect(board.columnLabels).to.have.length(40); + expect(board.columnLabels.slice(-3)).to.deep.equal(["ak", "al", ""]); + expect(board.rowLabels).to.have.length(40); + expect(board.rowLabels.slice(-3)).to.deep.equal(["37", "38", ""]); + + const moves = game.moves(); + expect(moves).to.have.length(38 * 38 + 1); + expect(moves).to.include.members(["a38", "z38", "aa38", "al1", "declare"]); + expect(game.validateMove("aa38").valid).to.be.true; + expect(game.validateMove("al1").valid).to.be.true; + expect(game.validateMove("am1").valid).to.be.false; + expect(game.validateMove("al0").valid).to.be.false; + + const click = game.handleClick("", 1, 27); + expect(click.valid).to.be.true; + expect(click.move).to.equal("aa38"); + expect(game.handleClick("", 1, 39).valid).to.be.false; + + game.move("al1"); + expect(game.board[37][37]).to.equal(1); + expect(game.board[37][38]).to.equal(1); + expect(game.board[38][37]).to.equal(1); + expect(game.board[38][38]).to.equal(1); + + const restored = new MiradorGame(game.serialize()); + expect(restored.variants).to.deep.equal(["size-40"]); + expect(restored.render().board).to.include({ width: 40, height: 40 }); + }); + + it("accepts multi-letter placements during a challenge", () => { + const game = new MiradorGame(undefined, ["size-40"]); + game.move("declare"); + + const validation = game.validateMove("aa38-al1"); + expect(validation.valid).to.be.true; + expect(validation.complete).to.equal(0); + }); + + it("checks connections against the enlarged board edge", () => { + const game = new MiradorGame(undefined, ["size-40"]); + game.board[0][0] = 1; + game.board[0][25] = 1; + game.board[0][30] = 2; + + expect(game["isConnected"](true, 1)).to.be.false; + }); +}); From 3d026dbadcfc553f5c3b24a568a837d9c717cae8 Mon Sep 17 00:00:00 2001 From: samtcifihi <43021399+samtcifihi@users.noreply.github.com> Date: Sun, 13 Sep 2026 07:21:02 -0400 Subject: [PATCH 2/3] Clarify and generalize Mirador board sizes --- locales/en/apgames.json | 2 +- locales/eo/apgames.json | 2 +- src/games/mirador.ts | 11 ++++++++++- test/games/mirador.test.ts | 7 +++++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/locales/en/apgames.json b/locales/en/apgames.json index c77adea5..6370f3ff 100644 --- a/locales/en/apgames.json +++ b/locales/en/apgames.json @@ -156,7 +156,7 @@ "mimic": "A game where enemy pieces mimic your moves while you race to be the first player to reach the back row.", "minefield": "Connect opposite sides of the board, but certain piece configurations are forbidden.", "minimize": "The player with the largest smallest group wins the game.", - "mirador": "Mirador is a fast and exhilarating connection game played on a square grid (27 x 27 by default) by placing 2 x 2 squares to represent watchtowers. It uses line-of-sight connections and equivalent goals (making it more of a race than a pure connection game).", + "mirador": "Mirador is a fast and exhilarating connection game played on a square grid (28 x 28 by default) by placing 2 x 2 squares to represent watchtowers. It uses line-of-sight connections and equivalent goals (making it more of a race than a pure connection game).", "mixtour": "Two players create towers with the goal of creating a tower at least five high with your colour at the top. The twist here is two-fold: (1) you can move opponent's pieces and (2) movement range is determined by the *target* stack height and not the moving stack height.", "monkey": "You start with a single queen, composed of a stack of checkers. When you make noncapturing moves, the queen leaves a singleton behind. Queens and singletons move and capture like chess queens. To win, capture the opposing queen or leave them with no legal moves.", "morphos": "Orthogonal-only square connection game. On your turn, place a friendly stone or flip an enemy stone in one of three predetermined patterns. Win by connecting your two opposite sides of the board with a chain of stones of your color.", diff --git a/locales/eo/apgames.json b/locales/eo/apgames.json index e2e55a16..f8dbd3da 100644 --- a/locales/eo/apgames.json +++ b/locales/eo/apgames.json @@ -4489,7 +4489,7 @@ "mimic": "Imitanto (angle Mimic): Ludo, en kiu malamikaj pecoj imitas viajn movojn dum vi konkuras por esti la unua, kiu atingas la malantaŭan vicon.", "minefield": "Minkampo (angle Minefield): Konektu kontraŭajn flankojn de la tabulo, sed iuj pecaranĝoj estas malpermesitaj.", "minimize": "Minimumigo (angle Minimize): Gajnas la ludanto, kies plej malgranda grupo estas la plej granda.", - "mirador": "Mirador estas rapida kaj ekscita konektludo sur kvadrata krado (defaŭlte 27×27), kie kvadratoj 2×2 reprezentas gardoturojn. Ĝi uzas vidliniajn konektojn kaj ekvivalentajn celojn, do ĝi estas pli vetkuro ol pura konektludo.", + "mirador": "Mirador estas rapida kaj ekscita konektludo sur kvadrata krado (defaŭlte 28×28), kie kvadratoj 2×2 reprezentas gardoturojn. Ĝi uzas vidliniajn konektojn kaj ekvivalentajn celojn, do ĝi estas pli vetkuro ol pura konektludo.", "mixtour": "Du ludantoj konstruas turojn kun la celo fari turon almenaŭ kvin pecojn altan, kun sia koloro supre. Estas du apartaĵoj: (1) vi povas movi kontraŭulajn pecojn, kaj (2) la movdistanco dependas de la alto de la cela stako, ne de tiu de la movata stako.", "monkey": "Simia Damo (angle Monkey Queen): Vi komencas kun unu damo, kiu estas stako de dampecoj. Dum senkapta movo, la damo postlasas unuopan pecon. Damoj kaj unuopaj pecoj moviĝas kaj kaptas kiel ŝakaj damoj. Por venki, kaptu la kontraŭulan damon aŭ lasu la kontraŭulon sen validaj movoj.", "morphos": "Nur-orta kvadrata konektludo. Dum via vico, metu amikan ŝtonon aŭ renversu malamikan ŝtonon laŭ unu el tri antaŭdifinitaj ŝablonoj. Gajnu konektante viajn du kontraŭajn flankojn per ĉeno de samkoloraj ŝtonoj.", diff --git a/src/games/mirador.ts b/src/games/mirador.ts index b4d7339e..2946b503 100644 --- a/src/games/mirador.ts +++ b/src/games/mirador.ts @@ -57,6 +57,9 @@ export class MiradorGame extends GameBase { }, ], variants: [ + { + uid: "#board", + }, { uid: "size-40", group: "board", @@ -76,8 +79,14 @@ export class MiradorGame extends GameBase { public stack!: Array; public results: Array = []; + private static readonly defaultBoardSize = 28; + private get boardSize(): number { - return this.variants.includes("size-40") ? 40 : 28; + const sizeVariant = this.variants.find((variant) => /^size-\d+$/.test(variant)); + if (sizeVariant === undefined) { + return MiradorGame.defaultBoardSize; + } + return Number.parseInt(sizeVariant.slice("size-".length), 10); } private get gridSize(): number { diff --git a/test/games/mirador.test.ts b/test/games/mirador.test.ts index 2eb24035..e2426d1b 100644 --- a/test/games/mirador.test.ts +++ b/test/games/mirador.test.ts @@ -3,6 +3,13 @@ import { expect } from "chai"; import { MiradorGame } from "../../src/games/mirador"; describe("Mirador", () => { + it("names both board sizes in the variant metadata", () => { + expect(MiradorGame.gameinfo.variants).to.deep.equal([ + { uid: "#board" }, + { uid: "size-40", group: "board" }, + ]); + }); + it("keeps the standard 28x28 board as the default", () => { const game = new MiradorGame(); const rep = game.render(); From 4f11a1dc726cf29934c4cfb906455664e14380d1 Mon Sep 17 00:00:00 2001 From: samtcifihi <43021399+samtcifihi@users.noreply.github.com> Date: Sun, 13 Sep 2026 18:03:37 -0400 Subject: [PATCH 3/3] Fix Mirador challenge placement validation --- src/games/mirador.ts | 2 +- test/games/mirador.test.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/games/mirador.ts b/src/games/mirador.ts index 2946b503..bbf06870 100644 --- a/src/games/mirador.ts +++ b/src/games/mirador.ts @@ -393,7 +393,7 @@ export class MiradorGame extends GameBase { localBoard[coords[1]][coords[0]] = this.currplayer; localBoard[coords[1]][coords[0] + 1] = this.currplayer; localBoard[coords[1] + 1][coords[0]] = this.currplayer; - localBoard[coords[1] + 1][coords[0]] = this.currplayer; + localBoard[coords[1] + 1][coords[0] + 1] = this.currplayer; } else { if (!this.canPlace(this.board, this.currplayer, coords[0], coords[1])) { result.valid = false; diff --git a/test/games/mirador.test.ts b/test/games/mirador.test.ts index e2426d1b..92c2e1a3 100644 --- a/test/games/mirador.test.ts +++ b/test/games/mirador.test.ts @@ -67,6 +67,13 @@ describe("Mirador", () => { expect(validation.complete).to.equal(0); }); + it("rejects adjacent placements within the same challenge response", () => { + const game = new MiradorGame(undefined, ["size-40"]); + game.move("declare"); + + expect(game.validateMove("a38-c37").valid).to.be.false; + }); + it("checks connections against the enlarged board edge", () => { const game = new MiradorGame(undefined, ["size-40"]); game.board[0][0] = 1;