@my-games/sudoku-react 0.0.0 → 0.1.0
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/README.md +34 -4
- package/dist/GameSudoku.d.ts +30 -1
- package/dist/GameSudoku.d.ts.map +1 -1
- package/dist/index.css +366 -8
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +373 -8
- package/dist/index.js.map +1 -1
- package/dist/useGame.d.ts +22 -0
- package/dist/useGame.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/GameSudoku.tsx +341 -5
- package/src/index.ts +11 -3
- package/src/useGame.ts +62 -0
package/dist/index.js
CHANGED
|
@@ -1,14 +1,379 @@
|
|
|
1
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Sudoku } from "@my-games/sudoku-core";
|
|
1
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
// src/useGame.ts
|
|
5
|
+
function useGame(options = {}) {
|
|
6
|
+
const { difficulty, storage, saveKey, bestKey, freshStart, puzzle } = options;
|
|
7
|
+
const game = useMemo(() => new Sudoku({
|
|
8
|
+
difficulty,
|
|
9
|
+
storage,
|
|
10
|
+
saveKey,
|
|
11
|
+
bestKey,
|
|
12
|
+
freshStart,
|
|
13
|
+
puzzle
|
|
14
|
+
}), [
|
|
15
|
+
difficulty,
|
|
16
|
+
saveKey,
|
|
17
|
+
bestKey
|
|
18
|
+
]);
|
|
19
|
+
const gameRef = useRef(game);
|
|
20
|
+
gameRef.current = game;
|
|
21
|
+
const [state, setState] = useState(() => game.getState());
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
setState(game.getState());
|
|
24
|
+
return game.subscribe(setState);
|
|
25
|
+
}, [game]);
|
|
26
|
+
return {
|
|
27
|
+
state,
|
|
28
|
+
game,
|
|
29
|
+
select: (row, col) => gameRef.current.select(row, col),
|
|
30
|
+
input: (digit) => gameRef.current.input(digit),
|
|
31
|
+
erase: () => gameRef.current.erase(),
|
|
32
|
+
toggleNote: (digit) => gameRef.current.toggleNote(digit),
|
|
33
|
+
hint: () => gameRef.current.hint(),
|
|
34
|
+
newGame: (diff) => gameRef.current.newGame(diff),
|
|
35
|
+
undo: () => gameRef.current.undo(),
|
|
36
|
+
canUndo: () => gameRef.current.canUndo()
|
|
37
|
+
};
|
|
38
|
+
}
|
|
2
39
|
// src/GameSudoku.tsx
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
40
|
+
var PAD_DIGITS = [
|
|
41
|
+
1,
|
|
42
|
+
2,
|
|
43
|
+
3,
|
|
44
|
+
4,
|
|
45
|
+
5,
|
|
46
|
+
6,
|
|
47
|
+
7,
|
|
48
|
+
8,
|
|
49
|
+
9
|
|
50
|
+
];
|
|
51
|
+
var DIFFS = [
|
|
52
|
+
{
|
|
53
|
+
key: "easy",
|
|
54
|
+
label: "简单"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: "medium",
|
|
58
|
+
label: "中等"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: "hard",
|
|
62
|
+
label: "困难"
|
|
63
|
+
}
|
|
64
|
+
];
|
|
65
|
+
/** 9 宫 × 9 格的全局坐标映射。 */
|
|
66
|
+
var BOXES = (() => {
|
|
67
|
+
const out = [];
|
|
68
|
+
for (let bi = 0; bi < 9; bi++) {
|
|
69
|
+
const cells = [];
|
|
70
|
+
for (let ci = 0; ci < 9; ci++) cells.push({
|
|
71
|
+
row: Math.floor(bi / 3) * 3 + Math.floor(ci / 3),
|
|
72
|
+
col: bi % 3 * 3 + ci % 3
|
|
73
|
+
});
|
|
74
|
+
out.push(cells);
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
})();
|
|
78
|
+
function formatTime(totalSeconds) {
|
|
79
|
+
const m = Math.floor(totalSeconds / 60);
|
|
80
|
+
const s = totalSeconds % 60;
|
|
81
|
+
return `${m}:${String(s).padStart(2, "0")}`;
|
|
82
|
+
}
|
|
83
|
+
function cellValueOf(state, row, col) {
|
|
84
|
+
return state.grid[row]?.[col] ?? 0;
|
|
85
|
+
}
|
|
86
|
+
function cellClass(state, row, col) {
|
|
87
|
+
const v = cellValueOf(state, row, col);
|
|
88
|
+
const given = state.givenMask[row]?.[col] ?? false;
|
|
89
|
+
const sel = state.selected;
|
|
90
|
+
const cls = ["game-sudoku-cell"];
|
|
91
|
+
if (given) cls.push("is-given");
|
|
92
|
+
else if (v !== 0) cls.push("is-user");
|
|
93
|
+
if (state.conflicts[row]?.[col]) cls.push("is-conflict");
|
|
94
|
+
if (sel && sel.row === row && sel.col === col) cls.push("is-selected");
|
|
95
|
+
else if (sel) {
|
|
96
|
+
const sameBox = Math.floor(sel.row / 3) === Math.floor(row / 3) && Math.floor(sel.col / 3) === Math.floor(col / 3);
|
|
97
|
+
if (sel.row === row || sel.col === col || sameBox) cls.push("is-peer");
|
|
98
|
+
const selVal = cellValueOf(state, sel.row, sel.col);
|
|
99
|
+
if (v !== 0 && v === selVal) cls.push("is-same");
|
|
100
|
+
}
|
|
101
|
+
return cls.join(" ");
|
|
102
|
+
}
|
|
103
|
+
function cellNotesOf(state, row, col) {
|
|
104
|
+
return cellValueOf(state, row, col) === 0 ? state.notes[row]?.[col] ?? [] : [];
|
|
105
|
+
}
|
|
106
|
+
function GameSudokuInner(props, ref) {
|
|
107
|
+
const { difficulty = "easy", storage, saveKey, bestKey, freshStart = false, autofocus = true, showPad = true, title = "数独", className, showHint = false, onDigit, onSolved } = props;
|
|
108
|
+
const { state, game, select, input, erase, toggleNote, hint, newGame, undo } = useGame({
|
|
109
|
+
difficulty,
|
|
110
|
+
storage,
|
|
111
|
+
saveKey,
|
|
112
|
+
bestKey,
|
|
113
|
+
freshStart
|
|
114
|
+
});
|
|
115
|
+
const boardRef = useRef(null);
|
|
116
|
+
const [noteMode, setNoteMode] = useState(false);
|
|
117
|
+
const [, setTick] = useState(0);
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (state.status !== "playing") return;
|
|
120
|
+
const id = window.setInterval(() => setTick((t) => t + 1), 1e3);
|
|
121
|
+
return () => window.clearInterval(id);
|
|
122
|
+
}, [state.status]);
|
|
123
|
+
const applyDigit = useCallback((digit) => {
|
|
124
|
+
const changed = noteMode ? toggleNote(digit) : input(digit);
|
|
125
|
+
if (!noteMode) onDigit?.(digit, changed);
|
|
126
|
+
}, [
|
|
127
|
+
noteMode,
|
|
128
|
+
toggleNote,
|
|
129
|
+
input,
|
|
130
|
+
onDigit
|
|
131
|
+
]);
|
|
132
|
+
const handleKey = useCallback((ev) => {
|
|
133
|
+
if (document.activeElement !== boardRef.current) return;
|
|
134
|
+
if (/^[1-9]$/.test(ev.key)) {
|
|
135
|
+
applyDigit(Number(ev.key));
|
|
136
|
+
ev.preventDefault();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (ev.key === "Backspace" || ev.key === "Delete") {
|
|
140
|
+
erase();
|
|
141
|
+
ev.preventDefault();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (ev.key === "n" || ev.key === "N") {
|
|
145
|
+
setNoteMode((v) => !v);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const sel = state.selected;
|
|
149
|
+
if (!sel) return;
|
|
150
|
+
let { row, col } = sel;
|
|
151
|
+
switch (ev.key) {
|
|
152
|
+
case "ArrowUp":
|
|
153
|
+
row = (row + 8) % 9;
|
|
154
|
+
break;
|
|
155
|
+
case "ArrowDown":
|
|
156
|
+
row = (row + 1) % 9;
|
|
157
|
+
break;
|
|
158
|
+
case "ArrowLeft":
|
|
159
|
+
col = (col + 8) % 9;
|
|
160
|
+
break;
|
|
161
|
+
case "ArrowRight":
|
|
162
|
+
col = (col + 1) % 9;
|
|
163
|
+
break;
|
|
164
|
+
default: return;
|
|
165
|
+
}
|
|
166
|
+
ev.preventDefault();
|
|
167
|
+
select(row, col);
|
|
168
|
+
}, [
|
|
169
|
+
applyDigit,
|
|
170
|
+
erase,
|
|
171
|
+
select,
|
|
172
|
+
state.selected
|
|
173
|
+
]);
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
window.addEventListener("keydown", handleKey, { passive: false });
|
|
176
|
+
return () => window.removeEventListener("keydown", handleKey);
|
|
177
|
+
}, [handleKey]);
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (autofocus) boardRef.current?.focus({ preventScroll: true });
|
|
180
|
+
}, [autofocus]);
|
|
181
|
+
const lastStatus = useRef(state.status);
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (state.status === "won" && lastStatus.current !== "won") onSolved?.({
|
|
184
|
+
difficulty: state.difficulty,
|
|
185
|
+
seconds: state.elapsedSeconds
|
|
186
|
+
});
|
|
187
|
+
lastStatus.current = state.status;
|
|
188
|
+
}, [
|
|
189
|
+
state.status,
|
|
190
|
+
state.difficulty,
|
|
191
|
+
state.elapsedSeconds,
|
|
192
|
+
onSolved
|
|
193
|
+
]);
|
|
194
|
+
useImperativeHandle(ref, () => ({
|
|
195
|
+
select,
|
|
196
|
+
input,
|
|
197
|
+
erase,
|
|
198
|
+
toggleNote,
|
|
199
|
+
hint,
|
|
200
|
+
newGame,
|
|
201
|
+
undo
|
|
202
|
+
}), [
|
|
203
|
+
select,
|
|
204
|
+
input,
|
|
205
|
+
erase,
|
|
206
|
+
toggleNote,
|
|
207
|
+
hint,
|
|
208
|
+
newGame,
|
|
209
|
+
undo
|
|
210
|
+
]);
|
|
211
|
+
const won = state.status === "won";
|
|
212
|
+
const timeText = formatTime(won ? state.elapsedSeconds : game.getState().elapsedSeconds);
|
|
213
|
+
const bestText = state.bestSeconds === null ? "—" : formatTime(state.bestSeconds);
|
|
214
|
+
const renderCell = (row, col) => {
|
|
215
|
+
const value = cellValueOf(state, row, col);
|
|
216
|
+
const notes = cellNotesOf(state, row, col);
|
|
217
|
+
return /* @__PURE__ */ jsx("div", {
|
|
218
|
+
className: cellClass(state, row, col),
|
|
219
|
+
"data-row": row,
|
|
220
|
+
"data-col": col,
|
|
221
|
+
onClick: () => select(row, col),
|
|
222
|
+
children: value !== 0 ? value : /* @__PURE__ */ jsx("div", {
|
|
223
|
+
className: "cell-notes",
|
|
224
|
+
children: PAD_DIGITS.map((d) => /* @__PURE__ */ jsx("span", { children: notes.includes(d) ? d : "" }, d))
|
|
225
|
+
})
|
|
226
|
+
}, `${row}-${col}`);
|
|
227
|
+
};
|
|
228
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
229
|
+
className: ["game-sudoku", className].filter(Boolean).join(" "),
|
|
230
|
+
children: [
|
|
231
|
+
/* @__PURE__ */ jsxs("header", {
|
|
232
|
+
className: "game-sudoku-header",
|
|
233
|
+
children: [/* @__PURE__ */ jsxs("h1", {
|
|
234
|
+
className: "game-sudoku-title",
|
|
235
|
+
children: [title, /* @__PURE__ */ jsx("span", {
|
|
236
|
+
className: "subtitle",
|
|
237
|
+
children: "@my-games/sudoku-react"
|
|
238
|
+
})]
|
|
239
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
240
|
+
className: "game-sudoku-stats",
|
|
241
|
+
children: [
|
|
242
|
+
/* @__PURE__ */ jsxs("div", {
|
|
243
|
+
className: "game-sudoku-stat",
|
|
244
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
245
|
+
className: "label",
|
|
246
|
+
children: "用时"
|
|
247
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
248
|
+
className: "value",
|
|
249
|
+
children: timeText
|
|
250
|
+
})]
|
|
251
|
+
}),
|
|
252
|
+
/* @__PURE__ */ jsxs("div", {
|
|
253
|
+
className: "game-sudoku-stat",
|
|
254
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
255
|
+
className: "label",
|
|
256
|
+
children: "最佳"
|
|
257
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
258
|
+
className: "value",
|
|
259
|
+
children: bestText
|
|
260
|
+
})]
|
|
261
|
+
}),
|
|
262
|
+
/* @__PURE__ */ jsxs("div", {
|
|
263
|
+
className: `game-sudoku-stat${state.mistakeCount > 0 ? " is-danger" : ""}`,
|
|
264
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
265
|
+
className: "label",
|
|
266
|
+
children: "错误"
|
|
267
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
268
|
+
className: "value",
|
|
269
|
+
children: state.mistakeCount
|
|
270
|
+
})]
|
|
271
|
+
})
|
|
272
|
+
]
|
|
273
|
+
})]
|
|
274
|
+
}),
|
|
275
|
+
/* @__PURE__ */ jsxs("div", {
|
|
276
|
+
className: "game-sudoku-toolbar",
|
|
277
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
278
|
+
className: "game-sudoku-difficulty",
|
|
279
|
+
role: "group",
|
|
280
|
+
"aria-label": "难度",
|
|
281
|
+
children: DIFFS.map((d) => /* @__PURE__ */ jsx("button", {
|
|
282
|
+
className: `game-sudoku-chip${state.difficulty === d.key ? " is-active" : ""}`,
|
|
283
|
+
type: "button",
|
|
284
|
+
onClick: () => newGame(d.key),
|
|
285
|
+
children: d.label
|
|
286
|
+
}, d.key))
|
|
287
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
288
|
+
className: "game-sudoku-actions",
|
|
289
|
+
children: [
|
|
290
|
+
/* @__PURE__ */ jsxs("button", {
|
|
291
|
+
className: `game-sudoku-button${noteMode ? " is-toggled" : ""}`,
|
|
292
|
+
type: "button",
|
|
293
|
+
disabled: won,
|
|
294
|
+
onClick: () => setNoteMode((v) => !v),
|
|
295
|
+
children: ["笔记 ", noteMode ? "开" : "关"]
|
|
296
|
+
}),
|
|
297
|
+
/* @__PURE__ */ jsx("button", {
|
|
298
|
+
className: "game-sudoku-button",
|
|
299
|
+
type: "button",
|
|
300
|
+
disabled: won || state.historySize === 0,
|
|
301
|
+
onClick: () => undo(),
|
|
302
|
+
children: "撤销"
|
|
303
|
+
}),
|
|
304
|
+
showHint ? /* @__PURE__ */ jsx("button", {
|
|
305
|
+
className: "game-sudoku-button",
|
|
306
|
+
type: "button",
|
|
307
|
+
disabled: won,
|
|
308
|
+
onClick: () => hint(),
|
|
309
|
+
children: "提示"
|
|
310
|
+
}) : null,
|
|
311
|
+
/* @__PURE__ */ jsx("button", {
|
|
312
|
+
className: "game-sudoku-button is-primary",
|
|
313
|
+
type: "button",
|
|
314
|
+
onClick: () => newGame(),
|
|
315
|
+
children: "新游戏"
|
|
316
|
+
})
|
|
317
|
+
]
|
|
318
|
+
})]
|
|
319
|
+
}),
|
|
320
|
+
/* @__PURE__ */ jsxs("div", {
|
|
321
|
+
ref: boardRef,
|
|
322
|
+
className: "game-sudoku-board",
|
|
323
|
+
tabIndex: 0,
|
|
324
|
+
role: "application",
|
|
325
|
+
"aria-label": `数独棋盘 ${state.status}`,
|
|
326
|
+
style: { "--cols": 9 },
|
|
327
|
+
children: [BOXES.map((cells, bi) => /* @__PURE__ */ jsx("div", {
|
|
328
|
+
className: "game-sudoku-box",
|
|
329
|
+
children: cells.map(({ row, col }) => renderCell(row, col))
|
|
330
|
+
}, bi)), /* @__PURE__ */ jsxs("div", {
|
|
331
|
+
className: `game-sudoku-overlay${won ? " is-visible" : ""}`,
|
|
332
|
+
children: [
|
|
333
|
+
/* @__PURE__ */ jsx("h2", { children: "解开了!" }),
|
|
334
|
+
/* @__PURE__ */ jsxs("p", {
|
|
335
|
+
className: "meta",
|
|
336
|
+
children: [
|
|
337
|
+
"难度 ",
|
|
338
|
+
state.difficulty,
|
|
339
|
+
" · 用时 ",
|
|
340
|
+
timeText,
|
|
341
|
+
" · 提示 ",
|
|
342
|
+
state.hintCount,
|
|
343
|
+
" 次 · 错误",
|
|
344
|
+
" ",
|
|
345
|
+
state.mistakeCount,
|
|
346
|
+
" 次"
|
|
347
|
+
]
|
|
348
|
+
}),
|
|
349
|
+
/* @__PURE__ */ jsx("div", {
|
|
350
|
+
className: "row",
|
|
351
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
352
|
+
className: "game-sudoku-button is-primary",
|
|
353
|
+
type: "button",
|
|
354
|
+
onClick: () => newGame(),
|
|
355
|
+
children: "再来一局"
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
]
|
|
359
|
+
})]
|
|
360
|
+
}),
|
|
361
|
+
showPad ? /* @__PURE__ */ jsx("div", {
|
|
362
|
+
className: "game-sudoku-pad",
|
|
363
|
+
role: "group",
|
|
364
|
+
"aria-label": "数字键盘",
|
|
365
|
+
children: PAD_DIGITS.map((d) => /* @__PURE__ */ jsx("button", {
|
|
366
|
+
type: "button",
|
|
367
|
+
disabled: won,
|
|
368
|
+
onClick: () => applyDigit(d),
|
|
369
|
+
children: d
|
|
370
|
+
}, d))
|
|
371
|
+
}) : null
|
|
372
|
+
]
|
|
7
373
|
});
|
|
8
374
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export { GameSudoku, SUDOKU_PACKAGE, SUDOKU_STAGE };
|
|
375
|
+
var GameSudoku = forwardRef(GameSudokuInner);
|
|
376
|
+
GameSudoku.displayName = "GameSudoku";
|
|
377
|
+
export { GameSudoku, useGame };
|
|
13
378
|
|
|
14
379
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/GameSudoku.tsx","../src/index.ts"],"sourcesContent":["// 0.0.0 空骨架占位组件:仅 import core 样式以让 vite lib build 抽出非空\n// dist/index.css(verify:dist 守卫要求)。真实棋盘/输入/提示 UI 随引擎补入。\nimport '@my-games/sudoku-core/style.css';\n\nexport interface GameSudokuProps {\n title?: string;\n}\n\nexport function GameSudoku({ title = 'Sudoku' }: GameSudokuProps) {\n return (\n <div className=\"game-sudoku\">\n <p>{title} · React 占位组件(0.0.0)。真实棋盘随引擎后续提交补入。</p>\n </div>\n );\n}\n","export { GameSudoku, type GameSudokuProps } from './GameSudoku';\n\nexport const SUDOKU_PACKAGE = 'sudoku-react' as const;\nexport const SUDOKU_STAGE = '0.0.0' as const;\n"],"mappings":";;AAQA,SAAgB,WAAW,EAAE,QAAQ,YAA6B;CAChE,OACE,oBAAC,OAAD;EAAK,WAAU;EACb,UAAA,qBAAC,KAAD,EAAA,UAAA,CAAI,OAAM,qCAAsC,EAAA,CAAA;CAC7C,CAAA;AAET;;ACZA,IAAa,iBAAiB;AAC9B,IAAa,eAAe"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/useGame.ts","../src/GameSudoku.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react';\nimport {\n Sudoku,\n type Difficulty,\n type Digit,\n type GameOptions,\n type GameState\n} from '@my-games/sudoku-core';\n\n/**\n * `useGame` —— React Hook 版引擎桥接(与 Vue 的 useGame 语义对齐)。\n *\n * - 引擎实例用 useMemo 按「难度 + 键名」缓存;这些 prop 变化 = 重开一局,视为预期语义。\n * - useState 订阅引擎快照,useEffect 挂/卸订阅防泄漏。\n * - useRef 保最新 game 实例给命令式 handler,避免闭包过期。\n */\nexport interface UseGameReturn {\n state: GameState;\n game: Sudoku;\n select: (row: number, col: number) => void;\n input: (digit: Digit) => boolean;\n erase: () => boolean;\n toggleNote: (digit: Digit) => boolean;\n hint: () => boolean;\n newGame: (difficulty?: Difficulty) => void;\n undo: () => boolean;\n canUndo: () => boolean;\n}\n\nexport function useGame(options: GameOptions = {}): UseGameReturn {\n const { difficulty, storage, saveKey, bestKey, freshStart, puzzle } = options;\n\n const game = useMemo(\n () => new Sudoku({ difficulty, storage, saveKey, bestKey, freshStart, puzzle }),\n // 故意只依赖这些稳定原始值;storage / puzzle / rng / clock 引用不稳定,重开一次即可。\n [difficulty, saveKey, bestKey]\n );\n\n const gameRef = useRef(game);\n gameRef.current = game;\n\n const [state, setState] = useState<GameState>(() => game.getState());\n\n useEffect(() => {\n setState(game.getState());\n const unsub = game.subscribe(setState);\n return unsub;\n }, [game]);\n\n return {\n state,\n game,\n select: (row, col) => gameRef.current.select(row, col),\n input: (digit) => gameRef.current.input(digit),\n erase: () => gameRef.current.erase(),\n toggleNote: (digit) => gameRef.current.toggleNote(digit),\n hint: () => gameRef.current.hint(),\n newGame: (diff) => gameRef.current.newGame(diff),\n undo: () => gameRef.current.undo(),\n canUndo: () => gameRef.current.canUndo()\n };\n}\n","import {\n type CSSProperties,\n type ReactNode,\n type Ref,\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n useState\n} from 'react';\nimport '@my-games/sudoku-core/style.css';\nimport type { Difficulty, Digit, GameState, SaveStorage } from '@my-games/sudoku-core';\nimport { useGame } from './useGame';\n\n/**\n * GameSudoku —— React 18/19 组件,行为与 Vue 版对齐。\n * 键盘:数字 1-9 填数 / N 切笔记 / Backspace·Delete 擦除 / 方向键移动选中;\n * 严格判 `document.activeElement === board`,多实例并存只活动那个响应。\n */\n\nexport interface GameSudokuProps {\n difficulty?: Difficulty;\n storage?: SaveStorage | null;\n saveKey?: string;\n bestKey?: string;\n freshStart?: boolean;\n autofocus?: boolean;\n showPad?: boolean;\n title?: string;\n className?: string;\n showHint?: boolean;\n onDigit?: (digit: Digit, changed: boolean) => void;\n onSolved?: (payload: { difficulty: Difficulty; seconds: number }) => void;\n}\n\nexport interface GameSudokuHandle {\n select(row: number, col: number): void;\n input(digit: Digit): boolean;\n erase(): boolean;\n toggleNote(digit: Digit): boolean;\n hint(): boolean;\n newGame(difficulty?: Difficulty): void;\n undo(): boolean;\n}\n\nconst PAD_DIGITS: Digit[] = [1, 2, 3, 4, 5, 6, 7, 8, 9];\nconst DIFFS: Array<{ key: Difficulty; label: string }> = [\n { key: 'easy', label: '简单' },\n { key: 'medium', label: '中等' },\n { key: 'hard', label: '困难' }\n];\n\n/** 9 宫 × 9 格的全局坐标映射。 */\nconst BOXES: Array<Array<{ row: number; col: number }>> = (() => {\n const out: Array<Array<{ row: number; col: number }>> = [];\n for (let bi = 0; bi < 9; bi++) {\n const cells: Array<{ row: number; col: number }> = [];\n for (let ci = 0; ci < 9; ci++) {\n cells.push({\n row: Math.floor(bi / 3) * 3 + Math.floor(ci / 3),\n col: (bi % 3) * 3 + (ci % 3)\n });\n }\n out.push(cells);\n }\n return out;\n})();\n\nfunction formatTime(totalSeconds: number): string {\n const m = Math.floor(totalSeconds / 60);\n const s = totalSeconds % 60;\n return `${m}:${String(s).padStart(2, '0')}`;\n}\n\nfunction cellValueOf(state: GameState, row: number, col: number): number {\n return state.grid[row]?.[col] ?? 0;\n}\n\nfunction cellClass(state: GameState, row: number, col: number): string {\n const v = cellValueOf(state, row, col);\n const given = state.givenMask[row]?.[col] ?? false;\n const sel = state.selected;\n const cls = ['game-sudoku-cell'];\n if (given) cls.push('is-given');\n else if (v !== 0) cls.push('is-user');\n if (state.conflicts[row]?.[col]) cls.push('is-conflict');\n if (sel && sel.row === row && sel.col === col) {\n cls.push('is-selected');\n } else if (sel) {\n const sameBox =\n Math.floor(sel.row / 3) === Math.floor(row / 3) &&\n Math.floor(sel.col / 3) === Math.floor(col / 3);\n if (sel.row === row || sel.col === col || sameBox) cls.push('is-peer');\n const selVal = cellValueOf(state, sel.row, sel.col);\n if (v !== 0 && v === selVal) cls.push('is-same');\n }\n return cls.join(' ');\n}\n\nfunction cellNotesOf(state: GameState, row: number, col: number): Digit[] {\n return cellValueOf(state, row, col) === 0 ? (state.notes[row]?.[col] ?? []) : [];\n}\n\nfunction GameSudokuInner(props: GameSudokuProps, ref: Ref<GameSudokuHandle>) {\n const {\n difficulty = 'easy',\n storage,\n saveKey,\n bestKey,\n freshStart = false,\n autofocus = true,\n showPad = true,\n title = '数独',\n className,\n showHint = false,\n onDigit,\n onSolved\n } = props;\n\n const { state, game, select, input, erase, toggleNote, hint, newGame, undo } = useGame({\n difficulty,\n storage,\n saveKey,\n bestKey,\n freshStart\n });\n\n const boardRef = useRef<HTMLDivElement>(null);\n const [noteMode, setNoteMode] = useState(false);\n // 用时每秒刷新:core 只在操作时推快照,tick 触发重绘后由 game.getState() 读实时值。\n const [, setTick] = useState(0);\n useEffect(() => {\n if (state.status !== 'playing') return;\n const id = window.setInterval(() => setTick((t) => t + 1), 1000);\n return () => window.clearInterval(id);\n }, [state.status]);\n\n const applyDigit = useCallback(\n (digit: Digit) => {\n const changed = noteMode ? toggleNote(digit) : input(digit);\n if (!noteMode) onDigit?.(digit, changed);\n },\n [noteMode, toggleNote, input, onDigit]\n );\n\n const handleKey = useCallback(\n (ev: KeyboardEvent) => {\n if (document.activeElement !== boardRef.current) return;\n if (/^[1-9]$/.test(ev.key)) {\n applyDigit(Number(ev.key) as Digit);\n ev.preventDefault();\n return;\n }\n if (ev.key === 'Backspace' || ev.key === 'Delete') {\n erase();\n ev.preventDefault();\n return;\n }\n if (ev.key === 'n' || ev.key === 'N') {\n setNoteMode((v) => !v);\n return;\n }\n const sel = state.selected;\n if (!sel) return;\n let { row, col } = sel;\n switch (ev.key) {\n case 'ArrowUp':\n row = (row + 8) % 9;\n break;\n case 'ArrowDown':\n row = (row + 1) % 9;\n break;\n case 'ArrowLeft':\n col = (col + 8) % 9;\n break;\n case 'ArrowRight':\n col = (col + 1) % 9;\n break;\n default:\n return;\n }\n ev.preventDefault();\n select(row, col);\n },\n [applyDigit, erase, select, state.selected]\n );\n\n useEffect(() => {\n window.addEventListener('keydown', handleKey, { passive: false });\n return () => window.removeEventListener('keydown', handleKey);\n }, [handleKey]);\n\n useEffect(() => {\n if (autofocus) boardRef.current?.focus({ preventScroll: true });\n }, [autofocus]);\n\n const lastStatus = useRef(state.status);\n useEffect(() => {\n if (state.status === 'won' && lastStatus.current !== 'won') {\n onSolved?.({ difficulty: state.difficulty, seconds: state.elapsedSeconds });\n }\n lastStatus.current = state.status;\n }, [state.status, state.difficulty, state.elapsedSeconds, onSolved]);\n\n useImperativeHandle(\n ref,\n () => ({ select, input, erase, toggleNote, hint, newGame, undo }),\n [select, input, erase, toggleNote, hint, newGame, undo]\n );\n\n const won = state.status === 'won';\n const timeText = formatTime(won ? state.elapsedSeconds : game.getState().elapsedSeconds);\n const bestText = state.bestSeconds === null ? '—' : formatTime(state.bestSeconds);\n\n const renderCell = (row: number, col: number): ReactNode => {\n const value = cellValueOf(state, row, col);\n const notes = cellNotesOf(state, row, col);\n return (\n <div\n key={`${row}-${col}`}\n className={cellClass(state, row, col)}\n data-row={row}\n data-col={col}\n onClick={() => select(row, col)}\n >\n {value !== 0 ? (\n value\n ) : (\n <div className=\"cell-notes\">\n {PAD_DIGITS.map((d) => (\n <span key={d}>{notes.includes(d) ? d : ''}</span>\n ))}\n </div>\n )}\n </div>\n );\n };\n\n return (\n <div className={['game-sudoku', className].filter(Boolean).join(' ')}>\n <header className=\"game-sudoku-header\">\n <h1 className=\"game-sudoku-title\">\n {title}\n <span className=\"subtitle\">@my-games/sudoku-react</span>\n </h1>\n <div className=\"game-sudoku-stats\">\n <div className=\"game-sudoku-stat\">\n <span className=\"label\">用时</span>\n <span className=\"value\">{timeText}</span>\n </div>\n <div className=\"game-sudoku-stat\">\n <span className=\"label\">最佳</span>\n <span className=\"value\">{bestText}</span>\n </div>\n <div className={`game-sudoku-stat${state.mistakeCount > 0 ? ' is-danger' : ''}`}>\n <span className=\"label\">错误</span>\n <span className=\"value\">{state.mistakeCount}</span>\n </div>\n </div>\n </header>\n\n <div className=\"game-sudoku-toolbar\">\n <div className=\"game-sudoku-difficulty\" role=\"group\" aria-label=\"难度\">\n {DIFFS.map((d) => (\n <button\n key={d.key}\n className={`game-sudoku-chip${state.difficulty === d.key ? ' is-active' : ''}`}\n type=\"button\"\n onClick={() => newGame(d.key)}\n >\n {d.label}\n </button>\n ))}\n </div>\n <div className=\"game-sudoku-actions\">\n <button\n className={`game-sudoku-button${noteMode ? ' is-toggled' : ''}`}\n type=\"button\"\n disabled={won}\n onClick={() => setNoteMode((v) => !v)}\n >\n 笔记 {noteMode ? '开' : '关'}\n </button>\n <button\n className=\"game-sudoku-button\"\n type=\"button\"\n disabled={won || state.historySize === 0}\n onClick={() => undo()}\n >\n 撤销\n </button>\n {showHint ? (\n <button\n className=\"game-sudoku-button\"\n type=\"button\"\n disabled={won}\n onClick={() => hint()}\n >\n 提示\n </button>\n ) : null}\n <button className=\"game-sudoku-button is-primary\" type=\"button\" onClick={() => newGame()}>\n 新游戏\n </button>\n </div>\n </div>\n\n <div\n ref={boardRef}\n className=\"game-sudoku-board\"\n tabIndex={0}\n role=\"application\"\n aria-label={`数独棋盘 ${state.status}`}\n style={{ '--cols': 9 } as CSSProperties}\n >\n {BOXES.map((cells, bi) => (\n <div key={bi} className=\"game-sudoku-box\">\n {cells.map(({ row, col }) => renderCell(row, col))}\n </div>\n ))}\n\n <div className={`game-sudoku-overlay${won ? ' is-visible' : ''}`}>\n <h2>解开了!</h2>\n <p className=\"meta\">\n 难度 {state.difficulty} · 用时 {timeText} · 提示 {state.hintCount} 次 · 错误{' '}\n {state.mistakeCount} 次\n </p>\n <div className=\"row\">\n <button className=\"game-sudoku-button is-primary\" type=\"button\" onClick={() => newGame()}>\n 再来一局\n </button>\n </div>\n </div>\n </div>\n\n {showPad ? (\n <div className=\"game-sudoku-pad\" role=\"group\" aria-label=\"数字键盘\">\n {PAD_DIGITS.map((d) => (\n <button key={d} type=\"button\" disabled={won} onClick={() => applyDigit(d)}>\n {d}\n </button>\n ))}\n </div>\n ) : null}\n </div>\n );\n}\n\nexport const GameSudoku = forwardRef(GameSudokuInner);\nGameSudoku.displayName = 'GameSudoku';\n"],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Sudoku, Difficulty, Digit, GameOptions, GameState } from '@my-games/sudoku-core';
|
|
2
|
+
/**
|
|
3
|
+
* `useGame` —— React Hook 版引擎桥接(与 Vue 的 useGame 语义对齐)。
|
|
4
|
+
*
|
|
5
|
+
* - 引擎实例用 useMemo 按「难度 + 键名」缓存;这些 prop 变化 = 重开一局,视为预期语义。
|
|
6
|
+
* - useState 订阅引擎快照,useEffect 挂/卸订阅防泄漏。
|
|
7
|
+
* - useRef 保最新 game 实例给命令式 handler,避免闭包过期。
|
|
8
|
+
*/
|
|
9
|
+
export interface UseGameReturn {
|
|
10
|
+
state: GameState;
|
|
11
|
+
game: Sudoku;
|
|
12
|
+
select: (row: number, col: number) => void;
|
|
13
|
+
input: (digit: Digit) => boolean;
|
|
14
|
+
erase: () => boolean;
|
|
15
|
+
toggleNote: (digit: Digit) => boolean;
|
|
16
|
+
hint: () => boolean;
|
|
17
|
+
newGame: (difficulty?: Difficulty) => void;
|
|
18
|
+
undo: () => boolean;
|
|
19
|
+
canUndo: () => boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function useGame(options?: GameOptions): UseGameReturn;
|
|
22
|
+
//# sourceMappingURL=useGame.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGame.d.ts","sourceRoot":"","sources":["../src/useGame.ts"],"names":[],"mappings":"AACA,OAAO,EACL,MAAM,EACN,KAAK,UAAU,EACf,KAAK,KAAK,EACV,KAAK,WAAW,EAChB,KAAK,SAAS,EACf,MAAM,uBAAuB,CAAC;AAE/B;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,SAAS,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IACjC,KAAK,EAAE,MAAM,OAAO,CAAC;IACrB,UAAU,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IACtC,IAAI,EAAE,MAAM,OAAO,CAAC;IACpB,OAAO,EAAE,CAAC,UAAU,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IAC3C,IAAI,EAAE,MAAM,OAAO,CAAC;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC;CACxB;AAED,wBAAgB,OAAO,CAAC,OAAO,GAAE,WAAgB,GAAG,aAAa,CAgChE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@my-games/sudoku-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "React 18/19 component adapter for @my-games/sudoku-core — function component + hooks + forwardRef, mobile-ready. 数独的 React 组件适配器。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"react-dom": "^18.2.0 || ^19.0.0"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@my-games/sudoku-core": "0.
|
|
28
|
+
"@my-games/sudoku-core": "0.1.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/react": "^19.3.0",
|