dsh-dock 0.11.2 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +18 -18
- package/README.md +242 -396
- package/client.js +173 -9
- package/cordis.patch.yml +2 -2
- package/features/animation/host.js +119 -119
- package/features/animation/view.jsx +1662 -1662
- package/features/balance/host.js +494 -494
- package/features/balance/view.js +351 -351
- package/features/games/games/breakout.jsx +227 -227
- package/features/games/games/gomoku.jsx +244 -244
- package/features/games/games/racer.jsx +169 -169
- package/features/games/games/shared.jsx +118 -118
- package/features/games/games/snake.jsx +144 -144
- package/features/games/games/sokoban.jsx +180 -180
- package/features/games/games/tank.jsx +128 -128
- package/features/games/games/tetris.jsx +308 -308
- package/features/games/games/xiangqi.jsx +277 -277
- package/features/games/tank-core.js +152 -152
- package/features/games/view.jsx +639 -639
- package/features/games/xiangqi-core.js +299 -299
- package/features/heartbeat/view.js +35 -35
- package/features/mobile-relay/gateway.js +472 -472
- package/features/mobile-relay/host.js +570 -570
- package/features/mobile-relay/view.jsx +300 -300
- package/features/modelconfig/host.js +406 -406
- package/features/modelconfig/view.js +490 -490
- package/features/notify/host.js +374 -374
- package/features/notify/view.jsx +751 -751
- package/features/theme/view.js +61 -61
- package/features/tokenlog/host.js +882 -883
- package/features/tokenlog/view.jsx +1117 -932
- package/features/visionproxy/host.js +315 -315
- package/index.js +195 -195
- package/package.json +66 -64
- package/src/client.jsx +677 -677
- package/src/host-core.js +207 -207
- package/src/shared.js +192 -192
- package/src/task-track.js +354 -359
|
@@ -1,299 +1,299 @@
|
|
|
1
|
-
/* 趣味游戏 · 象棋核心规则(纯函数,无 React)——供组件复用,也便于单测。 */
|
|
2
|
-
export const ROWS = 10, COLS = 9;
|
|
3
|
-
// 类型:g 将/帅 · a 仕/士 · e 相/象 · h 马 · k 车 · c 炮 · p 兵/卒
|
|
4
|
-
// 颜色:r 红(下)· b 黑(上)
|
|
5
|
-
|
|
6
|
-
export function pieceChar(p) {
|
|
7
|
-
const map = {
|
|
8
|
-
g: { r: "帥", b: "將" }, a: { r: "仕", b: "士" }, e: { r: "相", b: "象" },
|
|
9
|
-
h: { r: "馬", b: "馬" }, k: { r: "車", b: "車" }, c: { r: "炮", b: "炮" },
|
|
10
|
-
p: { r: "兵", b: "卒" },
|
|
11
|
-
};
|
|
12
|
-
return map[p.t] && map[p.t][p.c];
|
|
13
|
-
}
|
|
14
|
-
export const VALUE = { g: 100000, k: 900, c: 450, h: 400, e: 200, a: 200, p: 100 };
|
|
15
|
-
|
|
16
|
-
export function initialBoard() {
|
|
17
|
-
const b = Array.from({ length: ROWS }, () => Array(COLS).fill(null));
|
|
18
|
-
const back = ["k", "h", "e", "a", "g", "a", "e", "h", "k"];
|
|
19
|
-
back.forEach((t, c) => { b[0][c] = { t, c: "b" }; });
|
|
20
|
-
b[2][1] = { t: "c", c: "b" }; b[2][7] = { t: "c", c: "b" };
|
|
21
|
-
// 黑卒在己方第三横线(row 3,紧邻楚河汉界一侧留一行间隔)。
|
|
22
|
-
[0, 2, 4, 6, 8].forEach((c) => { b[3][c] = { t: "p", c: "b" }; });
|
|
23
|
-
back.forEach((t, c) => { b[9][c] = { t, c: "r" }; });
|
|
24
|
-
b[7][1] = { t: "c", c: "r" }; b[7][7] = { t: "c", c: "r" };
|
|
25
|
-
// 红兵与红炮和黑方对称(row 6 / row 7)。
|
|
26
|
-
[0, 2, 4, 6, 8].forEach((c) => { b[6][c] = { t: "p", c: "r" }; });
|
|
27
|
-
return b;
|
|
28
|
-
}
|
|
29
|
-
export function cloneBoard(b) {
|
|
30
|
-
return b.map((row) => row.slice());
|
|
31
|
-
}
|
|
32
|
-
// 局面指纹:用于识别重复局面(防止 AI 无限循环将军)。
|
|
33
|
-
export function posKey(board) {
|
|
34
|
-
let s = "";
|
|
35
|
-
for (const row of board) for (const p of row) s += p ? p.t + p.c : ".";
|
|
36
|
-
return s;
|
|
37
|
-
}
|
|
38
|
-
function inBounds(r, c) { return r >= 0 && r < ROWS && c >= 0 && c < COLS; }
|
|
39
|
-
function inPalace(r, c, color) {
|
|
40
|
-
if (c < 3 || c > 5) return false;
|
|
41
|
-
return color === "r" ? r >= 7 && r <= 9 : r >= 0 && r <= 2;
|
|
42
|
-
}
|
|
43
|
-
function ownSide(r, color) { return color === "r" ? r >= 5 : r <= 4; }
|
|
44
|
-
function crossedRiver(r, color) { return color === "r" ? r <= 4 : r >= 5; }
|
|
45
|
-
function countBetween(board, r, c, tr, tc, dr, dc) {
|
|
46
|
-
let n = 0, rr = r + dr, cc = c + dc;
|
|
47
|
-
while (!(rr === tr && cc === tc)) {
|
|
48
|
-
if (rr < 0 || rr >= ROWS || cc < 0 || cc >= COLS) return -1;
|
|
49
|
-
if (board[rr][cc]) n += 1;
|
|
50
|
-
rr += dr; cc += dc;
|
|
51
|
-
}
|
|
52
|
-
return n;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function attacks(board, r, c, tr, tc) {
|
|
56
|
-
const p = board[r][c];
|
|
57
|
-
if (!p) return false;
|
|
58
|
-
const dr = Math.sign(tr - r), dc = Math.sign(tc - c);
|
|
59
|
-
const t = board[tr][tc];
|
|
60
|
-
if (p.t === "k") {
|
|
61
|
-
if (r !== tr && c !== tc) return false;
|
|
62
|
-
return countBetween(board, r, c, tr, tc, dr, dc) === 0;
|
|
63
|
-
}
|
|
64
|
-
if (p.t === "c") {
|
|
65
|
-
if (r !== tr && c !== tc) return false;
|
|
66
|
-
if (dr === 0 && dc === 0) return false;
|
|
67
|
-
return countBetween(board, r, c, tr, tc, dr, dc) === 1 && !!t;
|
|
68
|
-
}
|
|
69
|
-
if (p.t === "h") {
|
|
70
|
-
const adr = Math.abs(tr - r), adc = Math.abs(tc - c);
|
|
71
|
-
if (!((adr === 2 && adc === 1) || (adr === 1 && adc === 2))) return false;
|
|
72
|
-
const legR = adr === 2 ? r + dr : r;
|
|
73
|
-
const legC = adc === 2 ? c + dc : c;
|
|
74
|
-
return !board[legR][legC];
|
|
75
|
-
}
|
|
76
|
-
if (p.t === "g") {
|
|
77
|
-
return Math.abs(tr - r) + Math.abs(tc - c) === 1 && inPalace(tr, tc, p.c);
|
|
78
|
-
}
|
|
79
|
-
if (p.t === "a") {
|
|
80
|
-
return Math.abs(tr - r) === 1 && Math.abs(tc - c) === 1 && inPalace(tr, tc, p.c);
|
|
81
|
-
}
|
|
82
|
-
if (p.t === "e") {
|
|
83
|
-
const adr = Math.abs(tr - r), adc = Math.abs(tc - c);
|
|
84
|
-
if (!(adr === 2 && adc === 2)) return false;
|
|
85
|
-
if (!ownSide(tr, p.c)) return false;
|
|
86
|
-
const eyeR = r + dr, eyeC = c + dc;
|
|
87
|
-
return !board[eyeR][eyeC];
|
|
88
|
-
}
|
|
89
|
-
if (p.t === "p") {
|
|
90
|
-
const forward = p.c === "r" ? r - 1 : r + 1;
|
|
91
|
-
if (tr === forward && tc === c) return true;
|
|
92
|
-
if (crossedRiver(r, p.c) && tr === r && Math.abs(tc - c) === 1) return true;
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function findGeneral(board, color) {
|
|
99
|
-
for (let r = 0; r < ROWS; r += 1) {
|
|
100
|
-
for (let c = 0; c < COLS; c += 1) {
|
|
101
|
-
const p = board[r][c];
|
|
102
|
-
if (p && p.t === "g" && p.c === color) return { r, c };
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
export function pseudoMoves(board, r, c) {
|
|
108
|
-
const p = board[r][c];
|
|
109
|
-
if (!p) return [];
|
|
110
|
-
const out = [];
|
|
111
|
-
const push = (nr, nc) => { if (inBounds(nr, nc)) out.push([nr, nc]); };
|
|
112
|
-
const clear = (nr, nc) => !board[nr][nc];
|
|
113
|
-
const enemy = (nr, nc) => { const q = board[nr][nc]; return q && q.c !== p.c; };
|
|
114
|
-
if (p.t === "g") {
|
|
115
|
-
[[-1, 0], [1, 0], [0, -1], [0, 1]].forEach(([dr, dc]) => {
|
|
116
|
-
const nr = r + dr, nc = c + dc;
|
|
117
|
-
if (inPalace(nr, nc, p.c) && (clear(nr, nc) || enemy(nr, nc))) push(nr, nc);
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
if (p.t === "a") {
|
|
121
|
-
[[-1, -1], [-1, 1], [1, -1], [1, 1]].forEach(([dr, dc]) => {
|
|
122
|
-
const nr = r + dr, nc = c + dc;
|
|
123
|
-
if (inPalace(nr, nc, p.c) && (clear(nr, nc) || enemy(nr, nc))) push(nr, nc);
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
if (p.t === "e") {
|
|
127
|
-
[[-2, -2], [-2, 2], [2, -2], [2, 2]].forEach(([dr, dc]) => {
|
|
128
|
-
const nr = r + dr, nc = c + dc, er = r + dr / 2, ec = c + dc / 2;
|
|
129
|
-
if (inBounds(nr, nc) && ownSide(nr, p.c) && !board[er][ec] && (clear(nr, nc) || enemy(nr, nc))) push(nr, nc);
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
if (p.t === "h") {
|
|
133
|
-
[[-2, -1], [-2, 1], [2, -1], [2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2]].forEach(([dr, dc]) => {
|
|
134
|
-
const nr = r + dr, nc = c + dc;
|
|
135
|
-
const legR = Math.abs(dr) === 2 ? r + dr / 2 : r;
|
|
136
|
-
const legC = Math.abs(dc) === 2 ? c + dc / 2 : c;
|
|
137
|
-
if (inBounds(nr, nc) && !board[legR][legC] && (clear(nr, nc) || enemy(nr, nc))) push(nr, nc);
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
if (p.t === "k" || p.t === "c") {
|
|
141
|
-
[[-1, 0], [1, 0], [0, -1], [0, 1]].forEach(([dr, dc]) => {
|
|
142
|
-
let rr = r + dr, cc = c + dc, jumped = false;
|
|
143
|
-
while (inBounds(rr, cc)) {
|
|
144
|
-
const q = board[rr][cc];
|
|
145
|
-
if (p.t === "k") {
|
|
146
|
-
if (!q) push(rr, cc);
|
|
147
|
-
else { if (q.c !== p.c) push(rr, cc); break; }
|
|
148
|
-
} else {
|
|
149
|
-
if (!q && !jumped) push(rr, cc);
|
|
150
|
-
else if (q && !jumped) jumped = true;
|
|
151
|
-
else if (q && jumped) { if (q.c !== p.c) push(rr, cc); break; }
|
|
152
|
-
}
|
|
153
|
-
rr += dr; cc += dc;
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
if (p.t === "p") {
|
|
158
|
-
const forward = p.c === "r" ? -1 : 1;
|
|
159
|
-
[[forward, 0], [0, -1], [0, 1]].forEach(([dr, dc]) => {
|
|
160
|
-
const nr = r + dr, nc = c + dc;
|
|
161
|
-
if (!inBounds(nr, nc) || (dc !== 0 && !crossedRiver(r, p.c))) return;
|
|
162
|
-
if (clear(nr, nc) || enemy(nr, nc)) push(nr, nc);
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
return out;
|
|
166
|
-
}
|
|
167
|
-
export function applyMove(board, fr, fc, tr, tc) {
|
|
168
|
-
const nb = cloneBoard(board);
|
|
169
|
-
nb[tr][tc] = nb[fr][fc];
|
|
170
|
-
nb[fr][fc] = null;
|
|
171
|
-
return nb;
|
|
172
|
-
}
|
|
173
|
-
export function inCheck(board, color) {
|
|
174
|
-
const g = findGeneral(board, color);
|
|
175
|
-
if (!g) return true;
|
|
176
|
-
const enemy = color === "r" ? "b" : "r";
|
|
177
|
-
for (let r = 0; r < ROWS; r += 1) {
|
|
178
|
-
const p = board[r][g.c];
|
|
179
|
-
if (p && p.t === "g" && p.c === enemy) {
|
|
180
|
-
let clear = true;
|
|
181
|
-
const lo = Math.min(r, g.r), hi = Math.max(r, g.r);
|
|
182
|
-
for (let i = lo + 1; i < hi; i += 1) if (board[i][g.c]) { clear = false; break; }
|
|
183
|
-
if (clear) return true;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
for (let r = 0; r < ROWS; r += 1) {
|
|
187
|
-
for (let c = 0; c < COLS; c += 1) {
|
|
188
|
-
const p = board[r][c];
|
|
189
|
-
if (p && p.c === enemy && attacks(board, r, c, g.r, g.c)) return true;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
export function legalMoves(board, color) {
|
|
195
|
-
const out = [];
|
|
196
|
-
for (let r = 0; r < ROWS; r += 1) {
|
|
197
|
-
for (let c = 0; c < COLS; c += 1) {
|
|
198
|
-
const p = board[r][c];
|
|
199
|
-
if (!p || p.c !== color) continue;
|
|
200
|
-
for (const [nr, nc] of pseudoMoves(board, r, c)) {
|
|
201
|
-
const nb = applyMove(board, r, c, nr, nc);
|
|
202
|
-
if (!inCheck(nb, color)) out.push([r, c, nr, nc]);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return out;
|
|
207
|
-
}
|
|
208
|
-
export function hasLegalMoves(board, color) {
|
|
209
|
-
return legalMoves(board, color).length > 0;
|
|
210
|
-
}
|
|
211
|
-
function evaluatePosition(board) {
|
|
212
|
-
let score = 0;
|
|
213
|
-
for (let r = 0; r < ROWS; r += 1) {
|
|
214
|
-
for (let c = 0; c < COLS; c += 1) {
|
|
215
|
-
const p = board[r][c];
|
|
216
|
-
if (!p) continue;
|
|
217
|
-
let v = VALUE[p.t];
|
|
218
|
-
if (p.t === "p") v += crossedRiver(r, p.c) ? 50 : 0;
|
|
219
|
-
score += p.c === "b" ? v : -v;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return score;
|
|
223
|
-
}
|
|
224
|
-
function evalFrom(board, color) {
|
|
225
|
-
const e = evaluatePosition(board);
|
|
226
|
-
return color === "b" ? e : -e;
|
|
227
|
-
}
|
|
228
|
-
function negamax(board, color, depth, alpha, beta) {
|
|
229
|
-
const moves = legalMoves(board, color);
|
|
230
|
-
if (moves.length === 0) {
|
|
231
|
-
return color === "b" ? -1000000 + (10 - depth) : 1000000 - (10 - depth);
|
|
232
|
-
}
|
|
233
|
-
if (depth === 0) return evalFrom(board, color);
|
|
234
|
-
const enemy = color === "r" ? "b" : "r";
|
|
235
|
-
const ordered = orderMoves(board, moves);
|
|
236
|
-
let best = -Infinity;
|
|
237
|
-
for (const m of ordered) {
|
|
238
|
-
const nb = applyMove(board, m[0], m[1], m[2], m[3]);
|
|
239
|
-
const score = -negamax(nb, enemy, depth - 1, -beta, -alpha);
|
|
240
|
-
if (score > best) best = score;
|
|
241
|
-
if (best > alpha) alpha = best;
|
|
242
|
-
if (alpha >= beta) break;
|
|
243
|
-
}
|
|
244
|
-
return best;
|
|
245
|
-
}
|
|
246
|
-
function orderMoves(board, moves) {
|
|
247
|
-
const scored = moves.map((m) => {
|
|
248
|
-
let s = 0;
|
|
249
|
-
const target = board[m[2]][m[3]];
|
|
250
|
-
if (target) s += VALUE[target.t] * 10;
|
|
251
|
-
return { m, s };
|
|
252
|
-
});
|
|
253
|
-
scored.sort((a, b) => b.s - a.s);
|
|
254
|
-
return scored.map((o) => o.m);
|
|
255
|
-
}
|
|
256
|
-
// avoid:局面指纹 → 出现次数 的计数表。
|
|
257
|
-
// 优先只在"从未出现过的局面"里选优;若全部重复,给出现次数越多的走法越大的罚分,
|
|
258
|
-
// 避免来回倒子的死循环。根节点对评分相近(±TIE_EPSILON)的走法随机挑一个,
|
|
259
|
-
// 防止每局都走出完全相同的呆板着法。
|
|
260
|
-
export function bestMove(board, color, depth, avoid) {
|
|
261
|
-
const moves = legalMoves(board, color);
|
|
262
|
-
if (!moves.length) return null;
|
|
263
|
-
const enemy = color === "r" ? "b" : "r";
|
|
264
|
-
const ordered = orderMoves(board, moves);
|
|
265
|
-
let pool;
|
|
266
|
-
if (avoid && avoid.size) {
|
|
267
|
-
const seenCount = (m) => avoid.get(posKey(applyMove(board, m[0], m[1], m[2], m[3]))) || 0;
|
|
268
|
-
const minSeen = Math.min(...ordered.map(seenCount));
|
|
269
|
-
pool = minSeen === 0
|
|
270
|
-
? ordered.filter((m) => seenCount(m) === 0).map((m) => ({ m, pen: 0 }))
|
|
271
|
-
: ordered.map((m) => ({ m, pen: seenCount(m) * 60 })).sort((a, b) => a.pen - b.pen);
|
|
272
|
-
} else {
|
|
273
|
-
pool = ordered.map((m) => ({ m, pen: 0 }));
|
|
274
|
-
}
|
|
275
|
-
if (depth <= 1) {
|
|
276
|
-
return pickAmongBest(pool.map(({ m, pen }) => ({ m, s: evalFrom(applyMove(board, m[0], m[1], m[2], m[3]), color) - pen })));
|
|
277
|
-
}
|
|
278
|
-
let bestScore = -Infinity;
|
|
279
|
-
let alpha = -Infinity;
|
|
280
|
-
const scored = [];
|
|
281
|
-
for (const { m, pen } of pool) {
|
|
282
|
-
const nb = applyMove(board, m[0], m[1], m[2], m[3]);
|
|
283
|
-
// 窗口下界放宽 TIE_EPSILON:近似并列的走法仍能拿到精确评分,供随机挑选。
|
|
284
|
-
const s = -negamax(nb, enemy, depth - 1, -Infinity, -alpha) - pen;
|
|
285
|
-
scored.push({ m, s });
|
|
286
|
-
if (s > bestScore) {
|
|
287
|
-
bestScore = s;
|
|
288
|
-
alpha = Math.max(alpha, bestScore - TIE_EPSILON);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
return pickAmongBest(scored);
|
|
292
|
-
}
|
|
293
|
-
const TIE_EPSILON = 12;
|
|
294
|
-
function pickAmongBest(scored) {
|
|
295
|
-
let bestScore = -Infinity;
|
|
296
|
-
for (const o of scored) if (o.s > bestScore) bestScore = o.s;
|
|
297
|
-
const ties = scored.filter((o) => o.s >= bestScore - TIE_EPSILON);
|
|
298
|
-
return ties[Math.floor(Math.random() * ties.length)].m;
|
|
299
|
-
}
|
|
1
|
+
/* 趣味游戏 · 象棋核心规则(纯函数,无 React)——供组件复用,也便于单测。 */
|
|
2
|
+
export const ROWS = 10, COLS = 9;
|
|
3
|
+
// 类型:g 将/帅 · a 仕/士 · e 相/象 · h 马 · k 车 · c 炮 · p 兵/卒
|
|
4
|
+
// 颜色:r 红(下)· b 黑(上)
|
|
5
|
+
|
|
6
|
+
export function pieceChar(p) {
|
|
7
|
+
const map = {
|
|
8
|
+
g: { r: "帥", b: "將" }, a: { r: "仕", b: "士" }, e: { r: "相", b: "象" },
|
|
9
|
+
h: { r: "馬", b: "馬" }, k: { r: "車", b: "車" }, c: { r: "炮", b: "炮" },
|
|
10
|
+
p: { r: "兵", b: "卒" },
|
|
11
|
+
};
|
|
12
|
+
return map[p.t] && map[p.t][p.c];
|
|
13
|
+
}
|
|
14
|
+
export const VALUE = { g: 100000, k: 900, c: 450, h: 400, e: 200, a: 200, p: 100 };
|
|
15
|
+
|
|
16
|
+
export function initialBoard() {
|
|
17
|
+
const b = Array.from({ length: ROWS }, () => Array(COLS).fill(null));
|
|
18
|
+
const back = ["k", "h", "e", "a", "g", "a", "e", "h", "k"];
|
|
19
|
+
back.forEach((t, c) => { b[0][c] = { t, c: "b" }; });
|
|
20
|
+
b[2][1] = { t: "c", c: "b" }; b[2][7] = { t: "c", c: "b" };
|
|
21
|
+
// 黑卒在己方第三横线(row 3,紧邻楚河汉界一侧留一行间隔)。
|
|
22
|
+
[0, 2, 4, 6, 8].forEach((c) => { b[3][c] = { t: "p", c: "b" }; });
|
|
23
|
+
back.forEach((t, c) => { b[9][c] = { t, c: "r" }; });
|
|
24
|
+
b[7][1] = { t: "c", c: "r" }; b[7][7] = { t: "c", c: "r" };
|
|
25
|
+
// 红兵与红炮和黑方对称(row 6 / row 7)。
|
|
26
|
+
[0, 2, 4, 6, 8].forEach((c) => { b[6][c] = { t: "p", c: "r" }; });
|
|
27
|
+
return b;
|
|
28
|
+
}
|
|
29
|
+
export function cloneBoard(b) {
|
|
30
|
+
return b.map((row) => row.slice());
|
|
31
|
+
}
|
|
32
|
+
// 局面指纹:用于识别重复局面(防止 AI 无限循环将军)。
|
|
33
|
+
export function posKey(board) {
|
|
34
|
+
let s = "";
|
|
35
|
+
for (const row of board) for (const p of row) s += p ? p.t + p.c : ".";
|
|
36
|
+
return s;
|
|
37
|
+
}
|
|
38
|
+
function inBounds(r, c) { return r >= 0 && r < ROWS && c >= 0 && c < COLS; }
|
|
39
|
+
function inPalace(r, c, color) {
|
|
40
|
+
if (c < 3 || c > 5) return false;
|
|
41
|
+
return color === "r" ? r >= 7 && r <= 9 : r >= 0 && r <= 2;
|
|
42
|
+
}
|
|
43
|
+
function ownSide(r, color) { return color === "r" ? r >= 5 : r <= 4; }
|
|
44
|
+
function crossedRiver(r, color) { return color === "r" ? r <= 4 : r >= 5; }
|
|
45
|
+
function countBetween(board, r, c, tr, tc, dr, dc) {
|
|
46
|
+
let n = 0, rr = r + dr, cc = c + dc;
|
|
47
|
+
while (!(rr === tr && cc === tc)) {
|
|
48
|
+
if (rr < 0 || rr >= ROWS || cc < 0 || cc >= COLS) return -1;
|
|
49
|
+
if (board[rr][cc]) n += 1;
|
|
50
|
+
rr += dr; cc += dc;
|
|
51
|
+
}
|
|
52
|
+
return n;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function attacks(board, r, c, tr, tc) {
|
|
56
|
+
const p = board[r][c];
|
|
57
|
+
if (!p) return false;
|
|
58
|
+
const dr = Math.sign(tr - r), dc = Math.sign(tc - c);
|
|
59
|
+
const t = board[tr][tc];
|
|
60
|
+
if (p.t === "k") {
|
|
61
|
+
if (r !== tr && c !== tc) return false;
|
|
62
|
+
return countBetween(board, r, c, tr, tc, dr, dc) === 0;
|
|
63
|
+
}
|
|
64
|
+
if (p.t === "c") {
|
|
65
|
+
if (r !== tr && c !== tc) return false;
|
|
66
|
+
if (dr === 0 && dc === 0) return false;
|
|
67
|
+
return countBetween(board, r, c, tr, tc, dr, dc) === 1 && !!t;
|
|
68
|
+
}
|
|
69
|
+
if (p.t === "h") {
|
|
70
|
+
const adr = Math.abs(tr - r), adc = Math.abs(tc - c);
|
|
71
|
+
if (!((adr === 2 && adc === 1) || (adr === 1 && adc === 2))) return false;
|
|
72
|
+
const legR = adr === 2 ? r + dr : r;
|
|
73
|
+
const legC = adc === 2 ? c + dc : c;
|
|
74
|
+
return !board[legR][legC];
|
|
75
|
+
}
|
|
76
|
+
if (p.t === "g") {
|
|
77
|
+
return Math.abs(tr - r) + Math.abs(tc - c) === 1 && inPalace(tr, tc, p.c);
|
|
78
|
+
}
|
|
79
|
+
if (p.t === "a") {
|
|
80
|
+
return Math.abs(tr - r) === 1 && Math.abs(tc - c) === 1 && inPalace(tr, tc, p.c);
|
|
81
|
+
}
|
|
82
|
+
if (p.t === "e") {
|
|
83
|
+
const adr = Math.abs(tr - r), adc = Math.abs(tc - c);
|
|
84
|
+
if (!(adr === 2 && adc === 2)) return false;
|
|
85
|
+
if (!ownSide(tr, p.c)) return false;
|
|
86
|
+
const eyeR = r + dr, eyeC = c + dc;
|
|
87
|
+
return !board[eyeR][eyeC];
|
|
88
|
+
}
|
|
89
|
+
if (p.t === "p") {
|
|
90
|
+
const forward = p.c === "r" ? r - 1 : r + 1;
|
|
91
|
+
if (tr === forward && tc === c) return true;
|
|
92
|
+
if (crossedRiver(r, p.c) && tr === r && Math.abs(tc - c) === 1) return true;
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function findGeneral(board, color) {
|
|
99
|
+
for (let r = 0; r < ROWS; r += 1) {
|
|
100
|
+
for (let c = 0; c < COLS; c += 1) {
|
|
101
|
+
const p = board[r][c];
|
|
102
|
+
if (p && p.t === "g" && p.c === color) return { r, c };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
export function pseudoMoves(board, r, c) {
|
|
108
|
+
const p = board[r][c];
|
|
109
|
+
if (!p) return [];
|
|
110
|
+
const out = [];
|
|
111
|
+
const push = (nr, nc) => { if (inBounds(nr, nc)) out.push([nr, nc]); };
|
|
112
|
+
const clear = (nr, nc) => !board[nr][nc];
|
|
113
|
+
const enemy = (nr, nc) => { const q = board[nr][nc]; return q && q.c !== p.c; };
|
|
114
|
+
if (p.t === "g") {
|
|
115
|
+
[[-1, 0], [1, 0], [0, -1], [0, 1]].forEach(([dr, dc]) => {
|
|
116
|
+
const nr = r + dr, nc = c + dc;
|
|
117
|
+
if (inPalace(nr, nc, p.c) && (clear(nr, nc) || enemy(nr, nc))) push(nr, nc);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (p.t === "a") {
|
|
121
|
+
[[-1, -1], [-1, 1], [1, -1], [1, 1]].forEach(([dr, dc]) => {
|
|
122
|
+
const nr = r + dr, nc = c + dc;
|
|
123
|
+
if (inPalace(nr, nc, p.c) && (clear(nr, nc) || enemy(nr, nc))) push(nr, nc);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (p.t === "e") {
|
|
127
|
+
[[-2, -2], [-2, 2], [2, -2], [2, 2]].forEach(([dr, dc]) => {
|
|
128
|
+
const nr = r + dr, nc = c + dc, er = r + dr / 2, ec = c + dc / 2;
|
|
129
|
+
if (inBounds(nr, nc) && ownSide(nr, p.c) && !board[er][ec] && (clear(nr, nc) || enemy(nr, nc))) push(nr, nc);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (p.t === "h") {
|
|
133
|
+
[[-2, -1], [-2, 1], [2, -1], [2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2]].forEach(([dr, dc]) => {
|
|
134
|
+
const nr = r + dr, nc = c + dc;
|
|
135
|
+
const legR = Math.abs(dr) === 2 ? r + dr / 2 : r;
|
|
136
|
+
const legC = Math.abs(dc) === 2 ? c + dc / 2 : c;
|
|
137
|
+
if (inBounds(nr, nc) && !board[legR][legC] && (clear(nr, nc) || enemy(nr, nc))) push(nr, nc);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (p.t === "k" || p.t === "c") {
|
|
141
|
+
[[-1, 0], [1, 0], [0, -1], [0, 1]].forEach(([dr, dc]) => {
|
|
142
|
+
let rr = r + dr, cc = c + dc, jumped = false;
|
|
143
|
+
while (inBounds(rr, cc)) {
|
|
144
|
+
const q = board[rr][cc];
|
|
145
|
+
if (p.t === "k") {
|
|
146
|
+
if (!q) push(rr, cc);
|
|
147
|
+
else { if (q.c !== p.c) push(rr, cc); break; }
|
|
148
|
+
} else {
|
|
149
|
+
if (!q && !jumped) push(rr, cc);
|
|
150
|
+
else if (q && !jumped) jumped = true;
|
|
151
|
+
else if (q && jumped) { if (q.c !== p.c) push(rr, cc); break; }
|
|
152
|
+
}
|
|
153
|
+
rr += dr; cc += dc;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (p.t === "p") {
|
|
158
|
+
const forward = p.c === "r" ? -1 : 1;
|
|
159
|
+
[[forward, 0], [0, -1], [0, 1]].forEach(([dr, dc]) => {
|
|
160
|
+
const nr = r + dr, nc = c + dc;
|
|
161
|
+
if (!inBounds(nr, nc) || (dc !== 0 && !crossedRiver(r, p.c))) return;
|
|
162
|
+
if (clear(nr, nc) || enemy(nr, nc)) push(nr, nc);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
export function applyMove(board, fr, fc, tr, tc) {
|
|
168
|
+
const nb = cloneBoard(board);
|
|
169
|
+
nb[tr][tc] = nb[fr][fc];
|
|
170
|
+
nb[fr][fc] = null;
|
|
171
|
+
return nb;
|
|
172
|
+
}
|
|
173
|
+
export function inCheck(board, color) {
|
|
174
|
+
const g = findGeneral(board, color);
|
|
175
|
+
if (!g) return true;
|
|
176
|
+
const enemy = color === "r" ? "b" : "r";
|
|
177
|
+
for (let r = 0; r < ROWS; r += 1) {
|
|
178
|
+
const p = board[r][g.c];
|
|
179
|
+
if (p && p.t === "g" && p.c === enemy) {
|
|
180
|
+
let clear = true;
|
|
181
|
+
const lo = Math.min(r, g.r), hi = Math.max(r, g.r);
|
|
182
|
+
for (let i = lo + 1; i < hi; i += 1) if (board[i][g.c]) { clear = false; break; }
|
|
183
|
+
if (clear) return true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (let r = 0; r < ROWS; r += 1) {
|
|
187
|
+
for (let c = 0; c < COLS; c += 1) {
|
|
188
|
+
const p = board[r][c];
|
|
189
|
+
if (p && p.c === enemy && attacks(board, r, c, g.r, g.c)) return true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
export function legalMoves(board, color) {
|
|
195
|
+
const out = [];
|
|
196
|
+
for (let r = 0; r < ROWS; r += 1) {
|
|
197
|
+
for (let c = 0; c < COLS; c += 1) {
|
|
198
|
+
const p = board[r][c];
|
|
199
|
+
if (!p || p.c !== color) continue;
|
|
200
|
+
for (const [nr, nc] of pseudoMoves(board, r, c)) {
|
|
201
|
+
const nb = applyMove(board, r, c, nr, nc);
|
|
202
|
+
if (!inCheck(nb, color)) out.push([r, c, nr, nc]);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
export function hasLegalMoves(board, color) {
|
|
209
|
+
return legalMoves(board, color).length > 0;
|
|
210
|
+
}
|
|
211
|
+
function evaluatePosition(board) {
|
|
212
|
+
let score = 0;
|
|
213
|
+
for (let r = 0; r < ROWS; r += 1) {
|
|
214
|
+
for (let c = 0; c < COLS; c += 1) {
|
|
215
|
+
const p = board[r][c];
|
|
216
|
+
if (!p) continue;
|
|
217
|
+
let v = VALUE[p.t];
|
|
218
|
+
if (p.t === "p") v += crossedRiver(r, p.c) ? 50 : 0;
|
|
219
|
+
score += p.c === "b" ? v : -v;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return score;
|
|
223
|
+
}
|
|
224
|
+
function evalFrom(board, color) {
|
|
225
|
+
const e = evaluatePosition(board);
|
|
226
|
+
return color === "b" ? e : -e;
|
|
227
|
+
}
|
|
228
|
+
function negamax(board, color, depth, alpha, beta) {
|
|
229
|
+
const moves = legalMoves(board, color);
|
|
230
|
+
if (moves.length === 0) {
|
|
231
|
+
return color === "b" ? -1000000 + (10 - depth) : 1000000 - (10 - depth);
|
|
232
|
+
}
|
|
233
|
+
if (depth === 0) return evalFrom(board, color);
|
|
234
|
+
const enemy = color === "r" ? "b" : "r";
|
|
235
|
+
const ordered = orderMoves(board, moves);
|
|
236
|
+
let best = -Infinity;
|
|
237
|
+
for (const m of ordered) {
|
|
238
|
+
const nb = applyMove(board, m[0], m[1], m[2], m[3]);
|
|
239
|
+
const score = -negamax(nb, enemy, depth - 1, -beta, -alpha);
|
|
240
|
+
if (score > best) best = score;
|
|
241
|
+
if (best > alpha) alpha = best;
|
|
242
|
+
if (alpha >= beta) break;
|
|
243
|
+
}
|
|
244
|
+
return best;
|
|
245
|
+
}
|
|
246
|
+
function orderMoves(board, moves) {
|
|
247
|
+
const scored = moves.map((m) => {
|
|
248
|
+
let s = 0;
|
|
249
|
+
const target = board[m[2]][m[3]];
|
|
250
|
+
if (target) s += VALUE[target.t] * 10;
|
|
251
|
+
return { m, s };
|
|
252
|
+
});
|
|
253
|
+
scored.sort((a, b) => b.s - a.s);
|
|
254
|
+
return scored.map((o) => o.m);
|
|
255
|
+
}
|
|
256
|
+
// avoid:局面指纹 → 出现次数 的计数表。
|
|
257
|
+
// 优先只在"从未出现过的局面"里选优;若全部重复,给出现次数越多的走法越大的罚分,
|
|
258
|
+
// 避免来回倒子的死循环。根节点对评分相近(±TIE_EPSILON)的走法随机挑一个,
|
|
259
|
+
// 防止每局都走出完全相同的呆板着法。
|
|
260
|
+
export function bestMove(board, color, depth, avoid) {
|
|
261
|
+
const moves = legalMoves(board, color);
|
|
262
|
+
if (!moves.length) return null;
|
|
263
|
+
const enemy = color === "r" ? "b" : "r";
|
|
264
|
+
const ordered = orderMoves(board, moves);
|
|
265
|
+
let pool;
|
|
266
|
+
if (avoid && avoid.size) {
|
|
267
|
+
const seenCount = (m) => avoid.get(posKey(applyMove(board, m[0], m[1], m[2], m[3]))) || 0;
|
|
268
|
+
const minSeen = Math.min(...ordered.map(seenCount));
|
|
269
|
+
pool = minSeen === 0
|
|
270
|
+
? ordered.filter((m) => seenCount(m) === 0).map((m) => ({ m, pen: 0 }))
|
|
271
|
+
: ordered.map((m) => ({ m, pen: seenCount(m) * 60 })).sort((a, b) => a.pen - b.pen);
|
|
272
|
+
} else {
|
|
273
|
+
pool = ordered.map((m) => ({ m, pen: 0 }));
|
|
274
|
+
}
|
|
275
|
+
if (depth <= 1) {
|
|
276
|
+
return pickAmongBest(pool.map(({ m, pen }) => ({ m, s: evalFrom(applyMove(board, m[0], m[1], m[2], m[3]), color) - pen })));
|
|
277
|
+
}
|
|
278
|
+
let bestScore = -Infinity;
|
|
279
|
+
let alpha = -Infinity;
|
|
280
|
+
const scored = [];
|
|
281
|
+
for (const { m, pen } of pool) {
|
|
282
|
+
const nb = applyMove(board, m[0], m[1], m[2], m[3]);
|
|
283
|
+
// 窗口下界放宽 TIE_EPSILON:近似并列的走法仍能拿到精确评分,供随机挑选。
|
|
284
|
+
const s = -negamax(nb, enemy, depth - 1, -Infinity, -alpha) - pen;
|
|
285
|
+
scored.push({ m, s });
|
|
286
|
+
if (s > bestScore) {
|
|
287
|
+
bestScore = s;
|
|
288
|
+
alpha = Math.max(alpha, bestScore - TIE_EPSILON);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return pickAmongBest(scored);
|
|
292
|
+
}
|
|
293
|
+
const TIE_EPSILON = 12;
|
|
294
|
+
function pickAmongBest(scored) {
|
|
295
|
+
let bestScore = -Infinity;
|
|
296
|
+
for (const o of scored) if (o.s > bestScore) bestScore = o.s;
|
|
297
|
+
const ties = scored.filter((o) => o.s >= bestScore - TIE_EPSILON);
|
|
298
|
+
return ties[Math.floor(Math.random() * ties.length)].m;
|
|
299
|
+
}
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
// dsh-dock · 功能模块【心跳监视】· 客户端视图(纯 Client 功能,无宿主半部)
|
|
2
|
-
import react from "react";
|
|
3
|
-
|
|
4
|
-
// 面板加载时刻:心跳视图与首页总揽共用的运行时长基准(同一数字,不分谁先挂载)
|
|
5
|
-
const loadedAt = Date.now();
|
|
6
|
-
function uptimeText() {
|
|
7
|
-
const sec = Math.max(0, Math.floor((Date.now() - loadedAt) / 1000));
|
|
8
|
-
const m = Math.floor(sec / 60);
|
|
9
|
-
return "面板已运行 " + (m > 0 ? m + " 分 " : "") + (sec % 60) + " 秒";
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function HeartbeatView(props) {
|
|
13
|
-
const ctx = props && props.ctx;
|
|
14
|
-
const [txt, setTxt] = react.useState(uptimeText());
|
|
15
|
-
react.useEffect(() => (ctx && typeof ctx.interval === "function" ? ctx.interval(() => setTxt(uptimeText()), 1000) : undefined), []);
|
|
16
|
-
return react.createElement("div", null, txt);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function HeartbeatStat(props) {
|
|
20
|
-
const ctx = props && props.ctx;
|
|
21
|
-
const [txt, setTxt] = react.useState(uptimeText());
|
|
22
|
-
react.useEffect(() => (ctx && typeof ctx.interval === "function" ? ctx.interval(() => setTxt(uptimeText()), 1000) : undefined), []);
|
|
23
|
-
return react.createElement("span", null, txt);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const feature = {
|
|
27
|
-
id: "heartbeat",
|
|
28
|
-
name: "心跳监视",
|
|
29
|
-
order: 140,
|
|
30
|
-
accent: "#34d399",
|
|
31
|
-
description: "示例功能:面板侧运行时长心跳(纯 Client)",
|
|
32
|
-
defaultEnabled: false,
|
|
33
|
-
View: HeartbeatView,
|
|
34
|
-
HomeStat: HeartbeatStat,
|
|
35
|
-
};
|
|
1
|
+
// dsh-dock · 功能模块【心跳监视】· 客户端视图(纯 Client 功能,无宿主半部)
|
|
2
|
+
import react from "react";
|
|
3
|
+
|
|
4
|
+
// 面板加载时刻:心跳视图与首页总揽共用的运行时长基准(同一数字,不分谁先挂载)
|
|
5
|
+
const loadedAt = Date.now();
|
|
6
|
+
function uptimeText() {
|
|
7
|
+
const sec = Math.max(0, Math.floor((Date.now() - loadedAt) / 1000));
|
|
8
|
+
const m = Math.floor(sec / 60);
|
|
9
|
+
return "面板已运行 " + (m > 0 ? m + " 分 " : "") + (sec % 60) + " 秒";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function HeartbeatView(props) {
|
|
13
|
+
const ctx = props && props.ctx;
|
|
14
|
+
const [txt, setTxt] = react.useState(uptimeText());
|
|
15
|
+
react.useEffect(() => (ctx && typeof ctx.interval === "function" ? ctx.interval(() => setTxt(uptimeText()), 1000) : undefined), []);
|
|
16
|
+
return react.createElement("div", null, txt);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function HeartbeatStat(props) {
|
|
20
|
+
const ctx = props && props.ctx;
|
|
21
|
+
const [txt, setTxt] = react.useState(uptimeText());
|
|
22
|
+
react.useEffect(() => (ctx && typeof ctx.interval === "function" ? ctx.interval(() => setTxt(uptimeText()), 1000) : undefined), []);
|
|
23
|
+
return react.createElement("span", null, txt);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const feature = {
|
|
27
|
+
id: "heartbeat",
|
|
28
|
+
name: "心跳监视",
|
|
29
|
+
order: 140,
|
|
30
|
+
accent: "#34d399",
|
|
31
|
+
description: "示例功能:面板侧运行时长心跳(纯 Client)",
|
|
32
|
+
defaultEnabled: false,
|
|
33
|
+
View: HeartbeatView,
|
|
34
|
+
HomeStat: HeartbeatStat,
|
|
35
|
+
};
|