Skip to content

Commit e67aedc

Browse files
committed
Cleaning up some minor inconsistencies in Root Bound
1 parent 8df1738 commit e67aedc

File tree

2 files changed

+28
-45
lines changed

2 files changed

+28
-45
lines changed

locales/en/apgames.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@
256256
"queensland": "After the first game completes, the board resets and you play a second game but with blue playing first. Highest combined score wins.",
257257
"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.",
258258
"razzle": "The implementation here is the so called tournament rules.",
259-
"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.",
259+
"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.",
260260
"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.",
261261
"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.",
262262
"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.",

src/games/rootbound.ts

Lines changed: 27 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class RootBoundGame extends GameBase {
3636
name: "Root Bound",
3737
uid: "rootbound",
3838
playercounts: [2],
39-
version: "20240729",
39+
version: "20250109",
4040
dateAdded: "2024-02-25",
4141
// i18next.t("apgames:descriptions.rootbound")
4242
description: "apgames:descriptions.rootbound",
@@ -357,33 +357,33 @@ export class RootBoundGame extends GameBase {
357357
}
358358

359359
const moves: string[] = [];
360-
const prohibitedCells: string[] = [];
360+
const claimedCells: string[] = [];
361361
const claimedRegions = this.computeClaimedRegions();
362362
for (const claimedRegion of claimedRegions) {
363-
if (claimedRegion[0] !== 3) prohibitedCells.push(...claimedRegion[3]);
363+
if (claimedRegion[0] !== 3) claimedCells.push(...claimedRegion[3]);
364364
}
365365

366+
const prohibitedFirstCells: string[] = [];
366367
if (this.stack.length === 3) {
367-
prohibitedCells.push(...this.getEmptyNeighborsOfGroup(0));
368+
prohibitedFirstCells.push(...this.getEmptyNeighborsOfGroup(0));
368369
}
369370

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

375376
if (this.stack.length > 1) {
376-
377377
const boardClone = deepclone(this.board) as Map<string, CellContent>;
378378

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

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

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

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

668-
if (this.isNewRules()) {
669-
const board = this.resolveBoardAndUpdateScore();
670-
if (this.checkEOGTrigger()) {
671-
this.board = board;
672-
this.resolveEOG();
673-
}
668+
if (this.checkEOGTrigger()) {
669+
this.board = this.resolveBoardAndUpdateScore(true);
670+
this.resolveEOG();
674671
} else {
675-
this.updateScore(claimedRegions);
676-
if (this.checkEOGTrigger()) {
677-
this.resolveEOG();
678-
}
672+
this.resolveBoardAndUpdateScore(false);
679673
}
680674

681675
this.saveState();
682676
return this;
683677
}
684678

685-
private resolveBoardAndUpdateScore(): Map<string, CellContent> {
679+
private resolveBoardAndUpdateScore(includeInResult: boolean): Map<string, CellContent> {
686680

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

@@ -696,7 +690,7 @@ export class RootBoundGame extends GameBase {
696690
let groupsRemoved = false;
697691
for (const group of keyValueArray[1]) {
698692
if (!liveGroups.includes(group) && !this.canSeeAllyGroup(group, liveGroups, board)) {
699-
this.removeGroup(group, false, board);
693+
this.removeGroup(group, includeInResult, board);
700694
groupsRemoved = true;
701695
}
702696
}
@@ -705,11 +699,7 @@ export class RootBoundGame extends GameBase {
705699
}
706700
}
707701

708-
if (claimedRegions.filter(c => c[0] !== null).length < 2) {
709-
this.updateScore(originalRegions, this.board);
710-
} else {
711-
this.updateScore(claimedRegions, board);
712-
}
702+
this.updateScore(claimedRegions, board);
713703
return board;
714704
}
715705

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

741-
if (this.isNewRules()) {
742-
for (const cell of (this.listCells() as string[]).filter(c => board!.has(c))) {
743-
if (board.get(cell)![0] === 1) {
744-
this.scores[0]++;
745-
} else {
746-
this.scores[1]++;
747-
}
731+
for (const cell of (this.listCells() as string[]).filter(c => board!.has(c))) {
732+
if (board.get(cell)![0] === 1) {
733+
this.scores[0]++;
734+
} else {
735+
this.scores[1]++;
748736
}
737+
}
749738

750-
if (this.firstpasser !== undefined) {
751-
if (this.firstpasser === 1) this.scores[0] += 0.5;
752-
else this.scores[1] += 0.5;
753-
}
739+
if (this.firstpasser !== undefined) {
740+
if (this.firstpasser === 1) this.scores[0] += 0.5;
741+
else this.scores[1] += 0.5;
754742
}
755743
return this;
756744
}
@@ -815,11 +803,6 @@ export class RootBoundGame extends GameBase {
815803
return deadGroups;
816804
}
817805

818-
private isNewRules(): boolean {
819-
if (this.version < 20240729) return false;
820-
return true;
821-
}
822-
823806
private checkEOGTrigger(): boolean {
824807
return this.lastmove === "pass" && this.stack[this.stack.length - 1].lastmove === "pass";
825808
}

0 commit comments

Comments
 (0)