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
4 changes: 3 additions & 1 deletion packages/abalone-engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
"./types": "./src/types.ts"
},
"scripts": {
"test": "vitest run"
"test": "vitest run",
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
"@vitest/coverage-v8": "4.1.12",
"typescript": "5.9.3",
"vitest": "4.1.12"
}
Expand Down
42 changes: 41 additions & 1 deletion packages/abalone-engine/src/game-state.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest"
import type { SetupKey } from "#abalone-engine/board-setups"
import type { GameState } from "#abalone-engine/game-state"
import type { GameState, MoveDetails } from "#abalone-engine/game-state"
import {
canNextMove,
canPrevMove,
Expand Down Expand Up @@ -107,6 +107,14 @@ describe("isValidSelection", () => {
expect(isValidSelection(state, "0,0")).toBe(false)
expect(isValidSelection(resignGame(state), "4,0")).toBe(false)
})

it("treats a bot game with no colour recorded as the human playing black", () => {
const unset = {
...createGameState("standard", "white", "ai"),
playerColor: undefined as unknown as GameState["playerColor"],
}
expect(isValidSelection(unset, "4,0")).toBe(true)
})
})

describe("makeMove", () => {
Expand Down Expand Up @@ -236,6 +244,29 @@ describe("walking through history", () => {
expect(goToMove(played, -1)).toBe(played)
expect(goToMove(played, 99)).toBe(played)
})

//only makeMove writes history, so these entries are hand-edited: the board
//must still restore and simply skip a highlight it cannot work out
it("restores an entry whose move it cannot draw", () => {
const withDetails = (patch: Partial<MoveDetails>): GameState => {
const history = [...played.moveHistory]
const details = history[1].moveDetails as MoveDetails
history[1] = { ...history[1], moveDetails: { ...details, ...patch } }
return { ...played, moveHistory: history }
}

const noMarbles = goToMove(withDetails({ marbles: [] }), 1)
expect(noMarbles.black.has("1,0")).toBe(true)
expect(noMarbles.lastMove).toBeNull()

const farDestination = withDetails({ destination: "-4,0" })
expect(goToMove(farDestination, 1).lastMove).toBeNull()

const noShoved = withDetails({
shovedMarbles: undefined as unknown as string[],
})
expect(goToMove(noShoved, 1).lastMove?.shovedMarbles).toEqual([])
})
})

describe("truncateToMove", () => {
Expand Down Expand Up @@ -278,6 +309,15 @@ describe("undoTargetIndex", () => {
expect(undoTargetIndex(ai)).toBe(2)
})

it("takes back as black when the bot game has no colour recorded", () => {
const ai = play(createGameState("standard", "white", "ai"), SHUFFLE)
const unset = {
...ai,
playerColor: undefined as unknown as GameState["playerColor"],
}
expect(undoTargetIndex(unset)).toBe(2)
})

it("has nothing to take back at the start or before the player has moved", () => {
expect(undoTargetIndex(createGameState())).toBe(-1)
const botOpened = play(
Expand Down
10 changes: 10 additions & 0 deletions packages/abalone-engine/src/moves.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ describe("destinationsFor — plain moves", () => {
expect(new Set(backward)).toEqual(new Set(forward))
})

it("offers a line across the rows in one order whichever end leads", () => {
//the step from "1,0" to "0,0" climbs a row, which CANONICAL_HEADING flips,
//so this reading takes the other branch from the one above
const black = ["0,0", "1,0"]
const downward = destinations(black, [], ["0,0", "1,0"])
const upward = destinations(black, [], ["1,0", "0,0"])
expect(upward).toEqual(downward)
expect(upward).toHaveLength(6)
})

it("blocks a broadside when any landing square is taken", () => {
const found = destinations(["0,0", "0,1"], ["1,0"], ["0,0", "0,1"])
expect(found).not.toContain("1,1")
Expand Down
17 changes: 17 additions & 0 deletions packages/abalone-engine/src/rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ describe("selectMarble", () => {
selectMarble(board, ["0,0", "0,1", "0,2"], "1,0", "black"),
).toEqual(["1,0"])
})

it("grows a white line and starts over on black", () => {
const whites = boardOf(["1,1"], ["0,3", "0,4"])
expect(selectMarble(whites, ["0,3"], "0,4", "white")).toEqual([
"0,3",
"0,4",
])
expect(selectMarble(whites, ["0,3"], "1,1", "black")).toEqual(["1,1"])
})
})

describe("deselectMarble", () => {
Expand Down Expand Up @@ -104,6 +113,14 @@ describe("selectRun", () => {
expect(selectRun(board, "1,1", "0,0")).toBeNull()
expect(selectRun(board, "0,0", "9,9")).toBeNull()
})

it("returns null when the run would leave the board on the way", () => {
//from the rim the walk runs off the board on several headings before it
//finds the reach, and must give up on each rather than wrap around
const rim = boardOf(["0,4", "0,3", "0,2"], [])
expect(selectRun(rim, "0,4", "0,2")).toEqual(["0,4", "0,3", "0,2"])
expect(selectRun(rim, "0,4", "-4,4")).toBeNull()
})
})

describe("getPossibleMoves", () => {
Expand Down
15 changes: 15 additions & 0 deletions packages/abalone-engine/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,20 @@ export default defineConfig({
},
test: {
environment: "node",
coverage: {
provider: "v8",
include: ["src/**/*.ts"],
exclude: ["src/**/*.test.ts"],
reporter: ["text", "json-summary"],
//today's numbers rounded down: the engine holds every rule of the game,
//so a change that ships an untested branch here fails the CI gate, which
//runs this script instead of the plain test one
thresholds: {
lines: 99,
branches: 98,
functions: 100,
statements: 99,
},
},
},
})
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading