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
16 changes: 10 additions & 6 deletions src/games/selfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ const after = (board: Board, from: string, to: string): Board => {
return next;
};

/** Whether `player` could connect all their pieces with a single move (if it were their turn). */
const canWinInOne = (graph: HexTriGraph, board: Board, player: playerid): boolean =>
piecesOf(board, player).some(from =>
graph.neighbours(from).some(to => !board.has(to) && isConnected(graph, after(board, from, to), player))
);

/** Whether the move would enclose an opposing piece without winning the game outright. */
const wouldEnclose = (graph: HexTriGraph, board: Board, from: string, to: string): boolean => {
const player = board.get(from)!;
Expand Down Expand Up @@ -291,7 +297,7 @@ export class SelfoGame extends GameBase {
/**
* Random starting position: both colours scattered with no group larger
* than the board radius (and never one big enough to be connected), no
* piece already enclosed, and no immediate win for the first player (and,
* piece already enclosed, and no colour one move away from winning (and,
* with `no-enclosure`, at least one legal move for both).
*/
private randomStart(): Board {
Expand All @@ -307,11 +313,9 @@ export class SelfoGame extends GameBase {
for (const c of first) { board.set(c, 1); }
for (const c of second) { board.set(c, 2); }
if (hasEnclosedPiece(this.graph, board, 1) || hasEnclosedPiece(this.graph, board, 2)) { continue; }
// the first player must not be able to win on their first move
const wins = piecesOf(board, 1).some(from =>
this.graph.neighbours(from).some(to => !board.has(to) && isConnected(this.graph, after(board, from, to), 1))
);
if (wins) { continue; }
// neither colour may be one move away from winning (the second player
// could otherwise win at once, or take that colour with the pie rule)
if (canWinInOne(this.graph, board, 1) || canWinInOne(this.graph, board, 2)) { continue; }
fallback = board;
if (!this.noEnclosure) { return board; }
if (legalMoves(this.graph, board, 1, true).length > 0 && legalMoves(this.graph, board, 2, true).length > 0) {
Expand Down
33 changes: 33 additions & 0 deletions test/games/selfo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ const connected = (cells: string[], g: HexTriGraph = graph): boolean => {
return seen.size === set.size;
};

// Whether `player` could connect all their pieces with a single move, if it were their turn.
const canWinInOne = (board: Map<string, playerid>, player: playerid, g: HexTriGraph): boolean => {
const own = [...board.entries()].filter(([, p]) => p === player).map(([c]) => c);
return own.some(from => g.neighbours(from).some(to =>
!board.has(to) && connected(own.map(c => c === from ? to : c), g)
));
};

const corners = (graph.listCells() as string[]).filter(c => graph.neighbours(c).length === 3);

describe("Selfo", () => {
Expand Down Expand Up @@ -59,6 +67,31 @@ describe("Selfo", () => {
});
}

it("never starts with a colour one move away from winning", () => {
// the small board is where this used to happen (about 2% of the starts)
for (const [variants, radius] of [[["r2f5"], 2], [["r2f5", "no-enclosure"], 2], [[], 3]] as [string[], number][]) {
const g = new HexTriGraph(radius + 1, radius * 2 + 1);
for (let i = 0; i < 200; i++) {
const game = new SelfoGame(undefined, variants);
expect(canWinInOne(game.board, 1, g), `black, ${variants.join(",")}`).to.be.false;
expect(canWinInOne(game.board, 2, g), `white, ${variants.join(",")}`).to.be.false;
}
}
});

it("detects a colour that is one move away from winning", () => {
// c3 + n0 + t: t can step next to c3 and complete the group (see "moves and winning")
const nbrs = graph.neighbours("c3").sort();
const n0 = nbrs[0];
const n1 = nbrs.find(n => n !== n0 && !graph.neighbours(n0).includes(n))!;
const t = graph.neighbours(n1).sort().find(c => c !== "c3" && !nbrs.includes(c))!;
const board = new Map<string, playerid>([["c3", 1], [n0, 1], [t, 1]]);
expect(canWinInOne(board, 1, graph)).to.be.true;
// two pieces at distance 3 or more cannot be joined by a single step
const far = corners.find(c => c !== corners[0] && graph.path(corners[0], c)!.length > 3)!;
expect(canWinInOne(new Map<string, playerid>([[corners[0], 1], [far, 1]]), 1, graph)).to.be.false;
});

it("records the starting position", () => {
const game = new SelfoGame(undefined, ["r2f5"]);
const [first, second] = game.getStartingPosition().split("|");
Expand Down
Loading