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
8 changes: 8 additions & 0 deletions locales/en/apgames.json
Original file line number Diff line number Diff line change
Expand Up @@ -4984,6 +4984,10 @@
"OFFENSE": "Offensive player",
"COUNTDOWN": "Plies remaining"
},
"mvolcano": {
"PYRAMIDSCAPTURED": "Pyramids captured",
"UNCAPTUREDCOLOURS": "Colours not yet captured"
},
"pacru": {
"TARGET": "Tiles to win",
"TILES": "Tiles on board"
Expand All @@ -5005,6 +5009,10 @@
"RESERVE": "Reserve",
"SCORES": "Scores",
"STACKSIZES": "Stack heights",
"tintas": {
"MAJORITIES": "Majorities won",
"MONOCHROME": "Monochrome potential"
},
"tumbleweed": "Score (pieces + influence)",
"ESTIMATEDSCORES": "Estimated scores",
"killallgo": {
Expand Down
12 changes: 11 additions & 1 deletion playground/playground.js
Original file line number Diff line number Diff line change
Expand Up @@ -1146,6 +1146,9 @@ function isPerPlayerStatusLabel(status) {
}

function formatSidebarScoreValue(value, playerNames, glyphRenderOptions) {
if (Array.isArray(value)) {
return value.map(v => formatSidebarStatusValue(v, playerNames, glyphRenderOptions)).join(" ");
}
if (APGames?.isStructuredRenderLabel?.(value)) {
return APGames.resolveRenderLabel(value, playerNames, playgroundTranslate);
}
Expand Down Expand Up @@ -1406,7 +1409,14 @@ function _renderScoresSection(game, gamename, playerNames, gameFlags, glyphRende
playerDiv.appendChild(swatch);

const scoreTextSpan = document.createElement('span');
scoreTextSpan.textContent = `${playerName}: ${formatSidebarScoreValue(playerScore, playerNames, glyphRenderOptions)}`;
const scoreText = formatSidebarScoreValue(playerScore, playerNames, glyphRenderOptions);
if (Array.isArray(playerScore)) {
// A row of status values may hold glyph markup
scoreTextSpan.textContent = `${playerName}: `;
scoreTextSpan.insertAdjacentHTML('beforeend', scoreText);
} else {
scoreTextSpan.textContent = `${playerName}: ${scoreText}`;
}
playerDiv.appendChild(scoreTextSpan);
scoresBlockHTML += playerDiv.outerHTML;
});
Expand Down
9 changes: 8 additions & 1 deletion src/games/_base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,20 @@ export interface IStashEntry {

/**
* Represents a set of scores for the players.
* A player's entry may also be a list of status values (text or glyphs, as in `IStatus`),
* which the front shows in a row, such as a row of coloured pieces.
*
* @export
* @interface IScores
*/
export interface IScores {
// `name`, the section's title, is text only. To allow glyphs in `name` (say, a token icon
// above plain counts), widen its type to `RenderLabel | StatusValue[]` as `scores` is, and
// have the front resolve it in `resolveSidebarScores` and draw it in `GameStatus` (game
// and Lab) as it does a list entry. The front must ship that before any game sends one,
// or the panel breaks.
name: RenderLabel;
scores: (number | string | RenderLabel)[];
scores: (number | string | RenderLabel | StatusValue[])[];
spoiler?: boolean;
}

Expand Down
14 changes: 12 additions & 2 deletions src/games/mvolcano.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GameBase, IAPGameState, IClickResult, IIndividualState, IRenderOpts, IScores, IValidationResult, type ChatLogCollectContext, type ChatLogEntry, type ChatLogLine, type RenderLabel } from "./_base.js";
import { GameBase, IAPGameState, IClickResult, IIndividualState, IRenderOpts, IScores, IValidationResult, type ChatLogCollectContext, type ChatLogEntry, type ChatLogLine, type RenderLabel, type StatusValue } from "./_base.js";
import type { APGamesInformation } from "../schemas/gameinfo.js";
import { APRenderRep, AreaStackingExpanded, Glyph } from "@abstractplay/renderer/build/schemas/schema";
import type { APMoveResult } from "../schemas/moveresults.js";
Expand Down Expand Up @@ -1008,7 +1008,17 @@ export class MvolcanoGame extends GameBase {
}

public sidebarScores(): IScores[] {
return [{ name: this.neutralAreaLabel("apgames:status.SCORES"), scores: [this.getPlayerScore(1), this.getPlayerScore(2)] }]
// The non-white colours each player has yet to capture (capturing all seven ends
// the game), drawn in their customisable colours in palette order.
const missing = ([1, 2] as playerid[]).map(player => {
const capped = new Set<string>(this.captured[player - 1].map(p => p[0]));
return allColours.filter(c => !capped.has(c)).map(c => ({ glyph: "piece", colour: allColours.indexOf(c) + 1 }) as StatusValue);
});
return [
{ name: this.neutralAreaLabel("apgames:status.SCORES"), scores: [this.getPlayerScore(1), this.getPlayerScore(2)] },
{ name: this.neutralAreaLabel("apgames:status.mvolcano.UNCAPTUREDCOLOURS"), scores: missing, spoiler: true },
{ name: this.neutralAreaLabel("apgames:status.mvolcano.PYRAMIDSCAPTURED"), scores: [this.captured[0].length, this.captured[1].length], spoiler: true },
]
}

protected recordExportExclude(): string[] {
Expand Down
16 changes: 15 additions & 1 deletion src/games/tintas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Direction, Grid, rectangle, defineHex, Orientation } from "honeycomb-grid";
import { GameBase, IAPGameState, IClickResult, IIndividualState, IValidationResult } from "./_base.js";
import { GameBase, IAPGameState, IClickResult, IIndividualState, IScores, IValidationResult, type StatusValue } from "./_base.js";
import type { APGamesInformation } from "../schemas/gameinfo.js";
import { APRenderRep, BoardBasic, RowCol } from "@abstractplay/renderer/build/schemas/schema";
import type { APMoveResult } from "../schemas/moveresults.js";
Expand Down Expand Up @@ -852,6 +852,20 @@ export class TintasGame extends GameBase {
return rep;
}

public sidebarScores(): IScores[] {
// Colours are listed in palette order and drawn as they are on the board.
const colours = [1, 2, 3, 4, 5, 6, 7] as CellContents[];
const glyphs = (list: CellContents[]) => list.map(c => ({ glyph: "piece", colour: c }) as StatusValue);
// A player can still take all seven of a colour only while their opponent has none of it.
const monochrome = ([1, 2] as playerid[]).map(player => glyphs(colours.filter(c => !this.captured[player % 2].includes(c))));
// Four of a colour's seven discs are a majority of it.
const majorities = ([1, 2] as playerid[]).map(player => glyphs(colours.filter(c => this.captured[player - 1].filter(n => n === c).length >= 4)));
return [
{ name: this.neutralAreaLabel("apgames:status.tintas.MONOCHROME"), scores: monochrome, spoiler: true },
{ name: this.neutralAreaLabel("apgames:status.tintas.MAJORITIES"), scores: majorities, spoiler: true },
];
}

public inCheck(): number[] {
const checked: number[] = [];
for (const p of [1,2] as playerid[]) {
Expand Down
55 changes: 55 additions & 0 deletions test/games/mvolcano.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
import "mocha";
import { expect } from "chai";
import { MvolcanoGame, CellContents } from "../../src/games/mvolcano";
import type { IScores } from "../../src/games/_base";
import { isStructuredRenderLabel } from "../../src/common/render-label";

function findScores(scores: IScores[], key: string): IScores {
const found = scores.find((s) => isStructuredRenderLabel(s.name) && s.name.textKey === key);
expect(found, key).to.not.be.undefined;
return found!;
}

function colours(entry: unknown): number[] {
expect(entry).to.be.an("array");
return (entry as { glyph: string, colour: number }[]).map((v) => {
expect(v.glyph).to.equal("piece");
return v.colour;
});
}

describe("Mega-Volcano", () => {
describe("sidebarScores", () => {
it("lists every colour as uncaptured and no pyramids captured at the start", () => {
const scores = new MvolcanoGame().sidebarScores();
expect(scores).to.have.lengthOf(3);
expect(findScores(scores, "apgames:status.SCORES").spoiler).to.not.equal(true);

const uncaptured = findScores(scores, "apgames:status.mvolcano.UNCAPTUREDCOLOURS");
expect(uncaptured.spoiler).to.be.true;
expect(colours(uncaptured.scores[0])).to.deep.equal([1, 2, 3, 4, 5, 6, 7]);
expect(colours(uncaptured.scores[1])).to.deep.equal([1, 2, 3, 4, 5, 6, 7]);

const captured = findScores(scores, "apgames:status.mvolcano.PYRAMIDSCAPTURED");
expect(captured.spoiler).to.be.true;
expect(captured.scores).to.deep.equal([0, 0]);
});

it("drops captured colours in palette order and counts white pyramids", () => {
const g = new MvolcanoGame();
g.captured = [
[["RD", 1], ["WH", 2], ["GN", 3], ["RD", 2]] as CellContents[],
[["BN", 1]] as CellContents[],
];
const scores = g.sidebarScores();

const uncaptured = findScores(scores, "apgames:status.mvolcano.UNCAPTUREDCOLOURS");
expect(colours(uncaptured.scores[0])).to.deep.equal([2, 4, 5, 6, 7]);
expect(colours(uncaptured.scores[1])).to.deep.equal([1, 2, 3, 4, 5, 6]);

const captured = findScores(scores, "apgames:status.mvolcano.PYRAMIDSCAPTURED");
expect(captured.scores).to.deep.equal([4, 1]);
});
});
});
53 changes: 53 additions & 0 deletions test/games/tintas.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
import "mocha";
import { expect } from "chai";
import { TintasGame } from "../../src/games/tintas";
import type { IScores } from "../../src/games/_base";
import { isStructuredRenderLabel } from "../../src/common/render-label";

function findScores(scores: IScores[], key: string): IScores {
const found = scores.find((s) => isStructuredRenderLabel(s.name) && s.name.textKey === key);
expect(found, key).to.not.be.undefined;
return found!;
}

function colours(entry: unknown): number[] {
expect(entry).to.be.an("array");
return (entry as { glyph: string, colour: number }[]).map((v) => {
expect(v.glyph).to.equal("piece");
return v.colour;
});
}

describe("Tintas", () => {
describe("sidebarScores", () => {
it("lists every colour as monochrome potential and no majorities at the start", () => {
const scores = new TintasGame().sidebarScores();
expect(scores).to.have.lengthOf(2);

const monochrome = findScores(scores, "apgames:status.tintas.MONOCHROME");
expect(monochrome.spoiler).to.be.true;
expect(colours(monochrome.scores[0])).to.deep.equal([1, 2, 3, 4, 5, 6, 7]);
expect(colours(monochrome.scores[1])).to.deep.equal([1, 2, 3, 4, 5, 6, 7]);

const majorities = findScores(scores, "apgames:status.tintas.MAJORITIES");
expect(majorities.spoiler).to.be.true;
expect(colours(majorities.scores[0])).to.deep.equal([]);
expect(colours(majorities.scores[1])).to.deep.equal([]);
});

it("keeps a colour as monochrome potential only while the opponent has none of it", () => {
const g = new TintasGame();
g.captured = [[1, 1, 1, 1, 2, 3], [5, 2, 5, 5, 5]];
const scores = g.sidebarScores();

const monochrome = findScores(scores, "apgames:status.tintas.MONOCHROME");
expect(colours(monochrome.scores[0])).to.deep.equal([1, 3, 4, 6, 7]);
expect(colours(monochrome.scores[1])).to.deep.equal([4, 5, 6, 7]);

const majorities = findScores(scores, "apgames:status.tintas.MAJORITIES");
expect(colours(majorities.scores[0])).to.deep.equal([1]);
expect(colours(majorities.scores[1])).to.deep.equal([5]);
});
});
});
Loading