dsh-dock 0.6.0 → 0.7.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/README.md +6 -4
- package/client.js +620 -71
- package/features/animation/view.jsx +491 -18
- package/features/balance/view.js +25 -4
- package/features/games/games/breakout.jsx +225 -0
- package/features/games/games/gomoku.jsx +175 -0
- package/features/games/games/racer.jsx +171 -0
- package/features/games/games/snake.jsx +103 -0
- package/features/games/games/sokoban.jsx +153 -0
- package/features/games/games/tank.jsx +116 -0
- package/features/games/games/tetris.jsx +235 -0
- package/features/games/games/xiangqi.jsx +128 -0
- package/features/games/tank-core.js +152 -0
- package/features/games/view.jsx +632 -0
- package/features/games/xiangqi-core.js +270 -0
- package/features/mobile-relay/gateway.js +279 -0
- package/features/mobile-relay/host.js +328 -0
- package/features/mobile-relay/view.jsx +207 -0
- package/features/tokenlog/host.js +2 -2
- package/features/tokenlog/view.jsx +12 -21
- package/package.json +3 -2
- package/screenshots/dock-animation.png +0 -0
- package/screenshots/dock-modelconfig.png +0 -0
- package/src/client.jsx +45 -9
- package/src/host-core.js +4 -1
- package/screenshots/dock-robot.png +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/* 趣味游戏 · 象棋(棋盘对战)——纯 Client;玩家执红先行,AI 执黑。 */
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
initialBoard, cloneBoard, pieceChar, inCheck, hasLegalMoves,
|
|
5
|
+
legalMoves, bestMove, applyMove,
|
|
6
|
+
} from "../xiangqi-core.js";
|
|
7
|
+
|
|
8
|
+
export function XiangqiGame(props) {
|
|
9
|
+
const [board, setBoard] = useState(initialBoard);
|
|
10
|
+
const [turn, setTurn] = useState("r");
|
|
11
|
+
const [sel, setSel] = useState(null); // [r,c]
|
|
12
|
+
const [targets, setTargets] = useState([]); // [r,c][]
|
|
13
|
+
const [over, setOver] = useState(null); // 'r' | 'b' | 'draw'
|
|
14
|
+
const [check, setCheck] = useState(false);
|
|
15
|
+
const [last, setLast] = useState(null);
|
|
16
|
+
const [thinking, setThinking] = useState(false);
|
|
17
|
+
const [history, setHistory] = useState([]);
|
|
18
|
+
|
|
19
|
+
const stageRef = useRef(null);
|
|
20
|
+
const boardRef = useRef(board); boardRef.current = board;
|
|
21
|
+
const turnRef = useRef(turn); turnRef.current = turn;
|
|
22
|
+
const overRef = useRef(over); overRef.current = over;
|
|
23
|
+
const thinkingRef = useRef(thinking); thinkingRef.current = thinking;
|
|
24
|
+
const historyRef = useRef(history); historyRef.current = history;
|
|
25
|
+
|
|
26
|
+
const restart = useCallback(() => {
|
|
27
|
+
const b = initialBoard();
|
|
28
|
+
boardRef.current = b;
|
|
29
|
+
setBoard(b); setTurn("r"); setSel(null); setTargets([]); setOver(null); setCheck(false); setLast(null); setThinking(false); setHistory([]);
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const undo = useCallback(() => {
|
|
33
|
+
if (thinkingRef.current || overRef.current || turnRef.current !== "r") return;
|
|
34
|
+
const h = historyRef.current;
|
|
35
|
+
if (h.length < 2) return;
|
|
36
|
+
const hist = h.slice(0, -2);
|
|
37
|
+
const b = hist.length ? cloneBoard(hist[hist.length - 1]) : initialBoard();
|
|
38
|
+
boardRef.current = b;
|
|
39
|
+
setBoard(b); setHistory(hist); setSel(null); setTargets([]); setLast(null); setOver(null);
|
|
40
|
+
setCheck(inCheck(b, "r"));
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const moveRed = useCallback((tr, tc) => {
|
|
44
|
+
if (turnRef.current !== "r" || overRef.current || !sel) return;
|
|
45
|
+
const fr = sel[0], fc = sel[1];
|
|
46
|
+
const nb = applyMove(boardRef.current, fr, fc, tr, tc);
|
|
47
|
+
setHistory((h) => [...h, boardRef.current]);
|
|
48
|
+
boardRef.current = nb;
|
|
49
|
+
setBoard(nb);
|
|
50
|
+
setSel(null); setTargets([]); setLast([tr, tc]);
|
|
51
|
+
setTurn("b");
|
|
52
|
+
if (!hasLegalMoves(nb, "b")) { setOver("r"); setCheck(false); }
|
|
53
|
+
else setCheck(inCheck(nb, "b"));
|
|
54
|
+
}, [sel]);
|
|
55
|
+
|
|
56
|
+
const select = useCallback((r, c) => {
|
|
57
|
+
if (overRef.current || turnRef.current !== "r" || thinkingRef.current) return;
|
|
58
|
+
const p = boardRef.current[r][c];
|
|
59
|
+
if (p && p.c === "r") {
|
|
60
|
+
setSel([r, c]);
|
|
61
|
+
const list = legalMoves(boardRef.current, "r").filter((m) => m[0] === r && m[1] === c).map((m) => [m[2], m[3]]);
|
|
62
|
+
setTargets(list);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (sel && targets.some(([tr, tc]) => tr === r && tc === c)) { moveRed(r, c); return; }
|
|
66
|
+
setSel(null); setTargets([]);
|
|
67
|
+
}, [sel, targets, moveRed]);
|
|
68
|
+
|
|
69
|
+
// AI 回合(执黑)。
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (turn !== "b" || over || props.paused) return;
|
|
72
|
+
const t = setTimeout(() => {
|
|
73
|
+
const mv = bestMove(boardRef.current, "b", 3);
|
|
74
|
+
if (mv) {
|
|
75
|
+
const [fr, fc, tr, tc] = mv;
|
|
76
|
+
const nb = applyMove(boardRef.current, fr, fc, tr, tc);
|
|
77
|
+
setHistory((h) => [...h, boardRef.current]);
|
|
78
|
+
boardRef.current = nb;
|
|
79
|
+
setBoard(nb); setLast([tr, tc]); setTurn("r");
|
|
80
|
+
if (!hasLegalMoves(nb, "r")) { setOver("b"); setCheck(false); }
|
|
81
|
+
else setCheck(inCheck(nb, "r"));
|
|
82
|
+
} else setOver("r");
|
|
83
|
+
setThinking(false);
|
|
84
|
+
}, 90);
|
|
85
|
+
setThinking(true);
|
|
86
|
+
return () => clearTimeout(t);
|
|
87
|
+
}, [turn, over, props.paused]);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (props.paused) return;
|
|
91
|
+
if (stageRef.current) stageRef.current.focus();
|
|
92
|
+
}, [turn, over, props.paused]);
|
|
93
|
+
|
|
94
|
+
const onKeyDown = useCallback((event) => {
|
|
95
|
+
if (event.key.toLowerCase() === "r") { event.preventDefault(); restart(); }
|
|
96
|
+
else if (event.key.toLowerCase() === "u") { event.preventDefault(); undo(); }
|
|
97
|
+
}, [restart, undo]);
|
|
98
|
+
|
|
99
|
+
const status = over === "r" ? "红方胜 · 恭喜" : over === "b" ? "黑方胜 · AI 赢" : (thinking ? "AI 思考中…" : (check ? "将军!" : (turn === "r" ? "轮到你(红方)" : "轮到 AI(黑方)")));
|
|
100
|
+
|
|
101
|
+
return <section className="dgame-game" aria-label="象棋">
|
|
102
|
+
<div className="dgame-game-head"><div><h3>中国象棋</h3><p>你执红先行,AI 执黑;吃掉对方将/帅即胜。点击选中红子,再点高亮落点走棋。</p></div><div className="dgame-score"><span>当前</span><strong className="dg-gomoku-status">{status}</strong><button type="button" onClick={undo}>撤销</button><button type="button" onClick={restart}>重开</button></div></div>
|
|
103
|
+
<div className="dgame-xiangqi-board" ref={stageRef} tabIndex={0} onKeyDown={onKeyDown} role="grid" aria-label="中国象棋棋盘">
|
|
104
|
+
{board.map((row, r) => row.map((p, c) => {
|
|
105
|
+
const isSel = sel && sel[0] === r && sel[1] === c;
|
|
106
|
+
const isTargetMark = targets.some(([tr, tc]) => tr === r && tc === c);
|
|
107
|
+
const isLast = last && last[0] === r && last[1] === c;
|
|
108
|
+
return <button type="button" key={r + "-" + c} role="gridcell"
|
|
109
|
+
className={"dgame-xiangqi-cell" + (isSel ? " sel" : "") + (isTargetMark ? " target" : "") + (isLast ? " last" : "")}
|
|
110
|
+
aria-label={"第" + (r + 1) + "行第" + (c + 1) + "列" + (p ? pieceChar(p) : " 空")}
|
|
111
|
+
onClick={() => select(r, c)}>
|
|
112
|
+
{p ? <span className={"dgame-xiangqi-pc " + p.c}>{pieceChar(p)}</span> : (isTargetMark ? <span className="dgame-xiangqi-dot" aria-hidden="true" /> : null)}
|
|
113
|
+
</button>;
|
|
114
|
+
}))}
|
|
115
|
+
</div>
|
|
116
|
+
<div className="dgame-controls"><button type="button" onClick={undo}>撤销</button><span>{status} · R 重开 · U 撤销</span><button type="button" onClick={restart}>重开</button></div>
|
|
117
|
+
</section>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function XiangqiPreview() {
|
|
121
|
+
return <span className="dgcov-prev dgcov-prev-xiangqi" aria-hidden="true">
|
|
122
|
+
{["車馬象士將象馬車", "·炮·····炮·", "卒.卒.卒.卒.卒", "兵.兵.兵.兵.兵", "·炮·····炮·", "車馬相仕帥相馬車"].map((row, r) => <b key={r}>{row}</b>)}
|
|
123
|
+
</span>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const xiangqiGame = { Game: XiangqiGame, Preview: XiangqiPreview, css: `
|
|
127
|
+
.dgame-xiangqi-board{display:grid;grid-template-columns:repeat(9,1fr);grid-template-rows:repeat(10,1fr);align-self:center;width:min(100%,320px);aspect-ratio:9/10;padding:6px;border-radius:8px;background:linear-gradient(135deg,#e8c07a,#c08a3e);border:1px solid color-mix(in srgb,#8a5a1c 55%,transparent);box-shadow:0 4px 18px rgb(0 0 0/.35),inset 0 0 0 2px rgba(0,0,0,.14);outline:none;background-image:repeating-linear-gradient(90deg,transparent 0 calc(100%/8),rgba(0,0,0,.14) calc(100%/8) calc(100%/8 + 2px)),repeating-linear-gradient(0deg,transparent 0 calc(100%/9),rgba(85,52,0,.28) calc(100%/9) calc(100%/9 + 2px))}.dgame-xiangqi-board:focus-visible{box-shadow:0 0 0 3px rgba(0,0,0,.25)}.dgame-xiangqi-cell{position:relative;background:transparent;border:none;cursor:pointer;padding:0;margin:0;outline:none;display:grid;place-items:center}.dgame-xiangqi-cell.sel{box-shadow:inset 0 0 0 3px rgba(255,200,80,.55)}.dgame-xiangqi-cell.target .dgame-xiangqi-dot{width:10px;height:10px;border-radius:50%;background:rgba(34,211,238,.55);box-shadow:0 0 8px rgba(34,211,238,.6)}.dgame-xiangqi-cell.last{box-shadow:inset 0 0 0 3px rgba(248,113,113,.5)}.dgame-xiangqi-pc{font-size:clamp(11px,2.2vw,15px);font-weight:700;display:block;z-index:1}.dgame-xiangqi-pc.r{color:#b91c1c;text-shadow:0 1px 0 rgba(255,255,255,.5)}.dgame-xiangqi-pc.b{color:#1f2937;text-shadow:0 1px 0 rgba(255,255,255,.35)}
|
|
128
|
+
` };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/* 趣味游戏 · 坦克大战核心逻辑(纯函数,无 React)——供组件复用,便于单测。 */
|
|
2
|
+
export const GRID = 15;
|
|
3
|
+
export const EMPTY = 0, STEEL = 1, BRICK = 2, BASE = 3;
|
|
4
|
+
const DIRS = { up: [-1, 0], down: [1, 0], left: [0, -1], right: [0, 1] };
|
|
5
|
+
export const DIR_KEYS = Object.keys(DIRS);
|
|
6
|
+
const SPAWNS = [{ r: 0, c: 0 }, { r: 0, c: 7 }, { r: 0, c: 14 }];
|
|
7
|
+
|
|
8
|
+
function cloneGrid(g) { return g.map((row) => row.slice()); }
|
|
9
|
+
function inBounds(r, c) { return r >= 0 && r < GRID && c >= 0 && c < GRID; }
|
|
10
|
+
function cellBlockedForTank(grid, r, c) { return !inBounds(r, c) || grid[r][c] !== EMPTY; }
|
|
11
|
+
|
|
12
|
+
export function makeGrid() {
|
|
13
|
+
const g = Array.from({ length: GRID }, () => Array(GRID).fill(EMPTY));
|
|
14
|
+
const set = (r, c, v) => { if (r >= 0 && r < GRID && c >= 0 && c < GRID) g[r][c] = v; };
|
|
15
|
+
for (let r = 2; r <= 5; r += 1) { set(r, 3, STEEL); set(r, 11, STEEL); set(r, 7, STEEL); }
|
|
16
|
+
for (let c = 4; c <= 10; c += 1) set(8, c, BRICK);
|
|
17
|
+
set(9, 5, BRICK); set(9, 6, BRICK); set(9, 8, BRICK); set(9, 9, BRICK); set(9, 10, BRICK);
|
|
18
|
+
set(13, 7, BASE);
|
|
19
|
+
set(12, 6, BRICK); set(12, 7, BRICK); set(12, 8, BRICK);
|
|
20
|
+
set(13, 6, BRICK); set(13, 8, BRICK);
|
|
21
|
+
return g;
|
|
22
|
+
}
|
|
23
|
+
export function findBase(grid) {
|
|
24
|
+
for (let r = 0; r < GRID; r += 1) for (let c = 0; c < GRID; c += 1) if (grid[r][c] === BASE) return { r, c };
|
|
25
|
+
return { r: 13, c: 7 };
|
|
26
|
+
}
|
|
27
|
+
function playerSpawn() { return { r: 11, c: 7, dir: "up", alive: true, lives: 3, cooldown: 0 }; }
|
|
28
|
+
|
|
29
|
+
export function initialState() {
|
|
30
|
+
return {
|
|
31
|
+
grid: makeGrid(),
|
|
32
|
+
player: playerSpawn(),
|
|
33
|
+
enemies: [],
|
|
34
|
+
bullets: [],
|
|
35
|
+
score: 0,
|
|
36
|
+
wave: 1,
|
|
37
|
+
remaining: 4,
|
|
38
|
+
active: 2,
|
|
39
|
+
spawnTick: 0,
|
|
40
|
+
over: false, win: false,
|
|
41
|
+
respawnTicks: 0,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function spawnBullet(r, c, dir, from) {
|
|
45
|
+
const [dr, dc] = DIRS[dir];
|
|
46
|
+
return { r: r + dr, c: c + dc, dr, dc, from };
|
|
47
|
+
}
|
|
48
|
+
function spawnOne(grid, enemies) {
|
|
49
|
+
for (const s of SPAWNS) {
|
|
50
|
+
if (grid[s.r][s.c] === EMPTY && !enemies.some((e) => e.r === s.r && e.c === s.c)) {
|
|
51
|
+
return { r: s.r, c: s.c, dir: "down", hp: 1, cooldown: 3, moveTick: 0 };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function step(state, input) {
|
|
58
|
+
if (state.over || state.win) return state;
|
|
59
|
+
const grid = cloneGrid(state.grid);
|
|
60
|
+
let player = Object.assign({}, state.player);
|
|
61
|
+
let enemies = state.enemies.map((e) => Object.assign({}, e));
|
|
62
|
+
let bullets = state.bullets.map((b) => Object.assign({}, b));
|
|
63
|
+
let remaining = state.remaining, score = state.score, over = state.over;
|
|
64
|
+
let respawnTicks = state.respawnTicks;
|
|
65
|
+
let spawnTick = state.spawnTick + 1;
|
|
66
|
+
|
|
67
|
+
// 玩家移动
|
|
68
|
+
if (player.alive && input.dir && DIRS[input.dir]) {
|
|
69
|
+
const [dr, dc] = DIRS[input.dir];
|
|
70
|
+
player.dir = input.dir;
|
|
71
|
+
const nr = player.r + dr, nc = player.c + dc;
|
|
72
|
+
if (!cellBlockedForTank(grid, nr, nc)) { player.r = nr; player.c = nc; }
|
|
73
|
+
}
|
|
74
|
+
// 玩家射击
|
|
75
|
+
if (player.alive && input.shoot && player.cooldown <= 0) {
|
|
76
|
+
bullets.push(spawnBullet(player.r, player.c, player.dir, "p"));
|
|
77
|
+
player.cooldown = 9;
|
|
78
|
+
}
|
|
79
|
+
player.cooldown = Math.max(0, player.cooldown - 1);
|
|
80
|
+
|
|
81
|
+
// 敌人移动/射击
|
|
82
|
+
for (let i = 0; i < enemies.length; i += 1) {
|
|
83
|
+
const e = enemies[i];
|
|
84
|
+
e.cooldown -= 1; e.moveTick += 1;
|
|
85
|
+
if (e.moveTick % 12 === 0) {
|
|
86
|
+
const [dr, dc] = DIRS[e.dir];
|
|
87
|
+
const nr = e.r + dr, nc = e.c + dc;
|
|
88
|
+
if (cellBlockedForTank(grid, nr, nc) || enemies.some((o, oi) => oi !== i && o.r === nr && o.c === nc)) {
|
|
89
|
+
const opts = DIR_KEYS.filter((d) => {
|
|
90
|
+
const [ddr, ddc] = DIRS[d];
|
|
91
|
+
const tr = e.r + ddr, tc = e.c + ddc;
|
|
92
|
+
return !cellBlockedForTank(grid, tr, tc) && !enemies.some((o, oi) => oi !== i && o.r === tr && o.c === tc);
|
|
93
|
+
});
|
|
94
|
+
if (opts.length) e.dir = opts[Math.floor(Math.random() * opts.length)];
|
|
95
|
+
} else { e.r = nr; e.c = nc; }
|
|
96
|
+
}
|
|
97
|
+
if (e.cooldown <= 0) {
|
|
98
|
+
bullets.push(spawnBullet(e.r, e.c, e.dir, "e"));
|
|
99
|
+
e.cooldown = 9 + Math.floor(Math.random() * 9);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 子弹移动/碰撞
|
|
104
|
+
let baseHit = false, playerHit = false;
|
|
105
|
+
const nextBullets = [];
|
|
106
|
+
const base = findBase(grid);
|
|
107
|
+
for (const b of bullets) {
|
|
108
|
+
const nr = b.r + b.dr, nc = b.c + b.dc;
|
|
109
|
+
if (!inBounds(nr, nc)) continue;
|
|
110
|
+
const cell = grid[nr][nc];
|
|
111
|
+
if (cell === STEEL) continue;
|
|
112
|
+
if (cell === BRICK) { grid[nr][nc] = EMPTY; continue; }
|
|
113
|
+
if (cell === BASE) { if (b.from === "e") baseHit = true; continue; }
|
|
114
|
+
if (b.from === "p") {
|
|
115
|
+
const idx = enemies.findIndex((e) => e.r === nr && e.c === nc);
|
|
116
|
+
if (idx >= 0) { enemies.splice(idx, 1); score += 100; continue; }
|
|
117
|
+
} else if (player.alive && player.r === nr && player.c === nc) {
|
|
118
|
+
player.lives -= 1;
|
|
119
|
+
if (player.lives <= 0) over = true;
|
|
120
|
+
else { player.alive = false; respawnTicks = 24; }
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
b.r = nr; b.c = nc;
|
|
124
|
+
nextBullets.push(b);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 生成敌人
|
|
128
|
+
if (remaining > 0 && enemies.length < state.active && spawnTick % 9 === 0) {
|
|
129
|
+
const fresh = spawnOne(grid, enemies);
|
|
130
|
+
if (fresh) { enemies.push(fresh); remaining -= 1; }
|
|
131
|
+
}
|
|
132
|
+
// 玩家重生
|
|
133
|
+
if (!player.alive && !over) {
|
|
134
|
+
if (respawnTicks <= 0) { player.alive = true; player.r = 11; player.c = 7; player.dir = "up"; player.cooldown = 0; }
|
|
135
|
+
else respawnTicks -= 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const win = remaining <= 0 && enemies.length === 0;
|
|
139
|
+
return Object.assign({}, state, {
|
|
140
|
+
grid, player, enemies, bullets: nextBullets, score, remaining,
|
|
141
|
+
over: over || baseHit, win, spawnTick, respawnTicks,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function nextWave(state) {
|
|
146
|
+
return Object.assign({}, state, {
|
|
147
|
+
wave: state.wave + 1,
|
|
148
|
+
remaining: Math.min(3 + state.wave, 8),
|
|
149
|
+
active: Math.min(2 + Math.floor(state.wave / 2), 4),
|
|
150
|
+
enemies: [], win: false, spawnTick: 0,
|
|
151
|
+
});
|
|
152
|
+
}
|