Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add components for new board patterns that work with baduk #258

Merged
merged 3 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions packages/shared/src/game_map.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { GridBaduk } from "./variants/baduk";
import { AbstractGame } from "./abstract_game";
import { BadukWithAbstractBoard } from "./variants/badukWithAbstractBoard";
import { Phantom } from "./variants/phantom";
Expand All @@ -14,12 +13,13 @@ import { Keima } from "./variants/keima";
import { OneColorGo } from "./variants/one_color";
import { DriftGo } from "./variants/drift";
import { QuantumGo } from "./variants/quantum";
import { Baduk } from "./variants/baduk";

export const game_map: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[variant: string]: new (config?: any) => AbstractGame;
} = {
baduk: GridBaduk,
baduk: Baduk,
badukWithAbstractBoard: BadukWithAbstractBoard,
phantom: Phantom,
parallel: ParallelGo,
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ export {
export { type KeimaState } from "./variants/keima";
export * from "./variants/drift";
export * from "./variants/baduk_utils";
export * from "./lib/abstractBoard/boardFactory";
export * from "./lib/abstractBoard/intersection";

export const SITE_NAME = "Go Variants";
11 changes: 9 additions & 2 deletions packages/shared/src/lib/abstractBoard/boardFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,18 @@ function createGridBoard<TIntersection extends Intersection>(

export function createGraph<TIntersection extends Intersection, TColor>(
intersections: TIntersection[],
startColor: TColor,
): Graph<TColor> {
const adjacencyMatrix = intersections.map((intersection) =>
intersection.neighbours.map((_neighbour, index) => index),
intersection.neighbours.map((neighbour) =>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice fix! Could be worth a simple unit test - something like:

test("createGraph", () => {
  const intersections = createGridBoard({
    type: BoardPattern.Grid;
    width: 3;
    height: 3;
  });
  const graph = createGraph(intersections, 0);

  expect(graph.serialize()).toEqual(...);
});

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I Added a simple unit test.
It's rather difficult to write a meaningful unit test about the graph boards, because its rather abstract, and the one I added is not independent of the implementation (i.e. another implementation may work just fine, but fail the test).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah, that's a good point, I agree implementation-independence is a good quality to strive for. I think your comment in the code is a sufficient mitigation, but some thoughts on how to create implementation-independent graph tests in general -

When testing createGraph, we could build intersections from scratch instead of a factory:

const intersections = [Intersection(...), Intersection(...);
intersections[0].connectTo(intersections[1], true);

const graph = createGraph(intersections);

// This should hold true regardless of impl (unless we decide to re-map IDs)
expect(graph.neighbors(0)).toEqual([1]);

When testing factories, just assert on properties that should remain invariant. E.g. counts and sizes:

expect(intersections.length).toBe(18)
// hard to assert on each intersection because order dependent, but
// maxes, mins and other aggregators should be mostly invariant
for (intersection of intersections) {
    expect(intersection.Neighbors.length).toBeGreaterThanOrEqualTo(2);
    expect(intersection.Neighbors.length).toBeLessThanOrEqualTo(4);
    // Same with positions
    expect(intersection.Position.x).toBeGreaterThan(0.0);
}

intersections.indexOf(neighbour),
),
);
const graph = new Graph<TColor>(adjacencyMatrix);
intersections.forEach((intersection) =>
graph.set(intersection.id, startColor),
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we've got a lot of Array-style methods on Graph!

Suggested change
const graph = new Graph<TColor>(adjacencyMatrix);
intersections.forEach((intersection) =>
graph.set(intersection.id, startColor),
);
const graph = new Graph<TColor>(adjacencyMatrix).fill(startColor);

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed accordingly, thanks!

return new Graph<TColor>(adjacencyMatrix);
return graph;
}

function createRthBoard<TIntersection extends Intersection>(
Expand Down
40 changes: 32 additions & 8 deletions packages/shared/src/variants/__tests__/quantum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ const B = Color.BLACK;
const _ = Color.EMPTY;

test("Quantum stone placement", () => {
const game = new QuantumGo({ width: 2, height: 2, komi: 0.5 });
const game = new QuantumGo({
board: { type: "grid", width: 2, height: 2 },
komi: 0.5,
});

game.playMove(0, "aa");
expect(game.exportState().boards).toEqual([
Expand Down Expand Up @@ -37,22 +40,31 @@ test("Quantum stone placement", () => {
});

test("Test throws if incorrect player in the quantum stone phase", () => {
const game = new QuantumGo({ width: 2, height: 2, komi: 0.5 });
const game = new QuantumGo({
board: { type: "grid", width: 2, height: 2 },
komi: 0.5,
});

expect(() => game.playMove(1, "aa")).toThrow();
game.playMove(0, "aa");
expect(() => game.playMove(0, "bb")).toThrow();
});

test("Test throws stone played on top of stone in quantum phase", () => {
const game = new QuantumGo({ width: 2, height: 2, komi: 0.5 });
const game = new QuantumGo({
board: { type: "grid", width: 2, height: 2 },
komi: 0.5,
});

game.playMove(0, "aa");
expect(() => game.playMove(1, "aa")).toThrow();
});

test("Capture quantum stone", () => {
const game = new QuantumGo({ width: 5, height: 3, komi: 0.5 });
const game = new QuantumGo({
board: { type: "grid", width: 5, height: 3 },
komi: 0.5,
});

// .{B}. . W
// B{W}B . W
Expand Down Expand Up @@ -102,7 +114,10 @@ test("Capture quantum stone", () => {
});

test("Capture non-quantum stone", () => {
const game = new QuantumGo({ width: 5, height: 3, komi: 0.5 });
const game = new QuantumGo({
board: { type: "grid", width: 5, height: 3 },
komi: 0.5,
});

// .{B}. . W
// B W B .{W}
Expand Down Expand Up @@ -152,7 +167,10 @@ test("Capture non-quantum stone", () => {
});

test("Two passes ends the game", () => {
const game = new QuantumGo({ width: 4, height: 2, komi: 0.5 });
const game = new QuantumGo({
board: { type: "grid", width: 4, height: 2 },
komi: 0.5,
});

game.playMove(0, "ba");
game.playMove(1, "ca");
Expand Down Expand Up @@ -195,7 +213,10 @@ test("Two passes ends the game", () => {
});

test("Placing a stone in a captured quantum position", () => {
const game = new QuantumGo({ width: 9, height: 9, komi: 0.5 });
const game = new QuantumGo({
board: { type: "grid", width: 9, height: 9 },
komi: 0.5,
});

// https://www.govariants.com/game/660248dd5e01aefcbd63df6a

Expand Down Expand Up @@ -238,7 +259,10 @@ test("Placing a stone in a captured quantum position", () => {
});

test("Placing a white stone in a captured quantum position", () => {
const game = new QuantumGo({ width: 9, height: 9, komi: 0.5 });
const game = new QuantumGo({
board: { type: "grid", width: 9, height: 9 },
komi: 0.5,
});

// https://www.govariants.com/game/660248dd5e01aefcbd63df6a

Expand Down
68 changes: 25 additions & 43 deletions packages/shared/src/variants/baduk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import { GraphWrapper } from "../lib/graph";
import {
GridBadukConfig,
LegacyBadukConfig,
getWidthAndHeight,
NewBadukConfig,
isGridBadukConfig,
isLegacyBadukConfig,
mapToNewConfig,
} from "./baduk_utils";

export enum Color {
Expand All @@ -24,12 +26,7 @@ export enum Color {
WHITE = 2,
}

export type BadukConfig =
| LegacyBadukConfig
| {
komi: number;
board: BoardConfig;
};
export type BadukConfig = LegacyBadukConfig | NewBadukConfig;

export interface BadukState {
board: Color[][];
Expand All @@ -41,11 +38,10 @@ export interface BadukState {

export type BadukMove = { 0: string } | { 1: string };

// export declare type BadukBoardType<TColor> = Fillable<CoordinateLike, TColor>;
// Grid | GraphWrapper, so we have a better idea of serialize() return type
export declare type BadukBoard<TColor> = Grid<TColor> | GraphWrapper<TColor>;

export class Baduk extends AbstractGame<BadukConfig, BadukState> {
export class Baduk extends AbstractGame<NewBadukConfig, BadukState> {
protected captures = { 0: 0, 1: 0 };
private ko_detector = new SuperKoDetector();
protected score_board?: BadukBoard<Color>;
Expand All @@ -55,20 +51,21 @@ export class Baduk extends AbstractGame<BadukConfig, BadukState> {
/** after game ends, this is black points - white points */
public numeric_result?: number;

constructor(config?: BadukConfig) {
super(config);
constructor(config?: LegacyBadukConfig | BadukConfig) {
super(isLegacyBadukConfig(config) ? mapToNewConfig(config) : config);

if (isGridBadukConfig(this.config)) {
const { width, height } = getWidthAndHeight(this.config);

if (width >= 52 || height >= 52) {
if (this.config.board.width >= 52 || this.config.board.height >= 52) {
throw new Error("Baduk does not support sizes greater than 52");
}

this.board = new Grid<Color>(width, height).fill(Color.EMPTY);
this.board = new Grid<Color>(
this.config.board.width,
this.config.board.height,
).fill(Color.EMPTY);
} else {
const intersections = createBoard(this.config.board!, Intersection);
this.board = new GraphWrapper(createGraph(intersections));
const intersections = createBoard(this.config.board, Intersection);
this.board = new GraphWrapper(createGraph(intersections, Color.EMPTY));
}
}

Expand All @@ -86,6 +83,14 @@ export class Baduk extends AbstractGame<BadukConfig, BadukState> {
return this.phase === "gameover" ? [] : [this.next_to_play];
}

private decodeMove(move: string): Coordinate {
if (isGridBadukConfig(this.config)) {
return Coordinate.fromSgfRepr(move);
}
// graph boards encode moves with the unique identifier number
return new Coordinate(Number(move), 0);
}

override playMove(player: number, move: string): void {
if (player != this.next_to_play) {
throw Error(`It's not player ${player}'s turn!`);
Expand All @@ -104,7 +109,8 @@ export class Baduk extends AbstractGame<BadukConfig, BadukState> {
}

if (move != "pass") {
const decoded_move = Coordinate.fromSgfRepr(move);
const decoded_move = this.decodeMove(move);

const { x, y } = decoded_move;
const color = this.board.at(decoded_move);
if (color === undefined) {
Expand Down Expand Up @@ -215,7 +221,7 @@ export class Baduk extends AbstractGame<BadukConfig, BadukState> {
this.phase = "gameover";
}

defaultConfig(): GridBadukConfig {
defaultConfig(): NewBadukConfig {
return {
komi: 6.5,
board: {
Expand All @@ -241,27 +247,3 @@ export function groupHasLiberties(
function count_color<T>(value: T) {
return (total: number, color: T) => total + (color === value ? 1 : 0);
}

export class GridBaduk extends Baduk {
benjaminpjones marked this conversation as resolved.
Show resolved Hide resolved
// ! isn't typesafe, but we know board will be assigned in super()
declare board: Grid<Color>;
protected declare score_board?: Grid<Color>;
declare config: GridBadukConfig;
constructor(config?: GridBadukConfig) {
if (config && !isGridBadukConfig(config)) {
throw "GridBaduk requires a GridBadukConfig";
}
super(config);
}

override defaultConfig(): GridBadukConfig {
return {
komi: 6.5,
board: {
type: BoardPattern.Grid,
width: 19,
height: 19,
},
};
}
}
28 changes: 28 additions & 0 deletions packages/shared/src/variants/baduk_utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
BoardConfig,
BoardPattern,
GridBoardConfig,
} from "../lib/abstractBoard/boardFactory";
Expand All @@ -10,6 +11,11 @@ export type LegacyBadukConfig = {
komi: number;
};

export type NewBadukConfig = {
komi: number;
board: BoardConfig;
};

export type GridBadukConfig =
| LegacyBadukConfig
| {
Expand All @@ -31,3 +37,25 @@ export function getWidthAndHeight(config: GridBadukConfig): {
const height = "board" in config ? config.board.height : config.height;
return { width: width, height: height };
}

export function isLegacyBadukConfig(
config: object | undefined,
): config is LegacyBadukConfig {
return (
config !== undefined &&
"width" in config &&
"height" in config &&
"komi" in config
);
}

export function mapToNewConfig(config: LegacyBadukConfig): NewBadukConfig {
return {
...config,
board: {
type: BoardPattern.Grid,
width: config.width,
height: config.height,
},
};
}
41 changes: 34 additions & 7 deletions packages/shared/src/variants/drift.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,50 @@
import { GridBoardConfig } from "../lib/abstractBoard/boardFactory";
import { Coordinate } from "../lib/coordinate";
import { Grid } from "../lib/grid";
import { getGroup } from "../lib/group_utils";
import { Color, GridBaduk, groupHasLiberties } from "./baduk";
import { GridBadukConfig } from "./baduk_utils";
import { Baduk, Color, groupHasLiberties } from "./baduk";
import { LegacyBadukConfig, NewBadukConfig } from "./baduk_utils";

export type DriftGoConfig = GridBadukConfig & {
export type DriftGoConfig = { komi: number; board: GridBoardConfig } & {
yShift: number;
xShift: number;
};

export class DriftGo extends GridBaduk {
export type LegacyDriftGoConfig = LegacyBadukConfig & {
yShift: number;
xShift: number;
};

function isDriftGoConfig(config: object): config is DriftGoConfig {
return (
"komi" in config &&
"board" in config &&
"xShift" in config &&
"yShift" in config
);
}

export class DriftGo extends Baduk {
private typedConfig: DriftGoConfig;
declare board: Grid<Color>;

constructor(config?: DriftGoConfig) {
constructor(config?: DriftGoConfig | LegacyDriftGoConfig) {
super(config);
this.typedConfig = config ?? this.defaultConfig();
if (!isDriftGoConfig(this.config)) {
throw Error(
`Drift accepts only grid board config. Received config: ${JSON.stringify(config)}`,
);
}
this.typedConfig = this.config ?? this.defaultConfig();
}

defaultConfig(): DriftGoConfig {
return { ...super.defaultConfig(), xShift: 0, yShift: 1 };
return {
komi: 6.5,
board: { type: "grid", width: 19, height: 19 },
xShift: 0,
yShift: 1,
};
}

protected prepareForNextMove(move: string): void {
Expand Down
Loading
Loading