@johnmorrisdotca/jirai 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/game.test.ts DELETED
@@ -1,172 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { newGame, play, visibleGame } from "./game.ts";
3
- import { makeBoard, isSolvable, GenerationError } from "./generate.ts";
4
- import { DEFAULT_SETTINGS, GRIDS, MARKS, MOVES, STATUSES } from "./jirai.constants.ts";
5
- import { neighbours, neighboursOf, validCell, validSettings } from "./grid.ts";
6
- import { gameFromProgress, gameProgress, dailySeed } from "./keep.ts";
7
- import { boardModel } from "./draw.ts";
8
- import { hintFor } from "./deduce.ts";
9
- import type { Board, Game, Settings } from "./jirai.types.ts";
10
- import { decodeOrthogonalGame, encodeOrthogonalGame, makeOrthogonalBoard, newOrthogonalGame, orthogonalHint, orthogonalNeighbours } from "./orthogonal.ts";
11
-
12
- function fixedGame(minesAt: number[]): Game {
13
- const settings: Settings = { ...DEFAULT_SETTINGS, width: 5, height: 5, mines: minesAt.length, noGuess: false, opening: "safe" };
14
- const mines = Array.from({ length: 25 }, (_, cell) => minesAt.includes(cell));
15
- const clues = mines.map((mine, cell) => mine ? -1 : neighbours(settings, cell).filter((n) => mines[n]).length);
16
- const board: Board = { settings, mines, clues, first: 12, attempt: 0 };
17
- return { ...newGame(settings), board };
18
- }
19
-
20
- describe("the grid topologies", () => {
21
- for (const grid of Object.values(GRIDS)) it(`${grid} has reciprocal, unique neighbours and no self-neighbours`, () => {
22
- const settings = { ...DEFAULT_SETTINGS, width: 7, height: 5, grid };
23
- const all = neighboursOf(settings);
24
- for (let cell = 0; cell < all.length; cell += 1) {
25
- expect(new Set(all[cell]).size).toBe(all[cell]!.length);
26
- expect(all[cell]).not.toContain(cell);
27
- for (const n of all[cell]!) expect(all[n]).toContain(cell);
28
- }
29
- });
30
- it("uses four orthogonal neighbours, six hex neighbours, eight square neighbours, and joins both wrap seams", () => {
31
- expect(neighbours({ ...DEFAULT_SETTINGS, grid: GRIDS.orthogonal }, 40)).toHaveLength(4);
32
- expect(neighbours({ ...DEFAULT_SETTINGS, grid: GRIDS.hex }, 40)).toHaveLength(6);
33
- expect(neighbours(DEFAULT_SETTINGS, 40)).toHaveLength(8);
34
- expect(neighbours({ ...DEFAULT_SETTINGS, grid: GRIDS.wrap }, 0)).toEqual([1,8,9,10,17,72,73,80]);
35
- });
36
- it("uses only orthogonal clues, preserves playable voids, and verifies a no-guess deal", () => {
37
- const settings = { ...DEFAULT_SETTINGS, width: 9, height: 9, mines: 10, grid: GRIDS.orthogonal, seed: 19 };
38
- const board = makeBoard(settings, 40);
39
- expect(board.clues[40]).toBe(0);
40
- for (let cell = 0; cell < 81; cell += 1) {
41
- expect(board.clues[cell]).toBe(board.mines[cell] ? -1 : neighbours(settings, cell).filter(next => board.mines[next]).length);
42
- }
43
- expect(isSolvable(board)).toBe(true);
44
- const shaped = { ...settings, shape: "heart" as const };
45
- const shapedBoard = makeBoard(shaped, 40);
46
- for (let cell = 0; cell < 81; cell += 1) if (!validCell(shaped, cell)) {
47
- expect(shapedBoard.mines[cell]).toBe(false);
48
- expect(shapedBoard.clues[cell]).toBe(-2);
49
- expect(neighbours(shaped, cell)).toEqual([]);
50
- }
51
- const playing = play(newGame(settings), { kind: MOVES.reveal, cell: 40 });
52
- const proof = hintFor(visibleGame(playing));
53
- expect(proof.safe.every(cell => !playing.board!.mines[cell])).toBe(true);
54
- expect(proof.mines.every(cell => playing.board!.mines[cell])).toBe(true);
55
- });
56
- it("rejects malformed or excessive settings before making a board", () => {
57
- for (const change of [{ width: 1 }, { width: 61 }, { width: 60, height: 60 }, { mines: 81 }, { seed: NaN }, { grid: "__proto__" }, { mines: 1.5 }, { noGuess: "yes" }]) expect(validSettings({ ...DEFAULT_SETTINGS, ...change })).toBe(false);
58
- });
59
- });
60
-
61
- describe("dealing and verified boards", () => {
62
- for (const grid of Object.values(GRIDS)) it(`${grid} deals exact counts and a clear first opening for many seeds`, () => {
63
- for (let seed = 0; seed < 40; seed += 1) {
64
- const settings = { ...DEFAULT_SETTINGS, grid, seed, noGuess: false };
65
- const board = makeBoard(settings, 40);
66
- expect(board.mines.filter(Boolean)).toHaveLength(10);
67
- expect(board.clues[40]).toBe(0);
68
- for (let cell = 0; cell < 81; cell += 1) {
69
- expect(board.clues[cell]).toBe(board.mines[cell] ? -1 : neighbours(settings, cell).filter((n) => board.mines[n]).length);
70
- }
71
- expect(makeBoard(settings, 40)).toEqual(board);
72
- }
73
- });
74
- for (const grid of Object.values(GRIDS)) it(`${grid} only advertises boards its visible-clue solver actually finishes`, () => {
75
- for (let seed = 0; seed < 15; seed += 1) expect(isSolvable(makeBoard({ ...DEFAULT_SETTINGS, grid, seed }, 40))).toBe(true);
76
- });
77
- it("reports a failed proof budget rather than returning an unverified board", () => {
78
- let failures = 0;
79
- for (let seed = 0; seed < 50; seed += 1) {
80
- try { makeBoard({ ...DEFAULT_SETTINGS, width: 5, height: 5, mines: 10, opening: "safe", seed }, 12, { attempts: 1 }); }
81
- catch (error) { expect(error).toBeInstanceOf(GenerationError); expect((error as GenerationError).code).toBe("exhausted"); failures += 1; }
82
- }
83
- expect(failures).toBeGreaterThan(0);
84
- });
85
- it("rejects a dense board that cannot protect the chosen opening", () => {
86
- expect(() => makeBoard({ ...DEFAULT_SETTINGS, width: 3, height: 3, mines: 1 }, 4)).toThrow(GenerationError);
87
- });
88
- });
89
-
90
- describe("moves", () => {
91
- it("floods empty ground and wins without requiring any flags", () => {
92
- const game = fixedGame([0]);
93
- const before = structuredClone(game);
94
- const next = play(game, { kind: MOVES.reveal, cell: 24 });
95
- expect(next.status).toBe(STATUSES.won);
96
- expect(next.marks.filter((mark) => mark === MARKS.open)).toHaveLength(24);
97
- expect(game).toEqual(before);
98
- });
99
- it("does not open a flagged cell, including inside a flood", () => {
100
- let game = fixedGame([0]);
101
- game = play(game, { kind: MOVES.mark, cell: 24 });
102
- expect(play(game, { kind: MOVES.reveal, cell: 24 })).toBe(game);
103
- game = play(game, { kind: MOVES.reveal, cell: 20 });
104
- expect(game.marks[24]).toBe(MARKS.flag);
105
- expect(game.status).toBe(STATUSES.playing);
106
- });
107
- it("cycles notes through flag, question and covered", () => {
108
- let game = newGame();
109
- for (const mark of [MARKS.flag, MARKS.question, MARKS.covered]) { game = play(game, { kind: MOVES.mark, cell: 0 }); expect(game.marks[0]).toBe(mark); }
110
- expect(game.board).toBeNull();
111
- });
112
- it("chords a matched number and loses if the flags were wrong", () => {
113
- let game = fixedGame([0,24]);
114
- game = play(game, { kind: MOVES.reveal, cell: 6 });
115
- expect(play(game, { kind: MOVES.chord, cell: 6 })).toBe(game);
116
- game = play(game, { kind: MOVES.mark, cell: 1 });
117
- game = play(game, { kind: MOVES.chord, cell: 6 });
118
- expect(game.status).toBe(STATUSES.lost); expect(game.exploded).toBe(0);
119
- expect(play(game, { kind: MOVES.mark, cell: 3 })).toBe(game);
120
- });
121
- it("chords correctly flagged mines without revealing them", () => {
122
- let game = fixedGame([0,24]);
123
- game = play(game, { kind: MOVES.reveal, cell: 6 });
124
- game = play(game, { kind: MOVES.mark, cell: 0 });
125
- game = play(game, { kind: MOVES.chord, cell: 6 });
126
- expect(game.status).not.toBe(STATUSES.lost); expect(game.marks[0]).toBe(MARKS.flag);
127
- });
128
- it("keeps hidden answers out of both the clue view and drawing", () => {
129
- const game = fixedGame([0,24]);
130
- expect(visibleGame(game).clues.every((clue) => clue === null)).toBe(true);
131
- expect(boardModel(game).cells.every((cell) => cell.text === "" && cell.kind === MARKS.covered)).toBe(true);
132
- });
133
- it("replays a saved game from its seed and moves; malformed records are refused", () => {
134
- let game = newGame({ ...DEFAULT_SETTINGS, noGuess: false });
135
- game = play(game, { kind: MOVES.mark, cell: 0 }); game = play(game, { kind: MOVES.reveal, cell: 40 });
136
- expect(gameFromProgress(gameProgress(game))).toEqual(game);
137
- for (const data of ["garbage", "null", '{"version":2}', JSON.stringify({ version: 1, settings: DEFAULT_SETTINGS, moves: [{ kind: "reveal", cell: -1 }] })]) expect(gameFromProgress(data)).toBeNull();
138
- });
139
- it("keeps orthogonal save records distinct and preserves legacy square codes", () => {
140
- const legacy = JSON.stringify({ version: 1, settings: DEFAULT_SETTINGS, moves: [], helped: false });
141
- expect(gameFromProgress(legacy)).toEqual(newGame());
142
- const orthogonal = newGame({ ...DEFAULT_SETTINGS, grid: GRIDS.orthogonal });
143
- const code = gameProgress(orthogonal);
144
- expect(JSON.parse(code)).toMatchObject({ version: 2, variant: "orthogonal", settings: { grid: "orthogonal" } });
145
- expect(gameFromProgress(code)).toEqual(orthogonal);
146
- expect(gameFromProgress(JSON.stringify({ ...JSON.parse(code), variant: "square" }))).toBeNull();
147
- });
148
- it("exposes focused orthogonal helpers and variant-only progress codes", () => {
149
- const settings = { ...DEFAULT_SETTINGS, width: 9, height: 9, mines: 10, seed: 4 };
150
- const game = newOrthogonalGame(settings);
151
- expect(game.settings.grid).toBe("orthogonal");
152
- expect(orthogonalNeighbours(settings, 40)).toHaveLength(4);
153
- expect(makeOrthogonalBoard(settings, 40).settings.grid).toBe("orthogonal");
154
- expect(orthogonalHint(play(game, { kind: MOVES.reveal, cell: 40 })).contradiction).toBe(false);
155
- expect(decodeOrthogonalGame(encodeOrthogonalGame(game))).toEqual(game);
156
- expect(decodeOrthogonalGame(gameProgress(newGame()))).toBeNull();
157
- });
158
- it("has stable daily seeds and rejects impossible calendar dates", () => {
159
- expect(dailySeed("2026-10-04")).toBe(dailySeed("2026-10-04"));
160
- expect(dailySeed("2026-10-04", "orthogonal")).not.toBe(dailySeed("2026-10-04"));
161
- expect(dailySeed("2026-10-04", "hex")).not.toBe(dailySeed("2026-10-04"));
162
- expect(() => dailySeed("2026-02-30")).toThrow();
163
- });
164
- });
165
-
166
- export { fixedGame };
167
-
168
- it("keeps assisted status through public progress and rejects invalid assisted data", () => {
169
- const game = { ...newGame(), helped: true };
170
- expect(gameFromProgress(gameProgress(game))?.helped).toBe(true);
171
- expect(gameFromProgress(JSON.stringify({ version: 1, settings: game.settings, moves: [], helped: "yes" }))).toBeNull();
172
- });
package/src/shape.test.ts DELETED
@@ -1,56 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { activeCell, activeCells } from "./shape.ts";
3
- import { DEFAULT_SETTINGS, GRIDS } from "./jirai.constants.ts";
4
- import { neighboursOf, validSettings } from "./grid.ts";
5
- import { makeBoard, isSolvable } from "./generate.ts";
6
- import { newGame, play, visibleGame } from "./game.ts";
7
- import { boardModel } from "./draw.ts";
8
- import { gameFromProgress, gameProgress } from "./keep.ts";
9
- import type { Settings } from "./jirai.types.ts";
10
-
11
- describe("shaped minefields", () => {
12
- for (const shape of ["heart", "star", "hexagon"] as const) for (const grid of [GRIDS.square, GRIDS.orthogonal, GRIDS.hex]) {
13
- it(`${shape}/${grid} omits cells and retains reciprocal neighbours and proved deals`, () => {
14
- const settings: Settings = { ...DEFAULT_SETTINGS, width: 17, height: 17, mines: 12, shape, grid };
15
- for (const outside of [-1, 289, 1.5, NaN]) expect(activeCell(settings, outside)).toBe(false);
16
- const cells = activeCells(settings), adjacent = neighboursOf(settings);
17
- expect(cells.length).toBeGreaterThan(40); expect(cells.length).toBeLessThan(289);
18
- for (const c of cells) for (const n of adjacent[c]!) {
19
- expect(activeCell(settings, n)).toBe(true); expect(adjacent[n]).toContain(c);
20
- }
21
- const first = cells[Math.floor(cells.length / 2)]!;
22
- for (let seed = 1; seed <= 12; seed += 1) {
23
- const board = makeBoard({ ...settings, seed }, first);
24
- expect(board.mines.filter(Boolean)).toHaveLength(12); expect(isSolvable(board)).toBe(true);
25
- for (let c = 0; c < 289; c += 1) if (!activeCell(settings, c)) {
26
- expect(board.mines[c]).toBe(false); expect(board.clues[c]).toBe(-2); expect(adjacent[c]).toEqual([]);
27
- }
28
- }
29
- });
30
- }
31
- it("never draws or lets the player act on cut-out cells, and restores the shape", () => {
32
- const settings: Settings = { ...DEFAULT_SETTINGS, width: 17, height: 17, shape: "heart", mines: 12 };
33
- let game = newGame(settings);
34
- const absent = Array.from({ length: 289 }, (_, c) => c).find(c => !activeCell(settings, c))!;
35
- expect(play(game, { kind: "reveal", cell: absent })).toBe(game);
36
- expect(play(game, { kind: "mark", cell: absent })).toBe(game);
37
- expect(visibleGame(game).clues[absent]).toBe(-2);
38
- expect(boardModel(game).cells.some(c => c.cell === absent)).toBe(false);
39
- game = play(game, { kind: "reveal", cell: activeCells(settings)[50]! });
40
- expect(gameFromProgress(gameProgress(game))?.settings.shape).toBe("heart");
41
- });
42
- it("wins when every active safe cell opens, without requiring absent cells", () => {
43
- const settings: Settings = { ...DEFAULT_SETTINGS, width: 9, height: 9, shape: "star", mines: 1, noGuess: false };
44
- let game = newGame(settings);
45
- const first = activeCells(settings)[10]!;
46
- game = play(game, { kind: "reveal", cell: first });
47
- for (const c of activeCells(settings)) if (!game.board!.mines[c]) game = play(game, { kind: "reveal", cell: c });
48
- expect(game.status).toBe("won");
49
- });
50
- it("rejects unknown outlines, tiny silhouettes, wrap cut-outs and excess mines", () => {
51
- expect(validSettings({ ...DEFAULT_SETTINGS, shape: "toString" })).toBe(false);
52
- expect(validSettings({ ...DEFAULT_SETTINGS, width: 5, height: 5, shape: "heart" })).toBe(false);
53
- expect(validSettings({ ...DEFAULT_SETTINGS, shape: "heart", grid: "wrap" })).toBe(false);
54
- expect(validSettings({ ...DEFAULT_SETTINGS, width: 9, height: 9, shape: "star", mines: 70 })).toBe(false);
55
- });
56
- });