@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.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -0
- package/dist/modules/checkers.d.ts +9 -0
- package/dist/modules/checkers.js +37 -0
- package/dist/modules/controller.accept.d.ts +30 -0
- package/dist/modules/controller.accept.js +165 -0
- package/dist/modules/controller.d.ts +13 -0
- package/dist/modules/controller.grid.d.ts +20 -0
- package/dist/modules/controller.grid.js +29 -0
- package/dist/modules/controller.js +34 -0
- package/dist/modules/view.d.ts +10 -0
- package/dist/modules/view.js +9 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.js +1 -0
- package/dist/utils/ai.d.ts +4 -0
- package/dist/utils/ai.js +27 -0
- package/dist/utils/board.d.ts +32 -0
- package/dist/utils/board.js +199 -0
- package/dist/utils/checkers.d.ts +42 -0
- package/dist/utils/checkers.js +40 -0
- package/dist/view/index.d.ts +3 -0
- package/dist/view/index.js +196 -0
- package/package.json +59 -0
- package/src/index.ts +18 -0
- package/src/modules/checkers.ts +52 -0
- package/src/modules/controller.accept.ts +202 -0
- package/src/modules/controller.grid.ts +43 -0
- package/src/modules/controller.ts +50 -0
- package/src/modules/view.ts +17 -0
- package/src/tests/ai.test.ts +61 -0
- package/src/tests/board.test.ts +43 -0
- package/src/tests/checkers.test.ts +58 -0
- package/src/tests/setup.ts +34 -0
- package/src/types.ts +23 -0
- package/src/utils/ai.ts +31 -0
- package/src/utils/board.ts +252 -0
- package/src/utils/checkers.ts +96 -0
- package/src/view/index.tsx +309 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CheckersBoard, CheckersColor, CheckersMove, CheckersPiece, CheckersPos } from "./board.js";
|
|
2
|
+
export type CheckersGridItem = {
|
|
3
|
+
row: number;
|
|
4
|
+
column: number;
|
|
5
|
+
isDark: boolean;
|
|
6
|
+
piece: CheckersPiece | null;
|
|
7
|
+
isSelected?: boolean;
|
|
8
|
+
isLegalTarget?: boolean;
|
|
9
|
+
isFocus?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type CheckersPluginState = {
|
|
12
|
+
isShow: boolean;
|
|
13
|
+
isForce: boolean;
|
|
14
|
+
board?: CheckersBoard;
|
|
15
|
+
turn?: CheckersColor;
|
|
16
|
+
selected?: CheckersPos | null;
|
|
17
|
+
legalMoves?: CheckersMove[];
|
|
18
|
+
winner?: CheckersColor | null;
|
|
19
|
+
statusText?: string;
|
|
20
|
+
gridRow?: number | null;
|
|
21
|
+
gridColumn?: number | null;
|
|
22
|
+
gridItems?: CheckersGridItem[];
|
|
23
|
+
gridIndex?: number;
|
|
24
|
+
gridsCount?: number;
|
|
25
|
+
ai?: {
|
|
26
|
+
white: string;
|
|
27
|
+
black: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
export type CheckersCellClickPayload = {
|
|
31
|
+
row?: number;
|
|
32
|
+
column?: number;
|
|
33
|
+
};
|
|
34
|
+
export declare const statusForTurn: (turn: CheckersColor, winner?: CheckersColor | null) => "Black to move" | "Black wins" | "White to move" | "White wins";
|
|
35
|
+
export declare const buildGridItems: ({ board, selected, legalMoves, gridRow, gridColumn, }: {
|
|
36
|
+
board: CheckersBoard;
|
|
37
|
+
selected?: CheckersPos | null;
|
|
38
|
+
legalMoves?: CheckersMove[];
|
|
39
|
+
gridRow?: number | null;
|
|
40
|
+
gridColumn?: number | null;
|
|
41
|
+
}) => CheckersGridItem[];
|
|
42
|
+
export declare const createInitialCheckersState: () => Pick<CheckersPluginState, "board" | "turn" | "selected" | "legalMoves" | "winner" | "statusText" | "gridItems" | "gridIndex" | "gridsCount">;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { BOARD_SIZE, createInitialBoard, getAllMoves, isDarkSquare } from "./board.js";
|
|
2
|
+
export const statusForTurn = (turn, winner = null) => {
|
|
3
|
+
if (winner)
|
|
4
|
+
return winner === "white" ? "White wins" : "Black wins";
|
|
5
|
+
return turn === "white" ? "White to move" : "Black to move";
|
|
6
|
+
};
|
|
7
|
+
export const buildGridItems = ({ board, selected = null, legalMoves = [], gridRow = null, gridColumn = null, }) => {
|
|
8
|
+
const legalKeys = new Set(legalMoves.map((move) => `${move.to.row}:${move.to.column}`));
|
|
9
|
+
const items = [];
|
|
10
|
+
for (let row = 0; row < BOARD_SIZE; row++) {
|
|
11
|
+
for (let column = 0; column < BOARD_SIZE; column++) {
|
|
12
|
+
items.push({
|
|
13
|
+
row,
|
|
14
|
+
column,
|
|
15
|
+
isDark: isDarkSquare(row, column),
|
|
16
|
+
piece: board[row]?.[column] ?? null,
|
|
17
|
+
isSelected: selected?.row === row && selected?.column === column,
|
|
18
|
+
isLegalTarget: legalKeys.has(`${row}:${column}`),
|
|
19
|
+
isFocus: gridRow === row && gridColumn === column,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return items;
|
|
24
|
+
};
|
|
25
|
+
export const createInitialCheckersState = () => {
|
|
26
|
+
const board = createInitialBoard();
|
|
27
|
+
const turn = "white";
|
|
28
|
+
const legalMoves = getAllMoves(board, turn);
|
|
29
|
+
return {
|
|
30
|
+
board,
|
|
31
|
+
turn,
|
|
32
|
+
selected: null,
|
|
33
|
+
legalMoves: [],
|
|
34
|
+
winner: null,
|
|
35
|
+
statusText: statusForTurn(turn),
|
|
36
|
+
gridItems: buildGridItems({ board, legalMoves: [] }),
|
|
37
|
+
gridIndex: 0,
|
|
38
|
+
gridsCount: 1,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Flex, PositionBox, Screen, Text, createRenderFunc, useCallback, useEffect, useMemo, useState, useStoreState } from "@vnejs/uis.react";
|
|
3
|
+
import { getVneLength } from "@vnejs/uis.utils";
|
|
4
|
+
import { listBoardPieces } from "../utils/board.js";
|
|
5
|
+
const SELECTED_OPACITY = 0.7;
|
|
6
|
+
const GRAVEYARD_STACK = 0.55;
|
|
7
|
+
const cellBackground = (item) => {
|
|
8
|
+
if (item.isFocus)
|
|
9
|
+
return "rgba(255, 214, 64, 0.85)";
|
|
10
|
+
if (item.isLegalTarget)
|
|
11
|
+
return "rgba(80, 200, 120, 0.75)";
|
|
12
|
+
if (item.isSelected)
|
|
13
|
+
return "rgba(90, 160, 255, 0.8)";
|
|
14
|
+
return item.isDark ? "rgba(60, 40, 20, 0.95)" : "rgba(220, 200, 160, 0.95)";
|
|
15
|
+
};
|
|
16
|
+
const pieceLook = (piece) => {
|
|
17
|
+
const isWhite = piece.color === "white";
|
|
18
|
+
return {
|
|
19
|
+
width: "62%",
|
|
20
|
+
height: "62%",
|
|
21
|
+
borderRadius: "50%",
|
|
22
|
+
background: isWhite ? "#f4f0e6" : "#1a1a1a",
|
|
23
|
+
border: `${getVneLength(6)} solid ${piece.king ? "#d4af37" : isWhite ? "#bbb" : "#444"}`,
|
|
24
|
+
boxSizing: "border-box",
|
|
25
|
+
display: "flex",
|
|
26
|
+
alignItems: "center",
|
|
27
|
+
justifyContent: "center",
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
const kingMarkLook = () => ({
|
|
31
|
+
width: "50%",
|
|
32
|
+
height: "50%",
|
|
33
|
+
borderRadius: "50%",
|
|
34
|
+
boxSizing: "border-box",
|
|
35
|
+
border: `${getVneLength(6)} solid #d4af37`,
|
|
36
|
+
background: "transparent",
|
|
37
|
+
});
|
|
38
|
+
const CheckersScreen = ({ store, onMount, PARAMS, emit, EVENTS }) => {
|
|
39
|
+
const { isShow = false, isForce = false, board, gridItems = [], gridRow = null, gridColumn = null, selected = null, statusText = "", } = useStoreState(store, onMount);
|
|
40
|
+
const transition = PARAMS.CHECKERS.TRANSITION;
|
|
41
|
+
const propsByView = PARAMS.CHECKERS.VIEW_PROPS;
|
|
42
|
+
const rows = PARAMS.CHECKERS.ROWS;
|
|
43
|
+
const columns = PARAMS.CHECKERS.COLUMNS;
|
|
44
|
+
const cellSize = PARAMS.CHECKERS.CELL_SIZE;
|
|
45
|
+
const cellGap = PARAMS.CHECKERS.CELL_GAP;
|
|
46
|
+
const graveyardGap = PARAMS.CHECKERS.GRAVEYARD_GAP;
|
|
47
|
+
const boardSize = columns * cellSize + (columns - 1) * cellGap;
|
|
48
|
+
const playfieldWidth = cellSize + graveyardGap + boardSize + graveyardGap + cellSize;
|
|
49
|
+
const boardPieces = useMemo(() => (board ? listBoardPieces(board) : []), [board]);
|
|
50
|
+
const selectedId = useMemo(() => {
|
|
51
|
+
if (!selected || !board)
|
|
52
|
+
return null;
|
|
53
|
+
return board[selected.row]?.[selected.column]?.id ?? null;
|
|
54
|
+
}, [board, selected]);
|
|
55
|
+
const [visualPieces, setVisualPieces] = useState([]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!isShow) {
|
|
58
|
+
setVisualPieces([]);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
setVisualPieces((prev) => {
|
|
62
|
+
const nextById = new Map(boardPieces.map((piece) => [piece.id, piece]));
|
|
63
|
+
const next = [];
|
|
64
|
+
let whiteGraveyardCount = 0;
|
|
65
|
+
let blackGraveyardCount = 0;
|
|
66
|
+
for (const piece of prev) {
|
|
67
|
+
if (piece.zone === "graveyard") {
|
|
68
|
+
if (piece.color === "white")
|
|
69
|
+
whiteGraveyardCount = Math.max(whiteGraveyardCount, piece.graveyardIndex + 1);
|
|
70
|
+
else
|
|
71
|
+
blackGraveyardCount = Math.max(blackGraveyardCount, piece.graveyardIndex + 1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const piece of prev) {
|
|
75
|
+
const live = nextById.get(piece.id);
|
|
76
|
+
if (live) {
|
|
77
|
+
next.push({
|
|
78
|
+
id: live.id,
|
|
79
|
+
color: live.color,
|
|
80
|
+
king: live.king,
|
|
81
|
+
row: live.row,
|
|
82
|
+
column: live.column,
|
|
83
|
+
opacity: selectedId === live.id ? SELECTED_OPACITY : 1,
|
|
84
|
+
zone: "board",
|
|
85
|
+
graveyardIndex: 0,
|
|
86
|
+
});
|
|
87
|
+
nextById.delete(piece.id);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (piece.zone === "graveyard") {
|
|
91
|
+
next.push(piece);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const graveyardIndex = piece.color === "white" ? whiteGraveyardCount++ : blackGraveyardCount++;
|
|
95
|
+
next.push({
|
|
96
|
+
...piece,
|
|
97
|
+
opacity: 1,
|
|
98
|
+
zone: "graveyard",
|
|
99
|
+
graveyardIndex,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
for (const piece of nextById.values()) {
|
|
103
|
+
next.push({
|
|
104
|
+
id: piece.id,
|
|
105
|
+
color: piece.color,
|
|
106
|
+
king: piece.king,
|
|
107
|
+
row: piece.row,
|
|
108
|
+
column: piece.column,
|
|
109
|
+
opacity: selectedId === piece.id ? SELECTED_OPACITY : 1,
|
|
110
|
+
zone: "board",
|
|
111
|
+
graveyardIndex: 0,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return next;
|
|
115
|
+
});
|
|
116
|
+
}, [boardPieces, isShow, selectedId]);
|
|
117
|
+
const propsScreen = useMemo(() => ({ ...propsByView.screen, isShow, isForce }), [isShow, isForce, propsByView.screen]);
|
|
118
|
+
const propsStatus = useMemo(() => ({ ...propsByView.status, text: statusText, width: playfieldWidth }), [statusText, propsByView.status, playfieldWidth]);
|
|
119
|
+
const cellLen = useMemo(() => getVneLength(cellSize), [cellSize]);
|
|
120
|
+
const gapLen = useMemo(() => getVneLength(cellGap), [cellGap]);
|
|
121
|
+
const graveyardGapLen = useMemo(() => getVneLength(graveyardGap), [graveyardGap]);
|
|
122
|
+
const stepLen = useMemo(() => (cellGap ? `calc(${cellLen} + ${gapLen})` : cellLen), [cellGap, cellLen, gapLen]);
|
|
123
|
+
const boardOffsetX = useMemo(() => `calc(${cellLen} + ${graveyardGapLen})`, [cellLen, graveyardGapLen]);
|
|
124
|
+
const blackGraveyardX = useMemo(() => `calc(${cellLen} + ${graveyardGapLen} + ${getVneLength(boardSize)} + ${graveyardGapLen})`, [boardSize, cellLen, graveyardGapLen]);
|
|
125
|
+
const playfieldStyle = useMemo(() => ({
|
|
126
|
+
position: "relative",
|
|
127
|
+
width: getVneLength(playfieldWidth),
|
|
128
|
+
height: getVneLength(boardSize),
|
|
129
|
+
boxSizing: "border-box",
|
|
130
|
+
}), [boardSize, playfieldWidth]);
|
|
131
|
+
const boardStyle = useMemo(() => ({
|
|
132
|
+
position: "absolute",
|
|
133
|
+
top: 0,
|
|
134
|
+
left: boardOffsetX,
|
|
135
|
+
display: "grid",
|
|
136
|
+
gridTemplateColumns: `repeat(${columns}, ${cellLen})`,
|
|
137
|
+
gridTemplateRows: `repeat(${rows}, ${cellLen})`,
|
|
138
|
+
gap: gapLen,
|
|
139
|
+
width: getVneLength(boardSize),
|
|
140
|
+
height: getVneLength(boardSize),
|
|
141
|
+
boxSizing: "border-box",
|
|
142
|
+
}), [boardOffsetX, boardSize, cellLen, columns, gapLen, rows]);
|
|
143
|
+
const graveyardStyle = useCallback((side) => ({
|
|
144
|
+
position: "absolute",
|
|
145
|
+
top: 0,
|
|
146
|
+
left: side === "left" ? 0 : blackGraveyardX,
|
|
147
|
+
width: cellLen,
|
|
148
|
+
height: getVneLength(boardSize),
|
|
149
|
+
boxSizing: "border-box",
|
|
150
|
+
borderRadius: getVneLength(12),
|
|
151
|
+
background: "rgba(255, 255, 255, 0.06)",
|
|
152
|
+
}), [blackGraveyardX, boardSize, cellLen]);
|
|
153
|
+
const cellBaseStyle = useMemo(() => ({
|
|
154
|
+
width: cellLen,
|
|
155
|
+
height: cellLen,
|
|
156
|
+
boxSizing: "border-box",
|
|
157
|
+
transition: `background-color ${transition}ms`,
|
|
158
|
+
}), [cellLen, transition]);
|
|
159
|
+
const piecesLayerStyle = useMemo(() => ({
|
|
160
|
+
position: "absolute",
|
|
161
|
+
inset: 0,
|
|
162
|
+
pointerEvents: "none",
|
|
163
|
+
}), []);
|
|
164
|
+
const pieceTransform = useCallback((piece) => {
|
|
165
|
+
if (piece.zone === "graveyard") {
|
|
166
|
+
const x = piece.color === "white" ? "0px" : blackGraveyardX;
|
|
167
|
+
const y = `calc(${piece.graveyardIndex} * ${cellLen} * ${GRAVEYARD_STACK})`;
|
|
168
|
+
return `translate(${x}, ${y})`;
|
|
169
|
+
}
|
|
170
|
+
return `translate(calc(${boardOffsetX} + ${piece.column} * ${stepLen}), calc(${piece.row} * ${stepLen}))`;
|
|
171
|
+
}, [blackGraveyardX, boardOffsetX, cellLen, stepLen]);
|
|
172
|
+
const onCellClick = useCallback((row, column) => {
|
|
173
|
+
void emit(EVENTS.CHECKERS.CELL_CLICK, { row, column });
|
|
174
|
+
}, [emit, EVENTS.CHECKERS.CELL_CLICK]);
|
|
175
|
+
return (_jsx(Screen, { ...propsScreen, children: _jsx(PositionBox, { ...propsByView.position, children: _jsxs(Flex, { ...propsByView.flex, children: [_jsx(Text, { ...propsStatus }), _jsxs("div", { style: playfieldStyle, children: [_jsx("div", { style: graveyardStyle("left") }), _jsx("div", { style: graveyardStyle("right") }), _jsx("div", { style: boardStyle, children: gridItems.map((item) => {
|
|
176
|
+
const isFocus = gridRow === item.row && gridColumn === item.column;
|
|
177
|
+
return (_jsx("div", { onClick: () => onCellClick(item.row, item.column), "vne-cursor-type": "pointer", style: {
|
|
178
|
+
...cellBaseStyle,
|
|
179
|
+
background: cellBackground({ ...item, isFocus }),
|
|
180
|
+
} }, `${item.row}-${item.column}`));
|
|
181
|
+
}) }), _jsx("div", { style: piecesLayerStyle, children: visualPieces.map((piece) => (_jsx("div", { style: {
|
|
182
|
+
position: "absolute",
|
|
183
|
+
top: 0,
|
|
184
|
+
left: 0,
|
|
185
|
+
width: cellLen,
|
|
186
|
+
height: cellLen,
|
|
187
|
+
display: "flex",
|
|
188
|
+
alignItems: "center",
|
|
189
|
+
justifyContent: "center",
|
|
190
|
+
transform: pieceTransform(piece),
|
|
191
|
+
opacity: piece.opacity,
|
|
192
|
+
transition: `transform ${transition}ms, opacity ${transition}ms`,
|
|
193
|
+
willChange: "transform, opacity",
|
|
194
|
+
}, children: _jsx("div", { style: pieceLook(piece), children: piece.king ? _jsx("div", { style: kingMarkLook() }) : null }) }, piece.id))) })] })] }) }) }));
|
|
195
|
+
};
|
|
196
|
+
export const render = createRenderFunc(CheckersScreen);
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vnejs/plugins.views.screens.checkers",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src",
|
|
18
|
+
"tsconfig.json"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "npx @vnejs/monorepo test",
|
|
22
|
+
"build": "npx @vnejs/monorepo package",
|
|
23
|
+
"publish:major:plugin": "npm run publish:major",
|
|
24
|
+
"publish:minor:plugin": "npm run publish:minor",
|
|
25
|
+
"publish:patch:plugin": "npm run publish:patch",
|
|
26
|
+
"publish:major": "npx @vnejs/monorepo publish major --access public",
|
|
27
|
+
"publish:minor": "npx @vnejs/monorepo publish minor --access public",
|
|
28
|
+
"publish:patch": "npx @vnejs/monorepo publish patch --access public"
|
|
29
|
+
},
|
|
30
|
+
"author": "",
|
|
31
|
+
"license": "ISC",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@vnejs/contracts.views.screens.checkers": "~0.2.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@vnejs/helpers": "~0.2.0",
|
|
37
|
+
"@vnejs/module.core": "~0.2.0",
|
|
38
|
+
"@vnejs/module.components": "~0.2.0",
|
|
39
|
+
"@vnejs/module.controller.accept": "~0.2.0",
|
|
40
|
+
"@vnejs/module.controller.arrows.grid": "~0.2.0",
|
|
41
|
+
"@vnejs/contracts.controls": "~0.2.0",
|
|
42
|
+
"@vnejs/shared": "~0.2.0",
|
|
43
|
+
"@vnejs/uis.react": "~0.2.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@vnejs/configs.ts-common": "~0.2.0",
|
|
47
|
+
"@vnejs/configs.vitest": "~0.2.0",
|
|
48
|
+
"@vnejs/test-utils": "~0.2.0",
|
|
49
|
+
"@vnejs/contracts.controls": "~0.2.0",
|
|
50
|
+
"@vnejs/contracts.views.screens.checkers": "~0.2.0",
|
|
51
|
+
"@vnejs/helpers": "~0.2.0",
|
|
52
|
+
"@vnejs/module.core": "~0.2.0",
|
|
53
|
+
"@vnejs/module.components": "~0.2.0",
|
|
54
|
+
"@vnejs/module.controller.accept": "~0.2.0",
|
|
55
|
+
"@vnejs/module.controller.arrows.grid": "~0.2.0",
|
|
56
|
+
"@vnejs/shared": "~0.2.0",
|
|
57
|
+
"@vnejs/uis.react": "~0.2.0"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import "@vnejs/contracts.views.screens.checkers";
|
|
2
|
+
|
|
3
|
+
import { regPlugin } from "@vnejs/shared";
|
|
4
|
+
import { CONSTANTS, PARAMS, PLUGIN_NAME, SUBSCRIBE_EVENTS } from "@vnejs/contracts.views.screens.checkers";
|
|
5
|
+
|
|
6
|
+
import { CheckersController } from "./modules/controller.js";
|
|
7
|
+
import { CheckersControllerAccept } from "./modules/controller.accept.js";
|
|
8
|
+
import { CheckersControllerGrid } from "./modules/controller.grid.js";
|
|
9
|
+
import { Checkers } from "./modules/checkers.js";
|
|
10
|
+
import { CheckersView } from "./modules/view.js";
|
|
11
|
+
|
|
12
|
+
regPlugin(PLUGIN_NAME, { constants: CONSTANTS, events: SUBSCRIBE_EVENTS, params: PARAMS }, [
|
|
13
|
+
CheckersController,
|
|
14
|
+
CheckersControllerGrid,
|
|
15
|
+
CheckersControllerAccept,
|
|
16
|
+
Checkers,
|
|
17
|
+
CheckersView,
|
|
18
|
+
]);
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { ModuleControllerAccept } from "@vnejs/module.controller.accept";
|
|
2
|
+
import type { ControlsPopPayload, ControlsPushPayload } from "@vnejs/contracts.controls";
|
|
3
|
+
|
|
4
|
+
import type { CheckersAiType } from "@vnejs/contracts.views.screens.checkers";
|
|
5
|
+
|
|
6
|
+
import type { CheckersPluginConstants, CheckersPluginEvents, CheckersPluginParams, CheckersPluginSettings } from "../types.js";
|
|
7
|
+
import { applyMove, findMove, getAllMoves, getWinner, oppositeColor, samePos, type CheckersPos } from "../utils/board.js";
|
|
8
|
+
import { pickMove } from "../utils/ai.js";
|
|
9
|
+
import { buildGridItems, statusForTurn, type CheckersCellClickPayload, type CheckersPluginState } from "../utils/checkers.js";
|
|
10
|
+
|
|
11
|
+
export class CheckersControllerAccept extends ModuleControllerAccept<
|
|
12
|
+
CheckersPluginEvents,
|
|
13
|
+
CheckersPluginConstants,
|
|
14
|
+
CheckersPluginSettings,
|
|
15
|
+
CheckersPluginParams,
|
|
16
|
+
CheckersPluginState
|
|
17
|
+
> {
|
|
18
|
+
name = "checkers.controller.accept";
|
|
19
|
+
|
|
20
|
+
EVENT_SHOW = this.EVENTS.CHECKERS.SHOW;
|
|
21
|
+
EVENT_HIDE = this.EVENTS.CHECKERS.HIDE;
|
|
22
|
+
EVENT_ACCEPT = this.EVENTS.CHECKERS.ACCEPT;
|
|
23
|
+
EVENT_STATE_UPDATE = this.EVENTS.CHECKERS.STATE_UPDATE;
|
|
24
|
+
|
|
25
|
+
CONTROLS_ZINDEX = this.PARAMS.CHECKERS.ZINDEX;
|
|
26
|
+
|
|
27
|
+
subscribe = () => {
|
|
28
|
+
this.on(this.EVENT_SHOW, this.onShow);
|
|
29
|
+
this.on(this.EVENT_HIDE, this.onHide);
|
|
30
|
+
this.on(this.EVENT_ACCEPT, this.onAccept);
|
|
31
|
+
if (this.EVENT_STATE_UPDATE) this.on(this.EVENT_STATE_UPDATE, this.updateState);
|
|
32
|
+
this.on(this.EVENTS.CHECKERS.CELL_CLICK, this.onCellClick);
|
|
33
|
+
this.on(this.EVENTS.CHECKERS.AI_TURN, this.onAiTurn);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
onShow = () => void this.onShowAndMaybeAi();
|
|
37
|
+
|
|
38
|
+
onShowAndMaybeAi = async () => {
|
|
39
|
+
await this.emit(this.EVENTS.CONTROLS.PUSH, {
|
|
40
|
+
key: this.name,
|
|
41
|
+
controls: this.CONTROLS,
|
|
42
|
+
index: this.CONTROLS_ZINDEX + 1,
|
|
43
|
+
checkNext: true,
|
|
44
|
+
} satisfies ControlsPushPayload);
|
|
45
|
+
|
|
46
|
+
// SHOW handlers run in parallel with controller.beforeShow — wait for board.
|
|
47
|
+
await this.waitCheck(() => Boolean(this.state.board && this.state.turn));
|
|
48
|
+
await this.onAiTurn();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
onHide = () => {
|
|
52
|
+
this.shared.checkersAiBusy = false;
|
|
53
|
+
this.setDefaultState();
|
|
54
|
+
|
|
55
|
+
return this.emit(this.EVENTS.CONTROLS.POP, { key: this.name } satisfies ControlsPopPayload);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
controllerFor = (turn: NonNullable<CheckersPluginState["turn"]>) =>
|
|
59
|
+
(this.state.ai?.[turn] ?? this.shared.checkersAi?.[turn] ?? this.CONST.CHECKERS.AI_TYPES.PLAYER) as CheckersAiType;
|
|
60
|
+
|
|
61
|
+
isHumanTurn = () => {
|
|
62
|
+
const turn = this.state.turn;
|
|
63
|
+
if (!turn) return false;
|
|
64
|
+
return this.controllerFor(turn) === this.CONST.CHECKERS.AI_TYPES.PLAYER;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
onCellClick = async ({ row, column }: CheckersCellClickPayload = {}) => {
|
|
68
|
+
if (this.shared.checkersAiBusy || !this.isHumanTurn()) return;
|
|
69
|
+
if (row == null || column == null) return;
|
|
70
|
+
|
|
71
|
+
await this.emit(this.EVENTS.CHECKERS.STATE_UPDATE, {
|
|
72
|
+
gridRow: row,
|
|
73
|
+
gridColumn: column,
|
|
74
|
+
} satisfies Partial<CheckersPluginState>);
|
|
75
|
+
await this.emit(this.EVENTS.CHECKERS.UPDATE_FAST);
|
|
76
|
+
await this.onAccept();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
onAccept = async () => {
|
|
80
|
+
if (this.shared.checkersAiBusy) return;
|
|
81
|
+
|
|
82
|
+
if (this.state.winner) {
|
|
83
|
+
await this.emit(this.EVENTS.CHECKERS.FINISH);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!this.isHumanTurn()) return;
|
|
88
|
+
|
|
89
|
+
const { gridRow, gridColumn, board, turn, selected } = this.state;
|
|
90
|
+
if (gridRow == null || gridColumn == null || !board || !turn) return;
|
|
91
|
+
|
|
92
|
+
const focus: CheckersPos = { row: gridRow, column: gridColumn };
|
|
93
|
+
const piece = board[focus.row]?.[focus.column] ?? null;
|
|
94
|
+
|
|
95
|
+
if (selected && findMove(this.state.legalMoves ?? [], selected, focus)) {
|
|
96
|
+
await this.playMove(selected, focus);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (piece && piece.color === turn) {
|
|
101
|
+
const all = getAllMoves(board, turn);
|
|
102
|
+
const legalMoves = all.filter((move) => samePos(move.from, focus));
|
|
103
|
+
if (legalMoves.length === 0) return;
|
|
104
|
+
|
|
105
|
+
const nextSelected = selected && samePos(selected, focus) ? null : focus;
|
|
106
|
+
const nextLegal = nextSelected ? legalMoves : [];
|
|
107
|
+
|
|
108
|
+
await this.emitBoardState({
|
|
109
|
+
selected: nextSelected,
|
|
110
|
+
legalMoves: nextLegal,
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (selected) {
|
|
116
|
+
await this.emitBoardState({ selected: null, legalMoves: [] });
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
onAiTurn = async () => {
|
|
121
|
+
if (this.state.winner || !this.state.board || !this.state.turn) return;
|
|
122
|
+
if (this.isHumanTurn()) return;
|
|
123
|
+
if (this.shared.checkersAiBusy) return;
|
|
124
|
+
|
|
125
|
+
this.shared.checkersAiBusy = true;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
while (!this.state.winner && this.state.board && this.state.turn && !this.isHumanTurn()) {
|
|
129
|
+
await this.waitTimeout(this.PARAMS.CHECKERS.AI_DELAY);
|
|
130
|
+
|
|
131
|
+
const { board, turn } = this.state;
|
|
132
|
+
if (!board || !turn || this.state.winner) break;
|
|
133
|
+
|
|
134
|
+
const move = pickMove(board, turn, this.controllerFor(turn));
|
|
135
|
+
if (!move) break;
|
|
136
|
+
|
|
137
|
+
const played = await this.playMove(move.from, move.to, { scheduleAi: false });
|
|
138
|
+
if (!played) break;
|
|
139
|
+
}
|
|
140
|
+
} finally {
|
|
141
|
+
this.shared.checkersAiBusy = false;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
playMove = async (from: CheckersPos, to: CheckersPos, { scheduleAi = true }: { scheduleAi?: boolean } = {}) => {
|
|
146
|
+
const { board, turn } = this.state;
|
|
147
|
+
if (!board || !turn) return false;
|
|
148
|
+
|
|
149
|
+
// Always resolve against current legal moves — `legalMoves: []` after a move is truthy and must not win over getAllMoves.
|
|
150
|
+
const move = findMove(getAllMoves(board, turn), from, to);
|
|
151
|
+
if (!move) return false;
|
|
152
|
+
|
|
153
|
+
const nextBoard = applyMove(board, move);
|
|
154
|
+
const nextTurn = oppositeColor(turn);
|
|
155
|
+
const winner = getWinner(nextBoard, nextTurn);
|
|
156
|
+
|
|
157
|
+
await this.emitBoardState({
|
|
158
|
+
board: nextBoard,
|
|
159
|
+
turn: nextTurn,
|
|
160
|
+
selected: null,
|
|
161
|
+
legalMoves: [],
|
|
162
|
+
winner,
|
|
163
|
+
statusText: statusForTurn(nextTurn, winner),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (winner) {
|
|
167
|
+
await this.waitTimeout(this.PARAMS.CHECKERS.TRANSITION);
|
|
168
|
+
await this.emit(this.EVENTS.CHECKERS.FINISH);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (scheduleAi) await this.onAiTurn();
|
|
173
|
+
return true;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
emitBoardState = async (
|
|
177
|
+
patch: Partial<CheckersPluginState> & {
|
|
178
|
+
board?: CheckersPluginState["board"];
|
|
179
|
+
selected?: CheckersPluginState["selected"];
|
|
180
|
+
legalMoves?: CheckersPluginState["legalMoves"];
|
|
181
|
+
},
|
|
182
|
+
) => {
|
|
183
|
+
const board = patch.board ?? this.state.board;
|
|
184
|
+
if (!board) return;
|
|
185
|
+
|
|
186
|
+
const selected = patch.selected !== undefined ? patch.selected : this.state.selected;
|
|
187
|
+
const legalMoves = patch.legalMoves !== undefined ? patch.legalMoves : this.state.legalMoves;
|
|
188
|
+
const gridItems = buildGridItems({
|
|
189
|
+
board,
|
|
190
|
+
selected: selected ?? null,
|
|
191
|
+
legalMoves: legalMoves ?? [],
|
|
192
|
+
gridRow: this.state.gridRow ?? null,
|
|
193
|
+
gridColumn: this.state.gridColumn ?? null,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await this.emit(this.EVENTS.CHECKERS.STATE_UPDATE, {
|
|
197
|
+
...patch,
|
|
198
|
+
gridItems,
|
|
199
|
+
} satisfies Partial<CheckersPluginState>);
|
|
200
|
+
await this.emit(this.EVENTS.CHECKERS.UPDATE_FAST);
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ModuleControllerArrowsGrid } from "@vnejs/module.controller.arrows.grid";
|
|
2
|
+
|
|
3
|
+
import type { CheckersPluginConstants, CheckersPluginEvents, CheckersPluginParams, CheckersPluginSettings } from "../types.js";
|
|
4
|
+
import { buildGridItems, type CheckersPluginState } from "../utils/checkers.js";
|
|
5
|
+
import { createInitialBoard } from "../utils/board.js";
|
|
6
|
+
|
|
7
|
+
export class CheckersControllerGrid extends ModuleControllerArrowsGrid<
|
|
8
|
+
CheckersPluginEvents,
|
|
9
|
+
CheckersPluginConstants,
|
|
10
|
+
CheckersPluginSettings,
|
|
11
|
+
CheckersPluginParams,
|
|
12
|
+
CheckersPluginState
|
|
13
|
+
> {
|
|
14
|
+
name = "checkers.controller.grid";
|
|
15
|
+
|
|
16
|
+
EVENT_SHOW = this.EVENTS.CHECKERS.SHOW;
|
|
17
|
+
EVENT_HIDE = this.EVENTS.CHECKERS.HIDE;
|
|
18
|
+
EVENT_STATE_UPDATE = this.EVENTS.CHECKERS.STATE_UPDATE;
|
|
19
|
+
EVENT_UPDATE_FAST = this.EVENTS.CHECKERS.UPDATE_FAST;
|
|
20
|
+
EVENT_GRID_NEXT = this.EVENTS.CHECKERS.GRID_NEXT;
|
|
21
|
+
EVENT_GRID_PREV = this.EVENTS.CHECKERS.GRID_PREV;
|
|
22
|
+
EVENT_GRID_CLEAR = this.EVENTS.CHECKERS.GRID_CLEAR;
|
|
23
|
+
EVENT_GRID_UPDATE_ITEMS = this.EVENTS.CHECKERS.GRID_UPDATE_ITEMS;
|
|
24
|
+
|
|
25
|
+
CONTROLS_ZINDEX = this.PARAMS.CHECKERS.ZINDEX;
|
|
26
|
+
|
|
27
|
+
isDisableGridChangeHorizontal = true;
|
|
28
|
+
isDisableGridChangeVertical = true;
|
|
29
|
+
|
|
30
|
+
getMaxRows = () => this.PARAMS.CHECKERS.ROWS;
|
|
31
|
+
getMaxColumns = () => this.PARAMS.CHECKERS.COLUMNS;
|
|
32
|
+
|
|
33
|
+
getGridItems = async () => {
|
|
34
|
+
const board = this.state.board ?? createInitialBoard();
|
|
35
|
+
return buildGridItems({
|
|
36
|
+
board,
|
|
37
|
+
selected: this.state.selected ?? null,
|
|
38
|
+
legalMoves: this.state.legalMoves ?? [],
|
|
39
|
+
gridRow: this.state.gridRow ?? null,
|
|
40
|
+
gridColumn: this.state.gridColumn ?? null,
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
}
|