@vsuryav/agent-sim 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.
Files changed (154) hide show
  1. package/README.md +25 -0
  2. package/bin/agent-sim.js +25 -0
  3. package/package.json +72 -0
  4. package/src/app-paths.ts +29 -0
  5. package/src/app-sync.test.ts +75 -0
  6. package/src/app-sync.ts +110 -0
  7. package/src/cli.ts +129 -0
  8. package/src/collector/claude-code.test.ts +102 -0
  9. package/src/collector/claude-code.ts +133 -0
  10. package/src/collector/codex-cli.test.ts +116 -0
  11. package/src/collector/codex-cli.ts +149 -0
  12. package/src/collector/db.test.ts +59 -0
  13. package/src/collector/db.ts +125 -0
  14. package/src/collector/names.test.ts +21 -0
  15. package/src/collector/names.ts +28 -0
  16. package/src/collector/personality.test.ts +40 -0
  17. package/src/collector/personality.ts +46 -0
  18. package/src/collector/remote-sync.test.ts +31 -0
  19. package/src/collector/remote-sync.ts +171 -0
  20. package/src/collector/sync.test.ts +67 -0
  21. package/src/collector/sync.ts +148 -0
  22. package/src/collector/types.ts +1 -0
  23. package/src/engine/bootstrap/state.ts +3 -0
  24. package/src/engine/buddy/CompanionSprite.tsx +371 -0
  25. package/src/engine/buddy/companion.ts +133 -0
  26. package/src/engine/buddy/prompt.ts +36 -0
  27. package/src/engine/buddy/sprites.ts +514 -0
  28. package/src/engine/buddy/types.ts +148 -0
  29. package/src/engine/buddy/useBuddyNotification.tsx +98 -0
  30. package/src/engine/ink/Ansi.tsx +292 -0
  31. package/src/engine/ink/bidi.ts +139 -0
  32. package/src/engine/ink/clearTerminal.ts +74 -0
  33. package/src/engine/ink/colorize.ts +231 -0
  34. package/src/engine/ink/components/AlternateScreen.tsx +80 -0
  35. package/src/engine/ink/components/App.tsx +658 -0
  36. package/src/engine/ink/components/AppContext.ts +21 -0
  37. package/src/engine/ink/components/Box.tsx +214 -0
  38. package/src/engine/ink/components/Button.tsx +192 -0
  39. package/src/engine/ink/components/ClockContext.tsx +112 -0
  40. package/src/engine/ink/components/CursorDeclarationContext.ts +32 -0
  41. package/src/engine/ink/components/ErrorOverview.tsx +109 -0
  42. package/src/engine/ink/components/Link.tsx +42 -0
  43. package/src/engine/ink/components/Newline.tsx +39 -0
  44. package/src/engine/ink/components/NoSelect.tsx +68 -0
  45. package/src/engine/ink/components/RawAnsi.tsx +57 -0
  46. package/src/engine/ink/components/ScrollBox.tsx +237 -0
  47. package/src/engine/ink/components/Spacer.tsx +20 -0
  48. package/src/engine/ink/components/StdinContext.ts +49 -0
  49. package/src/engine/ink/components/TerminalFocusContext.tsx +52 -0
  50. package/src/engine/ink/components/TerminalSizeContext.tsx +7 -0
  51. package/src/engine/ink/components/Text.tsx +254 -0
  52. package/src/engine/ink/constants.ts +2 -0
  53. package/src/engine/ink/dom.ts +484 -0
  54. package/src/engine/ink/events/click-event.ts +38 -0
  55. package/src/engine/ink/events/dispatcher.ts +233 -0
  56. package/src/engine/ink/events/emitter.ts +39 -0
  57. package/src/engine/ink/events/event-handlers.ts +73 -0
  58. package/src/engine/ink/events/event.ts +11 -0
  59. package/src/engine/ink/events/focus-event.ts +21 -0
  60. package/src/engine/ink/events/input-event.ts +205 -0
  61. package/src/engine/ink/events/keyboard-event.ts +51 -0
  62. package/src/engine/ink/events/terminal-event.ts +107 -0
  63. package/src/engine/ink/events/terminal-focus-event.ts +19 -0
  64. package/src/engine/ink/focus.ts +181 -0
  65. package/src/engine/ink/frame.ts +124 -0
  66. package/src/engine/ink/get-max-width.ts +27 -0
  67. package/src/engine/ink/global.d.ts +18 -0
  68. package/src/engine/ink/hit-test.ts +130 -0
  69. package/src/engine/ink/hooks/use-animation-frame.ts +57 -0
  70. package/src/engine/ink/hooks/use-app.ts +8 -0
  71. package/src/engine/ink/hooks/use-declared-cursor.ts +73 -0
  72. package/src/engine/ink/hooks/use-input.ts +92 -0
  73. package/src/engine/ink/hooks/use-interval.ts +67 -0
  74. package/src/engine/ink/hooks/use-search-highlight.ts +53 -0
  75. package/src/engine/ink/hooks/use-selection.ts +104 -0
  76. package/src/engine/ink/hooks/use-stdin.ts +8 -0
  77. package/src/engine/ink/hooks/use-tab-status.ts +72 -0
  78. package/src/engine/ink/hooks/use-terminal-focus.ts +16 -0
  79. package/src/engine/ink/hooks/use-terminal-title.ts +31 -0
  80. package/src/engine/ink/hooks/use-terminal-viewport.ts +96 -0
  81. package/src/engine/ink/ink.tsx +1723 -0
  82. package/src/engine/ink/instances.ts +10 -0
  83. package/src/engine/ink/layout/engine.ts +6 -0
  84. package/src/engine/ink/layout/geometry.ts +97 -0
  85. package/src/engine/ink/layout/node.ts +152 -0
  86. package/src/engine/ink/layout/yoga.ts +308 -0
  87. package/src/engine/ink/line-width-cache.ts +24 -0
  88. package/src/engine/ink/log-update.ts +773 -0
  89. package/src/engine/ink/measure-element.ts +23 -0
  90. package/src/engine/ink/measure-text.ts +47 -0
  91. package/src/engine/ink/node-cache.ts +54 -0
  92. package/src/engine/ink/optimizer.ts +93 -0
  93. package/src/engine/ink/output.ts +797 -0
  94. package/src/engine/ink/parse-keypress.ts +801 -0
  95. package/src/engine/ink/reconciler.ts +512 -0
  96. package/src/engine/ink/render-border.ts +231 -0
  97. package/src/engine/ink/render-node-to-output.ts +1462 -0
  98. package/src/engine/ink/render-to-screen.ts +231 -0
  99. package/src/engine/ink/renderer.ts +178 -0
  100. package/src/engine/ink/root.ts +184 -0
  101. package/src/engine/ink/screen.ts +1486 -0
  102. package/src/engine/ink/searchHighlight.ts +93 -0
  103. package/src/engine/ink/selection.ts +917 -0
  104. package/src/engine/ink/squash-text-nodes.ts +92 -0
  105. package/src/engine/ink/stringWidth.ts +222 -0
  106. package/src/engine/ink/styles.ts +771 -0
  107. package/src/engine/ink/supports-hyperlinks.ts +57 -0
  108. package/src/engine/ink/tabstops.ts +46 -0
  109. package/src/engine/ink/terminal-focus-state.ts +47 -0
  110. package/src/engine/ink/terminal-querier.ts +212 -0
  111. package/src/engine/ink/terminal.ts +248 -0
  112. package/src/engine/ink/termio/ansi.ts +75 -0
  113. package/src/engine/ink/termio/csi.ts +319 -0
  114. package/src/engine/ink/termio/dec.ts +60 -0
  115. package/src/engine/ink/termio/esc.ts +67 -0
  116. package/src/engine/ink/termio/osc.ts +493 -0
  117. package/src/engine/ink/termio/parser.ts +394 -0
  118. package/src/engine/ink/termio/sgr.ts +308 -0
  119. package/src/engine/ink/termio/tokenize.ts +319 -0
  120. package/src/engine/ink/termio/types.ts +236 -0
  121. package/src/engine/ink/useTerminalNotification.ts +126 -0
  122. package/src/engine/ink/warn.ts +9 -0
  123. package/src/engine/ink/widest-line.ts +19 -0
  124. package/src/engine/ink/wrap-text.ts +74 -0
  125. package/src/engine/ink/wrapAnsi.ts +20 -0
  126. package/src/engine/native-ts/yoga-layout/enums.ts +134 -0
  127. package/src/engine/native-ts/yoga-layout/index.ts +2578 -0
  128. package/src/engine/stubs/bootstrap-state.ts +4 -0
  129. package/src/engine/stubs/debug.ts +6 -0
  130. package/src/engine/stubs/log.ts +4 -0
  131. package/src/engine/utils/debug.ts +5 -0
  132. package/src/engine/utils/earlyInput.ts +4 -0
  133. package/src/engine/utils/env.ts +15 -0
  134. package/src/engine/utils/envUtils.ts +4 -0
  135. package/src/engine/utils/execFileNoThrow.ts +24 -0
  136. package/src/engine/utils/fullscreen.ts +4 -0
  137. package/src/engine/utils/intl.ts +9 -0
  138. package/src/engine/utils/log.ts +3 -0
  139. package/src/engine/utils/semver.ts +13 -0
  140. package/src/engine/utils/sliceAnsi.ts +10 -0
  141. package/src/engine/utils/theme.ts +17 -0
  142. package/src/game/App.tsx +141 -0
  143. package/src/game/agents/behavior.ts +249 -0
  144. package/src/game/agents/speech.ts +57 -0
  145. package/src/game/canvas.ts +98 -0
  146. package/src/game/launch.ts +36 -0
  147. package/src/game/ship/ShipView.tsx +145 -0
  148. package/src/game/ship/ship-map.ts +172 -0
  149. package/src/game/ui/AgentBio.tsx +72 -0
  150. package/src/game/ui/HUD.tsx +63 -0
  151. package/src/game/ui/StatusBar.tsx +49 -0
  152. package/src/game/useKeyboard.ts +62 -0
  153. package/src/main.tsx +22 -0
  154. package/src/run-interactive.ts +74 -0
@@ -0,0 +1,36 @@
1
+ // Launch the TUI game from the CLI. Loads data from DB and renders.
2
+ import React from 'react';
3
+ import { APP_NAME } from '../app-paths.js';
4
+ import { getDb } from '../collector/db.js';
5
+ import { createRoot } from '../engine/ink/root.js';
6
+ import { GameApp } from './App.js';
7
+
8
+ export async function launchGame(): Promise<void> {
9
+ const db = getDb();
10
+ const agents = db.prepare('SELECT * FROM agents ORDER BY created_at').all() as any[];
11
+ const clans = db.prepare('SELECT * FROM clans ORDER BY name').all() as any[];
12
+ const phase = (db.prepare("SELECT value FROM world WHERE key = 'phase'").get() as any)?.value ?? 'space';
13
+ const day = parseInt((db.prepare("SELECT value FROM world WHERE key = 'day'").get() as any)?.value ?? '0', 10);
14
+ const seed = parseInt((db.prepare("SELECT value FROM world WHERE key = 'seed'").get() as any)?.value ?? '42', 10);
15
+
16
+ if (agents.length === 0) {
17
+ console.log('\n No agents found yet. Start an agentic coding session first.');
18
+ console.log(` Run \`${APP_NAME} sync\` to manually scan for sessions.\n`);
19
+ return;
20
+ }
21
+
22
+ const root = await createRoot({ exitOnCtrlC: true });
23
+ let quitting = false;
24
+
25
+ const quit = () => {
26
+ if (quitting) return;
27
+ quitting = true;
28
+ root.unmount();
29
+ };
30
+
31
+ root.render(
32
+ React.createElement(GameApp, { agents, clans, phase, day, seed, onQuit: quit })
33
+ );
34
+
35
+ await root.waitUntilExit();
36
+ }
@@ -0,0 +1,145 @@
1
+ // Ship interior renderer for Phase 1-2.
2
+ // Draws ship tiles, stars, agents, speech bubbles using Canvas -> Text spans.
3
+
4
+ import React, { useMemo, useRef } from 'react';
5
+ import { useAnimationFrame } from '../../engine/ink/hooks/use-animation-frame.js';
6
+ import Box from '../../engine/ink/components/Box.js';
7
+ import Text from '../../engine/ink/components/Text.js';
8
+ import { Canvas, type TextSpan } from '../canvas.js';
9
+ import { getTileVisual, type ShipLayout } from './ship-map.js';
10
+ import { lerpAgent, type AgentSim } from '../agents/behavior.js';
11
+ import { stateSpeech } from '../agents/speech.js';
12
+ import type { Color } from '../../engine/ink/styles.js';
13
+
14
+ interface Props {
15
+ ship: ShipLayout;
16
+ agents: AgentSim[];
17
+ viewWidth: number;
18
+ viewHeight: number;
19
+ seed: number;
20
+ selectedAgentId: string | null;
21
+ paused: boolean;
22
+ }
23
+
24
+ interface Star { x: number; y: number; char: string; brightness: number; }
25
+
26
+ function generateStars(w: number, h: number, seed: number): Star[] {
27
+ const stars: Star[] = [];
28
+ let s = seed;
29
+ const rand = () => { s = (s * 1103515245 + 12345) & 0x7fffffff; return s / 0x7fffffff; };
30
+ const chars = ['.', '*', '+', '`'];
31
+ for (let y = 0; y < h * 2; y++) {
32
+ for (let x = 0; x < w * 2; x++) {
33
+ if (rand() < 0.015) {
34
+ stars.push({ x, y, char: chars[Math.floor(rand() * chars.length)]!, brightness: 0.3 + rand() * 0.7 });
35
+ }
36
+ }
37
+ }
38
+ return stars;
39
+ }
40
+
41
+ // Memoized row component to prevent unnecessary re-renders
42
+ function CanvasRow({ spans }: { spans: TextSpan[] }): React.ReactNode {
43
+ return (
44
+ <Box>
45
+ {spans.map((span, i) => (
46
+ <Text key={i} color={span.fg as Color} backgroundColor={span.bg as Color}>{span.text}</Text>
47
+ ))}
48
+ </Box>
49
+ );
50
+ }
51
+
52
+ export function ShipView({ ship, agents, viewWidth, viewHeight, seed, selectedAgentId, paused }: Props): React.ReactNode {
53
+ const [animRef, time] = useAnimationFrame(paused ? null : 100); // 10fps for stability
54
+ const frame = Math.floor(time / 100);
55
+ const lastSpeechTime = useRef(0);
56
+
57
+ // Reuse canvas
58
+ const canvasRef = useRef<Canvas | null>(null);
59
+ if (!canvasRef.current || canvasRef.current.width !== viewWidth || canvasRef.current.height !== viewHeight) {
60
+ canvasRef.current = new Canvas(viewWidth, viewHeight);
61
+ }
62
+ const canvas = canvasRef.current;
63
+
64
+ const stars = useMemo(() => generateStars(ship.width, ship.height, seed), [ship.width, ship.height, seed]);
65
+
66
+ // Camera centered on ship
67
+ const selected = agents.find(a => a.id === selectedAgentId);
68
+ const camX = Math.round((selected ? selected.renderX : ship.width / 2) - viewWidth / 2);
69
+ const camY = Math.round((selected ? selected.renderY : ship.height / 2) - viewHeight / 2);
70
+
71
+ // Lerp agents
72
+ for (const agent of agents) { lerpAgent(agent, 100); }
73
+
74
+ // Speech at ~3Hz
75
+ if (time - lastSpeechTime.current > 300) {
76
+ lastSpeechTime.current = time;
77
+ for (const agent of agents) {
78
+ if (agent.speechTimer > 0) agent.speechTimer--;
79
+ if (agent.speechTimer <= 0) agent.speech = null;
80
+ if (!agent.speech) {
81
+ const speech = stateSpeech(agent.state);
82
+ if (speech) { agent.speech = speech; agent.speechTimer = 10; }
83
+ }
84
+ }
85
+ }
86
+
87
+ // Clear
88
+ canvas.clear('#222233', '#0a0a14');
89
+
90
+ // Stars
91
+ for (const star of stars) {
92
+ const sx = star.x - camX;
93
+ const sy = star.y - camY;
94
+ if (sx >= 0 && sx < viewWidth && sy >= 0 && sy < viewHeight) {
95
+ const twinkle = Math.sin(frame * 0.15 + star.x * 7 + star.y * 13) * 0.3 + 0.7;
96
+ const b = Math.floor(star.brightness * twinkle * 140 + 40);
97
+ const bh = Math.max(0, Math.min(255, b)).toString(16).padStart(2, '0');
98
+ canvas.set(sx, sy, star.char, `#${bh}${bh}ff`);
99
+ }
100
+ }
101
+
102
+ // Ship tiles
103
+ for (let wy = 0; wy < ship.height; wy++) {
104
+ for (let wx = 0; wx < ship.width; wx++) {
105
+ const sx = wx - camX;
106
+ const sy = wy - camY;
107
+ if (sx < 0 || sx >= viewWidth || sy < 0 || sy >= viewHeight) continue;
108
+ const tile = ship.tiles[wy]?.[wx];
109
+ if (!tile || tile === 'void') continue;
110
+ const v = getTileVisual(tile);
111
+ canvas.set(sx, sy, v.char, v.fg, v.bg);
112
+ }
113
+ }
114
+
115
+ // Agents
116
+ for (const agent of agents) {
117
+ const sx = Math.round(agent.renderX) - camX;
118
+ const sy = Math.round(agent.renderY) - camY;
119
+ if (sx < 0 || sx >= viewWidth || sy < 0 || sy >= viewHeight) continue;
120
+
121
+ const isSel = agent.id === selectedAgentId;
122
+ canvas.set(sx, sy, agent.state === 'sleeping' ? 'z' : agent.sprite,
123
+ isSel ? '#ffffff' : agent.color, isSel ? '#444444' : '#1a2a1a');
124
+
125
+ // Speech
126
+ if (agent.speech && sy > 0) {
127
+ const text = agent.speech.slice(0, Math.min(14, viewWidth - sx));
128
+ canvas.drawText(Math.max(0, sx - 1), sy - 1, text, '#eeeecc', '#333322');
129
+ }
130
+
131
+ // Name below selected
132
+ if (isSel && sy < viewHeight - 1) {
133
+ const tag = agent.name.slice(0, 12);
134
+ canvas.drawText(Math.max(0, sx - Math.floor(tag.length / 2)), sy + 1, tag, '#cccccc', '#222222');
135
+ }
136
+ }
137
+
138
+ const spanRows = canvas.toSpanRows();
139
+
140
+ return (
141
+ <Box ref={animRef} flexDirection="column" flexGrow={1}>
142
+ {spanRows.map((spans, y) => <CanvasRow key={y} spans={spans} />)}
143
+ </Box>
144
+ );
145
+ }
@@ -0,0 +1,172 @@
1
+ // Ship interior layout for Phase 1-2 (space, before planetfall).
2
+ // The ship grows as crew count increases. Tile map is a simple 2D grid.
3
+
4
+ export type TileType =
5
+ | 'void' // empty space (stars)
6
+ | 'hull' // outer ship wall
7
+ | 'wall' // inner wall
8
+ | 'floor' // walkable floor
9
+ | 'window' // transparent hull (see stars)
10
+ | 'door' // walkable passage between rooms
11
+ | 'console' // bridge control panel
12
+ | 'bed' // crew quarters
13
+ | 'plant' // garden/plant
14
+ | 'crate' // cargo storage
15
+ | 'engine' // engine component
16
+ | 'table'; // mess hall table
17
+
18
+ export interface TileVisual {
19
+ char: string;
20
+ fg: string;
21
+ bg: string;
22
+ }
23
+
24
+ // Visual mapping for each tile type
25
+ const TILE_VISUALS: Record<TileType, TileVisual> = {
26
+ void: { char: ' ', fg: '#222233', bg: '#0a0a14' },
27
+ hull: { char: '█', fg: '#445566', bg: '#334455' },
28
+ wall: { char: '▓', fg: '#556677', bg: '#3a4a5a' },
29
+ floor: { char: '·', fg: '#3a4a3a', bg: '#1a2a1a' },
30
+ window: { char: '░', fg: '#4488cc', bg: '#223355' },
31
+ door: { char: '▒', fg: '#887744', bg: '#2a2a1a' },
32
+ console: { char: '◈', fg: '#44ddcc', bg: '#1a2a2a' },
33
+ bed: { char: '▬', fg: '#aa7744', bg: '#1a2a1a' },
34
+ plant: { char: '♣', fg: '#44cc44', bg: '#1a2a1a' },
35
+ crate: { char: '▣', fg: '#aa8855', bg: '#1a2a1a' },
36
+ engine: { char: '⚙', fg: '#dd8844', bg: '#1a1a1a' },
37
+ table: { char: '▫', fg: '#886644', bg: '#1a2a1a' },
38
+ };
39
+
40
+ export function getTileVisual(type: TileType): TileVisual {
41
+ return TILE_VISUALS[type];
42
+ }
43
+
44
+ export function isWalkable(type: TileType): boolean {
45
+ return type === 'floor' || type === 'door';
46
+ }
47
+
48
+ export interface Room {
49
+ name: string;
50
+ x: number;
51
+ y: number;
52
+ w: number;
53
+ h: number;
54
+ }
55
+
56
+ export interface ShipLayout {
57
+ width: number;
58
+ height: number;
59
+ tiles: TileType[][];
60
+ rooms: Room[];
61
+ spawnPoints: Array<{ x: number; y: number }>;
62
+ }
63
+
64
+ // Ship layout encoded as string map.
65
+ // Legend: ~ void, # hull, | wall, . floor, = window, + door,
66
+ // C console, B bed, P plant, X crate, E engine, T table
67
+ const SHIP_SMALL = [
68
+ '~~~~~~~~~~#####~~~~~~~~~~',
69
+ '~~~~~~~~##=...=##~~~~~~~~',
70
+ '~~~~~~~#...CCC...#~~~~~~~',
71
+ '~~~~~~~#...CCC...#~~~~~~~',
72
+ '~~~~~~~#.........#~~~~~~~',
73
+ '~~~~~~~=.........=~~~~~~~',
74
+ '~~~~~###+###.###+###~~~~~',
75
+ '~~~~~#.....#.#.....#~~~~~',
76
+ '~~~~~#.BB..+.+..XX.#~~~~~',
77
+ '~~~~~#.BB..#.#..XX.#~~~~~',
78
+ '~~~~~#.....#.#.....#~~~~~',
79
+ '~~~~~###+###.###+###~~~~~',
80
+ '~~~~~#.....#.#.....#~~~~~',
81
+ '~~~~~#.TT..+.+..EE.#~~~~~',
82
+ '~~~~~#.TT..#.#..EE.#~~~~~',
83
+ '~~~~~#.PP..#.#.....#~~~~~',
84
+ '~~~~~###+###.###+###~~~~~',
85
+ '~~~~~~~#.........#~~~~~~~',
86
+ '~~~~~~~#.........#~~~~~~~',
87
+ '~~~~~~~=.........=~~~~~~~',
88
+ '~~~~~~~###=#=#=###~~~~~~~',
89
+ '~~~~~~~~~~#####~~~~~~~~~~',
90
+ ];
91
+
92
+ // Bigger ship unlocked at 6+ agents - adds more rooms
93
+ const SHIP_MEDIUM = [
94
+ '~~~~~~~~~~~~######~~~~~~~~~~~~',
95
+ '~~~~~~~~~###=....=###~~~~~~~~~',
96
+ '~~~~~~~~#....CCCC....#~~~~~~~~',
97
+ '~~~~~~~~#....CCCC....#~~~~~~~~',
98
+ '~~~~~~~~#............#~~~~~~~~',
99
+ '~~~~~~~~=............=~~~~~~~~',
100
+ '~~~~~~###+####..####+###~~~~~~',
101
+ '~~~~~~#......#..#......#~~~~~~',
102
+ '~~~~~~#.BBB..+..+..XXX.#~~~~~~',
103
+ '~~~~~~#.BBB..#..#..XXX.#~~~~~~',
104
+ '~~~~~~#......#..#......#~~~~~~',
105
+ '~~~~##+####+##..##+####+##~~~~',
106
+ '~~~~#......#......#......#~~~~',
107
+ '~~~~#.BBB..+......+..PP..#~~~~',
108
+ '~~~~#.BBB..#......#..PP..#~~~~',
109
+ '~~~~#......#......#......#~~~~',
110
+ '~~~~##+####+##..##+####+##~~~~',
111
+ '~~~~~~#......#..#......#~~~~~~',
112
+ '~~~~~~#.TTT..+..+..EEE.#~~~~~~',
113
+ '~~~~~~#.TTT..#..#..EEE.#~~~~~~',
114
+ '~~~~~~#.PP...#..#..EEE.#~~~~~~',
115
+ '~~~~~~###+####..####+###~~~~~~',
116
+ '~~~~~~~~#............#~~~~~~~~',
117
+ '~~~~~~~~#............#~~~~~~~~',
118
+ '~~~~~~~~=............=~~~~~~~~',
119
+ '~~~~~~~~####=##=####~~~~~~~~~',
120
+ '~~~~~~~~~~~~######~~~~~~~~~~~~',
121
+ ];
122
+
123
+ const CHAR_TO_TILE: Record<string, TileType> = {
124
+ '~': 'void',
125
+ '#': 'hull',
126
+ '|': 'wall',
127
+ '.': 'floor',
128
+ '=': 'window',
129
+ '+': 'door',
130
+ 'C': 'console',
131
+ 'B': 'bed',
132
+ 'P': 'plant',
133
+ 'X': 'crate',
134
+ 'E': 'engine',
135
+ 'T': 'table',
136
+ };
137
+
138
+ function parseShipLayout(lines: string[]): ShipLayout {
139
+ const height = lines.length;
140
+ const width = Math.max(...lines.map(l => l.length));
141
+ const tiles: TileType[][] = [];
142
+ const spawnPoints: Array<{ x: number; y: number }> = [];
143
+
144
+ for (let y = 0; y < height; y++) {
145
+ const row: TileType[] = [];
146
+ const line = lines[y]!;
147
+ for (let x = 0; x < width; x++) {
148
+ const ch = x < line.length ? line[x]! : '~';
149
+ const tile = CHAR_TO_TILE[ch] ?? 'void';
150
+ row.push(tile);
151
+ // Collect floor tiles as potential spawn points
152
+ if (tile === 'floor') {
153
+ spawnPoints.push({ x, y });
154
+ }
155
+ }
156
+ tiles.push(row);
157
+ }
158
+
159
+ // Detect rooms (named areas)
160
+ const rooms: Room[] = [];
161
+ // Bridge is always the top area
162
+ rooms.push({ name: 'Bridge', x: Math.floor(width / 2) - 3, y: 2, w: 6, h: 3 });
163
+
164
+ return { width, height, tiles, rooms, spawnPoints };
165
+ }
166
+
167
+ export function generateShip(agentCount: number): ShipLayout {
168
+ if (agentCount >= 6) {
169
+ return parseShipLayout(SHIP_MEDIUM);
170
+ }
171
+ return parseShipLayout(SHIP_SMALL);
172
+ }
@@ -0,0 +1,72 @@
1
+ // Full agent profile view with personality trait bars and origin info.
2
+
3
+ import React from 'react';
4
+ import Box from '../../engine/ink/components/Box.js';
5
+ import Text from '../../engine/ink/components/Text.js';
6
+ import type { AgentSim } from '../agents/behavior.js';
7
+
8
+ interface Props {
9
+ agent: AgentSim;
10
+ onClose: () => void;
11
+ }
12
+
13
+ function TraitBar({ label, value, color }: { label: string; value: number; color: string }): React.ReactNode {
14
+ const barWidth = 20;
15
+ const filled = Math.round(value * barWidth);
16
+ const empty = barWidth - filled;
17
+ return (
18
+ <Box>
19
+ <Text color="#888888"> {label.padEnd(10)}</Text>
20
+ <Text color={color}>{'█'.repeat(filled)}</Text>
21
+ <Text color="#333333">{'░'.repeat(empty)}</Text>
22
+ <Text color="#666666">{' '}{(value * 100).toFixed(0)}%</Text>
23
+ </Box>
24
+ );
25
+ }
26
+
27
+ export function AgentBio({ agent, onClose }: Props): React.ReactNode {
28
+ const t = agent.traits;
29
+
30
+ return (
31
+ <Box flexDirection="column" paddingX={2} paddingY={1}>
32
+ <Box>
33
+ <Text color={agent.color} bold>
34
+ {' '}{agent.sprite} {agent.name}
35
+ </Text>
36
+ <Text color="#666666">
37
+ {' '}{agent.maturity} · {agent.clan}
38
+ </Text>
39
+ </Box>
40
+
41
+ <Text color="#444444"> {'─'.repeat(40)}</Text>
42
+
43
+ <Box flexDirection="column" marginTop={1}>
44
+ <Text color="#888888"> Personality</Text>
45
+ <TraitBar label="Builder" value={t.builder} color="#ff8844" />
46
+ <TraitBar label="Creator" value={t.creator} color="#44cc88" />
47
+ <TraitBar label="Explorer" value={t.explorer} color="#4488ff" />
48
+ <TraitBar label="Leader" value={t.leader} color="#ff44aa" />
49
+ <TraitBar label="Thinker" value={t.thinker} color="#aa88ff" />
50
+ <TraitBar label="Scholar" value={t.scholar} color="#ffcc44" />
51
+ </Box>
52
+
53
+ <Box flexDirection="column" marginTop={1}>
54
+ <Text color="#888888"> Status</Text>
55
+ <Text color="#666666"> Energy {energyBar(agent.energy)}</Text>
56
+ <Text color="#666666"> State {agent.state}</Text>
57
+ <Text color="#666666"> Position ({agent.x}, {agent.y})</Text>
58
+ </Box>
59
+
60
+ <Box marginTop={1}>
61
+ <Text color="#555566"> Press [b] or [esc] to return</Text>
62
+ </Box>
63
+ </Box>
64
+ );
65
+ }
66
+
67
+ function energyBar(energy: number): string {
68
+ const width = 20;
69
+ const filled = Math.round((energy / 100) * width);
70
+ const color = energy > 60 ? '█' : energy > 30 ? '▓' : '░';
71
+ return color.repeat(filled) + '·'.repeat(width - filled) + ` ${Math.round(energy)}%`;
72
+ }
@@ -0,0 +1,63 @@
1
+ // Top status bar showing game state, phase, selected agent info.
2
+
3
+ import React from 'react';
4
+ import Box from '../../engine/ink/components/Box.js';
5
+ import Text from '../../engine/ink/components/Text.js';
6
+ import type { AgentSim } from '../agents/behavior.js';
7
+
8
+ interface Props {
9
+ phase: string;
10
+ agentCount: number;
11
+ clanCount: number;
12
+ day: number;
13
+ selectedAgent: AgentSim | null;
14
+ paused: boolean;
15
+ }
16
+
17
+ const PHASE_LABELS: Record<string, string> = {
18
+ space: '🚀 SPACE',
19
+ ship: '🛸 SHIP',
20
+ planet: '🌍 PLANETFALL',
21
+ settlement: '🏘 SETTLEMENT',
22
+ civilization: '🏛 CIVILIZATION',
23
+ };
24
+
25
+ const STATE_LABELS: Record<string, string> = {
26
+ idle: '💭 thinking',
27
+ walking: '🚶 walking',
28
+ sleeping: '😴 sleeping',
29
+ chatting: '💬 chatting',
30
+ working: '⚒ working',
31
+ exploring: '🔍 exploring',
32
+ };
33
+
34
+ export function HUD({ phase, agentCount, clanCount, day, selectedAgent, paused }: Props): React.ReactNode {
35
+ return (
36
+ <Box flexDirection="row" width="100%" height={1}>
37
+ <Box flexGrow={1}>
38
+ <Text color="#888888" backgroundColor="#1a1a2a">
39
+ {' '}{PHASE_LABELS[phase] ?? phase}
40
+ {' · '}
41
+ {agentCount} agents
42
+ {' · '}
43
+ {clanCount} clans
44
+ {' · '}
45
+ Day {day}
46
+ {paused ? ' · ⏸ PAUSED' : ''}
47
+ {' '}
48
+ </Text>
49
+ </Box>
50
+ {selectedAgent && (
51
+ <Box>
52
+ <Text color={selectedAgent.color} backgroundColor="#1a1a2a">
53
+ {' '}{selectedAgent.name}{' '}
54
+ </Text>
55
+ <Text color="#666666" backgroundColor="#1a1a2a">
56
+ {selectedAgent.maturity} · {STATE_LABELS[selectedAgent.state] ?? selectedAgent.state}
57
+ {' '}
58
+ </Text>
59
+ </Box>
60
+ )}
61
+ </Box>
62
+ );
63
+ }
@@ -0,0 +1,49 @@
1
+ // Bottom keybinding guide bar.
2
+
3
+ import React from 'react';
4
+ import Box from '../../engine/ink/components/Box.js';
5
+ import Text from '../../engine/ink/components/Text.js';
6
+ import { APP_NAME } from '../../app-paths.js';
7
+
8
+ interface Props {
9
+ view: string;
10
+ }
11
+
12
+ export function StatusBar({ view }: Props): React.ReactNode {
13
+ return (
14
+ <Box flexDirection="row" width="100%" height={1}>
15
+ <Text color="#555566" backgroundColor="#111122">
16
+ {' [tab]'}
17
+ </Text>
18
+ <Text color="#888899" backgroundColor="#111122">
19
+ {' next agent '}
20
+ </Text>
21
+ <Text color="#555566" backgroundColor="#111122">
22
+ {' [space]'}
23
+ </Text>
24
+ <Text color="#888899" backgroundColor="#111122">
25
+ {' pause '}
26
+ </Text>
27
+ <Text color="#555566" backgroundColor="#111122">
28
+ {' [b]'}
29
+ </Text>
30
+ <Text color="#888899" backgroundColor="#111122">
31
+ {' bio '}
32
+ </Text>
33
+ <Text color="#555566" backgroundColor="#111122">
34
+ {' [q]'}
35
+ </Text>
36
+ <Text color="#888899" backgroundColor="#111122">
37
+ {' quit '}
38
+ </Text>
39
+ <Box flexGrow={1}>
40
+ <Text color="#333344" backgroundColor="#111122">
41
+ {''}
42
+ </Text>
43
+ </Box>
44
+ <Text color="#444455" backgroundColor="#111122">
45
+ {` ${APP_NAME} v0.1 `}
46
+ </Text>
47
+ </Box>
48
+ );
49
+ }
@@ -0,0 +1,62 @@
1
+ // Lightweight keyboard input hook that bypasses the engine's useInput.
2
+ // Reads raw stdin directly with minimal processing.
3
+
4
+ import { useEffect, useRef } from 'react';
5
+
6
+ export interface KeyEvent {
7
+ raw: string;
8
+ key: string;
9
+ ctrl: boolean;
10
+ tab: boolean;
11
+ escape: boolean;
12
+ space: boolean;
13
+ }
14
+
15
+ type KeyHandler = (event: KeyEvent) => void;
16
+
17
+ export function useKeyboard(handler: KeyHandler): void {
18
+ const handlerRef = useRef(handler);
19
+ handlerRef.current = handler;
20
+
21
+ useEffect(() => {
22
+ const stdin = process.stdin;
23
+ const wasRaw = stdin.isRaw;
24
+
25
+ if (stdin.isTTY) {
26
+ stdin.setRawMode(true);
27
+ stdin.resume();
28
+ }
29
+
30
+ const onData = (data: Buffer) => {
31
+ const str = data.toString('utf-8');
32
+ const event: KeyEvent = {
33
+ raw: str,
34
+ key: str,
35
+ ctrl: false,
36
+ tab: str === '\t',
37
+ escape: str === '\x1b',
38
+ space: str === ' ',
39
+ };
40
+
41
+ // Detect ctrl+key (0x01-0x1a)
42
+ if (str.length === 1) {
43
+ const code = str.charCodeAt(0);
44
+ if (code >= 1 && code <= 26) {
45
+ event.ctrl = true;
46
+ event.key = String.fromCharCode(code + 96); // ctrl+a = 1, etc.
47
+ }
48
+ }
49
+
50
+ handlerRef.current(event);
51
+ };
52
+
53
+ stdin.on('data', onData);
54
+
55
+ return () => {
56
+ stdin.removeListener('data', onData);
57
+ if (stdin.isTTY && !wasRaw) {
58
+ stdin.setRawMode(false);
59
+ }
60
+ };
61
+ }, []);
62
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ // Dev entry point: perform startup sync, then launch the TUI game.
3
+
4
+ import { DB_PATH, ensureLocalDataDir } from './app-paths.js';
5
+ import { initDb, closeDb } from './collector/db.js';
6
+ import { runInteractiveSession } from './run-interactive.js';
7
+
8
+ async function main() {
9
+ ensureLocalDataDir();
10
+ initDb(DB_PATH);
11
+
12
+ try {
13
+ await runInteractiveSession();
14
+ } finally {
15
+ closeDb();
16
+ }
17
+ }
18
+
19
+ main().catch(err => {
20
+ console.error('Fatal error:', err);
21
+ process.exit(1);
22
+ });