From a17b52395e8361f8070296333a3390faa910e7bd Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 23:20:21 +0000 Subject: [PATCH 1/3] Add Cascading Connection Goals variant to Gonnect Enables passing and a second end condition: two consecutive passes. An outright edge-to-edge connection still wins immediately as before. Otherwise the winner is found by checking, from the full board down to the centre point, the largest centred subboard on which exactly one player has an unbroken orthogonal connection between its two opposite sides; if no such subboard exists, the game is a draw. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Y6dm6EQVSpd9YtMQ8xjeMR --- locales/en/apgames.json | 6 ++ locales/eo/apgames.json | 6 ++ src/games/gonnect.ts | 125 +++++++++++++++++++++++++++++++++---- test/games/gonnect.test.ts | 72 +++++++++++++++++++++ 4 files changed, 198 insertions(+), 11 deletions(-) create mode 100644 test/games/gonnect.test.ts diff --git a/locales/en/apgames.json b/locales/en/apgames.json index 7df881147..f86f88bc1 100644 --- a/locales/en/apgames.json +++ b/locales/en/apgames.json @@ -604,6 +604,7 @@ "exxit": "Translations of the rules tend to omit certain nuances. This implementation conforms with the original French edition of the rules.\n\nBecause the board is built out as you play in irregular shapes, the hexes are labelled numerically instead of algebraically. This ensures that the labels don't change as the map grows.", "forms": "Forms was designed by Steven Meyers in 2007. After each move, all pieces outside the group that contains the last-moved piece are captured. The last player to move loses (so Forms is a misère game). The first version allowed a piece to capture any opponent's piece. Later, in 2011, Meyers developed a second version, **Advanced Forms**, where you could only capture orthogonally adjacent pieces, but a piece could also move to an empty cell if both were connected by a sequence of orthogonal steps over empty cells. The setup also changed to a checkered pattern. This second version is the default variant here at AP. More information can be found at [WAG](https://jpneto.github.io/world_abstract_games/forms.htm).", "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\nForced passes are no longer used when a player refills the draw pool (and submits) in the middle of his turn. That player's second partial turn is handled automatically by the server. There's also a draw pool variant that avoids the double-ply, 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.", + "gonnect": "In the base game, passing is not allowed, so play continues until someone connects or a player has no legal placement left.\n\nThe **Cascading Connection Goals** variant allows passing, and lets the game also end after two consecutive passes. If the game ends by an outright connection, that player wins as usual. Otherwise, the winner is found by comparing connections across a nested series of centred subboards: starting with the full board and shrinking by two rows and columns at a time (down to the single centre point), find the largest subboard on which exactly one player has an unbroken orthogonal connection between two opposite sides of that subboard; that player wins. If, at a given size, both players have such a connection or neither does, move on to the next smaller subboard. Because a single stone occupying the centre point always \"connects\" its own 1×1 subboard, a draw requires that the centre point be empty; on an odd-sized board with both players otherwise out of useful moves, this is expected to be extremely rare, if not impossible.", "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.", "guerrilla": "Either player wins by removing all of the opponent's pieces. The security player also wins if they survive long enough that the insurgents run out of stones to place.\n\nBy default, a match consists of two games with roles reversed. Each player plays one game as insurgents and one as security. When both players win as insurgents, the match goes to whoever used fewer stones (`66 − remaining` at the end of that game; lower score wins). Equal insurgent scores are the only draw in that case. A security win in a game scores 67. If one player sweeps both games (wins as insurgents in one and security in the other), they win the match outright. If both players only win as security, the match is drawn.", "scribe": "The glyph reference chart is shown in the left margin. A dot marks each player's most recent placement. When a mini grid fills, glyph points are tallied and the higher score wins that grid (ties go to the player who completed it).", @@ -2095,6 +2096,10 @@ }, "size-19": { "name": "19x19 board" + }, + "cascading": { + "name": "Cascading Connection Goals", + "description": "Passing is allowed. The game also ends after two consecutive passes, in which case the winner is found by comparing connections across a nested series of centred subboards." } }, "guerrilla": { @@ -6160,6 +6165,7 @@ }, "gonnect": { "INITIAL_INSTRUCTIONS": "Select an intersection to place a piece.", + "INVALID_PASS": "You cannot pass currently.", "KO": "Ko rule: you may not immediately recapture your opponent's single stone if they just used it to capture one of yours.", "SELF_CAPTURE": "You may not place your stones in self-capture." }, diff --git a/locales/eo/apgames.json b/locales/eo/apgames.json index 920557ed3..882b6c5e4 100644 --- a/locales/eo/apgames.json +++ b/locales/eo/apgames.json @@ -1611,6 +1611,10 @@ }, "size-19": { "name": "Tabulo 19×19" + }, + "cascading": { + "name": "Kaskadaj konektaj celoj", + "description": "Preterpasi estas permesite. La ludo ankaŭ finiĝas post du sinsekvaj preterpasoj; en tiu kazo, la venkinto estas trovita komparante konektojn tra serio de ĉiam pli malgrandaj centritaj subtabuloj." } }, "gyve": { @@ -4607,6 +4611,7 @@ "forms": "Forms estis projektita de Steven Meyers en 2007. Post ĉiu movo, ĉiuj pecoj ekster la grupo, kiu enhavas la laste movitan pecon, estas kaptitaj. La lasta movinto malgajnas (do Forms estas miserludo). La unua versio permesis al peco kapti ajnan kontraŭulan pecon. Poste, en 2011, Meyers kreis duan version, **Advanced Forms**, en kiu oni rajtis kapti nur ortangule apudajn pecojn, sed peco povis ankaŭ moviĝi al malplena ĉelo, se sekvo de ortaj paŝoj tra malplenaj ĉeloj konektis la du lokojn. Ankaŭ la komenca aranĝo ŝanĝiĝis al ŝaktabula ŝablono. Tiu dua versio estas la defaŭlta variaĵo ĉi tie ĉe AP. Pliaj informoj troveblas ĉe [WAG](https://jpneto.github.io/world_abstract_games/forms.htm).", "frogger": "Kiel en aliaj Decktet-ludoj ĉe Abstract Play, la kartaro montriĝas ĉe la malsupro de la tabulo kaj inkluzivas kaj la kartojn en la kartaro kaj la nekonatajn kartojn en la manoj de la aliaj ludantoj. Post la komenca disdono, ĉiuj kartoj estas prenataj el la malkaŝa prenrezervo, do la enhavo de la manoj iom post iom fariĝas konata. La stako de forĵetitaj kartoj ankaŭ montriĝas.\n\nDevigaj pasoj ne plu estas uzataj, kiam ludanto replenigas la prenrezervon (kaj submetas la movon) meze de sia vico. La servilo aŭtomate traktas la duan parton de la vico de tiu ludanto. Ekzistas ankaŭ variaĵo pri la prenrezervo, kiu laŭdezire ebligas eviti la dividon de la vico en du apartajn movojn.\n\nLa variaĵo \"Krokodiloj\" estas verko de Jorge Arroyo, la tradukinto de la angla versio de la reguloj. La altnivelaj reguloj kaj aliaj etaj variaĵoj estas verkoj de P. D. Magnus; ili aperas en The Decktet Book, kie la ludo nomiĝas Xing.", "garden": "Por tute klare montri, kio okazis dum la antaŭa vico, ĉiu movo aperas sur kvar apartaj tabuloj. La unua montras la ludon tuj post la komenca meto de la peco. La dua montras la staton post renverso de la apudaj pecoj. La tria montras eventualajn rikoltojn. La kvara montras la finan ludstaton kaj estas tiu, sur kiu vi faras viajn movojn.\n\nEn nia realigo, nigro estas ĉiam la „tome“-koloro, kiu rompas egalecon. La lasta ludanto, kiu rikoltis nigron, havos `0.1` post sia poentaro.", + "gonnect": "En la baza ludo, preterpasi ne estas permesite, do la ludo daŭras ĝis iu konektas aŭ ludanto ne plu havas validan lokon por meti ŝtonon.\n\nLa variaĵo **Kaskadaj konektaj celoj** permesas preterpasi, kaj ankaŭ lasas la ludon finiĝi post du sinsekvaj preterpasoj. Se la ludo finiĝas per plena konekto, tiu ludanto venkas kiel kutime. Alie, la venkinto estas trovita komparante konektojn tra serio de ĉiam pli malgrandaj centritaj subtabuloj: komencante per la tuta tabulo kaj malgrandigante ĝin je du vicoj kaj du kolumnoj ĉiufoje (ĝis la sola centra punkto), oni serĉas la plej grandan subtabulon, sur kiu ekzakte unu ludanto havas nerompitan ortogonalan konekton inter du kontraŭaj flankoj de tiu subtabulo; tiu ludanto venkas. Se, ĉe donita grando, ambaŭ ludantoj havas tian konekton aŭ neniu havas ĝin, oni pasas al la sekva malpli granda subtabulo. Ĉar unuopa ŝtono sur la centra punkto ĉiam \"konektas\" sian propran 1×1-subtabulon, remizo postulas, ke la centra punkto restu malplena; sur tabulo kun nepara flanklongo, kiam ambaŭ ludantoj jam ne havas utilajn movojn, oni atendas, ke tio estu ege malofta, se entute eblas.", "guerrilla": "Ĉiu el la du ludantoj povas venki per forigo de ĉiuj pecoj de sia kontraŭulo. La ludanto de la sekurecaj fortoj ankaŭ venkas, se tiuj fortoj postvivas ĝis la ribeluloj ne plu havas ŝtonojn por meti.\n\nDefaŭlte matĉo konsistas el du ludoj kun interŝanĝitaj roloj. Ĉiu ludanto unufoje regas la ribelulojn kaj unufoje la sekurecajn fortojn. Kiam ambaŭ ludantoj venkas kiel ribeluloj, la matĉon venkas tiu, kiu uzis malpli da ŝtonoj (66 − nombro de restantaj ŝtonoj ĉe la fino de tiu ludo; la malpli alta poentaro venkas). Tiuokaze la matĉo estas remiza nur se iliaj poentaroj kiel ribeluloj egalas. Venko kiel sekurecaj fortoj donas poentaron de 67. Se unu ludanto venkas en ambaŭ ludoj (en unu kiel ribeluloj kaj en la alia kiel sekurecaj fortoj), tiu venkas la tutan matĉon. Se ambaŭ ludantoj venkas nur kiel sekurecaj fortoj, la matĉo estas remiza.", "scribe": "La referenctabelo de glifoj aperas en la maldekstra marĝeno. Punkto markas la plej lastan meton de ĉiu ludanto. Kiam minikrado pleniĝas, la glifpoentoj estas sumataj kaj la pli alta poentaro gajnas tiun kradon (okaze de egaleco gajnas la ludanto, kiu kompletigis ĝin).", "gyges": "La celkvadratoj estas apudaj al ĉiuj ĉeloj en la lasta vico. La bildigilo nuntempe ne povas prezenti „flosantajn“ ĉelojn.", @@ -5972,6 +5977,7 @@ }, "gonnect": { "INITIAL_INSTRUCTIONS": "Elektu intersekcion por meti pecon.", + "INVALID_PASS": "Vi ne povas preterpasi nun.", "KO": "Regulo de Ko: vi ne rajtas tuj rekapti la ununuran ŝtonon de via kontraŭulo se ili nur uzis ĝin por kapti unu el viaj.", "SELF_CAPTURE": "Vi ne rajtas meti viajn ŝtonojn en mem-kaptado." }, diff --git a/src/games/gonnect.ts b/src/games/gonnect.ts index 78964dece..90bc09c1c 100644 --- a/src/games/gonnect.ts +++ b/src/games/gonnect.ts @@ -1,4 +1,4 @@ -import { GameBase, IAPGameState, IClickResult, IIndividualState, IValidationResult , type ChatLogCollectContext, type ChatLogLine} from "./_base.js"; +import { GameBase, IAPGameState, IClickResult, ICustomButton, IIndividualState, IValidationResult , type ChatLogCollectContext, type ChatLogLine} from "./_base.js"; import type { APGamesInformation } from "../schemas/gameinfo.js"; import { APRenderRep, RowCol } from "@abstractplay/renderer/build/schemas/schema"; import type { APMoveResult } from "../schemas/moveresults.js"; @@ -27,10 +27,12 @@ export class GonnectGame extends GameBase { name: "Gonnect", uid: "gonnect", playercounts: [2], - version: "20240719", + version: "20260913", dateAdded: "2024-08-02", // i18next.t("apgames:descriptions.gonnect") description: "apgames:descriptions.gonnect", + // i18next.t("apgames:notes.gonnect") + notes: "apgames:notes.gonnect", urls: ["https://boardgamegeek.com/boardgame/12146/gonnect"], bggid: "12146", people: [ @@ -51,9 +53,10 @@ export class GonnectGame extends GameBase { { uid: "size-9", group: "board" }, { uid: "#board", }, { uid: "size-19", group: "board" }, + { uid: "cascading" }, ], categories: ["goal>connect", "mechanic>place", "mechanic>capture", "board>shape>rect", "board>connect>rect", "components>simple"], - flags: ["pie"], + flags: ["pie", "custom-buttons"], }; public coords2algebraic(x: number, y: number): string { @@ -175,6 +178,9 @@ export class GonnectGame extends GameBase { } if (this.gameover) { return []; } const moves: string[] = []; + if (this.variants.includes("cascading")) { + moves.push("pass"); + } for (let row = 0; row < this.boardSize; row++) { for (let col = 0; col < this.boardSize; col++) { const cell = this.coords2algebraic(col, row); @@ -204,6 +210,13 @@ export class GonnectGame extends GameBase { return false; } + public getButtons(): ICustomButton[] { + if (this.variants.includes("cascading") && this.moves().includes("pass")) { + return [{ label: "pass", move: "pass" }]; + } + return []; + } + public handleClick(move: string, row: number, col: number, piece?: string): IClickResult { try { const cell = this.coords2algebraic(col, row); @@ -234,6 +247,17 @@ export class GonnectGame extends GameBase { result.message = i18next.t("apgames:validation.gonnect.INITIAL_INSTRUCTIONS"); return result; } + if (m === "pass") { + if (this.variants.includes("cascading")) { + result.valid = true; + result.complete = 1; + result.message = i18next.t("apgames:validation._general.VALID_MOVE"); + } else { + result.valid = false; + result.message = i18next.t("apgames:validation.gonnect.INVALID_PASS"); + } + return result; + } // Valid cell try { this.algebraic2coords(m); @@ -348,13 +372,17 @@ export class GonnectGame extends GameBase { if (m.length === 0) { return this; } this.dots = []; this.results = []; - this.results.push({ type: "place", where: m }); - this.board.set(m, this.currplayer); - const allCaptures = this.getCaptures(m, this.currplayer); - if (allCaptures.length > 0) { - for (const captures of allCaptures) { - for (const capture of captures) { this.board.delete(capture); } - this.results.push({ type: "capture", where: [...captures].join(), count: captures.size }); + if (m === "pass") { + this.results.push({ type: "pass" }); + } else { + this.results.push({ type: "place", where: m }); + this.board.set(m, this.currplayer); + const allCaptures = this.getCaptures(m, this.currplayer); + if (allCaptures.length > 0) { + for (const captures of allCaptures) { + for (const capture of captures) { this.board.delete(capture); } + this.results.push({ type: "capture", where: [...captures].join(), count: captures.size }); + } } } @@ -386,6 +414,67 @@ export class GonnectGame extends GameBase { return graph; } + private subboardConnection(player: playerid, lo: number, hi: number): string[] | null { + // Check whether `player` has an orthogonally connected path of stones spanning + // one side to the opposite side of the centred subboard occupying columns and + // rows `lo` to `hi` (inclusive). Returns the connecting path, or null if none. + const graph = new UndirectedGraph(); + for (const [cell, owner] of this.board.entries()) { + if (owner !== player) { continue; } + const [x, y] = this.algebraic2coords(cell); + if (x < lo || x > hi || y < lo || y > hi) { continue; } + graph.addNode(cell); + } + for (const node of graph.nodes()) { + const [x, y] = this.algebraic2coords(node); + for (const n of this.grid.adjacencies(x, y, false).map(c => this.coords2algebraic(...c))) { + if ( (graph.hasNode(n)) && (! graph.hasEdge(node, n)) ) { + graph.addEdge(node, n); + } + } + } + // north-south + for (let x = lo; x <= hi; x++) { + const north = this.coords2algebraic(x, lo); + if (!graph.hasNode(north)) { continue; } + for (let x2 = lo; x2 <= hi; x2++) { + const south = this.coords2algebraic(x2, hi); + if (!graph.hasNode(south)) { continue; } + if (north === south) { return [north]; } + const path = bidirectional(graph, north, south); + if (path !== null) { return path; } + } + } + // east-west + for (let y = lo; y <= hi; y++) { + const east = this.coords2algebraic(hi, y); + if (!graph.hasNode(east)) { continue; } + for (let y2 = lo; y2 <= hi; y2++) { + const west = this.coords2algebraic(lo, y2); + if (!graph.hasNode(west)) { continue; } + if (east === west) { return [east]; } + const path = bidirectional(graph, east, west); + if (path !== null) { return path; } + } + } + return null; + } + + private getCascadingWinner(): { winner: playerid, path: string[] } | undefined { + // Starting with the full board and shrinking by two each time, look for the largest + // centred subboard across which exactly one player has a connection. Returns undefined + // if no such subboard exists down to the centre point, meaning the game is drawn. + for (let i = this.boardSize; i >= 1; i -= 2) { + const lo = (this.boardSize - i) / 2; + const hi = lo + i - 1; + const path1 = this.subboardConnection(1, lo, hi); + const path2 = this.subboardConnection(2, lo, hi); + if (path1 !== null && path2 === null) { return { winner: 1, path: path1 }; } + if (path2 !== null && path1 === null) { return { winner: 2, path: path2 }; } + } + return undefined; + } + protected checkEOG(): GonnectGame { const otherPlayer = this.currplayer % 2 + 1 as playerid; const graph = this.buildGraph(otherPlayer); @@ -408,7 +497,19 @@ export class GonnectGame extends GameBase { } } } - if (!this.gameover && !this.hasMoves(this.currplayer)) { + if (!this.gameover && this.variants.includes("cascading") && + this.lastmove === "pass" && this.stack[this.stack.length - 1].lastmove === "pass") { + this.gameover = true; + const cascading = this.getCascadingWinner(); + if (cascading === undefined) { + this.winner = [1, 2]; + } else { + this.winner = [cascading.winner]; + this.connPath = [...cascading.path]; + } + this.results.push({ type: "eog", reason: "consecutive_passes" }); + } + if (!this.gameover && !this.variants.includes("cascading") && !this.hasMoves(this.currplayer)) { this.gameover = true; this.winner = [otherPlayer]; this.results.push({ type: "eog", reason: "stalemate" }); @@ -537,6 +638,8 @@ export class GonnectGame extends GameBase { this.pushNeutralChatLine(lines, "apresults:EOG.repetition", { count: 1 }); } else if (r.reason === "stalemate") { this.pushNeutralChatLine(lines, "apresults:EOG.stalemate"); + } else if (r.reason === "consecutive_passes") { + this.pushNeutralChatLine(lines, "apresults:EOG.consecutive_passes"); } else { this.pushNeutralChatLine(lines, "apresults:EOG.default"); } diff --git a/test/games/gonnect.test.ts b/test/games/gonnect.test.ts new file mode 100644 index 000000000..eada474dd --- /dev/null +++ b/test/games/gonnect.test.ts @@ -0,0 +1,72 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ + +import "mocha"; +import { expect } from "chai"; +import { GonnectGame } from '../../src/games'; + +describe("Gonnect", () => { + it("Passing is illegal by default", () => { + const g = new GonnectGame(undefined, ["size-9"]); + expect(g.moves().includes("pass")).to.be.false; + expect(() => g.move("pass")).to.throw(); + }); + + it("Passing is legal under the cascading variant", () => { + const g = new GonnectGame(undefined, ["size-9", "cascading"]); + expect(g.moves().includes("pass")).to.be.true; + g.move("pass"); + expect(g.lastmove).to.equal("pass"); + expect(g.gameover).to.be.false; + }); + + it("Two consecutive passes end the game", () => { + const g = new GonnectGame(undefined, ["size-9", "cascading"]); + g.move("pass"); + expect(g.gameover).to.be.false; + g.move("pass"); + expect(g.gameover).to.be.true; + }); + + it("A full connection still wins immediately under the cascading variant", () => { + const g = new GonnectGame(undefined, ["size-9", "cascading"]); + // Give player 1 a full north-south connection down column "a" without triggering EOG early. + for (let row = 1; row <= 9; row++) { + g.board.set(`a${row}`, 1); + } + g.board.delete("a9"); + g.move("a9"); + expect(g.gameover).to.be.true; + expect(g.winner).to.eql([1]); + }); + + it("An empty board ends in a draw after two passes (cascading)", () => { + const g = new GonnectGame(undefined, ["size-9", "cascading"]); + g.move("pass"); + g.move("pass"); + expect(g.gameover).to.be.true; + expect(g.winner).to.eql([1, 2]); + }); + + it("Cascading tiebreak: sole occupant of the centre point wins when nobody spans the full board", () => { + const g = new GonnectGame(undefined, ["size-9", "cascading"]); + // Tengen of a 9x9 board (0-indexed 4,4) is "e5". + g.board.set("e5", 2); + g.move("pass"); + g.move("pass"); + expect(g.gameover).to.be.true; + expect(g.winner).to.eql([2]); + }); + + it("Cascading tiebreak: a connection across a smaller centred subboard beats no connection at all", () => { + const g = new GonnectGame(undefined, ["size-9", "cascading"]); + // 9x9 board: the centred 3x3 subboard occupies columns/rows d-f (indices 3-5). + // Give player 1 a north-south run down column "e" across that subboard (e4,e5,e6). + for (const cell of ["e4", "e5", "e6"]) { + g.board.set(cell, 1); + } + g.move("pass"); + g.move("pass"); + expect(g.gameover).to.be.true; + expect(g.winner).to.eql([1]); + }); +}); From eba2434b4fb04a0a031ea5d5da690b5c51443600 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 23:34:39 +0000 Subject: [PATCH 2/3] Don't bump gonnect's version; add tengen-then-two-passes test The version identifier is for incompatible saved-state shapes. This change doesn't alter IMoveState/IGonnectState fields (pass already reuses the existing "place"-less results/lastmove machinery other games use), so no bump is warranted. Also add a test playing out the exact tengen scenario via real moves (place, pass, pass) rather than only via direct board manipulation. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Y6dm6EQVSpd9YtMQ8xjeMR --- src/games/gonnect.ts | 2 +- test/games/gonnect.test.ts | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/games/gonnect.ts b/src/games/gonnect.ts index 90bc09c1c..142386a25 100644 --- a/src/games/gonnect.ts +++ b/src/games/gonnect.ts @@ -27,7 +27,7 @@ export class GonnectGame extends GameBase { name: "Gonnect", uid: "gonnect", playercounts: [2], - version: "20260913", + version: "20240719", dateAdded: "2024-08-02", // i18next.t("apgames:descriptions.gonnect") description: "apgames:descriptions.gonnect", diff --git a/test/games/gonnect.test.ts b/test/games/gonnect.test.ts index eada474dd..802e1e3cc 100644 --- a/test/games/gonnect.test.ts +++ b/test/games/gonnect.test.ts @@ -47,6 +47,17 @@ describe("Gonnect", () => { expect(g.winner).to.eql([1, 2]); }); + it("Black playing tengen alone, then two passes, wins on the final 1x1 subboard", () => { + // Default 13x13 board; tengen is "g7". Play it out with real moves rather than + // manipulating the board directly, to exercise the actual move/pass pipeline. + const g = new GonnectGame(undefined, ["cascading"]); + g.move("g7"); // Black (player 1) takes tengen + g.move("pass"); // White passes + g.move("pass"); // Black passes: two consecutive passes end the game + expect(g.gameover).to.be.true; + expect(g.winner).to.eql([1]); + }); + it("Cascading tiebreak: sole occupant of the centre point wins when nobody spans the full board", () => { const g = new GonnectGame(undefined, ["size-9", "cascading"]); // Tengen of a 9x9 board (0-indexed 4,4) is "e5". From 55add19a7af89e8dd51e85b8963c65b77e2247a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 17:50:17 +0000 Subject: [PATCH 3/3] Mark gonnect's cascading variant as experimental Keeps it out of production selection until it's been validated on the dev server. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Y6dm6EQVSpd9YtMQ8xjeMR --- src/games/gonnect.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/games/gonnect.ts b/src/games/gonnect.ts index 142386a25..97fca5fe6 100644 --- a/src/games/gonnect.ts +++ b/src/games/gonnect.ts @@ -53,7 +53,7 @@ export class GonnectGame extends GameBase { { uid: "size-9", group: "board" }, { uid: "#board", }, { uid: "size-19", group: "board" }, - { uid: "cascading" }, + { uid: "cascading", experimental: true }, ], categories: ["goal>connect", "mechanic>place", "mechanic>capture", "board>shape>rect", "board>connect>rect", "components>simple"], flags: ["pie", "custom-buttons"],