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
7 changes: 5 additions & 2 deletions locales/en/apgames.json
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@
"trike": "Trike is a game on a triangular board. Players take turn moving a neutral pawn around the board (passing is not allowed). When a player moves the pawn, they place a checker of their own color, onto the destination point. When the pawn is trapped, the game is over. At the end of the game, each player gets a point for each checker of their own color adjacent to, or underneath, the pawn. The person with the highest score wins.",
"tritium": "Two players take turns creating and growing regions and claiming them. Once the board is full, the player who controls most tiles wins.",
"tumbleweed": "Tumbleweed is a game of line of sight. Towers are built, and cells are controlled by the player with the most towers that can see that cell. Compete to control the majority of the hexagonal board.",
"twinflames": "Players score by multiplying their two largest groups. Largest score wins. If tied, the second player wins. Uses a random setup to place some walls.",
"twinflames": "Players score by multiplying their two largest groups. Largest score wins. If tied, the second player wins. The game uses a random setup to place a fixed number of walls.",
"twixt": "Twixt is a connection game where players alternate turns placing pegs and links on a pegboard in an attempt to link their opposite sides.",
"unlur": "An asymmetric connection game where one player tries to form a Y and the other tries to connect opposite sides. Achieving the opponent's goal without achieving your own is a loss. The game starts with a contract phase where both players place the colour of the Y player until someone passes, and then the opponent becomes the Line player.",
"upperhand": "A shedding game where you try to be the first to place all your pieces. If a platform is created and at least three of the pieces are of a player's colour, an additional piece of that colour is automatically placed on top of it, causing chain reactions. The pie rule applies.",
Expand Down Expand Up @@ -309,7 +309,8 @@
"twixt": "The notation is based on Hansel notation at <http://www.ibiblio.org/twixtpuzzles/>. Some modifications are that link removal specifically specifies the link direction, and commas separate the moves. To add/remove links, click on the pegs between them. You can also remove a link by clicking on the line itself.",
"waldmeister": "Players play two games sequentially. In the first round, Player 1 is playing for colours and Player 2 is playing for heights. At the end of the first round, scores are tabulated and scoring groups highlighted. Then in the second round, Player 2 plays first and plays for colours, and Player 1 plays for heights.",
"witch": "The first player does not start as owning any pieces and may remove any piece (other than a crown) on their first turn. The second player chooses their colour on their first turn, after which, removing your opponent's pieces is no longer possible.",
"xana": "Players, on their turns, drop/move a stack (there's a limited amount of stackable pieces) and optionally drop two walls into empty hexes. Stacks without liberties are captured. A stack has liberty if at least one of the adjacent hexes is empty (there is no concept of group of stacks). A hex is accessible if it is empty and connected to a friendly stack by a path of empty hexes. The goal is to build the highest score of territory plus captures. Xana was designed in 2005."
"xana": "Players, on their turns, drop/move a stack (there's a limited amount of stackable pieces) and optionally drop two walls into empty hexes. Stacks without liberties are captured. A stack has liberty if at least one of the adjacent hexes is empty (there is no concept of group of stacks). A hex is accessible if it is empty and connected to a friendly stack by a path of empty hexes. The goal is to build the highest score of territory plus captures. Xana was designed in 2005.",
"y": "The game was designed by Charles Titus and Craige Schensted (Ea Ea) in 1953. A direct descended of Hex, the game was originally played on a triangular board, where the goal is to connect all three sides by a chain of friendly stones. Y is considered a more fundamental game than Hex, given its simpler rules and given that a Hex match can be setup on a sufficient large Y board. In their 1975 book **Mudcrack Y & Poly-Y**, the designers introduced the _mudcrack principle_ stating that connection games can be played on a wide range of progressively elaborate boards. One of those designs ('the bent Y', with three pentagonal cells) became the commercial Y board, instead of the standard triangular one."
},
"variants": {
"abande": {
Expand Down Expand Up @@ -6399,11 +6400,13 @@
"INITIAL_INSTRUCTIONS": "Drop pieces from reserve on empty cells or friendly stacks. Alternatively, move a stack: click it then click on an empty cell within moving range. Afterwards, and optionally, one or two walls can be placed on any empty accessible cells (closed adversary areas are not accessible).",
"DROP_MOVE_INSTRUCTIONS" : "Click again on the stack to place a new stone from the reserve. Otherwise, click N times on an empty cell (within moving range) to move N pieces.",
"ENEMY_PIECE" : "Cannot change an enemy piece.",
"INVALID_PLAYSECOND": "You cannot choose to swap positions from this board state.",
"MOVE_NOT_INSIDE_CIRCLE": "Stack cannot be moved outside its circle (i.e., its moving range).",
"MOVE_TO_OCCUPIED_CELL": "Pieces from a stack must be moved to an empty cell.",
"NOT_ENOUGH_PIECES_TO_MOVE": "The stack does not have that many pieces to be moved.",
"NOT_PLACED_ON_FRIEND": "Pieces cannot be moved from opposing stacks.",
"OCCUPIED_WALL": "Walls must be placed on empty cells (does not include cells from pieces just captured).",
"PIE_CHOICE": "Either play to be the second player, or click the Pie button to swap colors and be the first player. Your adversary will get 0.5 points for offering the pie.",
"RESERVE_EMPTY": "Reserve empty; no more placements possible.",
"SAME_WALL" : "Cannot place the same wall twice.",
"UNACCESSIBLE_PIECE": "Cannot place/move pieces on inaccessible cells.",
Expand Down
17 changes: 1 addition & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/games/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class ProductGame extends GameBase {
type: "designer",
name: "Nick Bentley",
urls: ["https://boardgamegeek.com/boardgamedesigner/7958/nick-bentley"],
apid: "52077877-93bb-4fff-9e5f-f1c41ac8e866",
},
{
type: "coder",
Expand Down
6 changes: 6 additions & 0 deletions src/games/shapechess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export class ShapeChessGame extends GameBase {
}
}

/* The original ruleset states that the symmetric adversary groups should wait for the adversary to make an action. I'm commenting the code in case I refactor back in the future
// now, do the same for the opponent's groups that might be formed due to currplayer's actions
const prevplayer = clone.currplayer % 2 + 1 as playerid;
for (const group of clone.getGroups(prevplayer)) {
Expand All @@ -222,6 +223,7 @@ export class ShapeChessGame extends GameBase {
}
}
}
*/
} // for (actions)
return [hadCaptures, clone];
}
Expand Down Expand Up @@ -468,6 +470,7 @@ export class ShapeChessGame extends GameBase {
}
}

/* see note about this above, at method willLastActionCapture()
// now, do the same for the opponent's groups that might be formed due to currplayer's actions
const prevplayer = this.currplayer % 2 + 1 as playerid;
for(const group of this.getGroups(prevplayer)) {
Expand All @@ -482,6 +485,7 @@ export class ShapeChessGame extends GameBase {
}
}
}
*/
} // for (actions)

const moves = actions.at(-1)!.split("-");
Expand Down Expand Up @@ -522,6 +526,7 @@ export class ShapeChessGame extends GameBase {
}
}

/*
const prevplayer = this.currplayer % 2 + 1 as playerid;
for(const group of this.getGroups(prevplayer)) {
const symmetry = this.computeSymmetry(group);
Expand All @@ -535,6 +540,7 @@ export class ShapeChessGame extends GameBase {
}
}
}
*/

if (partial) { return this; }

Expand Down
3 changes: 2 additions & 1 deletion src/games/twinflames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ export class TwinFlamesGame extends GameBase {
description: "apgames:descriptions.twinflames",
notes: "apgames:notes.twinflames",
urls: [
"https://docs.google.com/document/d/1oLiUll3GKfy1kt9wWIWi3--WrAMZ24x3DMdljPxIQIs/edit?tab=t.0#heading=h.gg6hnnlkcc3o" // TODO: update new link
"https://boardgamegeek.com/boardgame/470021/twin-flames",
],
people: [
{
type: "designer",
name: "Nick Bentley",
urls: ["https://boardgamegeek.com/boardgamedesigner/7958/nick-bentley"],
apid: "52077877-93bb-4fff-9e5f-f1c41ac8e866",
},
{
type: "coder",
Expand Down
63 changes: 54 additions & 9 deletions src/games/xana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface IMoveState extends IIndividualState {
scores: [number, number];
prisoners: [number, number];
reserve: [number, number];
swapped: boolean;
};

export interface IXanaState extends IAPGameState {
Expand Down Expand Up @@ -89,6 +90,7 @@ export class XanaGame extends GameBase {
public stack!: Array<IMoveState>;
public results: Array<APMoveResult> = [];
public graph: HexTriGraph = new HexTriGraph(BOARD_SIZE, 2*BOARD_SIZE-1);
public swapped = true;

private scores: [number, number] = [0, 0];
private prisoners: [number, number] = [0, 0]; // number of enemy pieces (not stacks) captured
Expand All @@ -110,6 +112,7 @@ export class XanaGame extends GameBase {
scores: [0, 0],
prisoners: [0, 0],
reserve: [RESERVE_SIZE, RESERVE_SIZE],
swapped: true,
};
this.stack = [fresh];
} else {
Expand Down Expand Up @@ -144,11 +147,22 @@ export class XanaGame extends GameBase {
this.prisoners = [...state.prisoners];
this.reserve = [...state.reserve];
this.graph = new HexTriGraph(BOARD_SIZE, 2*BOARD_SIZE-1);
this.swapped = false;
// We have to check the first state because we store the updated version in later states
if (state.swapped === undefined) {
this.swapped = this.stack.length < 3 || this.stack[2].lastmove !== "swap";
} else {
this.swapped = state.swapped;
}
return this;
}

/////////////// helper functions ///////////////

public isPieTurn(): boolean {
return this.stack.length === 2;
}

// all the cells accessible to the pieces of a given player
private accessibleCells(player: playerid): string[] {
const pieces = [...this.board.entries()].filter(e => e[1][0] === player)
Expand Down Expand Up @@ -339,7 +353,24 @@ export class XanaGame extends GameBase {
result.valid = true;
result.complete = -1;
result.canrender = true;
result.message = i18next.t("apgames:validation.xana.INITIAL_INSTRUCTIONS");
if ( this.isPieTurn() ) {
result.message = i18next.t("apgames:validation.xana.PIE_CHOICE");
} else {
result.message = i18next.t("apgames:validation.xana.INITIAL_INSTRUCTIONS");
}
return result;
}

if (m === "swap") {
if ( this.isPieTurn() ) {
result.valid = true;
result.complete = 1;
result.canrender = true;
result.message = i18next.t("apgames:validation._general.VALID_MOVE");
} else {
result.valid = false;
result.message = i18next.t("apgames:validation.xana.INVALID_PLAYSECOND");
}
return result;
}

Expand Down Expand Up @@ -491,6 +522,15 @@ export class XanaGame extends GameBase {

if (m === "pass") {
this.results.push({type: "pass"});
} else if (m === "swap") { // pie was accepted
this.swapped = true;
this.board.forEach((v, k) => {
if (v[0] !== 3) { // if it's not a wall, swap colors
this.board.set(k, [v[0] === 1 ? 2 : 1, v[1]]);
}
})
this.reserve = [this.reserve[1], this.reserve[0]];
this.results.push({ type: "pie" });
} else {
const commands: string[] = m.split(',');
const initialCell: string = m.split(/[<>]/)[0];
Expand Down Expand Up @@ -564,7 +604,7 @@ export class XanaGame extends GameBase {
this.stack[this.stack.length - 1].lastmove === "pass";

// if no shared accessible cells, the game is over, since all areas ownership are decided
if (this.stack.length > 3 && !this.gameover) {
if (this.stack.length > 4 && !this.gameover) {
const p1cells: string[] = this.accessibleCells(1);
const p2cells: Set<string> = new Set(this.accessibleCells(2));
const shareCells: string[] = p1cells.filter(c => p2cells.has(c));
Expand All @@ -585,13 +625,12 @@ export class XanaGame extends GameBase {
}

public getPlayerScore(player: playerid): number {
let nPrisoners = this.prisoners[player - 1];
if (player === 1) {
nPrisoners += 0.5; // P2 decides the pie, so P1 receives an additional 0.5 points
}
const nPrisoners = this.prisoners[player - 1];
const komi = player === 1 ? 0.5 : 0.0;

return this.getTerritories()
.filter(t => t.owner === player)
.reduce((prev, curr) => prev + curr.cells.length, nPrisoners);
.reduce((prev, curr) => prev + curr.cells.length, nPrisoners+komi);
}

// What pieces are adjacent to a given area?
Expand Down Expand Up @@ -669,6 +708,7 @@ export class XanaGame extends GameBase {
scores: [...this.scores],
prisoners: [...this.prisoners],
reserve: [...this.reserve],
swapped: this.swapped
};
}

Expand Down Expand Up @@ -708,7 +748,7 @@ export class XanaGame extends GameBase {
legend: {
A: { name: "piece", colour: this.getPlayerColour(1) },
B: { name: "piece", colour: this.getPlayerColour(2) },
C: { name: "piece", colour: wallColour }, // color 1 is red
C: { name: "piece", colour: wallColour },
},
pieces: pieces.map(r => r.join(",")).join("\n"),
};
Expand Down Expand Up @@ -744,7 +784,7 @@ export class XanaGame extends GameBase {
}

// add territorial dots for area controlled by players
if (this.stack.length > 2) {
if (this.stack.length > 3) {
const territories = this.getTerritories();
const markers: Array<MarkerDots> = []
for (const t of territories) {
Expand All @@ -764,10 +804,15 @@ export class XanaGame extends GameBase {
}

public getButtons(): ICustomButton[] {
if ( this.isPieTurn() ) {
return [{ label: "swap", move: "swap" }];
}
return [{ label: "pass", move: "pass" }];
}

public getPlayerColour(p: playerid): Colourfuncs {
p = (p == 1 && !this.swapped) || (p == 2 && this.swapped) ? 1 : 2;

if (p === 1) {
return {
func: "custom",
Expand Down
Loading