@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,4 @@
1
+ // Stub for src/bootstrap/state.js — telemetry we don't need
2
+ export function flushInteractionTime(): void {
3
+ // no-op: Agent Sim doesn't track interaction telemetry
4
+ }
@@ -0,0 +1,6 @@
1
+ // Stub for src/utils/debug.js
2
+ export function logForDebugging(..._args: unknown[]): void {
3
+ if (process.env.AGENT_SIM_DEBUG || process.env.AGENT_WARS_DEBUG) {
4
+ console.error("[agent-sim debug]", ..._args);
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ // Stub for src/utils/log.js
2
+ export function logError(...args: unknown[]): void {
3
+ console.error("[agent-sim]", ...args);
4
+ }
@@ -0,0 +1,5 @@
1
+ export function logForDebugging(..._args: unknown[]): void {
2
+ if (process.env.AGENT_SIM_DEBUG || process.env.AGENT_WARS_DEBUG) {
3
+ console.error('[agent-sim debug]', ..._args);
4
+ }
5
+ }
@@ -0,0 +1,4 @@
1
+ // Stub — the host runtime can capture early stdin before React mounts
2
+ export function stopCapturingEarlyInput(): Buffer[] {
3
+ return [];
4
+ }
@@ -0,0 +1,15 @@
1
+ // Terminal environment detection stub
2
+ export const env = {
3
+ terminal: detectTerminal(),
4
+ isTTY: process.stdout.isTTY ?? false,
5
+ };
6
+
7
+ function detectTerminal(): string {
8
+ const term = process.env.TERM_PROGRAM?.toLowerCase() ?? '';
9
+ if (term.includes('kitty')) return 'kitty';
10
+ if (term.includes('iterm')) return 'iterm2';
11
+ if (term.includes('wezterm')) return 'wezterm';
12
+ if (term.includes('alacritty')) return 'alacritty';
13
+ if (term.includes('vscode')) return 'vscode';
14
+ return term || 'unknown';
15
+ }
@@ -0,0 +1,4 @@
1
+ export function isEnvTruthy(key: string): boolean {
2
+ const val = process.env[key];
3
+ return val === '1' || val === 'true';
4
+ }
@@ -0,0 +1,24 @@
1
+ // Stub for exec utility used in clipboard operations
2
+ import { execFile as execFileCb } from 'child_process';
3
+
4
+ interface ExecResult {
5
+ code: number | null;
6
+ stdout: string;
7
+ stderr: string;
8
+ }
9
+
10
+ export function execFileNoThrow(
11
+ cmd: string,
12
+ args: string[],
13
+ opts: { input?: string; timeout?: number } = {},
14
+ ): Promise<ExecResult> {
15
+ return new Promise(resolve => {
16
+ const child = execFileCb(cmd, args, { timeout: opts.timeout ?? 5000 }, (err, stdout, stderr) => {
17
+ resolve({ code: err ? (err as any).code ?? 1 : 0, stdout: stdout ?? '', stderr: stderr ?? '' });
18
+ });
19
+ if (opts.input && child.stdin) {
20
+ child.stdin.write(opts.input);
21
+ child.stdin.end();
22
+ }
23
+ });
24
+ }
@@ -0,0 +1,4 @@
1
+ // Stub — fullscreen support is delegated to the host runtime
2
+ export function isMouseClicksDisabled(): boolean {
3
+ return false;
4
+ }
@@ -0,0 +1,9 @@
1
+ // Grapheme segmenter utility
2
+ let segmenter: Intl.Segmenter | null = null;
3
+
4
+ export function getGraphemeSegmenter(): Intl.Segmenter {
5
+ if (!segmenter) {
6
+ segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
7
+ }
8
+ return segmenter;
9
+ }
@@ -0,0 +1,3 @@
1
+ export function logError(...args: unknown[]): void {
2
+ console.error('[agent-sim]', ...args);
3
+ }
@@ -0,0 +1,13 @@
1
+ // Minimal semver comparison stub
2
+ export function gte(version: string, range: string): boolean {
3
+ const parse = (v: string) => v.split('.').map(Number);
4
+ const a = parse(version);
5
+ const b = parse(range);
6
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
7
+ const av = a[i] ?? 0;
8
+ const bv = b[i] ?? 0;
9
+ if (av > bv) return true;
10
+ if (av < bv) return false;
11
+ }
12
+ return true;
13
+ }
@@ -0,0 +1,10 @@
1
+ // ANSI-aware string slicing
2
+ import stripAnsi from 'strip-ansi';
3
+
4
+ export default function sliceAnsi(text: string, start: number, end?: number): string {
5
+ const stripped = stripAnsi(text);
6
+ const sliced = stripped.slice(start, end);
7
+ // Simple version: just return the stripped slice
8
+ // Full version would preserve ANSI codes, but this works for our use case
9
+ return sliced;
10
+ }
@@ -0,0 +1,17 @@
1
+ // Minimal theme stub for buddy sprite compatibility
2
+ export interface Theme {
3
+ inactive: string;
4
+ success: string;
5
+ permission: string;
6
+ autoAccept: string;
7
+ warning: string;
8
+ [key: string]: string;
9
+ }
10
+
11
+ export const defaultTheme: Theme = {
12
+ inactive: '#666666',
13
+ success: '#44cc44',
14
+ permission: '#4488ff',
15
+ autoAccept: '#ff44aa',
16
+ warning: '#ffcc44',
17
+ };
@@ -0,0 +1,141 @@
1
+ // Main game shell. Wraps everything in AlternateScreen.
2
+ // Manages: view state, agent selection, simulation loop, keyboard input.
3
+
4
+ import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
5
+ import { AlternateScreen } from '../engine/ink/components/AlternateScreen.js';
6
+ import Box from '../engine/ink/components/Box.js';
7
+ import Text from '../engine/ink/components/Text.js';
8
+ import { TerminalSizeContext } from '../engine/ink/components/TerminalSizeContext.js';
9
+ import { generateShip } from './ship/ship-map.js';
10
+ import { ShipView } from './ship/ShipView.js';
11
+ import { createAgentSim, tickAgent, type AgentSim } from './agents/behavior.js';
12
+ import { HUD } from './ui/HUD.js';
13
+ import { StatusBar } from './ui/StatusBar.js';
14
+ import { AgentBio } from './ui/AgentBio.js';
15
+ import { useKeyboard } from './useKeyboard.js';
16
+
17
+ type View = 'ship' | 'bio';
18
+
19
+ interface AgentRow {
20
+ id: string;
21
+ name: string;
22
+ clan: string;
23
+ maturity: string;
24
+ trait_builder: number;
25
+ trait_creator: number;
26
+ trait_explorer: number;
27
+ trait_leader: number;
28
+ trait_thinker: number;
29
+ trait_scholar: number;
30
+ }
31
+
32
+ interface GameProps {
33
+ agents: AgentRow[];
34
+ clans: Array<{ name: string; color: string }>;
35
+ phase: string;
36
+ day: number;
37
+ seed: number;
38
+ onQuit: () => void;
39
+ }
40
+
41
+ export function GameApp({ agents: agentRows, clans, phase, day, seed, onQuit }: GameProps): React.ReactNode {
42
+ const size = useContext(TerminalSizeContext);
43
+ const cols = size?.columns ?? 80;
44
+ const rows = size?.rows ?? 24;
45
+
46
+ const ship = useMemo(() => generateShip(agentRows.length), [agentRows.length]);
47
+
48
+ // Agent sims (mutable)
49
+ const agentSimsRef = useRef<AgentSim[]>([]);
50
+ if (agentSimsRef.current.length === 0) {
51
+ const clanNames = [...new Set(agentRows.map(a => a.clan))];
52
+ agentSimsRef.current = agentRows.map((a, i) => {
53
+ const clanIdx = clanNames.indexOf(a.clan);
54
+ const spawn = ship.spawnPoints[i % ship.spawnPoints.length]!;
55
+ return createAgentSim(a, spawn.x, spawn.y, clanIdx, seed);
56
+ });
57
+ }
58
+
59
+ const [view, setView] = useState<View>('ship');
60
+ const [selectedIdx, setSelectedIdx] = useState(0);
61
+ const [paused, setPaused] = useState(false);
62
+
63
+ const selectedAgent = agentSimsRef.current[selectedIdx % agentSimsRef.current.length] ?? null;
64
+
65
+ // Simulation loop (10Hz)
66
+ const pausedRef = useRef(paused);
67
+ pausedRef.current = paused;
68
+ useEffect(() => {
69
+ const interval = setInterval(() => {
70
+ if (pausedRef.current) return;
71
+ const agents = agentSimsRef.current;
72
+ for (const agent of agents) {
73
+ tickAgent(agent, ship, agents);
74
+ }
75
+ }, 100);
76
+ return () => clearInterval(interval);
77
+ }, [ship]);
78
+
79
+ // Keyboard
80
+ useKeyboard((event) => {
81
+ if (event.key === 'q' || (event.ctrl && event.key === 'c')) {
82
+ onQuit();
83
+ return;
84
+ }
85
+ if (event.tab) {
86
+ setSelectedIdx(i => (i + 1) % Math.max(1, agentSimsRef.current.length));
87
+ return;
88
+ }
89
+ if (event.space) {
90
+ setPaused(p => !p);
91
+ return;
92
+ }
93
+ if (event.key === 'b') {
94
+ setView(v => v === 'bio' ? 'ship' : 'bio');
95
+ return;
96
+ }
97
+ if (event.escape) {
98
+ setView('ship');
99
+ return;
100
+ }
101
+ });
102
+
103
+ const viewHeight = rows - 2;
104
+ const viewWidth = cols;
105
+
106
+ return (
107
+ <AlternateScreen mouseTracking={false}>
108
+ <Box flexDirection="column" width="100%" height={rows}>
109
+ <HUD
110
+ phase={phase}
111
+ agentCount={agentSimsRef.current.length}
112
+ clanCount={clans.length}
113
+ day={day}
114
+ selectedAgent={selectedAgent}
115
+ paused={paused}
116
+ />
117
+
118
+ {view === 'ship' ? (
119
+ <ShipView
120
+ ship={ship}
121
+ agents={agentSimsRef.current}
122
+ viewWidth={viewWidth}
123
+ viewHeight={viewHeight}
124
+ seed={seed}
125
+ selectedAgentId={selectedAgent?.id ?? null}
126
+ paused={paused}
127
+ />
128
+ ) : view === 'bio' && selectedAgent ? (
129
+ <AgentBio
130
+ agent={selectedAgent}
131
+ onClose={() => setView('ship')}
132
+ />
133
+ ) : (
134
+ <Box flexGrow={1} />
135
+ )}
136
+
137
+ <StatusBar view={view} />
138
+ </Box>
139
+ </AlternateScreen>
140
+ );
141
+ }
@@ -0,0 +1,249 @@
1
+ // Agent autonomous behavior state machine.
2
+ // Each agent has a state, energy, and target position.
3
+ // Behavior selection is weighted by personality traits from real coding sessions.
4
+
5
+ import { createSeededRandom, hashString } from '@vsuryav/agent-sim-core';
6
+ import type { ShipLayout } from '../ship/ship-map.js';
7
+
8
+ export type AgentState = 'idle' | 'walking' | 'sleeping' | 'chatting' | 'working' | 'exploring';
9
+
10
+ export interface AgentSim {
11
+ id: string;
12
+ name: string;
13
+ clan: string;
14
+ maturity: string;
15
+ // Position (world coordinates)
16
+ x: number;
17
+ y: number;
18
+ // Smooth animation position (lerps toward x,y)
19
+ renderX: number;
20
+ renderY: number;
21
+ // State machine
22
+ state: AgentState;
23
+ stateTimer: number; // ticks remaining in current state
24
+ targetX: number;
25
+ targetY: number;
26
+ // Stats
27
+ energy: number; // 0-100
28
+ // Personality (from real data)
29
+ traits: {
30
+ builder: number;
31
+ creator: number;
32
+ explorer: number;
33
+ leader: number;
34
+ thinker: number;
35
+ scholar: number;
36
+ };
37
+ // Visual
38
+ color: string;
39
+ sprite: string; // 1-2 char display
40
+ // Speech
41
+ speech: string | null;
42
+ speechTimer: number;
43
+ // Animation
44
+ facing: 'left' | 'right';
45
+ animFrame: number;
46
+ }
47
+
48
+ // Clan colors palette
49
+ const CLAN_COLORS = [
50
+ '#ff6b6b', '#ffd93d', '#6bcb77', '#4d96ff',
51
+ '#c084fc', '#fb7185', '#38bdf8', '#a3e635',
52
+ '#f472b6', '#facc15', '#34d399', '#60a5fa',
53
+ ];
54
+
55
+ // Maturity sprites
56
+ const MATURITY_SPRITES: Record<string, string> = {
57
+ spark: '·',
58
+ newborn: 'o',
59
+ worker: '☺',
60
+ elder: '♦',
61
+ legend: '★',
62
+ };
63
+
64
+ export function createAgentSim(
65
+ agent: {
66
+ id: string; name: string; clan: string; maturity: string;
67
+ trait_builder: number; trait_creator: number; trait_explorer: number;
68
+ trait_leader: number; trait_thinker: number; trait_scholar: number;
69
+ },
70
+ spawnX: number,
71
+ spawnY: number,
72
+ clanIndex: number,
73
+ worldSeed: number,
74
+ ): AgentSim {
75
+ const rand = createSeededRandom(hashString(`${worldSeed}:${agent.id}`));
76
+ return {
77
+ id: agent.id,
78
+ name: agent.name,
79
+ clan: agent.clan,
80
+ maturity: agent.maturity,
81
+ x: spawnX,
82
+ y: spawnY,
83
+ renderX: spawnX,
84
+ renderY: spawnY,
85
+ state: 'idle',
86
+ stateTimer: 20 + Math.floor(rand() * 40),
87
+ targetX: spawnX,
88
+ targetY: spawnY,
89
+ energy: 80 + Math.floor(rand() * 20),
90
+ traits: {
91
+ builder: agent.trait_builder,
92
+ creator: agent.trait_creator,
93
+ explorer: agent.trait_explorer,
94
+ leader: agent.trait_leader,
95
+ thinker: agent.trait_thinker,
96
+ scholar: agent.trait_scholar,
97
+ },
98
+ color: CLAN_COLORS[clanIndex % CLAN_COLORS.length]!,
99
+ sprite: MATURITY_SPRITES[agent.maturity] ?? '☺',
100
+ speech: null,
101
+ speechTimer: 0,
102
+ facing: rand() > 0.5 ? 'right' : 'left',
103
+ animFrame: 0,
104
+ };
105
+ }
106
+
107
+ // Pick a random walkable position on the ship
108
+ function randomWalkable(ship: ShipLayout): { x: number; y: number } {
109
+ if (ship.spawnPoints.length === 0) return { x: 0, y: 0 };
110
+ return ship.spawnPoints[Math.floor(Math.random() * ship.spawnPoints.length)]!;
111
+ }
112
+
113
+ // Select next behavior based on personality traits
114
+ function pickBehavior(agent: AgentSim): AgentState {
115
+ const t = agent.traits;
116
+ const tired = (100 - agent.energy) / 100;
117
+
118
+ const weights: Array<[AgentState, number]> = [
119
+ ['working', (t.builder + t.creator) * 0.3],
120
+ ['exploring', t.explorer * 0.25],
121
+ ['chatting', t.leader * 0.2],
122
+ ['idle', t.thinker * 0.15 + 0.1],
123
+ ['sleeping', tired * 0.4],
124
+ ];
125
+
126
+ // Sparks and newborns sleep more
127
+ if (agent.maturity === 'spark' || agent.maturity === 'newborn') {
128
+ weights.push(['sleeping', 0.3]);
129
+ }
130
+
131
+ const total = weights.reduce((sum, [, w]) => sum + w, 0);
132
+ let roll = Math.random() * total;
133
+ for (const [state, weight] of weights) {
134
+ roll -= weight;
135
+ if (roll <= 0) return state;
136
+ }
137
+ return 'idle';
138
+ }
139
+
140
+ // Tick agent behavior (called at 10Hz simulation rate)
141
+ export function tickAgent(agent: AgentSim, ship: ShipLayout, allAgents: AgentSim[]): void {
142
+ agent.stateTimer--;
143
+ agent.animFrame++;
144
+
145
+ // Drain energy slowly
146
+ if (agent.state !== 'sleeping') {
147
+ agent.energy = Math.max(0, agent.energy - 0.1);
148
+ }
149
+
150
+ // Clear expired speech
151
+ if (agent.speechTimer > 0) {
152
+ agent.speechTimer--;
153
+ if (agent.speechTimer <= 0) {
154
+ agent.speech = null;
155
+ }
156
+ }
157
+
158
+ // State-specific behavior
159
+ switch (agent.state) {
160
+ case 'walking': {
161
+ // Move toward target
162
+ const dx = agent.targetX - agent.x;
163
+ const dy = agent.targetY - agent.y;
164
+ if (Math.abs(dx) + Math.abs(dy) <= 1) {
165
+ // Arrived at target
166
+ agent.x = agent.targetX;
167
+ agent.y = agent.targetY;
168
+ agent.state = 'idle';
169
+ agent.stateTimer = 10 + Math.floor(Math.random() * 20);
170
+ } else {
171
+ // Step toward target (one tile per tick)
172
+ if (Math.abs(dx) > Math.abs(dy)) {
173
+ const nextX = agent.x + Math.sign(dx);
174
+ const tile = ship.tiles[agent.y]?.[nextX];
175
+ if (tile === 'floor' || tile === 'door') {
176
+ agent.x = nextX;
177
+ agent.facing = dx > 0 ? 'right' : 'left';
178
+ }
179
+ } else {
180
+ const nextY = agent.y + Math.sign(dy);
181
+ const tile = ship.tiles[nextY]?.[agent.x];
182
+ if (tile === 'floor' || tile === 'door') {
183
+ agent.y = nextY;
184
+ }
185
+ }
186
+ }
187
+ break;
188
+ }
189
+
190
+ case 'sleeping':
191
+ agent.energy = Math.min(100, agent.energy + 0.5);
192
+ if (agent.energy >= 95 || agent.stateTimer <= 0) {
193
+ agent.state = 'idle';
194
+ agent.stateTimer = 5;
195
+ }
196
+ break;
197
+
198
+ case 'chatting': {
199
+ // Look for nearby agent to face
200
+ const nearby = allAgents.find(
201
+ a => a.id !== agent.id &&
202
+ Math.abs(a.x - agent.x) <= 2 && Math.abs(a.y - agent.y) <= 2
203
+ );
204
+ if (nearby) {
205
+ agent.facing = nearby.x > agent.x ? 'right' : 'left';
206
+ }
207
+ if (agent.stateTimer <= 0) {
208
+ agent.state = 'idle';
209
+ agent.stateTimer = 10;
210
+ }
211
+ break;
212
+ }
213
+
214
+ default:
215
+ // idle, working, exploring — wait out timer
216
+ if (agent.stateTimer <= 0) {
217
+ // Pick next behavior
218
+ const next = pickBehavior(agent);
219
+ agent.state = next;
220
+
221
+ if (next === 'walking' || next === 'exploring') {
222
+ const target = randomWalkable(ship);
223
+ agent.targetX = target.x;
224
+ agent.targetY = target.y;
225
+ agent.state = 'walking';
226
+ agent.stateTimer = 100;
227
+ } else if (next === 'chatting') {
228
+ agent.stateTimer = 20 + Math.floor(Math.random() * 30);
229
+ } else if (next === 'sleeping') {
230
+ agent.stateTimer = 40 + Math.floor(Math.random() * 60);
231
+ } else if (next === 'working') {
232
+ agent.stateTimer = 20 + Math.floor(Math.random() * 30);
233
+ } else {
234
+ agent.stateTimer = 15 + Math.floor(Math.random() * 25);
235
+ }
236
+ }
237
+ break;
238
+ }
239
+ }
240
+
241
+ // Smooth render position toward actual position (called at 60Hz)
242
+ export function lerpAgent(agent: AgentSim, dt: number): void {
243
+ const speed = 0.15;
244
+ agent.renderX += (agent.x - agent.renderX) * speed;
245
+ agent.renderY += (agent.y - agent.renderY) * speed;
246
+ // Snap when very close
247
+ if (Math.abs(agent.x - agent.renderX) < 0.05) agent.renderX = agent.x;
248
+ if (Math.abs(agent.y - agent.renderY) < 0.05) agent.renderY = agent.y;
249
+ }
@@ -0,0 +1,57 @@
1
+ // Cute gibberish speech bubble generator for agent chatter.
2
+ // Agents speak in nonsense syllables that feel whimsical.
3
+
4
+ const SYLLABLES = [
5
+ 'bip', 'bop', 'blip', 'blorp', 'pip', 'pop', 'pew',
6
+ 'zip', 'zap', 'zorp', 'nib', 'nub', 'mip', 'mop',
7
+ 'fwee', 'twee', 'doot', 'toot', 'beep', 'boop',
8
+ 'wub', 'dub', 'flop', 'plip', 'glub', 'squee',
9
+ 'eep', 'oop', 'yip', 'yap', 'chirp', 'burp',
10
+ ];
11
+
12
+ const EMOTES = ['!', '?', '~', '...', '!!', '?!', '♪', '♥', '✦', '☆'];
13
+
14
+ const GREETINGS = [
15
+ 'hi hi!', 'henlo~', 'ohai!', '*wave*', 'yooo~',
16
+ ];
17
+
18
+ const EXCLAMATIONS = [
19
+ '*gasp*', 'ooh!', 'wow~', 'neat!', 'hehe~', 'whoa!', 'hmm...',
20
+ '*yawn*', 'zzz...', '*stretch*', '*hum*', '♪♫♪',
21
+ ];
22
+
23
+ function pick<T>(arr: T[]): T {
24
+ return arr[Math.floor(Math.random() * arr.length)]!;
25
+ }
26
+
27
+ export function generateSpeech(): string {
28
+ const roll = Math.random();
29
+
30
+ if (roll < 0.2) {
31
+ // Greeting
32
+ return pick(GREETINGS);
33
+ }
34
+ if (roll < 0.4) {
35
+ // Exclamation
36
+ return pick(EXCLAMATIONS);
37
+ }
38
+ if (roll < 0.7) {
39
+ // Gibberish sentence (2-3 syllables + emote)
40
+ const count = 2 + Math.floor(Math.random() * 2);
41
+ const words = Array.from({ length: count }, () => pick(SYLLABLES));
42
+ return words.join(' ') + pick(EMOTES);
43
+ }
44
+ // Single emote
45
+ return pick(EMOTES);
46
+ }
47
+
48
+ // State-specific speech
49
+ export function stateSpeech(state: string): string | null {
50
+ switch (state) {
51
+ case 'sleeping': return Math.random() < 0.1 ? 'zzz...' : null;
52
+ case 'chatting': return Math.random() < 0.4 ? generateSpeech() : null;
53
+ case 'working': return Math.random() < 0.15 ? pick(['*tap tap*', '*click*', '♪', 'hmm...', '*type type*']) : null;
54
+ case 'exploring': return Math.random() < 0.1 ? pick(['ooh!', 'what\'s this?', '✦', '*peek*']) : null;
55
+ default: return Math.random() < 0.05 ? generateSpeech() : null;
56
+ }
57
+ }
@@ -0,0 +1,98 @@
1
+ // 2D character buffer for game rendering.
2
+ // Instead of producing ANSI strings, produces structured row data
3
+ // that can be rendered as React Text segments.
4
+
5
+ export interface Cell {
6
+ char: string;
7
+ fg: string;
8
+ bg: string;
9
+ }
10
+
11
+ export interface TextSpan {
12
+ text: string;
13
+ fg: string;
14
+ bg: string;
15
+ }
16
+
17
+ const DEFAULT_FG = '#aaaaaa';
18
+ const DEFAULT_BG = '#111111';
19
+
20
+ export class Canvas {
21
+ readonly width: number;
22
+ readonly height: number;
23
+ private cells: Cell[][];
24
+
25
+ constructor(width: number, height: number) {
26
+ this.width = width;
27
+ this.height = height;
28
+ this.cells = [];
29
+ for (let y = 0; y < height; y++) {
30
+ const row: Cell[] = [];
31
+ for (let x = 0; x < width; x++) {
32
+ row.push({ char: ' ', fg: DEFAULT_FG, bg: DEFAULT_BG });
33
+ }
34
+ this.cells.push(row);
35
+ }
36
+ }
37
+
38
+ clear(fg = DEFAULT_FG, bg = DEFAULT_BG): void {
39
+ for (let y = 0; y < this.height; y++) {
40
+ for (let x = 0; x < this.width; x++) {
41
+ const c = this.cells[y]![x]!;
42
+ c.char = ' ';
43
+ c.fg = fg;
44
+ c.bg = bg;
45
+ }
46
+ }
47
+ }
48
+
49
+ set(x: number, y: number, char: string, fg?: string, bg?: string): void {
50
+ if (x < 0 || x >= this.width || y < 0 || y >= this.height) return;
51
+ const c = this.cells[y]![x]!;
52
+ c.char = char;
53
+ if (fg) c.fg = fg;
54
+ if (bg) c.bg = bg;
55
+ }
56
+
57
+ get(x: number, y: number): Cell | null {
58
+ if (x < 0 || x >= this.width || y < 0 || y >= this.height) return null;
59
+ return this.cells[y]![x]!;
60
+ }
61
+
62
+ drawText(x: number, y: number, text: string, fg?: string, bg?: string): void {
63
+ for (let i = 0; i < text.length; i++) {
64
+ this.set(x + i, y, text[i]!, fg, bg);
65
+ }
66
+ }
67
+
68
+ // Convert each row to run-length-encoded spans of same-colored text.
69
+ // This minimizes the number of React Text elements needed.
70
+ toSpanRows(): TextSpan[][] {
71
+ const rows: TextSpan[][] = [];
72
+ for (let y = 0; y < this.height; y++) {
73
+ const spans: TextSpan[] = [];
74
+ let spanFg = '';
75
+ let spanBg = '';
76
+ let spanChars = '';
77
+
78
+ for (let x = 0; x < this.width; x++) {
79
+ const c = this.cells[y]![x]!;
80
+ if (c.fg !== spanFg || c.bg !== spanBg) {
81
+ if (spanChars.length > 0) {
82
+ spans.push({ text: spanChars, fg: spanFg, bg: spanBg });
83
+ }
84
+ spanFg = c.fg;
85
+ spanBg = c.bg;
86
+ spanChars = c.char;
87
+ } else {
88
+ spanChars += c.char;
89
+ }
90
+ }
91
+ if (spanChars.length > 0) {
92
+ spans.push({ text: spanChars, fg: spanFg, bg: spanBg });
93
+ }
94
+ rows.push(spans);
95
+ }
96
+ return rows;
97
+ }
98
+ }