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
60 changes: 60 additions & 0 deletions locales/en/apgames.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
"sploof": "Create a string of four touching balls at any level visible as a straight line from directly above the board. Players start with two balls in their hand, but can take two balls from their stash every time a neutral ball is removed. Players also lose when they have no moves on their turn.",
"sponnect": "A simple Shibumi connection game where every internal space that can be buried in a fully-filled board is pre-filled with neutral pieces. You may pass on your turn as long as your opponent didn't pass immediately before.",
"spook": "Guide Spooky the ghost around the pyramid and try to be the first player to have all balls of your colour removed. The game takes place in two phases: the setup phase where the pyramid is built, and the getaway phase, where Spooky starts at the top of the pyramid and starts eliminating balls.",
"spora" : "A territorial game with limited pieces to build stacks that capture by enclosure and/or by sowing.",
"spree": "An impartial n-in-a-row game where players take turns handing a ball to their opponent to place. The goal is to try to make a full line of one of two colours (red and blue by default). There is a third wild neutral colour (green by default) that can play the role of either colour.",
"squaredance": "A game where you move groups of pieces by rotation, trying to force your opponent into a position where they cannot move.",
"stairs": "Stack higher than your opponent in this game of one-space, one-level jumps where you also have to move your lowest pieces first. To take the lead and win, you must surpass your opponent's tallest stack height or, failing that, their number of stacks at the tallest height.",
Expand Down Expand Up @@ -232,6 +233,7 @@
"witch": "Four types of pieces are arrayed randomly on a grid. Each player takes turns removing pieces until one reaches a score of 50.",
"wunchunk": "Unify your pieces as much as you can by the end of the game, while keeping your pieces separate in the early and mid-game so as to increase their power. Fewest groups at the end wins.",
"wyke": "A two-player game where one plays the builder and the other the destroyer. Claim cells by either completing or completely destroying lots. Win a certain number of lots or lots in a particular pattern to win.",
"xana": "A stack game where players have a limited amount of pieces, and an unlimited amount of walls, to conquer territory.",
"yavalath": "Form a line of four or more pieces of your colour *without* forming a line of three pieces first.",
"yonmoque": "Try to form four in a row in a game where not all spaces are equal and opposing pieces can be converted.",
"zola": "A game where your movement is constrained by your distance from the centre of the board. Capturing moves must not increase that distance. Non-capturing moves must increase that distance. First person to capture all opposing pieces wins."
Expand Down Expand Up @@ -2433,6 +2435,23 @@
"name": "5x5 board"
}
},
"spora": {
"size-9": {
"name": "9x9 board (48 pieces)"
},
"#board": {
"name": "13x13 board (99 pieces)"
},
"size-15": {
"name": "15x15 board (132 pieces)"
},
"size-19": {
"name": "19x19 board (211 pieces)"
},
"size-25": {
"name": "25x25 board (365 pieces)"
}
},
"spree": {
"#board": {
"name": "4x4 board"
Expand Down Expand Up @@ -2972,6 +2991,11 @@
"name": "6x6 board"
}
},
"xana": {
"#board": {
"name": "Hexhex 8 (169 spaces)"
}
},
"zola": {
"#board": {
"name": "6x6 board"
Expand Down Expand Up @@ -3566,6 +3590,9 @@
"GOALS": "Goals",
"HEIGHT": "heights"
},
"xana" : {
"RESERVE": "Reserve:"
},
"YEAR": "Year"
},
"validation": {
Expand Down Expand Up @@ -5670,6 +5697,25 @@
"WRONG_PREFIX_PLACE": "Placements should not include a prefix.",
"WRONG_PREFIX_REMOVE": "Removals should be prefixed with 'r'."
},
"spora": {
"INITIAL_SETUP": "Choose a number of points to add to the second player's score, and the next player will choose sides.",
"INSTRUCTIONS": "Place between one to four pieces either an empty intersection or on a friendly stack (self-captures are illegal). Then, optionally, select a different friendly stack, click on it as many times as pieces to move, then click on a orthogonal path that starts adjacent to this sowing stack (one piece per intersection). The path must follow Spora's rules.",
"END_PHASE_INSTRUCTIONS": "The adversary ended his reserve. You need to place the remaining {{remaining}} pieces from your reserve.",
"END_PHASE_LAST" : "This is your last stone to place! The game will end next.",
"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.",
"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.",
"SAME_PLACE_SOW_STACK": "The sowing stack cannot be the stack just created/enlarged.",
"SELF_CAPTURE": "You may not place your stones in self-capture.",
"SOW_FRIENDLY": "Only friendly stacks can be sowed.",
"SOW_INSTRUCTIONS": "Either select more pieces to sow, or start sowing with the ones already selected. When sowing, the next intersection must be adjacent to the previous one. Left and right turns are valid. Cannot sow over (and capture) opponent stacks with more than half the size of the number of pieces that still need to be sowed.",
"SOW_SIZE_SELECTION": "Either place more pieces (up to four), select a different friendly stack to sow (if any), or just finish your move.",
"SOW_TOO_LARGE": "The current sowing is larger than the available pieces at the sowing stack."
},
"spree": {
"ALREADY_WON": "You have already won the game, so you don't have to pick a colour to hand to your opponent.",
"CANNOT_PLACE": "{{where}} is not a valid place to put a ball.",
Expand Down Expand Up @@ -6120,6 +6166,20 @@
"TOO_MANY_CELLS": "You have selected more cells than the move type allows.",
"TOO_MANY_PIECES": "There are too many pieces at {{where}} to add {{num}} more."
},
"xana": {
"INITIAL_INSTRUCTIONS": "Drop pieces from reserve on empty cells or friendly stacks. In alternative, 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.",
"MOVE_NOT_INSIDE_CIRCLE": "Stack cannot be moved outside its circle, ie, its moving range.",
"MOVE_TO_OCCUPIED_CELL": "Pieces from a stack must be moved to an empty cell.",
"NOT_ENOUGH_PIECES_TO_MOVE": "The stacks does not have that many pieces to be moved.",
"NOT_PLACED_ON_FRIEND": "Pieces cannot be moved from adversary stacks.",
"OCCUPIED_WALL": "Walls must be placed on empty cells (does not include cells from pieces just captured).",
"RESERVE_EMPTY": "Reserve empty, no more placements possible.",
"SAME_WALL" : "Cannot place the same wall twice.",
"UNACCESSIBLE_PIECE": "Cannot place/move piece on unaccessible cells.",
"UNACCESSIBLE_WALL": "Cannot place walls on unaccessible cells."
},
"yavalath": {
"BAD_PASS": "You may only pass if you have been eliminated.",
"INITIAL_INSTRUCTIONS": "Click an empty cell to place a piece.",
Expand Down
13 changes: 11 additions & 2 deletions src/games/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ import { StilettoGame, IStilettoState } from "./stiletto";
import { BTTGame, IBTTState } from "./btt";
import { MinefieldGame, IMinefieldState } from "./minefield";
import { SentinelGame, ISentinelState } from "./sentinel";
import { XanaGame, IXanaState } from "./xana";
import { SporaGame, ISporaState } from "./spora";

export {
APGamesInformation, GameBase, GameBaseSimultaneous, IAPGameState,
Expand Down Expand Up @@ -471,6 +473,8 @@ export {
BTTGame, IBTTState,
MinefieldGame, IMinefieldState,
SentinelGame, ISentinelState,
XanaGame, IXanaState,
SporaGame, ISporaState,
};

const games = new Map<string, typeof AmazonsGame | typeof BlamGame | typeof CannonGame |
Expand Down Expand Up @@ -552,7 +556,8 @@ const games = new Map<string, typeof AmazonsGame | typeof BlamGame | typeof Cann
typeof BambooGame | typeof PluralityGame | typeof CrosshairsGame |
typeof MagnateGame | typeof ProductGame | typeof OonpiaGame |
typeof GoGame | typeof StilettoGame | typeof BTTGame |
typeof MinefieldGame | typeof SentinelGame
typeof MinefieldGame | typeof SentinelGame | typeof XanaGame |
typeof SporaGame
>();
// Manually add each game to the following array
[
Expand Down Expand Up @@ -588,7 +593,7 @@ const games = new Map<string, typeof AmazonsGame | typeof BlamGame | typeof Cann
SiegeOfJGame, StairsGame, EmuGame, DeckfishGame, BluestoneGame, SunspotGame, StawvsGame,
LascaGame, EmergoGame, FroggerGame, ArimaaGame, RampartGame, KrypteGame, EnsoGame, RincalaGame,
WaldMeisterGame, WunchunkGame, BambooGame, PluralityGame, CrosshairsGame, MagnateGame,
ProductGame, OonpiaGame, GoGame, StilettoGame, BTTGame, MinefieldGame, SentinelGame
ProductGame, OonpiaGame, GoGame, StilettoGame, BTTGame, MinefieldGame, SentinelGame, XanaGame, SporaGame,
].forEach((g) => {
if (games.has(g.gameinfo.uid)) {
throw new Error("Another game with the UID '" + g.gameinfo.uid + "' has already been used. Duplicates are not allowed.");
Expand Down Expand Up @@ -1068,6 +1073,10 @@ export const GameFactory = (game: string, ...args: any[]): GameBase|GameBaseSimu
return new MinefieldGame(...args);
case "sentinel":
return new SentinelGame(...args);
case "xana":
return new XanaGame(...args);
case "spora":
return new SporaGame(...args);
}
return;
}
95 changes: 74 additions & 21 deletions src/games/sentinel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { GameBase, IAPGameState, IClickResult, IIndividualState, IValidationResult } from "./_base";
import { APGamesInformation } from "../schemas/gameinfo";
import { APRenderRep } from "@abstractplay/renderer/src/schemas/schema";
import { APRenderRep, MarkerGlyph } from "@abstractplay/renderer/src/schemas/schema";
import { APMoveResult } from "../schemas/moveresults";
import { RectGrid, reviver, UserFacingError, Direction, allDirections } from "../common";
import i18next from "i18next";
Expand Down Expand Up @@ -61,8 +61,8 @@ export class SentinelGame extends GameBase {
},
],
categories: ["goal>annihilate", "goal>vigil", "mechanic>capture", "mechanic>move",
"mechanic>stack", "board>shape>rect", "components>simple>1per"],
flags: ["perspective", "experimental"]
"mechanic>stack", "board>shape>rect", "components>simple>2c"],
flags: ["experimental"]
};

public static coords2algebraic(x: number, y: number): string {
Expand Down Expand Up @@ -176,9 +176,11 @@ export class SentinelGame extends GameBase {
const ray = grid.ray(x, y, dir).map(n => SentinelGame.coords2algebraic(...n));
if (ray.length === 0) { // the stone is at the edge and can move out of the board
moves.push(`${cell}-off`);
}
if (ray.length > 0) { // there is, at least, an adjacent square in this direction
if (ray[0] === CENTER) { // cannot move into the center
} else { // there is, at least, an adjacent square in this direction
if (ray[0] === CENTER) { // cannot move into the center
continue;
}
if (! this.isSowable(ray[0], player) ) { // cannot make an unsowable stack
continue;
}
const adj = this.board.get(ray[0]); // get piece (if any) at adjacent cell
Expand All @@ -196,7 +198,7 @@ export class SentinelGame extends GameBase {
for (const dir of allDirections) {
const ray = grid.ray(x, y, dir).map(n => SentinelGame.coords2algebraic(...n));
// cannot sow over the center, and the entire stack must be sown inside the board
if (ray.includes(CENTER) || ray.length <= height) {
if ( ray.length <= height || ray.slice(0, height+1).includes(CENTER) ) {
continue;
}
// check if any intermediate stack remains sow-able (sowing a stack includes a new stone)
Expand Down Expand Up @@ -356,6 +358,8 @@ export class SentinelGame extends GameBase {
}
}

this.results = [];

if ( partial && !m.includes("-") ) { // if partial, set the points to be shown
this._points = this.findPoints(m).map(c => SentinelGame.algebraic2coords(c));
return this;
Expand All @@ -379,9 +383,11 @@ export class SentinelGame extends GameBase {
this.board.delete(cell);
this.board.set(cell, [this.currplayer, 1]);
}
this.results.push({ type: "move", from: start, to: end, count: this.path(start, end).length});
}
}
this.board.delete(start); // the original piece is always removed
this.results.push({ type: "capture", where: start, count: 1 });

// update currplayer
this.lastmove = m;
Expand All @@ -397,30 +403,43 @@ export class SentinelGame extends GameBase {
}

// return the number of line-of-sight to the center wrt player's pieces (needed for EOG)
private linesSeen(player : playerid): number {
let numLines = 0;
private linesSeen(player : playerid): string[] {
const whichCells = [];
for (const ray of RAYS) {
for (const cell of ray) {
// if the first non-empty cell has a friendly stone, the player 'sees' the center
if ( this.board.has(cell) ) {
if ( this.board.get(cell)![0] === player ) {
numLines += 1;
whichCells.push(cell);
}
break;
}
}
}
return numLines;
return whichCells;
}

protected checkEOG(): SentinelGame {
const prevPlayer = this.currplayer % 2 + 1 as playerid;

if ( this.linesSeen(prevPlayer) === 0 || this.linesSeen(this.currplayer) >= 5 ) {
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]);

if ( this.linesSeen(prevPlayer).length === 0 || this.linesSeen(this.currplayer).length >= 5 ) {
this.gameover = true;
this.winner = [this.currplayer];
}

if (p1Pieces.length === 0) {
this.gameover = true;
this.winner = [2];
}

if (p2Pieces.length === 0) {
this.gameover = true;
this.winner = [1];
}

if (this.gameover) {
this.results.push(
{type: "eog"},
Expand Down Expand Up @@ -479,7 +498,10 @@ export class SentinelGame extends GameBase {
}
pstr += pieces.join(",");
}
pstr = pstr.replace(/-{9}/g, "_");

const markers: Array<MarkerGlyph> = [
{ type: "glyph", glyph: "Center", points: [ {row: 4, col: 4} ] }
];

// Build rep
const rep: APRenderRep = {
Expand All @@ -488,13 +510,7 @@ export class SentinelGame extends GameBase {
style: "squares-checkered",
width: BOARD_SIZE,
height: BOARD_SIZE,
markers: [
{
type: "glyph",
glyph: "Center",
points: [ {row: 4, col: 4} ]
},
]
markers
},
legend: {
A: {
Expand All @@ -516,7 +532,44 @@ export class SentinelGame extends GameBase {
};

rep.annotations = [];
if (this._points.length > 0) { // show the dots where the selected piece can move to

// show the lines of sight
const [toX, toY] = SentinelGame.algebraic2coords(CENTER);
const prevplayer = this.currplayer % 2 + 1 as playerid;
for (const cell of this.linesSeen(this.currplayer)) {
const [fromX, fromY] = SentinelGame.algebraic2coords(cell);
rep.annotations.push({ type: "move", targets: [{row: fromY, col: fromX}, {row: toY, col: toX}],
style: "dashed", opacity: 0.3, arrow: false, strokeWidth: 0.05,
colour: this.currplayer });
}
for (const cell of this.linesSeen(prevplayer)) {
const [fromX, fromY] = SentinelGame.algebraic2coords(cell);
rep.annotations.push({ type: "move", targets: [{row: fromY, col: fromX}, {row: toY, col: toX}],
style: "dashed", opacity: 0.3, arrow: false, strokeWidth: 0.05,
colour: prevplayer });
}

// show the current move
if ( this.results.length > 0 ) {
for (const move of this.results) {
if (move.type === "place") {
const [x, y] = SentinelGame.algebraic2coords(move.where!);
rep.annotations.push({ type: "enter", targets: [{ row: y, col: x }] });
} else if (move.type === "move") {
const [fromX, fromY] = SentinelGame.algebraic2coords(move.from);
const [toX, toY] = SentinelGame.algebraic2coords(move.to);
rep.annotations.push({type: "move", targets: [{row: fromY, col: fromX}, {row: toY, col: toX}]});
} else if (move.type === "capture") {
for (const cell of move.where!.split(",")) {
const [x, y] = SentinelGame.algebraic2coords(cell);
rep.annotations.push({type: "exit", targets: [{row: y, col: x}]});
}
}
}
}

// show the dots where the selected piece can move to
if (this._points.length > 0) {
const points = [];
for (const [x,y] of this._points) {
points.push({row: y, col: x});
Expand Down
Loading
Loading