diff --git a/src/games/plurality.ts b/src/games/plurality.ts index 045360cb..ac4aabb4 100644 --- a/src/games/plurality.ts +++ b/src/games/plurality.ts @@ -1,4 +1,4 @@ -import { GameBase, IAPGameState, IClickResult, IIndividualState, IValidationResult } from "./_base"; +import { GameBase, IAPGameState, IClickResult, IIndividualState, IValidationResult, IScores } from "./_base"; import { APGamesInformation } from "../schemas/gameinfo"; import { APRenderRep, BoardBasic, MarkerDots, RowCol } from "@abstractplay/renderer/src/schemas/schema"; import { APMoveResult } from "../schemas/moveresults"; @@ -42,11 +42,13 @@ export class PluralityGame extends GameBase { { type: "designer", name: "João Pedro Neto", + urls: ["https://boardgamegeek.com/boardgamedesigner/3829/joao-pedro-neto"], + apid: "9228bccd-a1bd-452b-b94f-d05380e6638f", }, { type: "coder", name: "João Pedro Neto", - urls: [], + urls: ["https://boardgamegeek.com/boardgamedesigner/3829/joao-pedro-neto"], apid: "9228bccd-a1bd-452b-b94f-d05380e6638f", }, ], @@ -56,7 +58,7 @@ export class PluralityGame extends GameBase { { uid: "size-15", group: "board" }, { uid: "size-19", group: "board" }, ], - flags: ["scores", "experimental"] + flags: ["no-moves", "scores", "experimental"] }; public coords2algebraic(x: number, y: number): string { @@ -140,10 +142,13 @@ export class PluralityGame extends GameBase { return 13; } + /** + * get the orthogonal adjacent cells of cell (x,y) + */ private neighbors(x: number, y: number): number[][] { const result = []; for (const [dx,dy] of [[1,0],[-1,0],[0,1],[0,-1]]) { - if (x+dx >= 0 && x+dx < this.boardSize && + if (x+dx >= 0 && x+dx < this.boardSize && y+dy >= 0 && y+dy < this.boardSize) { const cell = this.coords2algebraic(x+dx, y+dy); if (! this.board.has(cell)) { @@ -154,6 +159,9 @@ export class PluralityGame extends GameBase { return result; } + /** + * does cell (x,y) creates a 2x2 area with any other three pieces already on board? + */ private isTaboo(x: number, y: number): boolean { for (const [x1,y1] of [[x+1,y+1],[x-1,y+1],[x+1,y-1],[x-1,y-1]]) { // (x1,y1) is an adjacent diagonal of cell (x,y) @@ -169,13 +177,13 @@ export class PluralityGame extends GameBase { } return false; } + /** - * This should generate a full list of valid moves from the current game state. If it is not reasonable for your game to generate such a list, you can remove this function and add the `no-moves` flag to the game's metadata. If you *can* efficiently generate a move list, though, I highly recommend it. It's helpful to players, and it makes your life easier later. + * Generates a full list of valid moves from the current game state. */ public moves(): string[] { if (this.gameover) { return []; } const moves: string[] = []; - let taboo; // can place on any empty space for (let y = 0; y < this.boardSize; y++) { @@ -188,7 +196,7 @@ export class PluralityGame extends GameBase { if (this.board.has(cell2)) continue; // check for 2nd stone taboo this.board.set(cell1, this.currplayer); // temporary add cell1 to check taboo - taboo = this.isTaboo(x2,y2); + let taboo = this.isTaboo(x2,y2); this.board.delete(cell1); // remove it! if (taboo) { continue; } // ------------------- end check @@ -204,35 +212,24 @@ export class PluralityGame extends GameBase { this.board.delete(cell2); // remove it! if (taboo) { continue; } // ------------------- end check - - // ok, no 2x2 was found, so add the two possible options - moves.push(cell2 + ',' + cell1 + ',' + cell3); // cell3 is enemy stone - moves.push(cell1 + ',' + cell2 + ',' + cell3); // cell3 is enemy stone - moves.push(cell1 + ',' + cell3 + ',' + cell2); // cell2 is enemy stone + // ok, no 2x2 was found, add all possible options + moves.push(cell1 + ',' + cell2 + ',' + cell3); // cell3 is the opponent piece + moves.push(cell2 + ',' + cell1 + ',' + cell3); // friendly pieces are interchangeable + moves.push(cell1 + ',' + cell3 + ',' + cell2); // cell2 is the opponent piece } } } } - moves.push("pass"); + moves.push("pass"); // passing is always possible return moves.sort((a,b) => a.localeCompare(b)) } - /** - * This is a helper function only needed for local testing, and only useful if you have a `moves()` function. - */ public randomMove(): string { const moves = this.moves(); return moves[Math.floor(Math.random() * moves.length)]; } - /** - * This takes information about the move in progress and the click the user just made and needs to return an updated move string and some description of how valid and complete the move is. - * - `valid` must be either true or false. As long as the move is even partially valid, it should return true. False tells the front end that it's wholly and unsalvageably invalid. - * - `complete` has three states: -1, 0, and 1. -1 means the move is for absolutely sure NOT complete. More input is needed. 0 means the move *could* be complete and submitted now, but further moves are possible. And 1 means the move is absolutely complete and no further input should be expected. - * - `canrender` is for games where the moves consist of multiple steps and need to be rendered as you go. If `canrender` is true, then even if `complete` is -1, it will be send to the renderer for updating. - * - `message` is a translatable string explaining what the user should do next. - */ public handleClick(move: string, row: number, col: number, piece?: string): IClickResult { try { let newmove = ""; @@ -245,7 +242,7 @@ export class PluralityGame extends GameBase { if (idx === -1) { newmove = move + "," + cell; // if not, just add move } else { - cells.splice(idx); // otherwise, remove/unplace it + cells.splice(idx); // otherwise, remove/unplace it newmove = cells.join(","); } } @@ -265,9 +262,6 @@ export class PluralityGame extends GameBase { } } - /** - * This goes hand in hand with `handleClick()` and can be leveraged in other areas of the code as well. It accepts a move string and then returns a description of the move's condition. See description of `handleClick()` for details. - */ public validateMove(m: string): IValidationResult { const result: IValidationResult = {valid: false, message: i18next.t("apgames:validation._general.DEFAULT_HANDLER")}; @@ -308,7 +302,8 @@ export class PluralityGame extends GameBase { // get all valid complete moves (so each move will be like "a1,b1,c1") const allMoves = this.moves(); - // does any of these moves make a taboo? A taboo will not be a prefix of any legal move + + // does any of these moves makes a taboo? A taboo will not be a prefix of any legal move if (! allMoves.some(legalMove => legalMove.startsWith(m))) { result.valid = false; result.message = i18next.t("apgames:validation.plurality.TABOO", { cell: currentMove }); @@ -361,18 +356,17 @@ export class PluralityGame extends GameBase { * Get all moves() in format [ "a1,a2,a3", "a1,a2,b2", "a1,a3,a2", "a1,b1,b2"...] * and returns a set with just the unique coordinates */ - public getUniqueIds(data: string[]): Set { - // flatMap flattens the resulting arrays into one single array - const allIds = data.flatMap(item => item.split(',')); - return new Set(allIds); // remove duplicates + public getUniqueCells(allMoves: string[]): Set { + const allCells = allMoves.flatMap(move => move.split(',')); + return new Set(allCells); // remove duplicates } /* * An area is owned if it is not possible to play inside it. * The set of possible moves are given by validMoves parameter */ - public isAreaOwned(myArea: Array, validMoves: Set): boolean { - return myArea.every(id => !validMoves.has(id)); + public isAreaOwned(area: Array, validMoves: Set): boolean { + return area.every(cell => !validMoves.has(cell)); } /** @@ -398,7 +392,7 @@ export class PluralityGame extends GameBase { * This is used in (1) computing scores, and (2) in the render process */ public getTerritories(): Territory[] { - const allValidMoves : Set = this.getUniqueIds([...this.moves()]); + const allValidMoves : Set = this.getUniqueCells([...this.moves()]); const p1Pieces = [...this.board.entries()].filter(([,owner]) => owner === 1).map(pair => pair[0]); const p2Pieces = [...this.board.entries()].filter(([,owner]) => owner === 2).map(pair => pair[0]); const allPieces = [...p1Pieces, ...p2Pieces]; @@ -448,7 +442,7 @@ export class PluralityGame extends GameBase { if (! result.valid) { throw new UserFacingError("VALIDATION_GENERAL", result.message) } - if (!partial && ! valid_moves.includes(m)) { + if (! partial && ! valid_moves.includes(m)) { throw new UserFacingError("VALIDATION_FAILSAFE", i18next.t("apgames:validation._general.FAILSAFE", {move: m})) } } @@ -501,12 +495,12 @@ export class PluralityGame extends GameBase { this.gameover = this.lastmove === "pass" && this.stack[this.stack.length - 1].lastmove === "pass"; if (this.gameover) { - const p1Score = this.scores[0]; - const p2Score = this.scores[1]; - this.winner = p1Score > p2Score ? [1] : [2]; // draws are not possible - } - - if (this.gameover) { + const terr = this.getTerritories(); + this.scores = [ + terr.filter(t => t.owner === 1).reduce((prev, curr) => prev + curr.cells.length, 0.0), + terr.filter(t => t.owner === 2).reduce((prev, curr) => prev + curr.cells.length, 0.5), + ]; + this.winner = this.scores[0] > this.scores[1] ? [1] : [2]; // draws are not possible this.results.push( {type: "eog"}, {type: "winners", players: [...this.winner]} @@ -515,9 +509,6 @@ export class PluralityGame extends GameBase { return this; } - /** - * Anything up in your IPluralityState definition needs to be here. - */ public state(): IPluralityState { return { game: PluralityGame.gameinfo.uid, @@ -529,10 +520,6 @@ export class PluralityGame extends GameBase { }; } - /** - * And same here for IMoveState. The base object uses these to save things. - * If you're new to TypeScript, you will want to familiarize yourself with the difference between reference types and value types. There's a reason you can't just say `board: this.board` in the below. You need to actually create a fresh map that duplicates `this.board`. - */ public moveState(): IMoveState { return { _version: PluralityGame.gameinfo.version, @@ -545,12 +532,6 @@ export class PluralityGame extends GameBase { }; } - /** - * And this is how you turn a game state into something people can see and interact with. - * The system tries to abstract things as much as possible. You don't have to know anything about computer graphics. You just need to be able to get the rendering engine to do what you want. - * To learn that, you will want to visit and learn how the renderer works. Basically you need to choose a board, load your pieces, populate the board, and then annotate any recent moves. - * You will see a fair bit of `// @ts-ignore`. This is not good practice generally, but I have found them necessary here. The type system is very strict, and sometimes that gets in the way. As long as your render actually works in the playground, you're OK, regardless of what type errors are thrown here. - */ public render(): APRenderRep { // Build piece string let pstr = ""; @@ -593,17 +574,14 @@ export class PluralityGame extends GameBase { // add territory dots const territories = this.getTerritories(); - let markers: Array | undefined = [] + let markers: Array = [] for (const t of territories) { if (t.owner !== undefined) { const points = t.cells.map(c => this.algebraic2coords(c)); markers.push({type: "dots", colour: t.owner, points: points.map(p => { return {col: p[0], row: p[1]}; }) as [RowCol, ...RowCol[]]}); } } - if (markers.length === 0) { - markers = undefined; - } - if (markers !== undefined) { + if (markers.length > 0) { (rep.board as BoardBasic).markers = markers; } @@ -611,11 +589,7 @@ export class PluralityGame extends GameBase { if (this.stack[this.stack.length - 1]._results.length > 0) { rep.annotations = []; for (const move of this.stack[this.stack.length - 1]._results) { - if (move.type === "move") { - const [fromX, fromY] = this.algebraic2coords(move.from); - const [toX, toY] = this.algebraic2coords(move.to); - rep.annotations.push({type: "move", targets: [{row: fromY, col: fromX}, {row: toY, col: toX}]}); - } else if (move.type === "place") { + if (move.type === "place") { const [x, y] = this.algebraic2coords(move.where!); rep.annotations.push({type: "enter", targets: [{row: y, col: x}]}); } @@ -625,6 +599,17 @@ export class PluralityGame extends GameBase { return rep; } + public getPlayersScores(): IScores[] { + return [ { name: i18next.t("apgames:status.SCORES"), + scores: [this.getPlayerScore(1), this.getPlayerScore(2)] } ]; + } + + public getPlayerScore(player: number): number { + const start = player == 2 ? 0.5 : 0.0; + const terr = this.getTerritories(); + return terr.filter(t => t.owner === player).reduce((prev, curr) => prev + curr.cells.length, start); + } + /** * This function is only for the local playground. */