@vnejs/plugins.views.screens.checkers 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.
@@ -0,0 +1 @@
1
+ import "@vnejs/contracts.views.screens.checkers";
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ import "@vnejs/contracts.views.screens.checkers";
2
+ import { regPlugin } from "@vnejs/shared";
3
+ import { CONSTANTS, PARAMS, PLUGIN_NAME, SUBSCRIBE_EVENTS } from "@vnejs/contracts.views.screens.checkers";
4
+ import { CheckersController } from "./modules/controller.js";
5
+ import { CheckersControllerAccept } from "./modules/controller.accept.js";
6
+ import { CheckersControllerGrid } from "./modules/controller.grid.js";
7
+ import { Checkers } from "./modules/checkers.js";
8
+ import { CheckersView } from "./modules/view.js";
9
+ regPlugin(PLUGIN_NAME, { constants: CONSTANTS, events: SUBSCRIBE_EVENTS, params: PARAMS }, [
10
+ CheckersController,
11
+ CheckersControllerGrid,
12
+ CheckersControllerAccept,
13
+ Checkers,
14
+ CheckersView,
15
+ ]);
@@ -0,0 +1,9 @@
1
+ import { ModuleCore } from "@vnejs/module.core";
2
+ import type { LineExecHandlerArg } from "@vnejs/module.core";
3
+ import type { CheckersPluginConstants, CheckersPluginEvents, CheckersPluginParams, CheckersPluginSettings } from "../types.js";
4
+ export declare class Checkers extends ModuleCore<CheckersPluginEvents, CheckersPluginConstants, CheckersPluginSettings, CheckersPluginParams> {
5
+ name: string;
6
+ postinject: () => void;
7
+ init: () => Promise<unknown[]> | undefined;
8
+ onLineExec: ({ line }?: LineExecHandlerArg) => Promise<unknown[]> | undefined;
9
+ }
@@ -0,0 +1,37 @@
1
+ import { tokenizeExecLine } from "@vnejs/helpers";
2
+ import { ModuleCore } from "@vnejs/module.core";
3
+ const AI_COLOR_VALUES = new Set(["white", "black"]);
4
+ const AI_TYPE_VALUES = new Set(["player", "sacrifice", "random", "strong"]);
5
+ export class Checkers extends ModuleCore {
6
+ name = "checkers";
7
+ postinject = () => {
8
+ this.shared.checkersAi ??= {
9
+ white: this.CONST.CHECKERS.AI_TYPES.PLAYER,
10
+ black: this.CONST.CHECKERS.AI_TYPES.PLAYER,
11
+ };
12
+ this.shared.checkersAiBusy ??= false;
13
+ };
14
+ init = () => this.emit(this.EVENTS.SCENARIO.LINE_EXEC_REG, { module: this.name, handler: this.onLineExec });
15
+ onLineExec = ({ line = "" } = {}) => {
16
+ const [action = "", colorRaw = "", typeRaw = ""] = tokenizeExecLine(line).map(String);
17
+ if (action === this.CONST.CHECKERS.EXEC_ACTIONS.SHOW) {
18
+ this.emitLog({ action });
19
+ return this.emit(this.EVENTS.CHECKERS.SHOW);
20
+ }
21
+ if (action !== this.CONST.CHECKERS.EXEC_ACTIONS.AI)
22
+ return;
23
+ if (!AI_COLOR_VALUES.has(colorRaw) || !AI_TYPE_VALUES.has(typeRaw)) {
24
+ this.emitLog({ action, color: colorRaw, type: typeRaw, error: "invalid_args" });
25
+ this.emitNext();
26
+ return;
27
+ }
28
+ const color = colorRaw;
29
+ const type = typeRaw;
30
+ this.shared.checkersAi = {
31
+ ...this.shared.checkersAi,
32
+ [color]: type,
33
+ };
34
+ this.emitLog({ action, color, type });
35
+ this.emitNext();
36
+ };
37
+ }
@@ -0,0 +1,30 @@
1
+ import { ModuleControllerAccept } from "@vnejs/module.controller.accept";
2
+ import type { CheckersAiType } from "@vnejs/contracts.views.screens.checkers";
3
+ import type { CheckersPluginConstants, CheckersPluginEvents, CheckersPluginParams, CheckersPluginSettings } from "../types.js";
4
+ import { type CheckersPos } from "../utils/board.js";
5
+ import { type CheckersCellClickPayload, type CheckersPluginState } from "../utils/checkers.js";
6
+ export declare class CheckersControllerAccept extends ModuleControllerAccept<CheckersPluginEvents, CheckersPluginConstants, CheckersPluginSettings, CheckersPluginParams, CheckersPluginState> {
7
+ name: string;
8
+ EVENT_SHOW: "vne:checkers:show";
9
+ EVENT_HIDE: "vne:checkers:hide";
10
+ EVENT_ACCEPT: "vne:checkers:accept";
11
+ EVENT_STATE_UPDATE: "vne:checkers:state_update";
12
+ CONTROLS_ZINDEX: number;
13
+ subscribe: () => void;
14
+ onShow: () => undefined;
15
+ onShowAndMaybeAi: () => Promise<void>;
16
+ onHide: () => Promise<unknown[]> | undefined;
17
+ controllerFor: (turn: NonNullable<CheckersPluginState["turn"]>) => CheckersAiType;
18
+ isHumanTurn: () => boolean;
19
+ onCellClick: ({ row, column }?: CheckersCellClickPayload) => Promise<void>;
20
+ onAccept: () => Promise<void>;
21
+ onAiTurn: () => Promise<void>;
22
+ playMove: (from: CheckersPos, to: CheckersPos, { scheduleAi }?: {
23
+ scheduleAi?: boolean;
24
+ }) => Promise<boolean>;
25
+ emitBoardState: (patch: Partial<CheckersPluginState> & {
26
+ board?: CheckersPluginState["board"];
27
+ selected?: CheckersPluginState["selected"];
28
+ legalMoves?: CheckersPluginState["legalMoves"];
29
+ }) => Promise<void>;
30
+ }
@@ -0,0 +1,165 @@
1
+ import { ModuleControllerAccept } from "@vnejs/module.controller.accept";
2
+ import { applyMove, findMove, getAllMoves, getWinner, oppositeColor, samePos } from "../utils/board.js";
3
+ import { pickMove } from "../utils/ai.js";
4
+ import { buildGridItems, statusForTurn } from "../utils/checkers.js";
5
+ export class CheckersControllerAccept extends ModuleControllerAccept {
6
+ name = "checkers.controller.accept";
7
+ EVENT_SHOW = this.EVENTS.CHECKERS.SHOW;
8
+ EVENT_HIDE = this.EVENTS.CHECKERS.HIDE;
9
+ EVENT_ACCEPT = this.EVENTS.CHECKERS.ACCEPT;
10
+ EVENT_STATE_UPDATE = this.EVENTS.CHECKERS.STATE_UPDATE;
11
+ CONTROLS_ZINDEX = this.PARAMS.CHECKERS.ZINDEX;
12
+ subscribe = () => {
13
+ this.on(this.EVENT_SHOW, this.onShow);
14
+ this.on(this.EVENT_HIDE, this.onHide);
15
+ this.on(this.EVENT_ACCEPT, this.onAccept);
16
+ if (this.EVENT_STATE_UPDATE)
17
+ this.on(this.EVENT_STATE_UPDATE, this.updateState);
18
+ this.on(this.EVENTS.CHECKERS.CELL_CLICK, this.onCellClick);
19
+ this.on(this.EVENTS.CHECKERS.AI_TURN, this.onAiTurn);
20
+ };
21
+ onShow = () => void this.onShowAndMaybeAi();
22
+ onShowAndMaybeAi = async () => {
23
+ await this.emit(this.EVENTS.CONTROLS.PUSH, {
24
+ key: this.name,
25
+ controls: this.CONTROLS,
26
+ index: this.CONTROLS_ZINDEX + 1,
27
+ checkNext: true,
28
+ });
29
+ // SHOW handlers run in parallel with controller.beforeShow — wait for board.
30
+ await this.waitCheck(() => Boolean(this.state.board && this.state.turn));
31
+ await this.onAiTurn();
32
+ };
33
+ onHide = () => {
34
+ this.shared.checkersAiBusy = false;
35
+ this.setDefaultState();
36
+ return this.emit(this.EVENTS.CONTROLS.POP, { key: this.name });
37
+ };
38
+ controllerFor = (turn) => (this.state.ai?.[turn] ?? this.shared.checkersAi?.[turn] ?? this.CONST.CHECKERS.AI_TYPES.PLAYER);
39
+ isHumanTurn = () => {
40
+ const turn = this.state.turn;
41
+ if (!turn)
42
+ return false;
43
+ return this.controllerFor(turn) === this.CONST.CHECKERS.AI_TYPES.PLAYER;
44
+ };
45
+ onCellClick = async ({ row, column } = {}) => {
46
+ if (this.shared.checkersAiBusy || !this.isHumanTurn())
47
+ return;
48
+ if (row == null || column == null)
49
+ return;
50
+ await this.emit(this.EVENTS.CHECKERS.STATE_UPDATE, {
51
+ gridRow: row,
52
+ gridColumn: column,
53
+ });
54
+ await this.emit(this.EVENTS.CHECKERS.UPDATE_FAST);
55
+ await this.onAccept();
56
+ };
57
+ onAccept = async () => {
58
+ if (this.shared.checkersAiBusy)
59
+ return;
60
+ if (this.state.winner) {
61
+ await this.emit(this.EVENTS.CHECKERS.FINISH);
62
+ return;
63
+ }
64
+ if (!this.isHumanTurn())
65
+ return;
66
+ const { gridRow, gridColumn, board, turn, selected } = this.state;
67
+ if (gridRow == null || gridColumn == null || !board || !turn)
68
+ return;
69
+ const focus = { row: gridRow, column: gridColumn };
70
+ const piece = board[focus.row]?.[focus.column] ?? null;
71
+ if (selected && findMove(this.state.legalMoves ?? [], selected, focus)) {
72
+ await this.playMove(selected, focus);
73
+ return;
74
+ }
75
+ if (piece && piece.color === turn) {
76
+ const all = getAllMoves(board, turn);
77
+ const legalMoves = all.filter((move) => samePos(move.from, focus));
78
+ if (legalMoves.length === 0)
79
+ return;
80
+ const nextSelected = selected && samePos(selected, focus) ? null : focus;
81
+ const nextLegal = nextSelected ? legalMoves : [];
82
+ await this.emitBoardState({
83
+ selected: nextSelected,
84
+ legalMoves: nextLegal,
85
+ });
86
+ return;
87
+ }
88
+ if (selected) {
89
+ await this.emitBoardState({ selected: null, legalMoves: [] });
90
+ }
91
+ };
92
+ onAiTurn = async () => {
93
+ if (this.state.winner || !this.state.board || !this.state.turn)
94
+ return;
95
+ if (this.isHumanTurn())
96
+ return;
97
+ if (this.shared.checkersAiBusy)
98
+ return;
99
+ this.shared.checkersAiBusy = true;
100
+ try {
101
+ while (!this.state.winner && this.state.board && this.state.turn && !this.isHumanTurn()) {
102
+ await this.waitTimeout(this.PARAMS.CHECKERS.AI_DELAY);
103
+ const { board, turn } = this.state;
104
+ if (!board || !turn || this.state.winner)
105
+ break;
106
+ const move = pickMove(board, turn, this.controllerFor(turn));
107
+ if (!move)
108
+ break;
109
+ const played = await this.playMove(move.from, move.to, { scheduleAi: false });
110
+ if (!played)
111
+ break;
112
+ }
113
+ }
114
+ finally {
115
+ this.shared.checkersAiBusy = false;
116
+ }
117
+ };
118
+ playMove = async (from, to, { scheduleAi = true } = {}) => {
119
+ const { board, turn } = this.state;
120
+ if (!board || !turn)
121
+ return false;
122
+ // Always resolve against current legal moves — `legalMoves: []` after a move is truthy and must not win over getAllMoves.
123
+ const move = findMove(getAllMoves(board, turn), from, to);
124
+ if (!move)
125
+ return false;
126
+ const nextBoard = applyMove(board, move);
127
+ const nextTurn = oppositeColor(turn);
128
+ const winner = getWinner(nextBoard, nextTurn);
129
+ await this.emitBoardState({
130
+ board: nextBoard,
131
+ turn: nextTurn,
132
+ selected: null,
133
+ legalMoves: [],
134
+ winner,
135
+ statusText: statusForTurn(nextTurn, winner),
136
+ });
137
+ if (winner) {
138
+ await this.waitTimeout(this.PARAMS.CHECKERS.TRANSITION);
139
+ await this.emit(this.EVENTS.CHECKERS.FINISH);
140
+ return true;
141
+ }
142
+ if (scheduleAi)
143
+ await this.onAiTurn();
144
+ return true;
145
+ };
146
+ emitBoardState = async (patch) => {
147
+ const board = patch.board ?? this.state.board;
148
+ if (!board)
149
+ return;
150
+ const selected = patch.selected !== undefined ? patch.selected : this.state.selected;
151
+ const legalMoves = patch.legalMoves !== undefined ? patch.legalMoves : this.state.legalMoves;
152
+ const gridItems = buildGridItems({
153
+ board,
154
+ selected: selected ?? null,
155
+ legalMoves: legalMoves ?? [],
156
+ gridRow: this.state.gridRow ?? null,
157
+ gridColumn: this.state.gridColumn ?? null,
158
+ });
159
+ await this.emit(this.EVENTS.CHECKERS.STATE_UPDATE, {
160
+ ...patch,
161
+ gridItems,
162
+ });
163
+ await this.emit(this.EVENTS.CHECKERS.UPDATE_FAST);
164
+ };
165
+ }
@@ -0,0 +1,13 @@
1
+ import { ModuleController } from "@vnejs/module.components";
2
+ import type { CheckersPluginConstants, CheckersPluginEvents, CheckersPluginParams, CheckersPluginSettings } from "../types.js";
3
+ import { type CheckersPluginState } from "../utils/checkers.js";
4
+ export declare class CheckersController extends ModuleController<CheckersPluginEvents, CheckersPluginConstants, CheckersPluginSettings, CheckersPluginParams, CheckersPluginState> {
5
+ name: string;
6
+ EVENT_UPDATE_VIEW: "vne:checkers:update";
7
+ CONTROLS: {};
8
+ CONTROLS_INDEX: number;
9
+ subscribe: () => void;
10
+ beforeShow: () => Promise<void>;
11
+ afterHide: () => void;
12
+ onFinish: () => Promise<void>;
13
+ }
@@ -0,0 +1,20 @@
1
+ import { ModuleControllerArrowsGrid } from "@vnejs/module.controller.arrows.grid";
2
+ import type { CheckersPluginConstants, CheckersPluginEvents, CheckersPluginParams, CheckersPluginSettings } from "../types.js";
3
+ import { type CheckersPluginState } from "../utils/checkers.js";
4
+ export declare class CheckersControllerGrid extends ModuleControllerArrowsGrid<CheckersPluginEvents, CheckersPluginConstants, CheckersPluginSettings, CheckersPluginParams, CheckersPluginState> {
5
+ name: string;
6
+ EVENT_SHOW: "vne:checkers:show";
7
+ EVENT_HIDE: "vne:checkers:hide";
8
+ EVENT_STATE_UPDATE: "vne:checkers:state_update";
9
+ EVENT_UPDATE_FAST: "vne:checkers:update_fast";
10
+ EVENT_GRID_NEXT: "vne:checkers:grid_next";
11
+ EVENT_GRID_PREV: "vne:checkers:grid_prev";
12
+ EVENT_GRID_CLEAR: "vne:checkers:grid_clear";
13
+ EVENT_GRID_UPDATE_ITEMS: "vne:checkers:grid_update_items";
14
+ CONTROLS_ZINDEX: number;
15
+ isDisableGridChangeHorizontal: boolean;
16
+ isDisableGridChangeVertical: boolean;
17
+ getMaxRows: () => number;
18
+ getMaxColumns: () => number;
19
+ getGridItems: () => Promise<import("../utils/checkers.js").CheckersGridItem[]>;
20
+ }
@@ -0,0 +1,29 @@
1
+ import { ModuleControllerArrowsGrid } from "@vnejs/module.controller.arrows.grid";
2
+ import { buildGridItems } from "../utils/checkers.js";
3
+ import { createInitialBoard } from "../utils/board.js";
4
+ export class CheckersControllerGrid extends ModuleControllerArrowsGrid {
5
+ name = "checkers.controller.grid";
6
+ EVENT_SHOW = this.EVENTS.CHECKERS.SHOW;
7
+ EVENT_HIDE = this.EVENTS.CHECKERS.HIDE;
8
+ EVENT_STATE_UPDATE = this.EVENTS.CHECKERS.STATE_UPDATE;
9
+ EVENT_UPDATE_FAST = this.EVENTS.CHECKERS.UPDATE_FAST;
10
+ EVENT_GRID_NEXT = this.EVENTS.CHECKERS.GRID_NEXT;
11
+ EVENT_GRID_PREV = this.EVENTS.CHECKERS.GRID_PREV;
12
+ EVENT_GRID_CLEAR = this.EVENTS.CHECKERS.GRID_CLEAR;
13
+ EVENT_GRID_UPDATE_ITEMS = this.EVENTS.CHECKERS.GRID_UPDATE_ITEMS;
14
+ CONTROLS_ZINDEX = this.PARAMS.CHECKERS.ZINDEX;
15
+ isDisableGridChangeHorizontal = true;
16
+ isDisableGridChangeVertical = true;
17
+ getMaxRows = () => this.PARAMS.CHECKERS.ROWS;
18
+ getMaxColumns = () => this.PARAMS.CHECKERS.COLUMNS;
19
+ getGridItems = async () => {
20
+ const board = this.state.board ?? createInitialBoard();
21
+ return buildGridItems({
22
+ board,
23
+ selected: this.state.selected ?? null,
24
+ legalMoves: this.state.legalMoves ?? [],
25
+ gridRow: this.state.gridRow ?? null,
26
+ gridColumn: this.state.gridColumn ?? null,
27
+ });
28
+ };
29
+ }
@@ -0,0 +1,34 @@
1
+ import { ModuleController } from "@vnejs/module.components";
2
+ import { createInitialCheckersState } from "../utils/checkers.js";
3
+ export class CheckersController extends ModuleController {
4
+ name = "checkers.controller";
5
+ EVENT_UPDATE_VIEW = this.EVENTS.CHECKERS.UPDATE;
6
+ CONTROLS = {};
7
+ CONTROLS_INDEX = this.PARAMS.CHECKERS.ZINDEX;
8
+ subscribe = () => {
9
+ this.on(this.EVENTS.CHECKERS.SHOW, this.onShow);
10
+ this.on(this.EVENTS.CHECKERS.HIDE, this.onHide);
11
+ this.on(this.EVENTS.CHECKERS.STATE_UPDATE, this.updateState);
12
+ this.on(this.EVENTS.CHECKERS.UPDATE_FAST, this.updateViewFast);
13
+ this.on(this.EVENTS.CHECKERS.FINISH, this.onFinish);
14
+ };
15
+ beforeShow = async () => {
16
+ this.shared.checkersAiBusy = false;
17
+ const initial = createInitialCheckersState();
18
+ const ai = {
19
+ white: this.shared.checkersAi?.white ?? this.CONST.CHECKERS.AI_TYPES.PLAYER,
20
+ black: this.shared.checkersAi?.black ?? this.CONST.CHECKERS.AI_TYPES.PLAYER,
21
+ };
22
+ await this.emit(this.EVENTS.CHECKERS.STATE_UPDATE, { ...initial, ai });
23
+ await this.emit(this.EVENTS.CHECKERS.GRID_UPDATE_ITEMS);
24
+ };
25
+ afterHide = () => {
26
+ this.shared.checkersAiBusy = false;
27
+ this.setDefaultState();
28
+ };
29
+ onFinish = async () => {
30
+ this.shared.checkersAiBusy = false;
31
+ await this.emit(this.EVENTS.CHECKERS.HIDE);
32
+ this.emitNext();
33
+ };
34
+ }
@@ -0,0 +1,10 @@
1
+ import { ModuleView } from "@vnejs/module.components";
2
+ import type { CheckersPluginConstants, CheckersPluginEvents, CheckersPluginParams, CheckersPluginSettings } from "../types.js";
3
+ import type { CheckersPluginState } from "../utils/checkers.js";
4
+ export declare class CheckersView extends ModuleView<CheckersPluginEvents, CheckersPluginConstants, CheckersPluginSettings, CheckersPluginParams, CheckersPluginState> {
5
+ name: string;
6
+ EVENT_UPDATE_VIEW: "vne:checkers:update";
7
+ LOC_LABEL: null;
8
+ TRANSITION: number;
9
+ renderFunc: import("@vnejs/module.components").ViewRenderFunc<CheckersPluginState>;
10
+ }
@@ -0,0 +1,9 @@
1
+ import { ModuleView } from "@vnejs/module.components";
2
+ import { render } from "../view/index.js";
3
+ export class CheckersView extends ModuleView {
4
+ name = "checkers.view";
5
+ EVENT_UPDATE_VIEW = this.EVENTS.CHECKERS.UPDATE;
6
+ LOC_LABEL = null;
7
+ TRANSITION = this.PARAMS.CHECKERS.TRANSITION;
8
+ renderFunc = render;
9
+ }
@@ -0,0 +1,9 @@
1
+ import type { ModuleComponentsConstants, ModuleComponentsEvents, ModuleComponentsParams, ModuleComponentsSettings } from "@vnejs/module.components";
2
+ import type { Constants as ControlsConstants, PluginName as ControlsPluginName } from "@vnejs/contracts.controls";
3
+ import type { Constants as ScenarioConstants, PluginName as ScenarioPluginName, SubscribeEvents as ScenarioSubscribeEvents } from "@vnejs/contracts.core.scenario";
4
+ import type { PluginName as LogsPluginName, SubscribeEvents as LogsSubscribeEvents } from "@vnejs/contracts.core.logs";
5
+ import type { Constants, Params, PluginName, SubscribeEvents } from "@vnejs/contracts.views.screens.checkers";
6
+ export type CheckersPluginEvents = ModuleComponentsEvents & Record<PluginName, SubscribeEvents> & Record<ScenarioPluginName, ScenarioSubscribeEvents> & Record<LogsPluginName, LogsSubscribeEvents>;
7
+ export type CheckersPluginConstants = ModuleComponentsConstants & Record<PluginName, Constants> & Record<ControlsPluginName, ControlsConstants> & Record<ScenarioPluginName, ScenarioConstants>;
8
+ export type CheckersPluginSettings = ModuleComponentsSettings;
9
+ export type CheckersPluginParams = ModuleComponentsParams & Record<PluginName, Params>;
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import { type CheckersBoard, type CheckersColor, type CheckersMove } from "./board.js";
2
+ export type CheckersAiType = "player" | "sacrifice" | "random" | "strong";
3
+ export declare const isHangingMove: (board: CheckersBoard, move: CheckersMove, color: CheckersColor) => boolean;
4
+ export declare const pickMove: (board: CheckersBoard, color: CheckersColor, type: CheckersAiType) => CheckersMove | null;
@@ -0,0 +1,27 @@
1
+ import { applyMove, getAllMoves, oppositeColor } from "./board.js";
2
+ export const isHangingMove = (board, move, color) => {
3
+ const nextBoard = applyMove(board, move);
4
+ const opponentMoves = getAllMoves(nextBoard, oppositeColor(color));
5
+ return opponentMoves.some((candidate) => candidate.captures.length > 0);
6
+ };
7
+ const pickRandom = (items) => {
8
+ if (items.length === 0)
9
+ return null;
10
+ return items[Math.floor(Math.random() * items.length)] ?? null;
11
+ };
12
+ export const pickMove = (board, color, type) => {
13
+ if (type === "player")
14
+ return null;
15
+ const moves = getAllMoves(board, color);
16
+ if (moves.length === 0)
17
+ return null;
18
+ if (type === "random")
19
+ return pickRandom(moves);
20
+ const hanging = moves.filter((move) => isHangingMove(board, move, color));
21
+ const safe = moves.filter((move) => !isHangingMove(board, move, color));
22
+ if (type === "sacrifice")
23
+ return pickRandom(hanging.length > 0 ? hanging : moves);
24
+ if (type === "strong")
25
+ return pickRandom(safe.length > 0 ? safe : hanging.length > 0 ? hanging : moves);
26
+ return pickRandom(moves);
27
+ };
@@ -0,0 +1,32 @@
1
+ export type CheckersColor = "white" | "black";
2
+ export type CheckersPiece = {
3
+ id: string;
4
+ color: CheckersColor;
5
+ king: boolean;
6
+ };
7
+ export type CheckersPos = {
8
+ row: number;
9
+ column: number;
10
+ };
11
+ export type CheckersMove = {
12
+ from: CheckersPos;
13
+ to: CheckersPos;
14
+ captures: CheckersPos[];
15
+ };
16
+ export type CheckersBoard = (CheckersPiece | null)[][];
17
+ export declare const BOARD_SIZE = 8;
18
+ export declare const oppositeColor: (color: CheckersColor) => CheckersColor;
19
+ export declare const isDarkSquare: (row: number, column: number) => boolean;
20
+ export declare const inBounds: (row: number, column: number) => boolean;
21
+ export declare const cloneBoard: (board: CheckersBoard) => CheckersBoard;
22
+ export declare const createInitialBoard: () => CheckersBoard;
23
+ export declare const samePos: (a: CheckersPos, b: CheckersPos) => boolean;
24
+ export declare const getMovesFrom: (board: CheckersBoard, from: CheckersPos) => CheckersMove[];
25
+ export declare const getAllMoves: (board: CheckersBoard, color: CheckersColor, onlyFrom?: CheckersPos) => CheckersMove[];
26
+ export declare const applyMove: (board: CheckersBoard, move: CheckersMove) => CheckersBoard;
27
+ /** Apply one hop of a multi-capture chain (first capture only). */
28
+ export declare const applyCaptureStep: (board: CheckersBoard, from: CheckersPos, to: CheckersPos, capture: CheckersPos) => CheckersBoard;
29
+ export declare const listBoardPieces: (board: CheckersBoard) => (CheckersPiece & CheckersPos)[];
30
+ export declare const countPieces: (board: CheckersBoard, color: CheckersColor) => number;
31
+ export declare const getWinner: (board: CheckersBoard, colorToMove: CheckersColor) => CheckersColor | null;
32
+ export declare const findMove: (moves: CheckersMove[], from: CheckersPos, to: CheckersPos) => CheckersMove | undefined;
@@ -0,0 +1,199 @@
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));