From 8b2f99db62d2686874a9f722035fe51c8d7331db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Neto?= Date: Sat, 16 May 2026 09:08:57 +0100 Subject: [PATCH] Add variant to Soccolot --- locales/en/apgames.json | 8 ++++++-- src/games/soccolot.ts | 42 ++++++++++++++++++++++++++++++++++------- src/games/synapse.ts | 4 ++-- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/locales/en/apgames.json b/locales/en/apgames.json index 539d4783..467e48ce 100644 --- a/locales/en/apgames.json +++ b/locales/en/apgames.json @@ -311,7 +311,7 @@ "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.\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.", - "synapse": "Synapse was first proposed, c.1070, by Pierre Berloquin. This game is played on a 4x4 board with 15 stones of each one of two colors. Each player chooses one to three matches of the same color, and place them in the next square, where the color defines the vertical or horizontal direction. The board could not have more than 25 stones in total. Later in 1976 a variant, by the same author, uses homogenous matches where the matches' heads define the direction. The current implemented version is based on this principle, using Looney Pyramids. This is a larger board with more pieces, proposed in 2005, by Joseph Kisenwether.", + "synapse": "Synapse was first proposed, c.1970, by Pierre Berloquin. This game is played on a 4x4 board with 15 stones of each one of two colors. Each player chooses one to three matches of the same color, and place them in the next square, where the color defines the vertical or horizontal direction. The board could not have more than 25 stones in total. Later in 1976 a variant, by the same author, uses homogenous matches where the matches' heads define the direction. The current implemented version is based on this principle, using Looney Pyramids. This is a larger board with more pieces, proposed in 2005, by Joseph Kisenwether.", "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.", @@ -2577,6 +2577,9 @@ }, "original": { "name": "8x8 board (6 men)" + }, + "swap": { + "name": "includes swap dribble" } }, "spire": { @@ -5989,6 +5992,7 @@ "BALL_INSTRUCTIONS": "Now selected an adjacent friendly man to kick the ball in the opposite direction", "ERROR_KICK_DRIBBLE": "Either (a) select a man to run, or (b) select a man and then select the adjacent ball, or (c) select the ball and then an adjacent man!", "DRIBBLE_INSTRUCTIONS": "Click an adjacent empty cell from the man, to move both man and ball in that direction (those cells must be both empty). It is also possible for the ball (or man) to move into the man's (or ball's) current position (if the other moves to an empty cell).", + "DRIBBLE_SWAP_INSTRUCTIONS": "Click an adjacent empty cell from the man, to move both man and ball in that direction (those cells must be both empty). It is also possible for the ball (or man) to move into the man's (or ball's) current position (if the other moves to an empty cell), or select the man again to swap the ball with it.", "KICK_INSTRUCTIONS": "Click an empty cell in the opposite direction of the man, to move the ball. Notice that the ball cannot jump over other pieces." }, "spire": { @@ -6247,7 +6251,7 @@ "TOO_LONG": "You may not add more stones if your first move is to create a new group." }, "synapse": { - "INITIAL_INSTRUCTIONS": "Place a friendly piece at the required square.", + "INITIAL_INSTRUCTIONS": "Place a friendly piece at the marked square.", "INVALID_MOVE": "Move {{move}} is invalid!", "PLACE_INSTRUCTIONS": "Click on the piece until it is the required size, then click on an orthogonal adjacent square to define the direction." }, diff --git a/src/games/soccolot.ts b/src/games/soccolot.ts index c8aac903..bd32f441 100644 --- a/src/games/soccolot.ts +++ b/src/games/soccolot.ts @@ -47,6 +47,7 @@ export class SoccolotGame extends GameBase { variants: [ { uid: "#board", }, // Speed Soccolot { uid: "original", group: "ruleset" }, + { uid: "swap", group: "ruleset" }, // adds swap dribble ], flags: ["experimental"] }; @@ -59,7 +60,7 @@ export class SoccolotGame extends GameBase { public variants: string[] = []; public stack!: Array; public results: Array = []; - private ruleset: "default" | "original"; + private ruleset: "default" | "original" | "swap"; private _points: [number, number][] = []; // if there are points here, the renderer will show them constructor(state?: ISoccolotState | string, variants?: string[]) { @@ -123,8 +124,9 @@ export class SoccolotGame extends GameBase { return this; } - private getRuleset(): "default" | "original" { + private getRuleset(): "default" | "original" | "swap" { if (this.variants.includes("original")) { return "original"; } + if (this.variants.includes("swap")) { return "swap"; } return "default"; } @@ -211,6 +213,15 @@ export class SoccolotGame extends GameBase { } } + if ( this.ruleset === "swap" ) { + for (const man of grid.neighbours(ball)) { + // find adjacent friendly Men + if ( this.board.has(man) && this.board.get(man)! === player ) { + moves.push(`${man},${ball}@`); // swap pieces + } + } + } + return moves.sort((a,b) => a.localeCompare(b)); } @@ -234,7 +245,11 @@ export class SoccolotGame extends GameBase { if ( moves[0] === this.getBall() ) { // it is a kick (final) newmove = `${move}>${cell}`; } else { // it is a dribble (final) - newmove = `${move}-${cell}`; + if ( this.ruleset === "swap" && cell === moves[0]) { + newmove = `${move}@`; // swap the ball with the man + } else { + newmove = `${move}-${cell}`; // move the ball with the man + } } } else { newmove = ""; // something went wrong, reset move @@ -268,7 +283,7 @@ export class SoccolotGame extends GameBase { } const prevplayer = this.currplayer % 2 + 1 as playerid; - const moves: string[] = m.split(/[,>-]/); + const moves: string[] = m.split(/[,>@-]/); if ( moves.length === 1 ) { if ( !this.board.has(m) || this.board.get(m)! === prevplayer ) { @@ -309,7 +324,11 @@ export class SoccolotGame extends GameBase { result.complete = -1; result.canrender = true; if (this.board.get(moves[0])! === this.currplayer) { - result.message = i18next.t("apgames:validation.soccolot.DRIBBLE_INSTRUCTIONS"); + if ( this.ruleset === "swap" ) { + result.message = i18next.t("apgames:validation.soccolot.DRIBBLE_SWAP_INSTRUCTIONS"); + } else { + result.message = i18next.t("apgames:validation.soccolot.DRIBBLE_INSTRUCTIONS"); + } } else { result.message = i18next.t("apgames:validation.soccolot.KICK_INSTRUCTIONS"); } @@ -330,7 +349,7 @@ export class SoccolotGame extends GameBase { // return the list of cells the current move can go to private findPoints(move: string): string[] { - const moves = move.split(/[,>-]/); + const moves = move.split(/[,>@-]/); const allMoves = this.moves(); const ball = this.getBall(); const res = []; @@ -352,7 +371,11 @@ export class SoccolotGame extends GameBase { // show available places to dribble (select moves like man,ball-newman) // or to kick (select moves like ball,man>newball) res.push(...allMoves.filter(m => m.startsWith(move)) + .filter(m => !m.includes('@')) .map(m => m.split(/[,>-]/)[2])); + if ( this.ruleset === "swap" && moves[1] === ball ) { // include swap option + res.push(moves[0]); // swap by clicking in the man again + } } return res; @@ -373,7 +396,7 @@ export class SoccolotGame extends GameBase { } this.results = []; - const moves = m.split(/[,>-]/); + const moves = m.split(/[,>@-]/); if ( partial ) { // if partial, set the points to be shown const g = this.graph; @@ -402,6 +425,11 @@ export class SoccolotGame extends GameBase { this.results.push({ type: "move", from: moves[0], to: moves[2] }); this.board.set(newBall, 3); this.results.push({ type: "move", from: moves[1], to: newBall }); + } else if ( m.includes('@') ) { // only for swap variant + this.board.set(moves[0], 3); // swap ball with man + this.results.push({ type: "move", from: moves[0], to: moves[1] }); + this.board.set(moves[1], this.currplayer); + this.results.push({ type: "move", from: moves[1], to: moves[0] }); } else { // a kick (ball,man>newball) this.board.delete(moves[0]); this.board.set(moves[2], 3); // moving the ball diff --git a/src/games/synapse.ts b/src/games/synapse.ts index 6d886fb1..d54591e3 100644 --- a/src/games/synapse.ts +++ b/src/games/synapse.ts @@ -63,7 +63,7 @@ export class SynapseGame extends GameBase { apid: "9228bccd-a1bd-452b-b94f-d05380e6638f", }, ], - categories: ["goal>score>immobilize", "mechanic>place", "board>shape>rect", "components>pyramids"], + categories: ["goal>immobilize", "mechanic>place", "board>shape>rect", "components>pyramids"], flags: ["player-stashes", "experimental"] }; @@ -384,7 +384,7 @@ export class SynapseGame extends GameBase { const starColour: Colourfuncs = { func: "custom", - default: "#FFDF00", // gold yellow + default: "#AD03DE", // vibrant purple (alternative: "#FFDF00", // gold yellow) palette: 3 };