diff --git a/locales/en/apgames.json b/locales/en/apgames.json index 3d3c03b8..a8da163f 100644 --- a/locales/en/apgames.json +++ b/locales/en/apgames.json @@ -276,7 +276,7 @@ "pacru": "This implementation adheres to the 2011 rule change that requires at least one opponent to have at least nine tiles on the board before meetings will trigger.", "pigs": "Unlike the old Super Duper Games implementation, this one implements the core rule set. Each player enters all five moves, and they are resolved at once.\n\nMovement is resolved before damage is applied.", "pigs2": "This is the same as the old Super Duper Games. The game starts by you programming three moves, but only one move is resolved and added each turn.\n\nMovement is resolved before damage is applied. Unlike most other AP games, if players resign or timeout, the game does not immediately end. Instead, they're eliminated and their robot becomes inactive, but the other players can play on.", - "pinch": "Pinch is a 2026 design of an original 2019 concept, propose a new perspective to the orthogonal connection genre. At its core is the **pinch capture**: when two friendly stones flank an enemy piece at a right angle, the captured stone is relocated rather than removed. This mechanic can trigger a cascade of further relocations, creating a chain-reaction that expands one local capture into changes over multiple sections of the board.", + "pinch": "Pinch is a 2026 design of an original 2019 concept, proposing a new perspective to the orthogonal connection genre. At its core is the **pinch capture**: when two friendly stones flank an enemy piece at a right angle, the captured stone is relocated rather than removed. This mechanic can trigger a cascade of further relocations, creating a chain-reaction that expands one local capture into changes over multiple sections of the board.", "pletore": "An intersection is controlled by a player if they 'pinch' it while the opponent does not. You 'pinch' an intersection if you have pieces that can see that intersection from two different axes. Your score is the sum of the number of stones of your color on the board and the number of empty points you control, plus a komi bonus and a button bonus. The button is used when the komi value plus the number of intersections on the board is even, and any player may take a turn off and claim the button if it has yet to be claimed. The game ends when both players pass in succession, but claiming the button does not count toward this. Players cannot pass until the entire board is occupied or controlled.", "plurality": "Plurality is a finite territorial game. The concept of territory exist disconnected from concepts like groups or liberty. Territories' ownership depend on the most represented color in their perimeters. On their turn, players drop two friendly stones and one adversary stone as a single orthogonally connected group (a tromino). It is forbidden to make a 2x2 shape of stones of any color configuration.", "queensland": "After the first game completes, the board resets and you play a second game but with blue playing first. Highest combined score wins.", @@ -293,7 +293,7 @@ "squirm": "Squirm is one of the first snake/serpent/root games, where groups are defined with at most two friendly connections per stone. But instead of being a territorial or stalemate-oriented game, Squirm is a scoring game, where players need to build the longest possible snake-like group (following the original ruleset), or adding has much points as possible (following either available variant).", "stawvs": "For clarity, the game pieces are represented as circles, though in real life, caps (small pyramids of distinct player colors) are usually used. The UI will sort and score your trees (trios consisting of one of each pyramid size) for you as in Mega-Volcano, but for reference the point values are: 7 points for each monochrome tree, 5 points for multicolor trees, and 1 point each for loose pyramids. Except in the variant where final pyramids aren't collected, the pyramids under player pieces are added to the players' collections and scored, but greyed-out pyramids remain on the board under the pieces to clarify the final position.", "stigmergy": "You control a space if more than half of the lines of sight to it end at a piece of your color. Your score is the sum of the number of pieces of your color on the board and the number of empty spaces you control, plus a komi bonus and a button bonus. The button is used when the komi value plus the number of spaces on the board is even, and any player may take a turn off and claim the button if it has yet to be claimed. The game ends when both players pass in succession, but claiming the button does not count toward this. Players cannot pass until the entire board is occupied or controlled.", - "stiletto": "A dagger represents an option for a double move. Stiletto is a 2003 game from the family of Gomoku, where players try to make a five in-a-row. The main difference is that the second player (traditionally White) starts with a dagger. This allows to compensate for Black's strong initial advantage. When players use the dagger, they pass it to the adversary.\n\nIn terms of 'power' available to players, this variant lies between Gomoku and Connect6. The fact that the power to play two stones in not shared, adds tension to the gameplay. The player that has the dagger wants to use it, but he decides to wait for a proper time. It is true that each time he delays using the dagger, there's a toll on the adversary: _the threat is stronger than the execution_, as the old Chess grandmasters said. The game also includes Ko fights, that emerge from the restriction of consecutive dagger uses.\n\n_Stiletto_, from the Italian, is a small, slender dagger with a long, narrow blade designed primarily for thrusting.", + "stiletto": "A dagger represents an option for a double move. Stiletto is a 2003 game from the family of Gomoku, where players try to make a five in-a-row. The main difference is that the second player (traditionally White) starts with a dagger. This allows to compensate for Black's strong initial advantage. When players use the dagger, they pass it to the adversary.\n\nIn terms of 'power' available to players, this variant lies between Gomoku and Connect6. The fact that the power to play two stones in not shared, adds tension to the gameplay.\n\nThe **Open-4** variant makes it illegal to create an open-4 with both dagger stones, which removes part of the dagger's power.\n\n_Stiletto_, from the Italian, is a small, slender dagger with a long, narrow blade designed primarily for thrusting.", "tablero": "When it's your turn, you will see the dice you have to work with, but once your move is complete, the dice will reroll. Exploration is not helpful because the dice roll is not finalized until after the move is submitted. As you scroll back through the game history, the dice you see are the dice for the *next* turn. The dice used to make the move you're seeing are displayed below the board.\n\nWhile most moves can be unambiguously made by simple clicks, sometimes a button is more helpful. The Place, Take, and Bump buttons to the left of the board are there if you need them.", "tafl": "The variant names are in the format {ruleset}-{board size}-{initial layout}-{optional: starting player}. For example, 'linnaean-9x9-tcross-w' is the linnaean rules on a 9x9 board with T-cross setup, and the starting player is the defenders. If starting player is not mentioned, then attackers start.", "taiji": "Moves are done with two clicks. The first tile you place is always the light one, and then the dark one.", @@ -2590,6 +2590,10 @@ "stiletto": { "#board": { "name": "19x19 board" + }, + "open4": { + "description": "Cannot use both stones to form an open-4.", + "name": "Open-4 restriction" } }, "stigmergy": { @@ -5884,7 +5888,8 @@ "INSTRUCTIONS_INACTIVE_DAGGER": "Place a piece onto an empty space. You have the dagger but it is inactive, since you used it last turn and there is no immediate losing threat.", "EXCESS": "You may only place two stones if you have an active dagger. Place just one stone.", "EXCESS_FIRST": "You have placed too many stones. At the start you may only place one stone as the first player.", - "EXTENSION": "It is illegal to use the dagger to extend three friendly stones, in the same line, to make a 5 in-a-row." + "EXTENSION": "It is illegal to use the dagger to extend three friendly stones, in the same line, to make a 5 in-a-row.", + "ILLEGAL_OPEN4": "It is illegal to use the dagger to create an open-4 with both stones." }, "stigmergy": { "INITIAL_INSTRUCTIONS": "Place a piece onto an empty space not controlled by the opponent, replace an enemy piece on a space you control, or 'pass' if all spaces are occupied or controlled. The game ends when both players pass in sequence.", diff --git a/src/games/domineering.ts b/src/games/domineering.ts index 0be738e3..2af896bf 100644 --- a/src/games/domineering.ts +++ b/src/games/domineering.ts @@ -27,6 +27,7 @@ export class DomineeringGame extends GameBase { dateAdded: "2026-04-25", // i18next.t("apgames:descriptions.domineering") description: "apgames:descriptions.domineering", + notes: "apgames:notes.domineering", urls: [ "https://boardgamegeek.com/boardgame/7450/stop-gate", "https://jpneto.github.io/world_abstract_games/modern_rules/2025_Quelhas.pdf" diff --git a/src/games/pinch.ts b/src/games/pinch.ts index 4aa07013..ddfcd35f 100644 --- a/src/games/pinch.ts +++ b/src/games/pinch.ts @@ -32,6 +32,7 @@ export class PinchGame extends GameBase { dateAdded: "2026-04-23", // i18next.t("apgames:descriptions.pinch") description: "apgames:descriptions.pinch", + notes: "apgames:notes.pinch", urls: [ "https://boardgamegeek.com/boardgame/285214/pinch", ], diff --git a/src/games/plurality.ts b/src/games/plurality.ts index b0e8f022..dc9aea18 100644 --- a/src/games/plurality.ts +++ b/src/games/plurality.ts @@ -39,6 +39,7 @@ export class PluralityGame extends GameBase { dateAdded: "2026-02-18", // i18next.t("apgames:descriptions.plurality") description: "apgames:descriptions.plurality", + notes: "apgames:notes.plurality", urls: ["https://boardgamegeek.com/boardgame/462846/plurality"], people: [ { diff --git a/src/games/sentinel.ts b/src/games/sentinel.ts index 8abd6278..39a26d57 100644 --- a/src/games/sentinel.ts +++ b/src/games/sentinel.ts @@ -42,7 +42,7 @@ export class SentinelGame extends GameBase { version: "20260328", dateAdded: "2026-04-22", description: "apgames:descriptions.sentinel", - // notes: "apgames:notes.sentinel", + notes: "apgames:notes.sentinel", urls: [ "https://boardgamegeek.com/thread/3651706/rules-of-sentinel", ], diff --git a/src/games/spora.ts b/src/games/spora.ts index b826a37e..9d98b24c 100644 --- a/src/games/spora.ts +++ b/src/games/spora.ts @@ -44,6 +44,7 @@ export class SporaGame extends GameBase { dateAdded: "2026-04-07", // i18next.t("apgames:descriptions.spora") description: "apgames:descriptions.spora", + notes: "apgames:notes.spora", urls: [ "https://boardgamegeek.com/thread/3493284/rules-of-spora" ], diff --git a/src/games/squirm.ts b/src/games/squirm.ts index b0afc3fa..b4431ba8 100644 --- a/src/games/squirm.ts +++ b/src/games/squirm.ts @@ -28,6 +28,7 @@ export class SquirmGame extends GameBase { dateAdded: "2026-04-22", // i18next.t("apgames:descriptions.squirm") description: "apgames:descriptions.squirm", + notes: "apgames:notes.squirm", urls: ["https://jpneto.github.io/world_abstract_games/squirm.htm"], people: [ { @@ -50,7 +51,7 @@ export class SquirmGame extends GameBase { { uid: "tromp", group: "ruleset" }, { uid: "neto", group: "ruleset" }, ], - flags: ["pie", "custom-buttons"] + flags: ["pie", "custom-buttons", "automove"] }; public numplayers = 2; diff --git a/src/games/stiletto.ts b/src/games/stiletto.ts index 2b343471..c3cf9761 100644 --- a/src/games/stiletto.ts +++ b/src/games/stiletto.ts @@ -32,6 +32,7 @@ export class StilettoGame extends InARowBase { dateAdded: "2026-03-07", // i18next.t("apgames:descriptions.Stiletto") description: "apgames:descriptions.stiletto", + notes: "apgames:notes.stiletto", urls: [ "https://boardgamegeek.com/boardgame/465550/stiletto", "https://jpneto.github.io/world_abstract_games/dagger_gomoku.htm", @@ -56,6 +57,9 @@ export class StilettoGame extends InARowBase { }, ], categories: ["goal>arrange", "mechanic>place", "board>shape>rect", "board>connect>rect", "components>simple>2c"], + variants: [ + { uid: "open4", group: "ruleset" }, + ], flags: ["no-moves", "custom-colours"], }; @@ -81,6 +85,7 @@ export class StilettoGame extends InARowBase { public variants: string[] = []; public lastDaggerUse = [0, -1]; // last #turn each player used the dagger public swapped = false; // abstract attribute of InARowBase + private ruleset: "default" | "open4"; constructor(state?: IStilettoState | string, variants?: string[]) { super(); @@ -112,6 +117,7 @@ export class StilettoGame extends InARowBase { this.stack = [...state.stack]; } this.load(); + this.ruleset = this.getRuleset(); } public load(idx = -1): StilettoGame { @@ -137,6 +143,12 @@ export class StilettoGame extends InARowBase { return this; } + private getRuleset(): "default" | "open4" { + if (this.variants.includes("open4")) { return "open4"; } + return "default"; + } + + private currentTurn(): number { return this.stack.length; } @@ -212,6 +224,53 @@ export class StilettoGame extends InARowBase { return false; } + private empty(xe: number, ye: number): boolean { + if ( this.board.has(this.coords2algebraic(xe, ye)) ) { return false; } + if ( xe < 0 || ye < 0 || xe >= this.boardSize || ye >= this.boardSize ) { return false; } + return true; + } + + public checkIllegalOpen4(moves : string[]): boolean { + const [move1, move2] = moves; + const [x1, y1] = this.algebraic2coords(move1); + const [x2, y2] = this.algebraic2coords(move2); + + this.board.set(move1, this.currplayer); // temporary add stones at moves1/moves2 + this.board.set(move2, this.currplayer); + + // search open-4 in all four directions (orthogonal || diagonal) + // we use move1 as a pivot, and try to find move2 in the line + let foundOpen4 = false; + let foundMove2 = false; + for (const [dx, dy] of [[1, 0], [0, 1], [1, 1], [1, -1]]) { + let count = 1; // move1 counted already + let x = x1 + dx; + let y = y1 + dy; + while (this.board.get(this.coords2algebraic(x, y)) === this.currplayer) { + if ( x === x2 && y === y2 ) { foundMove2 = true; } + count++; x += dx; y += dy; // count friendly friends in current direction + } + const end1 = [x, y]; // the next square in that direction + + x = x1 - dx; // move backwards now + y = x2 - dy; + while (this.board.get(this.coords2algebraic(x, y)) === this.currplayer) { + if ( x === x2 && y === y2 ) { foundMove2 = true; } + count++; x -= dx; y -= dy; // count friendly friends in opposite direction + } + const end2 = [x, y]; // the next square in that direction + + if ( count === 4 && this.empty(end1[0], end1[1]) && this.empty(end2[0], end2[1]) ) { + foundOpen4 = true; + break; + } + } + + this.board.delete(move1); // don't forget to remove stones + this.board.delete(move2); + return foundOpen4 && foundMove2; + } + private shuffle(xs: string[]): void { // Fisher-Yates Shuffle for (let i = xs.length - 1; i > 0; i--) { @@ -413,6 +472,12 @@ export class StilettoGame extends InARowBase { result.message = i18next.t("apgames:validation.stiletto.EXTENSION"); return result; } + + if (this.ruleset === "open4" && this.checkIllegalOpen4(moves)) { + result.valid = false; + result.message = i18next.t("apgames:validation.stiletto.ILLEGAL_OPEN4"); + return result; + } } // no more than two placements is possible diff --git a/src/games/xana.ts b/src/games/xana.ts index 29872936..18ba00cf 100644 --- a/src/games/xana.ts +++ b/src/games/xana.ts @@ -41,6 +41,7 @@ export class XanaGame extends GameBase { dateAdded: "2026-04-22", // i18next.t("apgames:descriptions.xana") description: "apgames:descriptions.xana", + notes: "apgames:notes.xana", urls: [ "https://boardgamegeek.com/thread/3482800", ], @@ -562,7 +563,17 @@ export class XanaGame extends GameBase { this.gameover = this.lastmove === "pass" && this.stack[this.stack.length - 1].lastmove === "pass"; - if (this.gameover) { + // if no shared accessible cells, the game is over, since all areas ownership are decided + if (this.stack.length > 3 && !this.gameover) { + const p1cells: string[] = this.accessibleCells(1); + const p2cells: Set = new Set(this.accessibleCells(2)); + const shareCells: string[] = p1cells.filter(c => p2cells.has(c)); + if ( shareCells.length == 0 ) { + this.gameover = true; + } + } + + if ( this.gameover ) { this.scores = [this.getPlayerScore(1), this.getPlayerScore(2)]; this.winner = this.scores[0] > this.scores[1] ? [1] : [2]; this.results.push(