@duetso/agent 0.1.138 → 0.1.140

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.
Files changed (70) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/cli/help.d.ts.map +1 -1
  3. package/dist/src/cli/help.js +20 -7
  4. package/dist/src/cli/help.js.map +1 -1
  5. package/dist/src/cli/memory-reflect.d.ts.map +1 -1
  6. package/dist/src/cli/memory-reflect.js +3 -2
  7. package/dist/src/cli/memory-reflect.js.map +1 -1
  8. package/dist/src/memory/observation-groups.d.ts +11 -1
  9. package/dist/src/memory/observation-groups.d.ts.map +1 -1
  10. package/dist/src/memory/observation-groups.js +15 -5
  11. package/dist/src/memory/observation-groups.js.map +1 -1
  12. package/dist/src/memory/observational-prompts.d.ts +21 -2
  13. package/dist/src/memory/observational-prompts.d.ts.map +1 -1
  14. package/dist/src/memory/observational-prompts.js +89 -12
  15. package/dist/src/memory/observational-prompts.js.map +1 -1
  16. package/dist/src/memory/observational.d.ts +36 -6
  17. package/dist/src/memory/observational.d.ts.map +1 -1
  18. package/dist/src/memory/observational.js +118 -53
  19. package/dist/src/memory/observational.js.map +1 -1
  20. package/dist/src/tui/app.d.ts.map +1 -1
  21. package/dist/src/tui/app.js +19 -0
  22. package/dist/src/tui/app.js.map +1 -1
  23. package/dist/src/tui/dino/index.d.ts +31 -0
  24. package/dist/src/tui/dino/index.d.ts.map +1 -0
  25. package/dist/src/tui/dino/index.js +313 -0
  26. package/dist/src/tui/dino/index.js.map +1 -0
  27. package/dist/src/tui/dino/input.d.ts +12 -0
  28. package/dist/src/tui/dino/input.d.ts.map +1 -0
  29. package/dist/src/tui/dino/input.js +44 -0
  30. package/dist/src/tui/dino/input.js.map +1 -0
  31. package/dist/src/tui/dino/persistence.d.ts +3 -0
  32. package/dist/src/tui/dino/persistence.d.ts.map +1 -0
  33. package/dist/src/tui/dino/persistence.js +36 -0
  34. package/dist/src/tui/dino/persistence.js.map +1 -0
  35. package/dist/src/tui/dino/render.d.ts +19 -0
  36. package/dist/src/tui/dino/render.d.ts.map +1 -0
  37. package/dist/src/tui/dino/render.js +164 -0
  38. package/dist/src/tui/dino/render.js.map +1 -0
  39. package/dist/src/tui/dino/state.d.ts +141 -0
  40. package/dist/src/tui/dino/state.d.ts.map +1 -0
  41. package/dist/src/tui/dino/state.js +156 -0
  42. package/dist/src/tui/dino/state.js.map +1 -0
  43. package/dist/src/tui/dino/tick.d.ts +10 -0
  44. package/dist/src/tui/dino/tick.d.ts.map +1 -0
  45. package/dist/src/tui/dino/tick.js +117 -0
  46. package/dist/src/tui/dino/tick.js.map +1 -0
  47. package/dist/src/tui/dino/visibility.d.ts +2 -0
  48. package/dist/src/tui/dino/visibility.d.ts.map +1 -0
  49. package/dist/src/tui/dino/visibility.js +18 -0
  50. package/dist/src/tui/dino/visibility.js.map +1 -0
  51. package/dist/src/tui/key-handlers.d.ts +7 -0
  52. package/dist/src/tui/key-handlers.d.ts.map +1 -1
  53. package/dist/src/tui/key-handlers.js +25 -1
  54. package/dist/src/tui/key-handlers.js.map +1 -1
  55. package/dist/src/tui/layout.d.ts +7 -0
  56. package/dist/src/tui/layout.d.ts.map +1 -1
  57. package/dist/src/tui/layout.js +10 -0
  58. package/dist/src/tui/layout.js.map +1 -1
  59. package/dist/src/tui/status-controller.d.ts +9 -0
  60. package/dist/src/tui/status-controller.d.ts.map +1 -1
  61. package/dist/src/tui/status-controller.js +28 -0
  62. package/dist/src/tui/status-controller.js.map +1 -1
  63. package/dist/src/turn-runner/prompts.js +1 -1
  64. package/dist/src/turn-runner/prompts.js.map +1 -1
  65. package/dist/src/turn-runner/tools.js +17 -21
  66. package/dist/src/turn-runner/tools.js.map +1 -1
  67. package/dist/src/turn-runner/turn-runner.d.ts.map +1 -1
  68. package/dist/src/turn-runner/turn-runner.js +1 -0
  69. package/dist/src/turn-runner/turn-runner.js.map +1 -1
  70. package/package.json +1 -1
@@ -0,0 +1,313 @@
1
+ // OpenTUI panel that hosts the dino game. Owns the tick timer, the
2
+ // `BoxRenderable` mounted into the layout, and the freeze/resume lifecycle
3
+ // that the StatusController drives via its `onRunningChange` hook.
4
+ //
5
+ // Lifecycle contract (matches the spec we agreed in design):
6
+ // - toggle() : user pressed Ctrl-G; expand/collapse the panel.
7
+ // - freeze() : agent flipped to needs-user; snap dino to ground
8
+ // and stop the world. No-op when collapsed.
9
+ // - resume() : agent flipped to busy; start the 3-2-1 countdown
10
+ // followed by a grace gap. No-op when collapsed or
11
+ // when no run is in progress.
12
+ // - handleKey(key) : input router forwards keystrokes here while the
13
+ // panel is expanded and the agent is busy. Returns
14
+ // true when the key was consumed.
15
+ // - destroy() : tear down the tick timer; persist high score.
16
+ import { BoxRenderable, TextRenderable } from "@opentui/core";
17
+ import { SIDEBAR_WIDTH } from "../sidebar.js";
18
+ import { COLORS } from "../theme.js";
19
+ import { actionForKey, applyJump, applyStart } from "./input.js";
20
+ import { loadHighScore, saveHighScore } from "./persistence.js";
21
+ import { EXPANDED_ROWS, renderCollapsedRow, renderExpanded } from "./render.js";
22
+ import { panelVisibleRowCount } from "./visibility.js";
23
+ import { beginCountdown, expandResumeKind, freezeRun, initialState, setFieldWidth, } from "./state.js";
24
+ import { tick } from "./tick.js";
25
+ /** Horizontal chrome around the dino panel: the panel sits inside the main
26
+ * layout column, which itself sits to the left of the sidebar. We deduct
27
+ * the sidebar plus a few cells of frame padding to land on a field width
28
+ * that fills the available column without overflowing it. */
29
+ const PANEL_CHROME_COLUMNS = 4;
30
+ /** Cadence of the physics loop. Chosen high enough that a jump arc feels
31
+ * smooth in a terminal but low enough that the redraw cost is negligible
32
+ * (15 fps ≈ 67ms per frame). The countdown layer uses this same value to
33
+ * size the 3-2-1 phase. */
34
+ const TICKS_PER_SECOND = 15;
35
+ const TICK_INTERVAL_MS = Math.round(1000 / TICKS_PER_SECOND);
36
+ export function createDinoPanel(opts) {
37
+ const { renderer } = opts;
38
+ // The panel is collapsed by default and re-collapses every time the
39
+ // agent goes idle. "Hint only" is the starting point for every busy
40
+ // cycle; if the user wants the game they toggle it open each time.
41
+ // We do not persist expanded state across sessions or across busy
42
+ // cycles — the simpler contract is more predictable.
43
+ let expanded = false;
44
+ // When expanded, tracks whether game keys (space, ArrowUp) belong to
45
+ // the dino or to the composer. Defaults to true on every expand so
46
+ // Ctrl-G straight into a playable game; flipping to false via a
47
+ // second Ctrl-G keeps the panel visible but releases the keys back
48
+ // to the textarea so the user can type a follow-up without
49
+ // collapsing the game.
50
+ let gameFocused = true;
51
+ let state = initialState(loadHighScore(), computeFieldWidth(renderer));
52
+ // Tracks whether the most recent freeze was caused by the agent needing
53
+ // input (so resume runs the 3-2-1 + grace gap) vs. a manual collapse
54
+ // (so re-expand resumes instantly per the spec).
55
+ let frozenByAgent = false;
56
+ // Mirrors the StatusController running signal. The collapsed hint row
57
+ // ("▶ Ctrl-G to play") only renders while this is true; at idle the
58
+ // panel is invisible and reserves no rows.
59
+ let agentBusy = false;
60
+ // Auto-expand the panel the first time the agent goes busy in this
61
+ // session so the game is visible by default. Ctrl-G is otherwise an
62
+ // easter egg — surfacing the full panel once teaches the user it's
63
+ // there. Every subsequent busy cycle starts collapsed again so we
64
+ // don't fight users who chose to dismiss it.
65
+ let hasAutoExpanded = false;
66
+ let ticker;
67
+ // Last persisted high score, so we only write on improvement. Hydrated
68
+ // from disk at construction time.
69
+ let persistedHighScore = state.highScore;
70
+ const view = new BoxRenderable(renderer, {
71
+ flexDirection: "column",
72
+ flexShrink: 0,
73
+ border: false,
74
+ });
75
+ // One TextRenderable per displayed row. The expanded layout reserves
76
+ // EXPANDED_ROWS rows; collapsed reuses the first row and hides the
77
+ // rest. Building a fixed row pool avoids reflowing the box on every
78
+ // frame.
79
+ const rows = Array.from({ length: EXPANDED_ROWS }, () => {
80
+ const row = new TextRenderable(renderer, {
81
+ content: "",
82
+ fg: COLORS.hint,
83
+ height: 1,
84
+ flexShrink: 0,
85
+ selectable: false,
86
+ });
87
+ view.add(row);
88
+ return row;
89
+ });
90
+ syncVisibility();
91
+ paint();
92
+ // Resize handler. The renderer fires "resize" on terminal size changes;
93
+ // we recompute the playfield width and let the next paint stretch the
94
+ // ground line / obstacles to the new column. We keep a reference so the
95
+ // panel's `destroy()` can detach it on teardown.
96
+ const handleResize = () => {
97
+ const next = computeFieldWidth(renderer);
98
+ if (next === state.fieldWidth)
99
+ return;
100
+ state = setFieldWidth(state, next);
101
+ paint();
102
+ };
103
+ renderer.on("resize", handleResize);
104
+ function isExpanded() {
105
+ return expanded;
106
+ }
107
+ function isGameFocused() {
108
+ return expanded && gameFocused;
109
+ }
110
+ function toggle() {
111
+ // The dino panel is a "while-you-wait" affordance: it only exists
112
+ // while the agent is actually busy. At rest there is no hint and no
113
+ // toggle — Ctrl-G is a no-op so a fresh `duet` session is clean.
114
+ if (!agentBusy)
115
+ return;
116
+ // Three-step cycle so the user can toggle keystroke ownership
117
+ // without losing what they typed:
118
+ // collapsed → expanded + game-focused
119
+ // expanded + game → expanded + composer-focused (typing)
120
+ // expanded + composer → collapsed
121
+ if (!expanded) {
122
+ expanded = true;
123
+ gameFocused = true;
124
+ }
125
+ else if (gameFocused) {
126
+ // Stay expanded; just release keys to the composer. The game
127
+ // keeps animating in the background so the user sees obstacles
128
+ // approach while they type.
129
+ gameFocused = false;
130
+ syncVisibility();
131
+ paint();
132
+ return;
133
+ }
134
+ else {
135
+ expanded = false;
136
+ gameFocused = true;
137
+ }
138
+ if (expanded) {
139
+ // Re-expanding into a frozen run: if the agent caused the freeze
140
+ // (needs-user → answered → busy again, but `resume()` no-op'd
141
+ // because the panel was collapsed), run the 3-2-1 + grace gap the
142
+ // live resume path uses. If the user collapsed manually, resume
143
+ // instantly — they explicitly chose to peek away.
144
+ const kind = expandResumeKind(state.phase, frozenByAgent);
145
+ if (kind === "countdown") {
146
+ state = beginCountdown(state, TICKS_PER_SECOND);
147
+ frozenByAgent = false;
148
+ }
149
+ else if (kind === "run") {
150
+ state = { ...state, phase: { kind: "running" } };
151
+ }
152
+ }
153
+ else {
154
+ // Collapsing while running snaps the world quiet without ending the
155
+ // run; the user can re-expand and pick up where they left off.
156
+ if (state.phase.kind === "running" || state.phase.kind === "grace") {
157
+ state = freezeRun(state);
158
+ frozenByAgent = false;
159
+ }
160
+ stopTicker();
161
+ }
162
+ syncVisibility();
163
+ paint();
164
+ if (expanded && stateNeedsTicking())
165
+ startTicker();
166
+ }
167
+ function resume() {
168
+ if (!expanded)
169
+ return;
170
+ // No run in progress → nothing to resume; the user starts a fresh run
171
+ // themselves by pressing space.
172
+ if (state.phase.kind !== "frozen")
173
+ return;
174
+ if (frozenByAgent) {
175
+ state = beginCountdown(state, TICKS_PER_SECOND);
176
+ }
177
+ else {
178
+ state = { ...state, phase: { kind: "running" } };
179
+ }
180
+ frozenByAgent = false;
181
+ startTicker();
182
+ paint();
183
+ }
184
+ function freeze() {
185
+ frozenByAgent = true;
186
+ state = freezeRun(state);
187
+ stopTicker();
188
+ paint();
189
+ }
190
+ function setAgentBusy(busy) {
191
+ if (agentBusy === busy)
192
+ return;
193
+ agentBusy = busy;
194
+ if (busy && !hasAutoExpanded) {
195
+ // First busy cycle of the session: open the panel so the user
196
+ // discovers the game. After this they own the toggle via Ctrl-G
197
+ // and we never auto-expand again.
198
+ hasAutoExpanded = true;
199
+ expanded = true;
200
+ gameFocused = true;
201
+ }
202
+ // When the agent goes idle, snap the panel back to its starting
203
+ // point (collapsed) so the next busy cycle begins at "hint only"
204
+ // rather than re-appearing already expanded. The user opts back in
205
+ // by pressing Ctrl-G again.
206
+ if (!busy) {
207
+ expanded = false;
208
+ gameFocused = true;
209
+ }
210
+ syncVisibility();
211
+ paint();
212
+ }
213
+ function handleKey(keyName) {
214
+ if (!expanded)
215
+ return false;
216
+ const action = actionForKey(state, keyName);
217
+ if (action === "ignore")
218
+ return false;
219
+ if (action === "jump") {
220
+ state = applyJump(state);
221
+ }
222
+ else if (action === "start") {
223
+ state = applyStart(state);
224
+ // Starting a run after a death needs the timer running again.
225
+ startTicker();
226
+ }
227
+ paint();
228
+ return true;
229
+ }
230
+ function destroy() {
231
+ stopTicker();
232
+ renderer.off("resize", handleResize);
233
+ if (state.highScore > persistedHighScore) {
234
+ saveHighScore(state.highScore);
235
+ persistedHighScore = state.highScore;
236
+ }
237
+ }
238
+ function stateNeedsTicking() {
239
+ return (state.phase.kind === "running" ||
240
+ state.phase.kind === "grace" ||
241
+ state.phase.kind === "countdown");
242
+ }
243
+ function startTicker() {
244
+ if (ticker !== undefined)
245
+ return;
246
+ if (!stateNeedsTicking())
247
+ return;
248
+ ticker = setInterval(() => {
249
+ state = tick(state, { random: Math.random, ticksPerSecond: TICKS_PER_SECOND });
250
+ if (state.highScore > persistedHighScore) {
251
+ saveHighScore(state.highScore);
252
+ persistedHighScore = state.highScore;
253
+ }
254
+ if (!stateNeedsTicking())
255
+ stopTicker();
256
+ paint();
257
+ }, TICK_INTERVAL_MS);
258
+ }
259
+ function stopTicker() {
260
+ if (ticker !== undefined) {
261
+ clearInterval(ticker);
262
+ ticker = undefined;
263
+ }
264
+ }
265
+ function syncVisibility() {
266
+ const visibleRows = panelVisibleRowCount(expanded, agentBusy);
267
+ for (let i = 0; i < rows.length; i++) {
268
+ rows[i].visible = i < visibleRows;
269
+ }
270
+ }
271
+ function paint() {
272
+ // At rest the panel is invisible; skip painting entirely so the row
273
+ // contents stay empty and no chrome leaks into the layout.
274
+ if (!agentBusy) {
275
+ for (const row of rows)
276
+ row.content = "";
277
+ return;
278
+ }
279
+ if (expanded) {
280
+ const frame = renderExpanded(state, gameFocused);
281
+ for (let i = 0; i < rows.length; i++) {
282
+ rows[i].content = frame[i] ?? "";
283
+ }
284
+ }
285
+ else {
286
+ rows[0].content = renderCollapsedRow(state.highScore);
287
+ for (let i = 1; i < rows.length; i++)
288
+ rows[i].content = "";
289
+ }
290
+ }
291
+ return {
292
+ view,
293
+ isExpanded,
294
+ isGameFocused,
295
+ toggle,
296
+ resume,
297
+ freeze,
298
+ setAgentBusy,
299
+ handleKey,
300
+ destroy,
301
+ };
302
+ }
303
+ /** Compute the current playfield width from the renderer's terminal size.
304
+ * The panel sits in the main layout column to the left of the sidebar, so
305
+ * we deduct the sidebar plus a small chrome margin and let `setFieldWidth`
306
+ * clamp into the supported [MIN_FIELD_WIDTH, MAX_FIELD_WIDTH] range. */
307
+ function computeFieldWidth(renderer) {
308
+ const terminal = renderer.terminalWidth;
309
+ if (!Number.isFinite(terminal) || terminal <= 0)
310
+ return 0; // clamped by setFieldWidth
311
+ return terminal - SIDEBAR_WIDTH - PANEL_CHROME_COLUMNS;
312
+ }
313
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/tui/dino/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { GameState } from "./state.js";
2
+ export type DinoAction = "jump" | "start" | "ignore";
3
+ /** Returns the action implied by a key press for the current game state.
4
+ * Returns `"ignore"` for keys the panel should let bubble (so e.g. typing
5
+ * letters while the panel is collapsed never gets eaten). */
6
+ export declare function actionForKey(state: GameState, keyName: string | undefined): DinoAction;
7
+ /** Apply a `jump` action: only takes effect when the dino is grounded so
8
+ * the player cannot double-jump. */
9
+ export declare function applyJump(state: GameState): GameState;
10
+ /** Apply a `start` action: begin a fresh run from `idle`/`gameover`. */
11
+ export declare function applyStart(state: GameState): GameState;
12
+ //# sourceMappingURL=input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../../../src/tui/dino/input.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAQrD;;8DAE8D;AAC9D,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,CAetF;AAED;qCACqC;AACrC,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,CAGrD;AAED,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,CAGtD"}
@@ -0,0 +1,44 @@
1
+ // Keystroke → game action. Pure so the panel can route keys through this
2
+ // without dragging OpenTUI key types into the reducer layer.
3
+ import { JUMP_VELOCITY, startRun } from "./state.js";
4
+ /** Names of keys that, when game is in a state that accepts input, trigger
5
+ * a jump. Arrow-only on purpose: the spacebar belongs to the composer so
6
+ * the user can type a follow-up while the game keeps running in the
7
+ * background. OpenTUI normalizes ArrowUp to `name: "up"`. */
8
+ const JUMP_KEYS = new Set(["up"]);
9
+ /** Returns the action implied by a key press for the current game state.
10
+ * Returns `"ignore"` for keys the panel should let bubble (so e.g. typing
11
+ * letters while the panel is collapsed never gets eaten). */
12
+ export function actionForKey(state, keyName) {
13
+ if (!keyName)
14
+ return "ignore";
15
+ if (state.phase.kind === "idle" || state.phase.kind === "gameover") {
16
+ // Any of the jump keys also acts as "start a run" so the user does not
17
+ // need a separate start key after a death.
18
+ if (JUMP_KEYS.has(keyName) || keyName === "return" || keyName === "enter")
19
+ return "start";
20
+ return "ignore";
21
+ }
22
+ if (state.phase.kind === "running" || state.phase.kind === "grace") {
23
+ if (JUMP_KEYS.has(keyName))
24
+ return "jump";
25
+ return "ignore";
26
+ }
27
+ // countdown / frozen swallow input so the user cannot pre-jump and then
28
+ // land on a cactus the instant the world unfreezes.
29
+ return "ignore";
30
+ }
31
+ /** Apply a `jump` action: only takes effect when the dino is grounded so
32
+ * the player cannot double-jump. */
33
+ export function applyJump(state) {
34
+ if (state.dinoY > 0 || state.dinoVy !== 0)
35
+ return state;
36
+ return { ...state, dinoVy: JUMP_VELOCITY };
37
+ }
38
+ /** Apply a `start` action: begin a fresh run from `idle`/`gameover`. */
39
+ export function applyStart(state) {
40
+ if (state.phase.kind !== "idle" && state.phase.kind !== "gameover")
41
+ return state;
42
+ return startRun(state);
43
+ }
44
+ //# sourceMappingURL=input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.js","sourceRoot":"","sources":["../../../../src/tui/dino/input.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,6DAA6D;AAG7D,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAIrD;;;8DAG8D;AAC9D,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AAElC;;8DAE8D;AAC9D,MAAM,UAAU,YAAY,CAAC,KAAgB,EAAE,OAA2B;IACxE,IAAI,CAAC,OAAO;QAAE,OAAO,QAAQ,CAAC;IAC9B,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QACnE,uEAAuE;QACvE,2CAA2C;QAC3C,IAAI,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,OAAO;YAAE,OAAO,OAAO,CAAC;QAC1F,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACnE,IAAI,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO,MAAM,CAAC;QAC1C,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,wEAAwE;IACxE,oDAAoD;IACpD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;qCACqC;AACrC,MAAM,UAAU,SAAS,CAAC,KAAgB;IACxC,IAAI,KAAK,CAAC,KAAK,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACxD,OAAO,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;AAC7C,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,UAAU,CAAC,KAAgB;IACzC,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,UAAU;QAAE,OAAO,KAAK,CAAC;IACjF,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC;AACzB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare function loadHighScore(): number;
2
+ export declare function saveHighScore(score: number): void;
3
+ //# sourceMappingURL=persistence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../../../src/tui/dino/persistence.ts"],"names":[],"mappings":"AAgBA,wBAAgB,aAAa,IAAI,MAAM,CAQtC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAOjD"}
@@ -0,0 +1,36 @@
1
+ // Reads/writes the dino panel's tiny persisted footprint under
2
+ // `~/.duet/dino/`. Only the high score is persisted: panel open/closed
3
+ // state is intentionally session-local because the spec is that every
4
+ // new busy cycle starts at "hint only" and the user toggles back in.
5
+ // Writes are best-effort: a failed write degrades the next run to the
6
+ // in-memory default but never propagates to the caller, because losing a
7
+ // high score is strictly better than crashing the TUI on a read-only
8
+ // home directory.
9
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { dirname, join } from "node:path";
12
+ const DINO_DIR = join(homedir(), ".duet", "dino");
13
+ const HIGHSCORE_FILE = join(DINO_DIR, "highscore");
14
+ export function loadHighScore() {
15
+ try {
16
+ const raw = readFileSync(HIGHSCORE_FILE, "utf8").trim();
17
+ const n = Number.parseInt(raw, 10);
18
+ return Number.isFinite(n) && n >= 0 ? n : 0;
19
+ }
20
+ catch {
21
+ return 0;
22
+ }
23
+ }
24
+ export function saveHighScore(score) {
25
+ try {
26
+ ensureDir(HIGHSCORE_FILE);
27
+ writeFileSync(HIGHSCORE_FILE, `${Math.max(0, Math.floor(score))}\n`, "utf8");
28
+ }
29
+ catch {
30
+ // Best-effort.
31
+ }
32
+ }
33
+ function ensureDir(filePath) {
34
+ mkdirSync(dirname(filePath), { recursive: true });
35
+ }
36
+ //# sourceMappingURL=persistence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persistence.js","sourceRoot":"","sources":["../../../../src/tui/dino/persistence.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,uEAAuE;AACvE,sEAAsE;AACtE,qEAAqE;AACrE,sEAAsE;AACtE,yEAAyE;AACzE,qEAAqE;AACrE,kBAAkB;AAElB,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;AAClD,MAAM,cAAc,GAAG,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;AAEnD,MAAM,UAAU,aAAa;IAC3B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACxD,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACnC,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,CAAC;IACX,CAAC;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,IAAI,CAAC;QACH,SAAS,CAAC,cAAc,CAAC,CAAC;QAC1B,aAAa,CAAC,cAAc,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC/E,CAAC;IAAC,MAAM,CAAC;QACP,eAAe;IACjB,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,QAAgB;IACjC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AACpD,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { type GameState } from "./state.js";
2
+ /** Total rendered rows of the expanded panel: title + 10 playfield rows +
3
+ * status row = 12 rows when expanded. Collapsed = 1 row. */
4
+ export declare const EXPANDED_ROWS = 12;
5
+ export declare const COLLAPSED_ROWS = 1;
6
+ export declare const PLAYFIELD_ROWS = 10;
7
+ /** Width of the dino sprite in cells. Anchored at `DINO_X` (left edge). */
8
+ export declare const DINO_WIDTH = 4;
9
+ /** Height of the dino sprite in rows; feet sit on the ground row, so the
10
+ * body extends up `DINO_HEIGHT - 1` rows above ground when grounded. */
11
+ export declare const DINO_HEIGHT = 3;
12
+ /** Single-row "press Ctrl-G HI 0142" hint shown when the panel is
13
+ * collapsed but the user has opened it at least once this session. */
14
+ export declare function renderCollapsedRow(highScore: number): string;
15
+ /** Full 12-row expanded panel. The phase determines which overlay (idle
16
+ * splash, countdown numerals, dim "agent needs you" hint, gameover
17
+ * banner) is composited on top of the playfield. */
18
+ export declare function renderExpanded(state: GameState, gameFocused?: boolean): string[];
19
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../../../src/tui/dino/render.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAEhE;6DAC6D;AAC7D,eAAO,MAAM,aAAa,KAAK,CAAC;AAChC,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,2EAA2E;AAC3E,eAAO,MAAM,UAAU,IAAI,CAAC;AAC5B;yEACyE;AACzE,eAAO,MAAM,WAAW,IAAI,CAAC;AAc7B;uEACuE;AACvE,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;qDAEqD;AACrD,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,EAAE,WAAW,GAAE,OAAc,GAAG,MAAM,EAAE,CAYtF"}
@@ -0,0 +1,164 @@
1
+ // state → string[]. Pure ASCII; no OpenTUI imports so tests can snapshot
2
+ // the rendered frame as plain strings.
3
+ import { DINO_X, GROUND_ROW } from "./state.js";
4
+ /** Total rendered rows of the expanded panel: title + 10 playfield rows +
5
+ * status row = 12 rows when expanded. Collapsed = 1 row. */
6
+ export const EXPANDED_ROWS = 12;
7
+ export const COLLAPSED_ROWS = 1;
8
+ export const PLAYFIELD_ROWS = 10;
9
+ /** Width of the dino sprite in cells. Anchored at `DINO_X` (left edge). */
10
+ export const DINO_WIDTH = 4;
11
+ /** Height of the dino sprite in rows; feet sit on the ground row, so the
12
+ * body extends up `DINO_HEIGHT - 1` rows above ground when grounded. */
13
+ export const DINO_HEIGHT = 3;
14
+ // Tiny T-rex sprites in pure ASCII so they line up cleanly in any
15
+ // terminal. The right edge points in the direction of travel: head and
16
+ // snout to the right, tail to the left. Two run poses alternate while
17
+ // grounded so the legs visibly cycle; a third pose is used mid-air.
18
+ //
19
+ // Each sprite is exactly DINO_HEIGHT rows by DINO_WIDTH cols. Empty
20
+ // cells use a literal space so the sprite never accidentally erases
21
+ // neighboring playfield characters.
22
+ const DINO_RUN_A = [" __", " /_)", " /\\ "];
23
+ const DINO_RUN_B = [" __", " /_)", " \\/ "];
24
+ const DINO_JUMP = [" __", " /_)", " || "];
25
+ /** Single-row "press Ctrl-G HI 0142" hint shown when the panel is
26
+ * collapsed but the user has opened it at least once this session. */
27
+ export function renderCollapsedRow(highScore) {
28
+ return `▶ Ctrl-G to play · HI ${pad4(highScore)}`;
29
+ }
30
+ /** Full 12-row expanded panel. The phase determines which overlay (idle
31
+ * splash, countdown numerals, dim "agent needs you" hint, gameover
32
+ * banner) is composited on top of the playfield. */
33
+ export function renderExpanded(state, gameFocused = true) {
34
+ const rows = buildPlayfield(state);
35
+ const title = renderTitle(state);
36
+ const statusRow = renderStatusRow(state, gameFocused);
37
+ const overlay = renderOverlay(state);
38
+ if (overlay) {
39
+ // Overlay replaces a centered slice of the playfield rows; the dino
40
+ // and obstacles still show around it so the user sees what they will
41
+ // wake up to.
42
+ overlayRows(rows, overlay);
43
+ }
44
+ return [title, ...rows, statusRow];
45
+ }
46
+ function renderTitle(state) {
47
+ const score = pad4(Math.floor(state.score));
48
+ const hi = pad4(state.highScore);
49
+ return ` duet dino · HI ${hi} · ${score}`;
50
+ }
51
+ function renderStatusRow(state, gameFocused) {
52
+ // When the user has Ctrl-G'd into typing mode the panel stays open
53
+ // and the game keeps animating, but the spacebar belongs to the
54
+ // composer. The status row advertises how to take the keys back so
55
+ // the mode is discoverable.
56
+ if (!gameFocused) {
57
+ return " typing mode · Ctrl-G to play · Ctrl-G again to close";
58
+ }
59
+ switch (state.phase.kind) {
60
+ case "idle":
61
+ return " press ↑ to start · Ctrl-G for typing mode";
62
+ case "running":
63
+ return " ↑ to jump · Ctrl-G for typing mode";
64
+ case "grace":
65
+ return " …";
66
+ case "countdown":
67
+ return " resuming…";
68
+ case "frozen":
69
+ return " ▲ agent needs you — answer above";
70
+ case "gameover":
71
+ return " game over · ↑ to retry · Ctrl-G for typing mode";
72
+ }
73
+ }
74
+ function buildPlayfield(state) {
75
+ const width = state.fieldWidth;
76
+ const rows = Array.from({ length: PLAYFIELD_ROWS }, () => Array.from({ length: width }, () => " "));
77
+ // Ground line.
78
+ const groundIndex = Math.min(GROUND_ROW, PLAYFIELD_ROWS - 1);
79
+ for (let x = 0; x < width; x++)
80
+ rows[groundIndex][x] = "_";
81
+ // Obstacles. Cactus is drawn as "#" stacked vertically, anchored at the
82
+ // ground line. A 1-cell-wide column keeps collision and rendering in
83
+ // exact agreement.
84
+ for (const o of state.obstacles) {
85
+ const col = Math.round(o.x);
86
+ if (col < 0 || col >= width)
87
+ continue;
88
+ for (let h = 0; h < o.height; h++) {
89
+ const row = groundIndex - 1 - h;
90
+ if (row >= 0)
91
+ rows[row][col] = "#";
92
+ }
93
+ }
94
+ // Dino. Multi-row T-rex sprite anchored at `DINO_X`, with feet on the
95
+ // ground row when grounded. Floor-rounded `y` so a sub-cell hop still
96
+ // visibly lifts off. The grounded pose alternates between two leg
97
+ // positions so the dino looks like it's running; airborne uses the
98
+ // tucked-legs jump sprite so the user can confirm a jump landed.
99
+ const grounded = state.dinoY === 0;
100
+ const sprite = grounded
101
+ ? // Alternate legs every ~2 score units so the cadence reads as
102
+ // running rather than flickering at every frame.
103
+ Math.floor(state.score * 2) % 2 === 0
104
+ ? DINO_RUN_A
105
+ : DINO_RUN_B
106
+ : DINO_JUMP;
107
+ const feetRow = groundIndex - Math.max(0, Math.floor(state.dinoY));
108
+ for (let dy = 0; dy < sprite.length; dy++) {
109
+ const targetRow = feetRow - (sprite.length - 1 - dy);
110
+ if (targetRow < 0 || targetRow >= PLAYFIELD_ROWS)
111
+ continue;
112
+ const line = sprite[dy];
113
+ for (let dx = 0; dx < line.length; dx++) {
114
+ const ch = line[dx];
115
+ if (ch === " ")
116
+ continue; // sprite holes leave the playfield intact
117
+ const col = DINO_X + dx;
118
+ if (col < 0 || col >= width)
119
+ continue;
120
+ rows[targetRow][col] = ch;
121
+ }
122
+ }
123
+ return rows.map((r) => r.join(""));
124
+ }
125
+ function renderOverlay(state) {
126
+ if (state.phase.kind === "countdown") {
127
+ // Big numeral derived from ticksRemaining. The tick layer counts down
128
+ // in whole-second blocks of `ticksPerSecond` so dividing reproduces 3
129
+ // → 2 → 1 cleanly regardless of the actual cadence.
130
+ const seconds = Math.ceil(state.phase.ticksRemaining / 15);
131
+ return bigNumeral(seconds);
132
+ }
133
+ if (state.phase.kind === "frozen") {
134
+ return ["agent needs you", "answer the prompt above"];
135
+ }
136
+ if (state.phase.kind === "gameover") {
137
+ return ["game over", `score ${Math.floor(state.score)}`];
138
+ }
139
+ return undefined;
140
+ }
141
+ function overlayRows(rows, overlay) {
142
+ const width = rows[0]?.length ?? 0;
143
+ const startRow = Math.max(0, Math.floor((rows.length - overlay.length) / 2));
144
+ for (let i = 0; i < overlay.length; i++) {
145
+ const target = startRow + i;
146
+ if (target >= rows.length)
147
+ break;
148
+ const line = overlay[i];
149
+ const startCol = Math.max(0, Math.floor((width - line.length) / 2));
150
+ const before = rows[target].slice(0, startCol);
151
+ const after = rows[target].slice(startCol + line.length);
152
+ rows[target] = (before + line + after).slice(0, width);
153
+ }
154
+ }
155
+ function bigNumeral(n) {
156
+ // Stylized but ASCII-only so it carries the 8-bit feel without leaning
157
+ // on Unicode block characters that render at inconsistent widths.
158
+ const label = n > 0 ? String(n) : "GO!";
159
+ return [`>>> ${label} <<<`];
160
+ }
161
+ function pad4(n) {
162
+ return n.toString().padStart(4, "0");
163
+ }
164
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../../../../src/tui/dino/render.ts"],"names":[],"mappings":"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"}