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
2 changes: 1 addition & 1 deletion locales/en/apgames.json
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,7 @@
"intermedium": "Intermedium (aka, Stepping Stones) is a 1974 game designed by Matt Crispis. This is a stack sowing game, where players try to surround the opposing home-base. Stacks only move diagonally, and each army starts in a different diagonal color, so they can never cross over (like two bishops at different square colors). Pieces are captured by enclosure, surrounding all orthogonal adjacencies of an opponent stack.\n\nSome words from the author: \"[Intermedium] is simple to learn, but may lead to complicated situations that require deep analysis and forward thinking. Single pieces do no move, but they are useful a stepping stones for your captures. And they may become alive by putting another piece over them. Before you master the game, avoid early attacks against the City that may prove costly. Build up your strength gradually and evenly by dominating, if possible, the center. You should also avoid building 'formidable' stacks of six or more pieces - if taken you may lose the game at one stroke. Leave some forces behind to defend your City in case of emergency.\"",
"invector": "Invector, designed by Mark Steere in 2026, is an elimination game inspired by the ancient Hawiian game of Kōnane. Invector begins with a checkerboard pattern of stones, has short range captures, and is extremely simple. Invector simply combines two basic, off-the-shelf ingredients: orthogonal, adjacent capturing moves, and orthogonal, adjacent moves closer to center.",
"jacynth": "More information on the Decktet system can be found on the [official Decktet website](https://www.decktet.com). Cards in players' hands are hidden from observers and opponents.",
"killallgo": "The Defender must form an unconditionally alive string (pass-alive according to Benson's algorithm): one that can survive any sequence of opposing moves without responding. Life dependent on seki or ko does not count. The Attacker wins when their unconditionally alive stones leave no room for such a string. Passing is not allowed after the opening.\n\nStones are placed and captured as in Go with Tromp-Taylor clearing: after a placement, opponent strings without liberties are removed, then your own string if it has none. Multi-stone suicide is therefore legal. Positional superko applies: no placement may recreate an earlier board position.",
"killallgo": "The Defender must form an unconditionally alive string (pass-alive according to Benson's algorithm): one that can survive any sequence of opposing moves without responding. Life dependent on seki or ko does not count. The Attacker wins when their unconditionally alive stones leave no room for such a string. Passing is not allowed after the opening, and a player with no legal placement loses.\n\nStones are placed and captured as in Go with Tromp-Taylor clearing: after a placement, opponent strings without liberties are removed, then your own string if it has none. Multi-stone suicide is therefore legal. Positional superko applies: no placement may recreate an earlier board position.",
"konane": "Several competing opening protocols exist, but the most common ruleset is the Naihe Ruleset, used by tournaments at the Bishop Museum in Hawaii and described in the BGG reference. This is what is implemented here.",
"knightline": "For convenience, your stack is split approximately in half on your first click. Subsequent clicks on the source or target stack move one piece at a time. When done, click on an empty cell or use the Complete Move button.\n\nThe coordinate system is adapted from the coder's coordinate system for Hive, a game on an unbounded (but still reasonably sized) hex board. The y axis is labeled with Cartesian coordinates (positive and negative integers). The x axis is labeled with letters in alphabetical order, with the origin at (m, 0). If motion on the x-axis exceeds the confines of the alphabet in either direction, the letters are prefixed with another letter (z is followed by aa, ab, ac, etc., and a is preceded by zz, zy, zx, etc.). The game is not expected to reach a width that requires more double coordinates, but for the record, az is followed by ba, za is preceded by yz, and so forth. The board cannot reach a size that would result in ambiguity.",
"lasca": "Maneuverability is a measure of how close your pieces are to promoting. Your maximum maneuverability is the board size times the number of stacks you control.\n\nMaterial is calculated by giving you a point for every friendly piece in a stack you control, plus an extra point if you have an officer on top of that stack.",
Expand Down
1 change: 1 addition & 0 deletions locales/en/apresults.json
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@
"unlur_foul": "The game has ended because one player has achieved the opponent's win condition without achieving their own.",
"druid_hex": "The game has ended because {{player}} connected the {{edges}} edges.",
"killallgo_double_pass": "The game has ended because both players passed consecutively without the Defender establishing unconditional life.",
"killallgo_no_moves": "The game has ended because the player to move has no legal placement.",
"killallgo_no_room": "The game has ended because the Attacker's unconditionally alive stones leave the Defender no room to form an unconditionally alive string.",
"killallgo_pass_alive": "The game has ended because the Defender formed an unconditionally alive string."
},
Expand Down
2 changes: 1 addition & 1 deletion locales/eo/apgames.json
Original file line number Diff line number Diff line change
Expand Up @@ -4665,7 +4665,7 @@
"xana": "Dum sia vico, ludanto metas aŭ movas stakon (la nombro de stakigeblaj pecoj estas limigita) kaj laŭvole metas du murojn sur malplenajn seslaterojn. Stakoj sen liberecoj estas kaptitaj. Stako havas liberecon, se almenaŭ unu el la ses apudaj seslateroj estas malplena (ne ekzistas koncepto de stakgrupo). Seslatero estas atingebla, se ĝi estas malplena kaj vojo de malplenaj seslateroj konektas ĝin al amika stako. La celo estas akiri la plej altan sumon de teritoria kaj kapta poentaroj. Xana estis projektita en 2005.",
"y": "Charles Titus kaj Craige Schensted (Ea Ea) projektis la ludon en 1953. Kiel rekta posteulo de Hex, ĝi origine estis ludata sur triangula tabulo, kaj la celo estis konekti ĉiujn tri flankojn per ĉeno de amikaj ŝtonoj. Y estas konsiderata pli fundamenta ludo ol Hex pro siaj pli simplaj reguloj kaj ĉar eblas aranĝi partion de Hex sur sufiĉe granda Y-tabulo. En sia libro de 1975 **Mudcrack Y & Poly-Y**, la projektintoj prezentis la _principon de ŝlimfendo_, laŭ kiu konektludoj povas okazi sur vasta gamo de iom post iom pli kompleksaj tabuloj. Unu el tiuj projektoj (\"la fleksita Y\", kun tri kvinangulaj ĉeloj) fariĝis la komerca Y-tabulo anstataŭ la norma triangula tabulo.",
"elOso": "La komenca aranĝo estas aŭtomate farata kiam vi lanĉas la ludon. La flankpanelo montras la nombrojn de pecoj por la Ĉielo, la Kaverno, la Grundo kaj la Kavo. Post ĉiu movo, uzu la regilojn de filmeroj por trairi la respondon de la Urso paŝon post paŝo. Dek aŭ pli da pecoj en la Ĉielo signifas venkon.",
"killallgo": "La defendanto devas formi senkondiĉe vivan ĉenon laŭ la algoritmo de Benson: ĉenon, kiu povas elteni ajnan sinsekvon de kontraŭulaj movoj sen respondo. Vivo dependa de seki aŭ koo ne sufiĉas. La atakanto venkas, kiam ties senkondiĉe vivaj ŝtonoj lasas neniun spacon por tia ĉeno. Preterpaso ne estas permesita post la malfermo.\n\nŜtonoj estas metataj kaj kaptataj kiel en Goo, kun la forigo laŭ Tromp-Taylor: post meto, la ĉenoj de la kontraŭulo sen liberecoj estas forigataj, poste via propra ĉeno, se ĝi havas neniun. Plurŝtona memmortigo do estas permesita. Validas pozicia superko: neniu meto rajtas rekrei pli fruan tabulpozicion.",
"killallgo": "La defendanto devas formi senkondiĉe vivan ĉenon laŭ la algoritmo de Benson: ĉenon, kiu povas elteni ajnan sinsekvon de kontraŭulaj movoj sen respondo. Vivo dependa de seki aŭ koo ne sufiĉas. La atakanto venkas, kiam ties senkondiĉe vivaj ŝtonoj lasas neniun spacon por tia ĉeno. Preterpaso ne estas permesita post la malfermo, kaj ludanto sen valida meto malgajnas.\n\nŜtonoj estas metataj kaj kaptataj kiel en Goo, kun la forigo laŭ Tromp-Taylor: post meto, la ĉenoj de la kontraŭulo sen liberecoj estas forigataj, poste via propra ĉeno, se ĝi havas neniun. Plurŝtona memmortigo do estas permesita. Validas pozicia superko: neniu meto rajtas rekrei pli fruan tabulpozicion.",
"lielow": "La valoro \"Movoj ĝis memmortigo\" sumas por ĉiu ludanto, kiom da movoj ĉiu el ties stakoj ankoraŭ povus fari — kreskante je unu kun ĉiu movo kaj ĉiam elektante la direkton, laŭ kiu ĝi plej longe restas sur la tabulo — antaŭ ol ĝi devus elportiĝi. La kalkulo ignoras ĉiujn aliajn stakojn sur la tabulo, ĉu amikajn, ĉu malamikajn. Ĝi do estas nur proksimuma mezuro de la senŝirmeco de la ludpecoj, ne la efektiva nombro da restantaj validaj movoj."
},
"homeworlds": {
Expand Down
1 change: 1 addition & 0 deletions locales/eo/apresults.json
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@
"gonnect_cascading_subboard": "{{player}} venkis sur la {{size}}x{{size}}-subtabulo.",
"gonnect_direct_connection": "{{player}} venkis per rekta konekto.",
"killallgo_double_pass": "La ludo finiĝis, ĉar ambaŭ ludantoj sinsekve preterpasis kaj la defendanto ne establis senkondiĉan vivon.",
"killallgo_no_moves": "La ludo finiĝis, ĉar la movonta ludanto ne havas validan meton.",
"killallgo_no_room": "La ludo finiĝis, ĉar la senkondiĉe vivaj ŝtonoj de la atakanto lasas al la defendanto neniun spacon por formi senkondiĉe vivan ĉenon.",
"killallgo_pass_alive": "La ludo finiĝis, ĉar la defendanto faris senkondiĉe vivantan ĉenon."
},
Expand Down
23 changes: 21 additions & 2 deletions src/games/killallgo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ export class KillAllGoGame extends GameBase {
],
},
],
categories: ["goal>annihilate", "mechanic>place", "mechanic>capture", "mechanic>enclose", "mechanic>asymmetry", "board>shape>rect", "board>connect>rect", "components>simple>1per"],
categories: ["goal>cripple", "goal>arrange", "mechanic>place", "mechanic>capture", "mechanic>enclose", "mechanic>asymmetry", "board>shape>rect", "board>connect>rect", "components>simple>1per"],
flags: ["experimental", "custom-colours", "custom-buttons"],
customizations: [
{
Expand Down Expand Up @@ -1375,8 +1375,10 @@ export class KillAllGoGame extends GameBase {
}
}

// Every ply that leaves the board in play may have settled the game.
// Every ply that leaves the board in play may have settled the game, or left the next
// player without a legal placement.
this.checkLifeAndDeath();
this.checkStalemate(seen);

// The final position of the ply is the saved board; keep only the intermediate ones.
this.interim.pop();
Expand Down Expand Up @@ -1451,6 +1453,21 @@ export class KillAllGoGame extends GameBase {
}
}

/**
* Passing is not allowed during play, so a player with no legal placement loses. Short of a
* superko repetition, that cannot happen before the life-and-death check has ended the game.
*/
private checkStalemate(seen: Set<string>): void {
if (this.gameover || this.phase !== "play") {
return;
}
const colour = this.colourOfSeat(this.currplayer)!;
if (this.geo.cells.some((cell) => !this.board.has(cell) && this.simulate(this.board, cell, colour, seen) !== undefined)) {
return;
}
this.endGame(this.otherSeat(this.currplayer), "no-moves");
}

private endGame(winner: playerid, reason: string, alive?: string[]): void {
this.gameover = true;
this.winner = [winner];
Expand Down Expand Up @@ -1679,6 +1696,8 @@ export class KillAllGoGame extends GameBase {
this.pushNeutralChatLine(lines, "apresults:EOG.killallgo_pass_alive");
} else if (r.reason === "no-room") {
this.pushNeutralChatLine(lines, "apresults:EOG.killallgo_no_room");
} else if (r.reason === "no-moves") {
this.pushNeutralChatLine(lines, "apresults:EOG.killallgo_no_moves");
} else if (r.reason === "double-pass") {
// Preserve the move log for games completed before normal-play passes were removed.
this.pushNeutralChatLine(lines, "apresults:EOG.killallgo_double_pass");
Expand Down
27 changes: 27 additions & 0 deletions test/games/killallgo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,33 @@ describe("Kill-All Go", () => {
}
});

it("makes a player with no legal placement lose", () => {
// No sequence of moves is known to leave a player without a legal placement before the
// life-and-death check ends the game, so the superko history is written directly: every
// position the Defender could create after the Attacker's b9 has already occurred.
const g = play(attackerIsPlayerOne(), ["e5", "a9", "e6"]);
const geo = makeGeometry(9);
const state = g.state();
const last = state.stack[state.stack.length - 1];
const after = new Map(last.board);
after.set("b9", RED);
const visited = geo.cells
.filter((cell) => !after.has(cell))
.map((cell) => ({ ...last, board: new Map(after).set(cell, BLUE), _results: [] }));
state.stack.splice(state.stack.length - 1, 0, ...visited);
const restored = new KillAllGoGame(state);
expect(restored.currplayer).to.equal(1);
expect(restored.moves()).to.include("b9");
restored.move("b9");
expect(restored.gameover).to.be.true;
expect(restored.winner).to.deep.equal([1]);
expect(restored.stack[restored.stack.length - 1]._results).to.deep.include({ type: "eog", reason: "no-moves" });
expect(restored.moves()).to.deep.equal([]);
const keys = restored.chatLogEntries(["Alice", "Bob"]).flatMap((e) => e.lines.map((l) => l.textKey));
expect(keys).to.include("apresults:EOG.killallgo_no_moves");
expect(restored.chatLog(["Alice", "Bob"]).flat().join("\n")).to.include("no legal placement");
});

it("forbids recreating an earlier position (positional superko)", () => {
const g = attackerIsPlayerOne();
// Blue: c5, b4, c3 around c4; Red: d5, e4, d3 around d4; Red throws in at c4, Blue captures with d4.
Expand Down
Loading