@modelstatus/cli 0.1.35 → 0.1.37
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/package.json +1 -1
- package/src/index.js +113 -0
- package/src/sources/scan-process.js +238 -0
- package/src/sources/scan-worker.js +148 -0
- package/src/tui/app.js +40 -1
- package/src/tui/game/DkGame.js +18 -184
- package/src/tui/game/dk-core.js +507 -226
- package/src/tui/game/dk-render.js +46 -0
- package/src/tui/game/input.js +169 -0
- package/src/tui/game/loop.js +337 -0
- package/src/tui/game/term.js +330 -0
- package/src/tui/views/scan.js +94 -80
package/src/tui/game/DkGame.js
CHANGED
|
@@ -1,187 +1,21 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* RETIRED. The old Ink-overlay Donkey Kong (a React component running a useTick
|
|
2
|
+
* frame loop, mounted as an overlay inside ScanView) has been replaced by the
|
|
3
|
+
* direct-ANSI, Ink-free game:
|
|
2
4
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* row 1: game stats (♥ lives · score · level · best · message) } HUD_ROWS = 2
|
|
8
|
-
* rows 2..2+BOARD_H-1: the board (one colored <Text> per row)
|
|
9
|
-
* last row: in-game KeyBar
|
|
10
|
-
* Total rows = BOARD_H + 3 == the terminal `height` (boardSize math), so the
|
|
11
|
-
* surrounding window chrome never jumps and Ink keeps diffing.
|
|
5
|
+
* - src/tui/game/loop.js — 60Hz fixed-timestep loop (own raw mode + alt screen)
|
|
6
|
+
* - src/tui/game/term.js — double-buffered diff renderer (never clears mid-play)
|
|
7
|
+
* - src/tui/game/input.js — raw-stdin held-key model
|
|
8
|
+
* - src/tui/game/dk-core.js— pure sub-cell fixed-point physics engine
|
|
12
9
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
import
|
|
20
|
-
import { Box, Text, useInput } from "ink";
|
|
21
|
-
import { h, C, LIGHT, GLYPH, KeyBar, SweepBar, useTick, cell, fmtNum } from "../ui.js";
|
|
22
|
-
import { GAME_GLYPH, GAME_COLORS, colorize } from "./dk-render.js";
|
|
23
|
-
import { initGame, stepGame, nextLevel, FRAME_MS, boardSize, MIN_W, MIN_H } from "./dk-core.js";
|
|
24
|
-
|
|
25
|
-
const BG_ON = !(process.env.MM_ASCII === "1" || process.env.TERM === "dumb") && process.env.NO_COLOR == null;
|
|
26
|
-
|
|
27
|
-
const GAME_KEYS = [
|
|
28
|
-
{ k: "←→", label: "move" },
|
|
29
|
-
{ k: "↑↓", label: "climb" },
|
|
30
|
-
{ k: "spc", label: "jump" },
|
|
31
|
-
{ k: "p", label: "pause" },
|
|
32
|
-
{ k: "r", label: "restart" },
|
|
33
|
-
{ k: "q", label: "back to scan" },
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
const freshInput = () => ({ left: false, right: false, up: false, down: false, jump: false });
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* DonkeyKong overlay.
|
|
40
|
-
* props:
|
|
41
|
-
* - width, height: the SAME numbers ScanView gets (drive boardSize)
|
|
42
|
-
* - scan: the useStreamingScan return (read-only — filesScanned, candidates, phase)
|
|
43
|
-
* - ui: shell helpers ({ showToast, setCapturing, setHandlesBack })
|
|
44
|
-
* - onExit: called when the player quits the game (q / esc / backspace)
|
|
45
|
-
* - active: input gate (mirrors ScanView's `active`)
|
|
46
|
-
* - level: starting level (default 1)
|
|
10
|
+
* Reached via `mm play` (standalone) or the Scan-tab P key, which UNMOUNTS Ink,
|
|
11
|
+
* runs the loop, then REMOUNTS the TUI (see src/tui/views/scan.js launchGame +
|
|
12
|
+
* src/tui/app.js appController). This file is kept only as a tombstone — nothing
|
|
13
|
+
* imports it. It intentionally exports nothing and pulls in no dependencies.
|
|
14
|
+
*
|
|
15
|
+
* This file can be deleted outright; it is retained as a no-op so an accidental
|
|
16
|
+
* stale import fails loudly rather than resurrecting the old flicker-prone path.
|
|
47
17
|
*/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const [game, setGame] = React.useState(() => initGame({ ...dims, level }));
|
|
54
|
-
const [paused, setPaused] = React.useState(false);
|
|
55
|
-
const pausedRef = React.useRef(false);
|
|
56
|
-
const inputRef = React.useRef(freshInput());
|
|
57
|
-
const highRef = React.useRef(0);
|
|
58
|
-
|
|
59
|
-
// Track the running session high score (kept in a ref so it survives restarts
|
|
60
|
-
// within this mount; shown in the HUD).
|
|
61
|
-
if (game && game.score > highRef.current) highRef.current = game.score;
|
|
62
|
-
|
|
63
|
-
const scanLive = scan.phase ? scan.phase !== "done" : true;
|
|
64
|
-
// The loop NEVER ticks at idle: only while (a) the overlay is up [it's mounted
|
|
65
|
-
// = up], (b) not paused, (c) the game isn't on a terminal frozen frame, and
|
|
66
|
-
// (d) the scan is still live (or just finishing). Per the design's idle rule.
|
|
67
|
-
const playing = game && game.status === "playing";
|
|
68
|
-
const ticking = playable && active && !paused && playing && (scanLive || game.frame < 1);
|
|
69
|
-
const tick = useTick(FRAME_MS, ticking);
|
|
70
|
-
|
|
71
|
-
// One advance per frame; read + clear the synchronous input ref each tick so a
|
|
72
|
-
// keypress in the same tick is seen (and isn't applied twice).
|
|
73
|
-
React.useEffect(() => {
|
|
74
|
-
if (!ticking) return;
|
|
75
|
-
setGame((g) => {
|
|
76
|
-
const ng = stepGame(g, { input: inputRef.current });
|
|
77
|
-
return ng;
|
|
78
|
-
});
|
|
79
|
-
inputRef.current = freshInput();
|
|
80
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
81
|
-
}, [tick]);
|
|
82
|
-
|
|
83
|
-
// Auto level-up a beat after a win: hold the "you saved her" frame for ~1s,
|
|
84
|
-
// then rebuild the next level carrying score + lives.
|
|
85
|
-
React.useEffect(() => {
|
|
86
|
-
if (game && game.status === "won") {
|
|
87
|
-
const t = setTimeout(() => setGame((g) => (g.status === "won" ? nextLevel(g, dims) : g)), 1000);
|
|
88
|
-
return () => clearTimeout(t);
|
|
89
|
-
}
|
|
90
|
-
return undefined;
|
|
91
|
-
}, [game && game.status, game && game.level]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
92
|
-
|
|
93
|
-
function exit() {
|
|
94
|
-
ui.setCapturing?.(false);
|
|
95
|
-
ui.setHandlesBack?.(false);
|
|
96
|
-
onExit();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Single input owner for the overlay. q / esc / backspace exit; r restarts;
|
|
100
|
-
// p toggles ONLY the game loop (the scan keeps running). Movement keys set the
|
|
101
|
-
// synchronous intent flags consumed next tick. Ctrl-C is NOT swallowed.
|
|
102
|
-
useInput(
|
|
103
|
-
(input, key) => {
|
|
104
|
-
if (!active) return;
|
|
105
|
-
if (key.ctrl && input === "c") return; // let the global handler exit the app
|
|
106
|
-
if (input === "q" || key.escape || key.backspace || key.delete) return exit();
|
|
107
|
-
if (input === "r") { setGame(initGame({ ...dims, level: 1 })); inputRef.current = freshInput(); return; }
|
|
108
|
-
if (input === "p") { pausedRef.current = !pausedRef.current; setPaused(pausedRef.current); return; }
|
|
109
|
-
const ir = inputRef.current;
|
|
110
|
-
if (key.leftArrow || input === "h") ir.left = true;
|
|
111
|
-
else if (key.rightArrow || input === "l") ir.right = true;
|
|
112
|
-
else if (key.upArrow || input === "k") ir.up = true;
|
|
113
|
-
else if (key.downArrow || input === "j") ir.down = true;
|
|
114
|
-
else if (input === " ") ir.jump = true;
|
|
115
|
-
},
|
|
116
|
-
{ isActive: active },
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
// --- too-small guard: a single line, no loop, nothing animates -----------
|
|
120
|
-
if (!playable) {
|
|
121
|
-
return h(
|
|
122
|
-
Box,
|
|
123
|
-
{ flexDirection: "column" },
|
|
124
|
-
h(Text, { color: "#d97706" }, " terminal too small for the game — resize to ~30x12, q to go back"),
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// ---- HUD row 1: live scan progress ----
|
|
129
|
-
const files = scan.filesScanned || 0;
|
|
130
|
-
const models = (scan.candidates && scan.candidates.length) || 0;
|
|
131
|
-
const dirs = scan.dirsSeen || 0;
|
|
132
|
-
const barW = Math.max(10, Math.min(22, BOARD_W - 28));
|
|
133
|
-
// SweepBar renders a <Box>, which can't nest inside a <Text> — so the live HUD
|
|
134
|
-
// row is a flex <Box> with the bar as a sibling of the text spans.
|
|
135
|
-
const hud1 = scanLive
|
|
136
|
-
? h(
|
|
137
|
-
Box,
|
|
138
|
-
{ flexDirection: "row" },
|
|
139
|
-
h(Text, { color: C.FG_FAINT }, " scanning "),
|
|
140
|
-
h(SweepBar, { tick, width: barW }),
|
|
141
|
-
h(Text, { color: C.FG_DIM }, ` files ${fmtNum(files)} · models ${fmtNum(models)} · dirs ${fmtNum(dirs)}`),
|
|
142
|
-
)
|
|
143
|
-
: h(
|
|
144
|
-
Text,
|
|
145
|
-
{},
|
|
146
|
-
h(Text, { color: "#16a34a" }, ` ${GLYPH.check} scan complete`),
|
|
147
|
-
h(Text, { color: C.FG_DIM }, ` · ${fmtNum(models)} models · q to view results`),
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
// ---- HUD row 2: game stats + transient message ----
|
|
151
|
-
const livesStr = "♥".repeat(Math.max(0, game.lives)) || "—";
|
|
152
|
-
const statusTag =
|
|
153
|
-
game.status === "over" ? ` ${game.message} · r restart · q back`
|
|
154
|
-
: game.status === "won" ? ` ${GLYPH.spark} ${game.message}`
|
|
155
|
-
: game.message ? ` ${game.message}` : "";
|
|
156
|
-
const hud2 = h(
|
|
157
|
-
Text,
|
|
158
|
-
{},
|
|
159
|
-
h(Text, { color: LIGHT.red }, ` ${livesStr}`),
|
|
160
|
-
h(Text, { color: C.FG_DIM }, " score "),
|
|
161
|
-
h(Text, { color: C.ACCENT, bold: true }, fmtNum(game.score)),
|
|
162
|
-
h(Text, { color: C.FG_DIM }, ` lvl ${game.level}` + (paused ? " ⏸ paused" : "")),
|
|
163
|
-
h(Text, { color: C.FG_FAINT }, ` best ${fmtNum(Math.max(highRef.current, game.score))}`),
|
|
164
|
-
h(Text, { color: game.status === "over" ? "#dc2626" : C.FG_STRONG }, statusTag),
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
// ---- board: one colored <Text> per row, padded to BOARD_W ----
|
|
168
|
-
const useColor = BG_ON;
|
|
169
|
-
const spanRows = colorize(game, { glyph: GAME_GLYPH, colors: GAME_COLORS });
|
|
170
|
-
const boardNodes = spanRows.map((spans, y) =>
|
|
171
|
-
h(
|
|
172
|
-
Text,
|
|
173
|
-
{ key: "b" + y, wrap: "truncate" },
|
|
174
|
-
...(useColor
|
|
175
|
-
? spans.map((sp, i) => h(Text, { key: i, color: sp.color }, sp.text))
|
|
176
|
-
: [h(Text, { key: 0, color: C.FG }, spans.map((sp) => sp.text).join("").slice(0, BOARD_W).padEnd(BOARD_W))]),
|
|
177
|
-
),
|
|
178
|
-
);
|
|
179
|
-
// Defensive height pin: render EXACTLY BOARD_H board rows.
|
|
180
|
-
while (boardNodes.length < BOARD_H) boardNodes.push(h(Text, { key: "bp" + boardNodes.length, color: C.FG }, cell("", BOARD_W)));
|
|
181
|
-
|
|
182
|
-
const keybar = h(KeyBar, { keys: GAME_KEYS, width });
|
|
183
|
-
|
|
184
|
-
return h(Box, { flexDirection: "column" }, hud1, hud2, ...boardNodes.slice(0, BOARD_H), keybar);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export default DonkeyKong;
|
|
18
|
+
throw new Error(
|
|
19
|
+
"DkGame.js is retired — the Ink overlay game was replaced by the direct-ANSI loop " +
|
|
20
|
+
"(src/tui/game/loop.js). Use `mm play` or the Scan-tab P key. Do not import DkGame.js.",
|
|
21
|
+
);
|