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 @@ -256,7 +256,7 @@
"queensland": "After the first game completes, the board resets and you play a second game but with blue playing first. Highest combined score wins.",
"quincunx": "A game consists of as many rounds as there are players. Highest accumulated score wins.\n\nBasic scoring: For each orthogonally adjacent card, add the ranks together.\n* 2-9 w/ matching Ace: gain, otherwise lose\n* 10: no effect\n* 11: draw a card\n* 12-19: gain sum - 10\n* 20: draw a card\n\nPairs: For each orthogonally adjacent card that is the same rank, gain 5 points. (**Exception:** If that pair is also part of a set, it does *not* also score as a pair.)\n\nSets: If the placed card creates a line (including diagonal) of three *or more* cards of the same rank, gain 30 pts for each line.\n\nStraights: If the placed card creates or extends sequence of three or more cards in rank order along a line (including diagonal), gain 20 points for each line.\n\nPower play: If you played an Ace or Crown orthogonally adjacent to the matching Ace or Crown, score the number of points equal to the sum of the ranks of all the *other* cards in the tableau that share that suit.\n\nMore 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.",
"razzle": "The implementation here is the so called tournament rules.",
"rootbound": "The estimated score shows the scores that the board would resolve to if the game were to end in the current board state. Consequently, the first player who forms a partition will briefly appear to have a maximum score. This will correct itself over the course of the game.\n\nThe implementation currently only provides beginner protection for the first move of the game. Be careful not to merge down to one Dead Group.",
"rootbound": "The estimated score shows the scores that the board would resolve to if the game were to end in the current board state. Consequently, the player with the largest group or the player who has the only territory will briefly appear to have a maximum score. This will correct itself over the course of the game.\n\nThe implementation only provides beginner protections for the first three moves of the game. Be careful not to merge down to one Dead Group.",
"siegeofj": "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 they are hidden from opponents until the deck is empty, at which point the players have perfect information, so the hands are revealed.",
"spire": "In this implementation, if you select only one space, it assumes that you placed the ball of your colour, and if you select two spaces, the first space is for the neutral ball and the second space is for the ball of your colour. If the first click is on a space where only one of the neutral ball or the ball of your colour is valid, it will automatically commit that ball.",
"spook": "When using the randomised board setup, the only fairness heuristics that we currently have are that (1) the number of balls in solid 5-ball pyramids of the same colour are equal for both players and (2), the second-highest layer must contain balls of both colours. Feel free to contact us on Discord if you think of other ways to make the game fairer.",
Expand Down
71 changes: 27 additions & 44 deletions src/games/rootbound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class RootBoundGame extends GameBase {
name: "Root Bound",
uid: "rootbound",
playercounts: [2],
version: "20240729",
version: "20250109",
dateAdded: "2024-02-25",
// i18next.t("apgames:descriptions.rootbound")
description: "apgames:descriptions.rootbound",
Expand Down Expand Up @@ -357,33 +357,33 @@ export class RootBoundGame extends GameBase {
}

const moves: string[] = [];
const prohibitedCells: string[] = [];
const claimedCells: string[] = [];
const claimedRegions = this.computeClaimedRegions();
for (const claimedRegion of claimedRegions) {
if (claimedRegion[0] !== 3) prohibitedCells.push(...claimedRegion[3]);
if (claimedRegion[0] !== 3) claimedCells.push(...claimedRegion[3]);
}

const prohibitedFirstCells: string[] = [];
if (this.stack.length === 3) {
prohibitedCells.push(...this.getEmptyNeighborsOfGroup(0));
prohibitedFirstCells.push(...this.getEmptyNeighborsOfGroup(0));
}

const validFirstMoves = (this.listCells() as string[]).filter(c => !prohibitedCells.includes(c) && this.isValidPlacement(player!, c)).sort();
const validFirstMoves = (this.listCells() as string[]).filter(c => !prohibitedFirstCells.includes(c) && !claimedCells.includes(c) && this.isValidPlacement(player!, c)).sort();
if (this.stack.length !== 2) {
moves.push(...validFirstMoves);
}

if (this.stack.length > 1) {

const boardClone = deepclone(this.board) as Map<string, CellContent>;

for (const firstMove of validFirstMoves) {
const neighbors: string[] = [];
if (this.stack.length === 2) neighbors.push(...this.getGraph().neighbours(firstMove).filter(c => !this.board.has(c)));
if (this.stack.length === 3) neighbors.push(...this.getGraph().neighbours(firstMove).filter(c => this.getEmptyNeighborsOfGroup(0).includes(c)));
const prohibitedSecondCells: string[] = [];
if (this.stack.length === 2) prohibitedSecondCells.push(...this.getGraph().neighbours(firstMove).filter(c => !this.board.has(c)));
if (this.stack.length === 3) prohibitedSecondCells.push(...this.getGraph().neighbours(firstMove).filter(c => this.getEmptyNeighborsOfGroup(0).includes(c)));

boardClone.set(firstMove, [player, 10000]);

const validSecondMoves = (this.listCells() as string[]).filter(c => !prohibitedCells.includes(c) && !neighbors.includes(c)
const validSecondMoves = (this.listCells() as string[]).filter(c => !prohibitedSecondCells.includes(c) && !claimedCells.includes(c)
&& this.isValidSecondPlacement(player!, c, boardClone)).sort();
for (const secondMove of validSecondMoves) {
if (!this.isRapidGrowthMove(firstMove, secondMove)) {
Expand Down Expand Up @@ -506,7 +506,7 @@ export class RootBoundGame extends GameBase {
}
}

if (this.stack.length === 3 && this.getEmptyNeighborsOfGroup(0).includes(cells[0])) {
if (this.stack.length === 3 && this.getEmptyNeighborsOfGroup(0).includes(cells[0]) && (cells.length < 2 || this.getEmptyNeighborsOfGroup(0).includes(cells[1]))) {
result.message = i18next.t("apgames:validation.rootbound.BAD_SECOND_MOVE");
return result;
}
Expand Down Expand Up @@ -665,24 +665,18 @@ export class RootBoundGame extends GameBase {
this.lastmove = m;
this.currplayer = ((this.currplayer as number) % this.numplayers) + 1 as PlayerId;

if (this.isNewRules()) {
const board = this.resolveBoardAndUpdateScore();
if (this.checkEOGTrigger()) {
this.board = board;
this.resolveEOG();
}
if (this.checkEOGTrigger()) {
this.board = this.resolveBoardAndUpdateScore(true);
this.resolveEOG();
} else {
this.updateScore(claimedRegions);
if (this.checkEOGTrigger()) {
this.resolveEOG();
}
this.resolveBoardAndUpdateScore(false);
}

this.saveState();
return this;
}

private resolveBoardAndUpdateScore(): Map<string, CellContent> {
private resolveBoardAndUpdateScore(includeInResult: boolean): Map<string, CellContent> {

const board = deepclone(this.board) as Map<string, CellContent>;

Expand All @@ -696,7 +690,7 @@ export class RootBoundGame extends GameBase {
let groupsRemoved = false;
for (const group of keyValueArray[1]) {
if (!liveGroups.includes(group) && !this.canSeeAllyGroup(group, liveGroups, board)) {
this.removeGroup(group, false, board);
this.removeGroup(group, includeInResult, board);
groupsRemoved = true;
}
}
Expand All @@ -705,11 +699,7 @@ export class RootBoundGame extends GameBase {
}
}

if (claimedRegions.filter(c => c[0] !== null).length < 2) {
this.updateScore(originalRegions, this.board);
} else {
this.updateScore(claimedRegions, board);
}
this.updateScore(claimedRegions, board);
return board;
}

Expand Down Expand Up @@ -738,19 +728,17 @@ export class RootBoundGame extends GameBase {
if (claimedRegion[0] === 2) this.scores[1] += claimedRegion[1];
}

if (this.isNewRules()) {
for (const cell of (this.listCells() as string[]).filter(c => board!.has(c))) {
if (board.get(cell)![0] === 1) {
this.scores[0]++;
} else {
this.scores[1]++;
}
for (const cell of (this.listCells() as string[]).filter(c => board!.has(c))) {
if (board.get(cell)![0] === 1) {
this.scores[0]++;
} else {
this.scores[1]++;
}
}

if (this.firstpasser !== undefined) {
if (this.firstpasser === 1) this.scores[0] += 0.5;
else this.scores[1] += 0.5;
}
if (this.firstpasser !== undefined) {
if (this.firstpasser === 1) this.scores[0] += 0.5;
else this.scores[1] += 0.5;
}
return this;
}
Expand Down Expand Up @@ -815,11 +803,6 @@ export class RootBoundGame extends GameBase {
return deadGroups;
}

private isNewRules(): boolean {
if (this.version < 20240729) return false;
return true;
}

private checkEOGTrigger(): boolean {
return this.lastmove === "pass" && this.stack[this.stack.length - 1].lastmove === "pass";
}
Expand Down