Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions locales/en/apgames.json
Original file line number Diff line number Diff line change
Expand Up @@ -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).",
Expand Down Expand Up @@ -2119,6 +2120,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": {
Expand Down Expand Up @@ -6197,6 +6202,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."
},
Expand Down
6 changes: 6 additions & 0 deletions locales/eo/apgames.json
Original file line number Diff line number Diff line change
Expand Up @@ -1647,6 +1647,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": {
Expand Down Expand Up @@ -4517,6 +4521,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 aperas ĉe la malsupro de la tabulo kaj inkluzivas kaj la kartojn en la kartaro kaj nekonatajn kartojn en la manoj de aliaj ludantoj. Post la unua mano, ĉiuj kartoj estas tirataj el la malkaŝa tirprovizo, do la kartoj enmane iom post iom malkaŝiĝas. Ankaŭ la forĵetamaso aperas.\n\nOni ne plu uzas devigajn preterpasojn kiam ludanto replenigas la tirprovizon kaj sendas la movon meze de sia vico. La servilo aŭtomate traktas la duan partan vicon de tiu ludanto. Laŭvole, alia variaĵo de la tirprovizo evitas la duoblan duonmovon.\n\nLa variaĵon \"Krokodiloj\" kreis Jorge Arroyo, la tradukinto de la anglaj reguloj. La altnivelajn regulojn kaj aliajn etajn variaĵojn kreis 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.",
Expand Down Expand Up @@ -5891,6 +5896,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."
},
Expand Down
123 changes: 113 additions & 10 deletions src/games/gonnect.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -31,6 +31,8 @@ export class GonnectGame extends GameBase {
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: [
Expand All @@ -51,9 +53,10 @@ export class GonnectGame extends GameBase {
{ uid: "size-9", group: "board" },
{ uid: "#board", },
{ uid: "size-19", group: "board" },
{ uid: "cascading", experimental: true },
],
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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 });
}
}
}

Expand Down Expand Up @@ -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);
Expand All @@ -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" });
Expand Down Expand Up @@ -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");
}
Expand Down
Loading
Loading