@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,309 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import type { ViewRenderFunc } from "@vnejs/module.components";
|
|
3
|
+
import type { ReactComponentProps } from "@vnejs/uis.react";
|
|
4
|
+
import { Flex, PositionBox, Screen, Text, createRenderFunc, useCallback, useEffect, useMemo, useState, useStoreState } from "@vnejs/uis.react";
|
|
5
|
+
import { getVneLength } from "@vnejs/uis.utils";
|
|
6
|
+
|
|
7
|
+
import type { CheckersPluginConstants, CheckersPluginEvents, CheckersPluginParams, CheckersPluginSettings } from "../types.js";
|
|
8
|
+
import type { CheckersColor, CheckersPiece } from "../utils/board.js";
|
|
9
|
+
import { listBoardPieces } from "../utils/board.js";
|
|
10
|
+
import type { CheckersGridItem, CheckersPluginState } from "../utils/checkers.js";
|
|
11
|
+
|
|
12
|
+
type CheckersComponentProps = ReactComponentProps<
|
|
13
|
+
CheckersPluginEvents,
|
|
14
|
+
CheckersPluginConstants,
|
|
15
|
+
CheckersPluginSettings,
|
|
16
|
+
CheckersPluginParams,
|
|
17
|
+
CheckersPluginState
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
type VisualPiece = {
|
|
21
|
+
id: string;
|
|
22
|
+
color: CheckersColor;
|
|
23
|
+
king: boolean;
|
|
24
|
+
row: number;
|
|
25
|
+
column: number;
|
|
26
|
+
opacity: number;
|
|
27
|
+
zone: "board" | "graveyard";
|
|
28
|
+
graveyardIndex: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const SELECTED_OPACITY = 0.7;
|
|
32
|
+
const GRAVEYARD_STACK = 0.55;
|
|
33
|
+
|
|
34
|
+
const cellBackground = (item: CheckersGridItem & { isFocus?: boolean }) => {
|
|
35
|
+
if (item.isFocus) return "rgba(255, 214, 64, 0.85)";
|
|
36
|
+
if (item.isLegalTarget) return "rgba(80, 200, 120, 0.75)";
|
|
37
|
+
if (item.isSelected) return "rgba(90, 160, 255, 0.8)";
|
|
38
|
+
return item.isDark ? "rgba(60, 40, 20, 0.95)" : "rgba(220, 200, 160, 0.95)";
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const pieceLook = (piece: Pick<CheckersPiece, "color" | "king">): CSSProperties => {
|
|
42
|
+
const isWhite = piece.color === "white";
|
|
43
|
+
return {
|
|
44
|
+
width: "62%",
|
|
45
|
+
height: "62%",
|
|
46
|
+
borderRadius: "50%",
|
|
47
|
+
background: isWhite ? "#f4f0e6" : "#1a1a1a",
|
|
48
|
+
border: `${getVneLength(6)} solid ${piece.king ? "#d4af37" : isWhite ? "#bbb" : "#444"}`,
|
|
49
|
+
boxSizing: "border-box",
|
|
50
|
+
display: "flex",
|
|
51
|
+
alignItems: "center",
|
|
52
|
+
justifyContent: "center",
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const kingMarkLook = (): CSSProperties => ({
|
|
57
|
+
width: "50%",
|
|
58
|
+
height: "50%",
|
|
59
|
+
borderRadius: "50%",
|
|
60
|
+
boxSizing: "border-box",
|
|
61
|
+
border: `${getVneLength(6)} solid #d4af37`,
|
|
62
|
+
background: "transparent",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const CheckersScreen = ({ store, onMount, PARAMS, emit, EVENTS }: CheckersComponentProps) => {
|
|
66
|
+
const {
|
|
67
|
+
isShow = false,
|
|
68
|
+
isForce = false,
|
|
69
|
+
board,
|
|
70
|
+
gridItems = [],
|
|
71
|
+
gridRow = null,
|
|
72
|
+
gridColumn = null,
|
|
73
|
+
selected = null,
|
|
74
|
+
statusText = "",
|
|
75
|
+
} = useStoreState<CheckersPluginState>(store, onMount);
|
|
76
|
+
|
|
77
|
+
const transition = PARAMS.CHECKERS.TRANSITION;
|
|
78
|
+
const propsByView = PARAMS.CHECKERS.VIEW_PROPS;
|
|
79
|
+
const rows = PARAMS.CHECKERS.ROWS;
|
|
80
|
+
const columns = PARAMS.CHECKERS.COLUMNS;
|
|
81
|
+
const cellSize = PARAMS.CHECKERS.CELL_SIZE;
|
|
82
|
+
const cellGap = PARAMS.CHECKERS.CELL_GAP;
|
|
83
|
+
const graveyardGap = PARAMS.CHECKERS.GRAVEYARD_GAP;
|
|
84
|
+
const boardSize = columns * cellSize + (columns - 1) * cellGap;
|
|
85
|
+
const playfieldWidth = cellSize + graveyardGap + boardSize + graveyardGap + cellSize;
|
|
86
|
+
|
|
87
|
+
const boardPieces = useMemo(() => (board ? listBoardPieces(board) : []), [board]);
|
|
88
|
+
const selectedId = useMemo(() => {
|
|
89
|
+
if (!selected || !board) return null;
|
|
90
|
+
return board[selected.row]?.[selected.column]?.id ?? null;
|
|
91
|
+
}, [board, selected]);
|
|
92
|
+
|
|
93
|
+
const [visualPieces, setVisualPieces] = useState<VisualPiece[]>([]);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!isShow) {
|
|
97
|
+
setVisualPieces([]);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
setVisualPieces((prev) => {
|
|
102
|
+
const nextById = new Map(boardPieces.map((piece) => [piece.id, piece]));
|
|
103
|
+
const next: VisualPiece[] = [];
|
|
104
|
+
let whiteGraveyardCount = 0;
|
|
105
|
+
let blackGraveyardCount = 0;
|
|
106
|
+
|
|
107
|
+
for (const piece of prev) {
|
|
108
|
+
if (piece.zone === "graveyard") {
|
|
109
|
+
if (piece.color === "white") whiteGraveyardCount = Math.max(whiteGraveyardCount, piece.graveyardIndex + 1);
|
|
110
|
+
else blackGraveyardCount = Math.max(blackGraveyardCount, piece.graveyardIndex + 1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const piece of prev) {
|
|
115
|
+
const live = nextById.get(piece.id);
|
|
116
|
+
if (live) {
|
|
117
|
+
next.push({
|
|
118
|
+
id: live.id,
|
|
119
|
+
color: live.color,
|
|
120
|
+
king: live.king,
|
|
121
|
+
row: live.row,
|
|
122
|
+
column: live.column,
|
|
123
|
+
opacity: selectedId === live.id ? SELECTED_OPACITY : 1,
|
|
124
|
+
zone: "board",
|
|
125
|
+
graveyardIndex: 0,
|
|
126
|
+
});
|
|
127
|
+
nextById.delete(piece.id);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (piece.zone === "graveyard") {
|
|
132
|
+
next.push(piece);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const graveyardIndex = piece.color === "white" ? whiteGraveyardCount++ : blackGraveyardCount++;
|
|
137
|
+
next.push({
|
|
138
|
+
...piece,
|
|
139
|
+
opacity: 1,
|
|
140
|
+
zone: "graveyard",
|
|
141
|
+
graveyardIndex,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const piece of nextById.values()) {
|
|
146
|
+
next.push({
|
|
147
|
+
id: piece.id,
|
|
148
|
+
color: piece.color,
|
|
149
|
+
king: piece.king,
|
|
150
|
+
row: piece.row,
|
|
151
|
+
column: piece.column,
|
|
152
|
+
opacity: selectedId === piece.id ? SELECTED_OPACITY : 1,
|
|
153
|
+
zone: "board",
|
|
154
|
+
graveyardIndex: 0,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return next;
|
|
159
|
+
});
|
|
160
|
+
}, [boardPieces, isShow, selectedId]);
|
|
161
|
+
|
|
162
|
+
const propsScreen = useMemo(() => ({ ...propsByView.screen, isShow, isForce }), [isShow, isForce, propsByView.screen]);
|
|
163
|
+
const propsStatus = useMemo(() => ({ ...propsByView.status, text: statusText, width: playfieldWidth }), [statusText, propsByView.status, playfieldWidth]);
|
|
164
|
+
|
|
165
|
+
const cellLen = useMemo(() => getVneLength(cellSize), [cellSize]);
|
|
166
|
+
const gapLen = useMemo(() => getVneLength(cellGap), [cellGap]);
|
|
167
|
+
const graveyardGapLen = useMemo(() => getVneLength(graveyardGap), [graveyardGap]);
|
|
168
|
+
const stepLen = useMemo(() => (cellGap ? `calc(${cellLen} + ${gapLen})` : cellLen), [cellGap, cellLen, gapLen]);
|
|
169
|
+
const boardOffsetX = useMemo(() => `calc(${cellLen} + ${graveyardGapLen})`, [cellLen, graveyardGapLen]);
|
|
170
|
+
const blackGraveyardX = useMemo(
|
|
171
|
+
() => `calc(${cellLen} + ${graveyardGapLen} + ${getVneLength(boardSize)} + ${graveyardGapLen})`,
|
|
172
|
+
[boardSize, cellLen, graveyardGapLen],
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const playfieldStyle = useMemo(
|
|
176
|
+
(): CSSProperties => ({
|
|
177
|
+
position: "relative",
|
|
178
|
+
width: getVneLength(playfieldWidth),
|
|
179
|
+
height: getVneLength(boardSize),
|
|
180
|
+
boxSizing: "border-box",
|
|
181
|
+
}),
|
|
182
|
+
[boardSize, playfieldWidth],
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const boardStyle = useMemo(
|
|
186
|
+
(): CSSProperties => ({
|
|
187
|
+
position: "absolute",
|
|
188
|
+
top: 0,
|
|
189
|
+
left: boardOffsetX,
|
|
190
|
+
display: "grid",
|
|
191
|
+
gridTemplateColumns: `repeat(${columns}, ${cellLen})`,
|
|
192
|
+
gridTemplateRows: `repeat(${rows}, ${cellLen})`,
|
|
193
|
+
gap: gapLen,
|
|
194
|
+
width: getVneLength(boardSize),
|
|
195
|
+
height: getVneLength(boardSize),
|
|
196
|
+
boxSizing: "border-box",
|
|
197
|
+
}),
|
|
198
|
+
[boardOffsetX, boardSize, cellLen, columns, gapLen, rows],
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const graveyardStyle = useCallback(
|
|
202
|
+
(side: "left" | "right"): CSSProperties => ({
|
|
203
|
+
position: "absolute",
|
|
204
|
+
top: 0,
|
|
205
|
+
left: side === "left" ? 0 : blackGraveyardX,
|
|
206
|
+
width: cellLen,
|
|
207
|
+
height: getVneLength(boardSize),
|
|
208
|
+
boxSizing: "border-box",
|
|
209
|
+
borderRadius: getVneLength(12),
|
|
210
|
+
background: "rgba(255, 255, 255, 0.06)",
|
|
211
|
+
}),
|
|
212
|
+
[blackGraveyardX, boardSize, cellLen],
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const cellBaseStyle = useMemo(
|
|
216
|
+
(): CSSProperties => ({
|
|
217
|
+
width: cellLen,
|
|
218
|
+
height: cellLen,
|
|
219
|
+
boxSizing: "border-box",
|
|
220
|
+
transition: `background-color ${transition}ms`,
|
|
221
|
+
}),
|
|
222
|
+
[cellLen, transition],
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const piecesLayerStyle = useMemo(
|
|
226
|
+
(): CSSProperties => ({
|
|
227
|
+
position: "absolute",
|
|
228
|
+
inset: 0,
|
|
229
|
+
pointerEvents: "none",
|
|
230
|
+
}),
|
|
231
|
+
[],
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const pieceTransform = useCallback(
|
|
235
|
+
(piece: VisualPiece) => {
|
|
236
|
+
if (piece.zone === "graveyard") {
|
|
237
|
+
const x = piece.color === "white" ? "0px" : blackGraveyardX;
|
|
238
|
+
const y = `calc(${piece.graveyardIndex} * ${cellLen} * ${GRAVEYARD_STACK})`;
|
|
239
|
+
return `translate(${x}, ${y})`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return `translate(calc(${boardOffsetX} + ${piece.column} * ${stepLen}), calc(${piece.row} * ${stepLen}))`;
|
|
243
|
+
},
|
|
244
|
+
[blackGraveyardX, boardOffsetX, cellLen, stepLen],
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const onCellClick = useCallback(
|
|
248
|
+
(row: number, column: number) => {
|
|
249
|
+
void emit(EVENTS.CHECKERS.CELL_CLICK, { row, column });
|
|
250
|
+
},
|
|
251
|
+
[emit, EVENTS.CHECKERS.CELL_CLICK],
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<Screen {...propsScreen}>
|
|
256
|
+
<PositionBox {...propsByView.position}>
|
|
257
|
+
<Flex {...propsByView.flex}>
|
|
258
|
+
<Text {...propsStatus} />
|
|
259
|
+
<div style={playfieldStyle}>
|
|
260
|
+
<div style={graveyardStyle("left")} />
|
|
261
|
+
<div style={graveyardStyle("right")} />
|
|
262
|
+
<div style={boardStyle}>
|
|
263
|
+
{gridItems.map((item) => {
|
|
264
|
+
const isFocus = gridRow === item.row && gridColumn === item.column;
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div
|
|
268
|
+
key={`${item.row}-${item.column}`}
|
|
269
|
+
onClick={() => onCellClick(item.row, item.column)}
|
|
270
|
+
vne-cursor-type="pointer"
|
|
271
|
+
style={{
|
|
272
|
+
...cellBaseStyle,
|
|
273
|
+
background: cellBackground({ ...item, isFocus }),
|
|
274
|
+
}}
|
|
275
|
+
/>
|
|
276
|
+
);
|
|
277
|
+
})}
|
|
278
|
+
</div>
|
|
279
|
+
<div style={piecesLayerStyle}>
|
|
280
|
+
{visualPieces.map((piece) => (
|
|
281
|
+
<div
|
|
282
|
+
key={piece.id}
|
|
283
|
+
style={{
|
|
284
|
+
position: "absolute",
|
|
285
|
+
top: 0,
|
|
286
|
+
left: 0,
|
|
287
|
+
width: cellLen,
|
|
288
|
+
height: cellLen,
|
|
289
|
+
display: "flex",
|
|
290
|
+
alignItems: "center",
|
|
291
|
+
justifyContent: "center",
|
|
292
|
+
transform: pieceTransform(piece),
|
|
293
|
+
opacity: piece.opacity,
|
|
294
|
+
transition: `transform ${transition}ms, opacity ${transition}ms`,
|
|
295
|
+
willChange: "transform, opacity",
|
|
296
|
+
}}
|
|
297
|
+
>
|
|
298
|
+
<div style={pieceLook(piece)}>{piece.king ? <div style={kingMarkLook()} /> : null}</div>
|
|
299
|
+
</div>
|
|
300
|
+
))}
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
</Flex>
|
|
304
|
+
</PositionBox>
|
|
305
|
+
</Screen>
|
|
306
|
+
);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
export const render: ViewRenderFunc<CheckersPluginState> = createRenderFunc(CheckersScreen);
|
package/tsconfig.json
ADDED