@vnejs/plugins.views.screens.checkers 0.2.2 → 0.2.3

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.
Files changed (53) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +21 -12
  3. package/dist/modules/board.d.ts +9 -0
  4. package/dist/modules/board.js +16 -0
  5. package/dist/modules/bridge.d.ts +8 -0
  6. package/dist/modules/bridge.js +10 -0
  7. package/dist/modules/click.d.ts +8 -0
  8. package/dist/modules/click.js +14 -0
  9. package/dist/modules/controller.accept.d.ts +7 -25
  10. package/dist/modules/controller.accept.js +12 -159
  11. package/dist/modules/controller.d.ts +4 -6
  12. package/dist/modules/controller.grid.d.ts +11 -11
  13. package/dist/modules/controller.grid.js +21 -25
  14. package/dist/modules/controller.js +8 -26
  15. package/dist/modules/player.interact.d.ts +9 -0
  16. package/dist/modules/player.interact.js +33 -0
  17. package/dist/modules/player.select.d.ts +9 -0
  18. package/dist/modules/player.select.js +29 -0
  19. package/dist/modules/view.d.ts +5 -5
  20. package/dist/modules/view.js +4 -4
  21. package/dist/types.d.ts +7 -5
  22. package/dist/utils/checkers.d.ts +19 -9
  23. package/dist/utils/checkers.js +30 -16
  24. package/dist/view/index.d.ts +2 -2
  25. package/dist/view/index.js +12 -12
  26. package/package.json +8 -2
  27. package/src/index.ts +21 -12
  28. package/src/modules/board.ts +29 -0
  29. package/src/modules/bridge.ts +20 -0
  30. package/src/modules/click.ts +28 -0
  31. package/src/modules/controller.accept.ts +20 -189
  32. package/src/modules/controller.grid.ts +23 -26
  33. package/src/modules/controller.ts +16 -36
  34. package/src/modules/player.interact.ts +48 -0
  35. package/src/modules/player.select.ts +42 -0
  36. package/src/modules/view.ts +12 -6
  37. package/src/tests/player.interact.test.ts +56 -0
  38. package/src/tests/setup.ts +21 -10
  39. package/src/types.ts +19 -6
  40. package/src/utils/checkers.ts +51 -25
  41. package/src/view/index.tsx +22 -23
  42. package/dist/modules/checkers.d.ts +0 -9
  43. package/dist/modules/checkers.js +0 -37
  44. package/dist/utils/ai.d.ts +0 -4
  45. package/dist/utils/ai.js +0 -27
  46. package/dist/utils/board.d.ts +0 -32
  47. package/dist/utils/board.js +0 -199
  48. package/src/modules/checkers.ts +0 -52
  49. package/src/tests/ai.test.ts +0 -61
  50. package/src/tests/board.test.ts +0 -43
  51. package/src/tests/checkers.test.ts +0 -58
  52. package/src/utils/ai.ts +0 -31
  53. package/src/utils/board.ts +0 -252
@@ -1,199 +0,0 @@
1
- export const BOARD_SIZE = 8;
2
- export const oppositeColor = (color) => (color === "white" ? "black" : "white");
3
- export const isDarkSquare = (row, column) => (row + column) % 2 === 1;
4
- export const inBounds = (row, column) => row >= 0 && row < BOARD_SIZE && column >= 0 && column < BOARD_SIZE;
5
- export const cloneBoard = (board) => board.map((row) => row.map((cell) => (cell ? { ...cell } : null)));
6
- export const createInitialBoard = () => {
7
- const board = Array.from({ length: BOARD_SIZE }, () => Array.from({ length: BOARD_SIZE }, () => null));
8
- let nextId = 0;
9
- for (let row = 0; row < BOARD_SIZE; row++) {
10
- for (let column = 0; column < BOARD_SIZE; column++) {
11
- if (!isDarkSquare(row, column))
12
- continue;
13
- if (row <= 2)
14
- board[row][column] = { id: `piece-${nextId++}`, color: "black", king: false };
15
- if (row >= 5)
16
- board[row][column] = { id: `piece-${nextId++}`, color: "white", king: false };
17
- }
18
- }
19
- return board;
20
- };
21
- const DIAGONALS = [
22
- { dr: -1, dc: -1 },
23
- { dr: -1, dc: 1 },
24
- { dr: 1, dc: -1 },
25
- { dr: 1, dc: 1 },
26
- ];
27
- const forwardRowDelta = (color) => (color === "white" ? -1 : 1);
28
- const posKey = (pos) => `${pos.row}:${pos.column}`;
29
- export const samePos = (a, b) => a.row === b.row && a.column === b.column;
30
- const getPiece = (board, pos) => board[pos.row]?.[pos.column] ?? null;
31
- const setPiece = (board, pos, piece) => {
32
- board[pos.row][pos.column] = piece;
33
- };
34
- const promoteIfNeeded = (piece, row) => {
35
- if (piece.king)
36
- return piece;
37
- if (piece.color === "white" && row === 0)
38
- return { ...piece, king: true };
39
- if (piece.color === "black" && row === BOARD_SIZE - 1)
40
- return { ...piece, king: true };
41
- return piece;
42
- };
43
- const collectSimpleMoves = (board, from, piece) => {
44
- const moves = [];
45
- const deltas = piece.king ? DIAGONALS : DIAGONALS.filter(({ dr }) => dr === forwardRowDelta(piece.color));
46
- for (const { dr, dc } of deltas) {
47
- if (piece.king) {
48
- let row = from.row + dr;
49
- let column = from.column + dc;
50
- while (inBounds(row, column) && !getPiece(board, { row, column })) {
51
- moves.push({ from, to: { row, column }, captures: [] });
52
- row += dr;
53
- column += dc;
54
- }
55
- continue;
56
- }
57
- const row = from.row + dr;
58
- const column = from.column + dc;
59
- if (!inBounds(row, column) || getPiece(board, { row, column }))
60
- continue;
61
- moves.push({ from, to: { row, column }, captures: [] });
62
- }
63
- return moves;
64
- };
65
- const collectImmediateCaptures = (board, from, piece, capturedKeys) => {
66
- const moves = [];
67
- for (const { dr, dc } of DIAGONALS) {
68
- if (piece.king) {
69
- let row = from.row + dr;
70
- let column = from.column + dc;
71
- let enemy = null;
72
- while (inBounds(row, column)) {
73
- const occupant = getPiece(board, { row, column });
74
- if (!occupant) {
75
- if (enemy) {
76
- moves.push({ from, to: { row, column }, captures: [enemy] });
77
- }
78
- row += dr;
79
- column += dc;
80
- continue;
81
- }
82
- if (enemy)
83
- break;
84
- if (occupant.color === piece.color || capturedKeys.has(posKey({ row, column })))
85
- break;
86
- enemy = { row, column };
87
- row += dr;
88
- column += dc;
89
- }
90
- continue;
91
- }
92
- const mid = { row: from.row + dr, column: from.column + dc };
93
- const to = { row: from.row + 2 * dr, column: from.column + 2 * dc };
94
- if (!inBounds(to.row, to.column) || getPiece(board, to))
95
- continue;
96
- const victim = getPiece(board, mid);
97
- if (!victim || victim.color === piece.color || capturedKeys.has(posKey(mid)))
98
- continue;
99
- moves.push({ from, to, captures: [mid] });
100
- }
101
- return moves;
102
- };
103
- const expandCaptureChains = (board, from, piece, origin, capturedSoFar, capturedKeys) => {
104
- const immediate = collectImmediateCaptures(board, from, piece, capturedKeys);
105
- if (immediate.length === 0) {
106
- return capturedSoFar.length > 0 ? [{ from: origin, to: from, captures: capturedSoFar }] : [];
107
- }
108
- const results = [];
109
- for (const step of immediate) {
110
- const nextBoard = cloneBoard(board);
111
- setPiece(nextBoard, from, null);
112
- for (const capture of step.captures)
113
- setPiece(nextBoard, capture, null);
114
- const nextPiece = promoteIfNeeded(piece, step.to.row);
115
- setPiece(nextBoard, step.to, nextPiece);
116
- const nextCaptures = [...capturedSoFar, ...step.captures];
117
- const nextKeys = new Set(capturedKeys);
118
- for (const capture of step.captures)
119
- nextKeys.add(posKey(capture));
120
- results.push(...expandCaptureChains(nextBoard, step.to, nextPiece, origin, nextCaptures, nextKeys));
121
- }
122
- return results;
123
- };
124
- export const getMovesFrom = (board, from) => {
125
- const piece = getPiece(board, from);
126
- if (!piece)
127
- return [];
128
- const captures = expandCaptureChains(board, from, piece, from, [], new Set());
129
- if (captures.length > 0)
130
- return captures;
131
- return collectSimpleMoves(board, from, piece);
132
- };
133
- export const getAllMoves = (board, color, onlyFrom) => {
134
- const moves = [];
135
- for (let row = 0; row < BOARD_SIZE; row++) {
136
- for (let column = 0; column < BOARD_SIZE; column++) {
137
- if (onlyFrom && !samePos(onlyFrom, { row, column }))
138
- continue;
139
- const piece = board[row]?.[column];
140
- if (!piece || piece.color !== color)
141
- continue;
142
- moves.push(...getMovesFrom(board, { row, column }));
143
- }
144
- }
145
- const hasCapture = moves.some((move) => move.captures.length > 0);
146
- return hasCapture ? moves.filter((move) => move.captures.length > 0) : moves;
147
- };
148
- export const applyMove = (board, move) => {
149
- const next = cloneBoard(board);
150
- const piece = getPiece(next, move.from);
151
- if (!piece)
152
- return next;
153
- setPiece(next, move.from, null);
154
- for (const capture of move.captures)
155
- setPiece(next, capture, null);
156
- setPiece(next, move.to, promoteIfNeeded(piece, move.to.row));
157
- return next;
158
- };
159
- /** Apply one hop of a multi-capture chain (first capture only). */
160
- export const applyCaptureStep = (board, from, to, capture) => {
161
- const next = cloneBoard(board);
162
- const piece = getPiece(next, from);
163
- if (!piece)
164
- return next;
165
- setPiece(next, from, null);
166
- setPiece(next, capture, null);
167
- setPiece(next, to, promoteIfNeeded(piece, to.row));
168
- return next;
169
- };
170
- export const listBoardPieces = (board) => {
171
- const pieces = [];
172
- for (let row = 0; row < BOARD_SIZE; row++) {
173
- for (let column = 0; column < BOARD_SIZE; column++) {
174
- const piece = board[row]?.[column];
175
- if (!piece)
176
- continue;
177
- pieces.push({ ...piece, row, column });
178
- }
179
- }
180
- return pieces;
181
- };
182
- export const countPieces = (board, color) => {
183
- let count = 0;
184
- for (const row of board) {
185
- for (const cell of row) {
186
- if (cell?.color === color)
187
- count++;
188
- }
189
- }
190
- return count;
191
- };
192
- export const getWinner = (board, colorToMove) => {
193
- if (countPieces(board, colorToMove) === 0)
194
- return oppositeColor(colorToMove);
195
- if (getAllMoves(board, colorToMove).length === 0)
196
- return oppositeColor(colorToMove);
197
- return null;
198
- };
199
- export const findMove = (moves, from, to) => moves.find((move) => samePos(move.from, from) && samePos(move.to, to));
@@ -1,52 +0,0 @@
1
- import { tokenizeExecLine } from "@vnejs/helpers";
2
- import { ModuleCore } from "@vnejs/module.core";
3
-
4
- import type { LineExecHandlerArg } from "@vnejs/module.core";
5
- import type { CheckersAiColor, CheckersAiType } from "@vnejs/contracts.views.screens.checkers";
6
-
7
- import type { CheckersPluginConstants, CheckersPluginEvents, CheckersPluginParams, CheckersPluginSettings } from "../types.js";
8
-
9
- const AI_COLOR_VALUES = new Set<string>(["white", "black"]);
10
- const AI_TYPE_VALUES = new Set<string>(["player", "sacrifice", "random", "strong"]);
11
-
12
- export class Checkers extends ModuleCore<CheckersPluginEvents, CheckersPluginConstants, CheckersPluginSettings, CheckersPluginParams> {
13
- name = "checkers";
14
-
15
- postinject = () => {
16
- this.shared.checkersAi ??= {
17
- white: this.CONST.CHECKERS.AI_TYPES.PLAYER,
18
- black: this.CONST.CHECKERS.AI_TYPES.PLAYER,
19
- };
20
- this.shared.checkersAiBusy ??= false;
21
- };
22
-
23
- init = () => this.emit(this.EVENTS.SCENARIO.LINE_EXEC_REG, { module: this.name, handler: this.onLineExec });
24
-
25
- onLineExec = ({ line = "" }: LineExecHandlerArg = {}) => {
26
- const [action = "", colorRaw = "", typeRaw = ""] = tokenizeExecLine(line).map(String);
27
-
28
- if (action === this.CONST.CHECKERS.EXEC_ACTIONS.SHOW) {
29
- this.emitLog({ action });
30
- return this.emit(this.EVENTS.CHECKERS.SHOW);
31
- }
32
-
33
- if (action !== this.CONST.CHECKERS.EXEC_ACTIONS.AI) return;
34
-
35
- if (!AI_COLOR_VALUES.has(colorRaw) || !AI_TYPE_VALUES.has(typeRaw)) {
36
- this.emitLog({ action, color: colorRaw, type: typeRaw, error: "invalid_args" });
37
- this.emitNext();
38
- return;
39
- }
40
-
41
- const color = colorRaw as CheckersAiColor;
42
- const type = typeRaw as CheckersAiType;
43
-
44
- this.shared.checkersAi = {
45
- ...this.shared.checkersAi,
46
- [color]: type,
47
- };
48
-
49
- this.emitLog({ action, color, type });
50
- this.emitNext();
51
- };
52
- }
@@ -1,61 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
2
-
3
- import { BOARD_SIZE, type CheckersBoard, type CheckersPiece } from "../utils/board.js";
4
- import { isHangingMove, pickMove } from "../utils/ai.js";
5
-
6
- const emptyBoard = (): CheckersBoard => Array.from({ length: BOARD_SIZE }, () => Array.from({ length: BOARD_SIZE }, () => null));
7
-
8
- const piece = (id: string, color: CheckersPiece["color"]): CheckersPiece => ({ id, color, king: false });
9
-
10
- /** White at (5,2): (4,1) hangs to black at (3,0); (4,3) is safe. */
11
- const hangingScenarioBoard = (): CheckersBoard => {
12
- const board = emptyBoard();
13
- board[5]![2] = piece("w1", "white");
14
- board[3]![0] = piece("b1", "black");
15
- return board;
16
- };
17
-
18
- describe("checkers ai", () => {
19
- afterEach(() => {
20
- vi.restoreAllMocks();
21
- });
22
-
23
- it("isHangingMove is true when opponent gets a capture", () => {
24
- const board = hangingScenarioBoard();
25
- const hanging = { from: { row: 5, column: 2 }, to: { row: 4, column: 1 }, captures: [] };
26
- const safe = { from: { row: 5, column: 2 }, to: { row: 4, column: 3 }, captures: [] };
27
-
28
- expect(isHangingMove(board, hanging, "white")).toBe(true);
29
- expect(isHangingMove(board, safe, "white")).toBe(false);
30
- });
31
-
32
- it("pickMove sacrifice prefers hanging moves", () => {
33
- vi.spyOn(Math, "random").mockReturnValue(0);
34
- const board = hangingScenarioBoard();
35
- const move = pickMove(board, "white", "sacrifice");
36
-
37
- expect(move).toEqual({ from: { row: 5, column: 2 }, to: { row: 4, column: 1 }, captures: [] });
38
- });
39
-
40
- it("pickMove strong prefers safe moves", () => {
41
- vi.spyOn(Math, "random").mockReturnValue(0);
42
- const board = hangingScenarioBoard();
43
- const move = pickMove(board, "white", "strong");
44
-
45
- expect(move).toEqual({ from: { row: 5, column: 2 }, to: { row: 4, column: 3 }, captures: [] });
46
- });
47
-
48
- it("pickMove random returns a legal move", () => {
49
- vi.spyOn(Math, "random").mockReturnValue(0);
50
- const board = hangingScenarioBoard();
51
- const move = pickMove(board, "white", "random");
52
-
53
- expect(move).not.toBeNull();
54
- expect(move?.from).toEqual({ row: 5, column: 2 });
55
- });
56
-
57
- it("pickMove player returns null", () => {
58
- const board = hangingScenarioBoard();
59
- expect(pickMove(board, "white", "player")).toBeNull();
60
- });
61
- });
@@ -1,43 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { applyMove, createInitialBoard, getAllMoves, getWinner, countPieces } from "../utils/board.js";
4
-
5
- describe("checkers board", () => {
6
- it("creates initial setup with 12 pieces each", () => {
7
- const board = createInitialBoard();
8
-
9
- expect(countPieces(board, "white")).toBe(12);
10
- expect(countPieces(board, "black")).toBe(12);
11
- });
12
-
13
- it("white has simple opening moves", () => {
14
- const board = createInitialBoard();
15
- const moves = getAllMoves(board, "white");
16
-
17
- expect(moves.length).toBeGreaterThan(0);
18
- expect(moves.every((move) => move.captures.length === 0)).toBe(true);
19
- });
20
-
21
- it("detects win when side has no pieces", () => {
22
- const board = createInitialBoard();
23
- for (let row = 0; row < 8; row++) {
24
- for (let column = 0; column < 8; column++) {
25
- if (board[row]?.[column]?.color === "black") board[row]![column] = null;
26
- }
27
- }
28
-
29
- expect(getWinner(board, "black")).toBe("white");
30
- });
31
-
32
- it("applies a simple move", () => {
33
- const board = createInitialBoard();
34
- const moves = getAllMoves(board, "white");
35
- const move = moves[0]!;
36
- const pieceId = board[move.from.row]![move.from.column]!.id;
37
- const next = applyMove(board, move);
38
-
39
- expect(next[move.from.row]![move.from.column]).toBeNull();
40
- expect(next[move.to.row]![move.to.column]?.color).toBe("white");
41
- expect(next[move.to.row]![move.to.column]?.id).toBe(pieceId);
42
- });
43
- });
@@ -1,58 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
-
3
- import { spyEvent } from "@vnejs/test-utils";
4
-
5
- import { Checkers } from "../modules/checkers.js";
6
- import { createCheckersTestModule, registerCheckersPluginVne, SUBSCRIBE_EVENTS } from "./setup.js";
7
-
8
- describe("Checkers", () => {
9
- beforeEach(() => {
10
- registerCheckersPluginVne();
11
- });
12
-
13
- it("line exec show emits SHOW", async () => {
14
- const { module, observer } = createCheckersTestModule(Checkers, { subscribe: false });
15
- const show = spyEvent(observer, SUBSCRIBE_EVENTS.SHOW);
16
-
17
- await (module as Checkers).init();
18
- await (module as Checkers).onLineExec({ line: "show" });
19
-
20
- expect(show).toHaveBeenCalledOnce();
21
- });
22
-
23
- it("line exec ignores unknown action", async () => {
24
- const { module, observer } = createCheckersTestModule(Checkers, { subscribe: false });
25
- const show = spyEvent(observer, SUBSCRIBE_EVENTS.SHOW);
26
-
27
- await (module as Checkers).init();
28
- await (module as Checkers).onLineExec({ line: "open" });
29
-
30
- expect(show).not.toHaveBeenCalled();
31
- });
32
-
33
- it("line exec ai sets shared.checkersAi and emits next", async () => {
34
- const { module } = createCheckersTestModule(Checkers, { subscribe: false });
35
- const checkers = module as Checkers;
36
- const emitNext = vi.spyOn(checkers, "emitNext");
37
-
38
- await checkers.postinject();
39
- await checkers.init();
40
- await checkers.onLineExec({ line: "ai black strong" });
41
-
42
- expect(checkers.shared.checkersAi).toEqual({ white: "player", black: "strong" });
43
- expect(emitNext).toHaveBeenCalledOnce();
44
- });
45
-
46
- it("line exec ai with invalid args still emits next", async () => {
47
- const { module } = createCheckersTestModule(Checkers, { subscribe: false });
48
- const checkers = module as Checkers;
49
- const emitNext = vi.spyOn(checkers, "emitNext");
50
-
51
- await checkers.postinject();
52
- await checkers.init();
53
- await checkers.onLineExec({ line: "ai red random" });
54
-
55
- expect(checkers.shared.checkersAi).toEqual({ white: "player", black: "player" });
56
- expect(emitNext).toHaveBeenCalledOnce();
57
- });
58
- });
package/src/utils/ai.ts DELETED
@@ -1,31 +0,0 @@
1
- import { applyMove, getAllMoves, oppositeColor, type CheckersBoard, type CheckersColor, type CheckersMove } from "./board.js";
2
-
3
- export type CheckersAiType = "player" | "sacrifice" | "random" | "strong";
4
-
5
- export const isHangingMove = (board: CheckersBoard, move: CheckersMove, color: CheckersColor) => {
6
- const nextBoard = applyMove(board, move);
7
- const opponentMoves = getAllMoves(nextBoard, oppositeColor(color));
8
- return opponentMoves.some((candidate) => candidate.captures.length > 0);
9
- };
10
-
11
- const pickRandom = <T>(items: T[]): T | null => {
12
- if (items.length === 0) return null;
13
- return items[Math.floor(Math.random() * items.length)] ?? null;
14
- };
15
-
16
- export const pickMove = (board: CheckersBoard, color: CheckersColor, type: CheckersAiType): CheckersMove | null => {
17
- if (type === "player") return null;
18
-
19
- const moves = getAllMoves(board, color);
20
- if (moves.length === 0) return null;
21
-
22
- if (type === "random") return pickRandom(moves);
23
-
24
- const hanging = moves.filter((move) => isHangingMove(board, move, color));
25
- const safe = moves.filter((move) => !isHangingMove(board, move, color));
26
-
27
- if (type === "sacrifice") return pickRandom(hanging.length > 0 ? hanging : moves);
28
- if (type === "strong") return pickRandom(safe.length > 0 ? safe : hanging.length > 0 ? hanging : moves);
29
-
30
- return pickRandom(moves);
31
- };