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
15 changes: 13 additions & 2 deletions locales/en/apgames.json
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,14 @@
},
"size-37": {
"name": "37x37 board"
},
"#ruleset": {
"description": "Area counting; situational superko",
"name": "AGA Rules"
},
"positional": {
"description": "Area counting; positional superko",
"name": "Positional Superko"
}
},
"gonnect": {
Expand Down Expand Up @@ -3583,6 +3591,9 @@
},
"tumbleweed": "Score (pieces + influence)",
"ESTIMATEDSCORES": "Estimated scores",
"spora" : {
"RESERVE": "Reserve:"
},
"TOPLACE": "Piece to place",
"valley": "Moon Tokens",
"waldmeister": {
Expand Down Expand Up @@ -4677,7 +4688,7 @@
"PARTIAL": "Select one of the highlighted cells to move the glider. If a bar has appeared at the edge of the board, you can click it to move the glider off the board."
},
"go": {
"INITIAL_SETUP": "Choose a number of points to add to the second player's score, and the next player will choose sides. This implementation uses Chinese area rules. Scores are based on the current area ownership. Super-Ko applies to prevent positional cycles. Players do need to capture all dead stones before ending the game with two consecutive passes.",
"INITIAL_SETUP": "Choose a number of points to add to the second player's score, and the next player will choose sides. This implementation uses Chinese area rules. Scores are based on the current area ownership. Super-Ko applies to prevent cycles. Players do need to capture all dead stones before ending the game with two consecutive passes.",
"CYCLE" : "A previous position cannot be repeated; super-Ko applies.",
"INSTRUCTIONS": "Select an intersection to place a piece.",
"INVALID_KOMI": "You must choose an number in increments of 0.5 (like 4 or 4.5) to add to the second player's score.",
Expand Down Expand Up @@ -5705,7 +5716,7 @@
"ENEMY_PIECE": "It is illegal to add pieces to an opponent stack.",
"INVALID_KOMI": "You must choose an number in increments of 0.5 (like 4 or 4.5) to add to the second player's score.",
"INVALID_PLAYSECOND": "You cannot choose to play second from this board state.",
"INVALID_SOW_PATH": "The selected path does not follow Spora rules: (a) pick (part of) a stack and leave one piece per orthogonal adjacent intersection, (b) sowing can turn left/right after each placed piece, (c) sowing a stack is only legal if the player can legally place all pieces of the stack, (d) a stack can never have more than four pieces.",
"INVALID_SOW_PATH": "The selected path does not follow Spora rules: (a) pick (part of) a stack and leave one piece per orthogonal adjacent intersection, (b) sowing can turn left/right after each placed piece, (c) sowing a stack is only legal if the player can legally place all pieces of the stack, (d) a stack can never have more than four pieces, (e) sowing onto intersections vacated by captures in the same turn is not allowed.",
"KOMI_CHOICE": "You may either make the first move on the board and let your opponent keep the bonus points (an integer) or you may choose \"Play second\" and take the bonus points for yourself.",
"MAXIMUM_STACK": "A stack can have, at most, four friendly pieces.",
"NOT_ENOUGH_PIECES": "The pieces in reserve are not enough for this placement.",
Expand Down
25 changes: 20 additions & 5 deletions src/games/go.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export class GoGame extends GameBase {
{ uid: "size-21", group: "board" },
{ uid: "size-25", group: "board" },
{ uid: "size-37", group: "board" },
{ uid: "positional", group: "ruleset" },
],
categories: ["goal>area", "mechanic>place", "mechanic>capture", "mechanic>enclose", "board>shape>rect", "components>simple>1per"],
flags: ["scores", "custom-buttons", "custom-colours"],
Expand Down Expand Up @@ -97,6 +98,7 @@ export class GoGame extends GameBase {

private boardSize = 19;
private grid: RectGrid;
private ruleset: "default" | "positional";

constructor(state?: IGoState | string, variants?: string[]) {
super();
Expand Down Expand Up @@ -134,6 +136,7 @@ export class GoGame extends GameBase {
this.stack = [...state.stack];
}
this.load();
this.ruleset = this.getRuleset();
this.grid = new RectGrid(this.boardSize, this.boardSize);
}

Expand Down Expand Up @@ -181,6 +184,11 @@ export class GoGame extends GameBase {
return 19;
}

private getRuleset(): "default" | "positional" {
if (this.variants.includes("positional")) { return "positional"; }
return "default";
}

public isKomiTurn(): boolean {
return this.stack.length === 1;
}
Expand Down Expand Up @@ -237,13 +245,20 @@ export class GoGame extends GameBase {
private numRepeats(): number {
let num = 0;
const sigCurr = this.signature();
//const parityCurr = this.stack.length % 2 === 0 ? "even" : "odd";
const parityCurr = this.stack.length % 2 === 0;

for (let i = 0; i < this.stack.length; i++) {
//const parity = i % 2 === 0 ? "even" : "odd";
const sig = this.signature(this.stack[i].board);
//if (sig === sigCurr && parity === parityCurr) {
if (sig === sigCurr) {
num++;

if (this.ruleset === "positional") { // apply positional superko
if (sig === sigCurr) {
num++;
}
} else { // apply situational superko
const parity = i % 2 === 0;
if (sig === sigCurr && parity === parityCurr) {
num++;
}
}
}
return num;
Expand Down
2 changes: 1 addition & 1 deletion src/games/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class ProductGame extends GameBase {
{ uid: "size-7", group: "board" },
{ uid: "1-group", group: "ruleset" },
],
flags: ["no-moves"]
flags: ["pie", "no-moves"]
};

public numplayers = 2;
Expand Down
87 changes: 61 additions & 26 deletions src/games/spora.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { GameBase, IAPGameState, IClickResult, ICustomButton, IIndividualState,
import { APGamesInformation } from "../schemas/gameinfo";
import { APRenderRep, BoardBasic, MarkerDots, RowCol } from "@abstractplay/renderer/src/schemas/schema";
import { APMoveResult } from "../schemas/moveresults";
import { replacer, reviver, UserFacingError, SquareOrthGraph } from "../common";
import { replacer, reviver, SquareOrthGraph, UserFacingError } from "../common";
import { connectedComponents } from "graphology-components";
import pako, { Data } from "pako";

Expand All @@ -24,9 +24,10 @@ interface IMoveState extends IIndividualState {
board: Map<string, cellcontents>;
lastmove?: string;
scores: [number, number];
reserve: [number, number];
maxGroups: [number, number]; // relevant for the opening to define which groups are alive
komi?: number;
swapped: boolean;
reserve: [number, number];
}

export interface ISporaState extends IAPGameState {
Expand Down Expand Up @@ -69,7 +70,7 @@ export class SporaGame extends GameBase {
],
categories: ["goal>area", "mechanic>place", "mechanic>move>sow", "mechanic>capture", "mechanic>stack",
"mechanic>enclose", "board>shape>rect", "components>simple>2c"],
flags: ["scores", "custom-buttons", "custom-colours", "experimental"],
flags: ["scores", "no-moves", "custom-buttons", "custom-colours", "experimental"],
};

public coords2algebraic(x: number, y: number): string {
Expand All @@ -89,9 +90,10 @@ export class SporaGame extends GameBase {
public results: Array<APMoveResult> = [];
public variants: string[] = [];
public scores: [number, number] = [0, 0];
public reserve: [number, number] = [this.getReserveSize(), this.getReserveSize()]; // #pieces off-board
public maxGroups: [number, number] = [0, 0];
public komi?: number;
public swapped = true;
public reserve: [number, number] = [this.getReserveSize(), this.getReserveSize()]; // number of pieces initially off-board

private boardSize = 13;

Expand All @@ -108,8 +110,9 @@ export class SporaGame extends GameBase {
currplayer: 1,
board: new Map(),
scores: [0, 0],
swapped: true,
reserve: [this.getReserveSize(), this.getReserveSize()],
maxGroups: [0, 0],
swapped: true,
};
this.stack = [fresh];
} else {
Expand Down Expand Up @@ -153,6 +156,7 @@ export class SporaGame extends GameBase {
this.boardSize = this.getBoardSize();
this.scores = [...state.scores];
this.reserve = [...state.reserve];
this.maxGroups = state.maxGroups === undefined ? [0,0] : [...state.maxGroups];
this.komi = state.komi;
this.swapped = false;
// We have to check the first state because we store the updated version in later states
Expand Down Expand Up @@ -183,8 +187,8 @@ export class SporaGame extends GameBase {
// lower and upper bounds for the amount of stones each player should have, then the arithmetic
// mean of these two bounds give us the initial budget
private getReserveSize() : number {
const a = 2*(this.getBoardSize() * this.getBoardSize())/3;
const b = (this.getBoardSize() * this.getBoardSize())/2;
const a = (this.getBoardSize() * this.getBoardSize())/2;
const b = 2*(this.getBoardSize() * this.getBoardSize())/3;
return Math.ceil((a+b)/2);
}

Expand Down Expand Up @@ -274,21 +278,17 @@ export class SporaGame extends GameBase {
return new SquareOrthGraph(this.boardSize, this.boardSize);
}

private toXY(c: string): [number, number] { // TODO: change to algebraic2coords
const x = c.charCodeAt(0) - "a".charCodeAt(0);
const y = Number(c.slice(1)) - 1;
return [x, y];
}

// check orthogonal adjacency
private isOrthAdjacent(a: string, b: string): boolean {
const [x1, y1] = this.toXY(a);
const [x2, y2] = this.toXY(b);
const [x1, y1] = this.algebraic2coords(a);
const [x2, y2] = this.algebraic2coords(b);
return Math.abs(x1 - x2) + Math.abs(y1 - y2) === 1;
}

// checks if the given path is legal according to Spora's rules
private isValidPath(start: string, remainingSowSize: number, path: string[]): boolean {
// we need (placedStack, n) because the player might sow over the just placed/increased stack
private isValidPath(placedStack: string, n: number,
start: string, remainingSowSize: number, path: string[]): boolean {
if (path.length === 0) return true;

// first step must be adjacent to start
Expand All @@ -302,8 +302,8 @@ export class SporaGame extends GameBase {

for (const cell of path) {
if (! this.isOrthAdjacent(prev, cell)) { return false; }
const [x1, y1] = this.toXY(prev);
const [x2, y2] = this.toXY(cell);
const [x1, y1] = this.algebraic2coords(prev); //this.toXY(prev); //
const [x2, y2] = this.algebraic2coords(cell); //this.toXY(cell); //
const dir: [number, number] = [x2 - x1, y2 - y1];

if (prevDir) { // check no 180° turn
Expand All @@ -319,10 +319,20 @@ export class SporaGame extends GameBase {
return false; // enemy stack is too big; this path is invalid
}
}
// also check if there's a friendly stack with size 4 (size 5 is illegal)
if ( this.board.has(cell) && this.board.get(cell)![0] === this.currplayer ) {
// also check if there's a friendly stack with size 4 (size 5 is illegal),
// unless the last piece is the start (ie, the sowing made a complete square)
if ( this.board.has(cell) && this.board.get(cell)![0] === this.currplayer && start !== cell ) {
const size = this.board.get(cell)![1];
if ( size === 4) {
if ( cell !== placedStack && size === 4 ) {
return false; // friendly stack is too big; this path is invalid
}
if ( cell === placedStack && size + n === 4 ) {
return false; // friendly stack is too big; this path is invalid
}
}

if (! this.board.has(cell) ) { // the player might have placed an entire 4-stack
if ( cell === placedStack && n === 4 ) {
return false; // friendly stack is too big; this path is invalid
}
}
Expand Down Expand Up @@ -355,6 +365,12 @@ export class SporaGame extends GameBase {
}
const groupsOwned = connectedComponents(gOwned.graph);

// if there's only one group, and that's all there has ever been
// then this single group is, by definition, alive
if (groupsOwned.length === 1 && this.maxGroups[p - 1] <= 1) {
return [];
}

// check connecting paths

// first generate a new graph with owned pcs and empties
Expand Down Expand Up @@ -646,7 +662,7 @@ export class SporaGame extends GameBase {
cellsPath = tokens[3].split('-');
}

if (! this.isValidPath(sowingStack, remainingSowSize, cellsPath) ) {
if (! this.isValidPath(initialCell, n, sowingStack, remainingSowSize, cellsPath) ) {
result.valid = false;
result.message = i18next.t("apgames:validation.spora.INVALID_SOW_PATH");
return result;
Expand All @@ -660,8 +676,8 @@ export class SporaGame extends GameBase {
}

private doCaptures(): string[] {
const firstPly = this.swapped ? 6 : 5;
if ( this.stack.length <= firstPly ) return [];
//const firstPly = this.swapped ? 6 : 5;
//if ( this.stack.length <= firstPly ) return [];
const result = [];
const prevplayer = this.currplayer === 1 ? 2 : 1;

Expand All @@ -676,6 +692,20 @@ export class SporaGame extends GameBase {
return result;
}

public updateGroupCounts(): void {
for (const p of [1, 2] as const) {
const owned = [...this.board.entries()].filter(e => e[1][0] === p).map(e => e[0]);
const gOwned = this.getGraph();
for (const node of gOwned.graph.nodes()) {
if (! owned.includes(node)) {
gOwned.graph.dropNode(node);
}
}
const groups = connectedComponents(gOwned.graph);
this.maxGroups[p - 1] = Math.max(this.maxGroups[p - 1], groups.length);
}
}

public move(m: string, {partial = false, trusted = false} = {}): SporaGame {
if (this.gameover) {
throw new UserFacingError("MOVES_GAMEOVER", i18next.t("apgames:MOVES_GAMEOVER"));
Expand Down Expand Up @@ -790,6 +820,9 @@ export class SporaGame extends GameBase {
}
}

if (this.stack.length > 3) {
this.updateGroupCounts();
}
if ( partial ) { return this; }

this.lastmove = m;
Expand All @@ -803,7 +836,7 @@ export class SporaGame extends GameBase {
}

protected checkEOG(): SporaGame {
if (this.stack.length <= 4) return this; // player must place at least one stack each
if (this.stack.length <= 4) return this; // players must place at least one stack each

const p1Pieces = [...this.board.entries()].filter(e => e[1][0] === 1).map(e => e[0]);
const p2Pieces = [...this.board.entries()].filter(e => e[1][0] === 2).map(e => e[0]);
Expand Down Expand Up @@ -843,6 +876,7 @@ export class SporaGame extends GameBase {
board: this.cloneBoard(),
scores: [...this.scores],
reserve: [...this.reserve],
maxGroups: [...this.maxGroups],
komi: this.komi,
swapped: this.swapped
};
Expand Down Expand Up @@ -911,7 +945,8 @@ export class SporaGame extends GameBase {
}
}

if ( this.stack.length > 4 ) { // only show territories after the initial moves
// add territory dots
if (this.maxGroups[0] > 0 && this.maxGroups[1] > 0) {
const territories = this.getTerritories();
const markers: Array<MarkerDots> = []
for (const t of territories) {
Expand Down
2 changes: 1 addition & 1 deletion src/games/xana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class XanaGame extends GameBase {
],
categories: ["goal>area", "mechanic>place", "mechanic>move", "mechanic>stack", "mechanic>enclose",
"board>shape>hex", "components>simple>3c"],
flags: ["pie", "custom-buttons", "custom-colours", "scores", "experimental"],
flags: ["pie", "no-moves", "custom-buttons", "custom-colours", "scores", "experimental"],
};

public numplayers = 2;
Expand Down
Loading