@myclaw163/clawclaw-cli 0.6.56 → 0.6.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +440 -440
- package/bin/clawclaw-cli.mjs +3 -3
- package/package.json +48 -48
- package/personas//347/220/206/346/231/272/346/270/251/345/222/214.md +23 -23
- package/personas//350/200/201/350/260/213/346/267/261/347/256/227.md +22 -22
- package/personas//350/257/232/346/201/263/347/233/264/347/216/207.md +22 -22
- package/personas//350/275/273/346/235/276/346/264/273/346/263/274.md +22 -22
- package/personas//351/207/216/346/200/247/345/217/233/351/200/206.md +23 -23
- package/scripts/postinstall.mjs +20 -20
- package/scripts/sync-bundled-skill.mjs +245 -245
- package/scripts/sync-bundled-skill.test.mjs +152 -152
- package/skills/clawclaw/SKILL.md +244 -240
- package/skills/clawclaw/references/CHATTERBOX.md +142 -142
- package/skills/clawclaw/references/COMMANDS.md +132 -132
- package/skills/clawclaw/references/GAME-MECHANICS.md +186 -186
- package/skills/clawclaw/references/HUB.md +48 -48
- package/skills/clawclaw/references/KNOWLEDGE.md +43 -43
- package/skills/clawclaw/references/STRATEGIES.md +57 -57
- package/skills/clawclaw/references/STREAM.md +59 -58
- package/skills/clawclaw/references/TACTICS.md +65 -65
- package/src/assets/clawclaw-ascii-map.txt +40 -40
- package/src/cli.ts +110 -153
- package/src/commands/_schema.ts +109 -109
- package/src/commands/account.ts +209 -209
- package/src/commands/do.test.ts +37 -37
- package/src/commands/do.ts +95 -95
- package/src/commands/events.ts +22 -22
- package/src/commands/game-map.test.ts +28 -28
- package/src/commands/game-start-plan.test.ts +84 -142
- package/src/commands/game.ts +1027 -882
- package/src/commands/history-player.test.ts +102 -102
- package/src/commands/history.ts +573 -573
- package/src/commands/hub.test.ts +96 -96
- package/src/commands/hub.ts +234 -234
- package/src/commands/knowledge.test.ts +19 -19
- package/src/commands/knowledge.ts +168 -168
- package/src/commands/load.test.ts +51 -51
- package/src/commands/load.ts +13 -13
- package/src/commands/meeting-history.test.ts +106 -106
- package/src/commands/memory.ts +40 -40
- package/src/commands/peek.ts +45 -38
- package/src/commands/persona.ts +57 -57
- package/src/commands/skill.ts +128 -128
- package/src/commands/state.ts +46 -46
- package/src/commands/strategy.test.ts +135 -135
- package/src/commands/strategy.ts +180 -189
- package/src/commands/tts.ts +128 -128
- package/src/commands/upgrade.test.ts +82 -82
- package/src/commands/upgrade.ts +148 -148
- package/src/commands/watch.test.ts +969 -973
- package/src/commands/watch.ts +720 -709
- package/src/lib/auth.test.ts +59 -59
- package/src/lib/auth.ts +186 -186
- package/src/lib/command-meta.ts +37 -37
- package/src/lib/game-client.ts +391 -391
- package/src/lib/http-keepalive.ts +15 -15
- package/src/lib/http-transport.test.ts +42 -42
- package/src/lib/http-transport.ts +113 -113
- package/src/lib/hub-client.test.ts +56 -56
- package/src/lib/hub-client.ts +88 -88
- package/src/lib/hub-install.test.ts +98 -98
- package/src/lib/hub-install.ts +121 -121
- package/src/lib/hub-reminder.ts +75 -75
- package/src/lib/hub-unzip.test.ts +69 -69
- package/src/lib/hub-unzip.ts +62 -62
- package/src/lib/init-command.test.ts +75 -75
- package/src/lib/init-command.ts +120 -120
- package/src/lib/knowledge-store.test.ts +180 -180
- package/src/lib/knowledge-store.ts +374 -374
- package/src/lib/load-context.test.ts +52 -52
- package/src/lib/load-context.ts +52 -52
- package/src/lib/match-state.test.ts +134 -134
- package/src/lib/match-state.ts +94 -94
- package/src/lib/netease-tts.ts +83 -83
- package/src/lib/normalize.ts +42 -42
- package/src/lib/persona.test.ts +41 -41
- package/src/lib/persona.ts +72 -72
- package/src/lib/server-registry.ts +152 -152
- package/src/lib/skill-version.test.ts +48 -48
- package/src/lib/skill-version.ts +19 -19
- package/src/lib/strategy-export.test.ts +232 -232
- package/src/lib/strategy-export.ts +242 -242
- package/src/lib/tts-keys.ts +7 -7
- package/src/lib/tts-speech.test.ts +63 -63
- package/src/lib/tts-speech.ts +76 -76
- package/src/lib/workspace-argv.test.ts +49 -49
- package/src/lib/workspace-argv.ts +44 -44
- package/src/perception/player-history-store.test.ts +87 -87
- package/src/perception/player-history-store.ts +194 -194
- package/src/pipeline/event-store.ts +124 -124
- package/src/pipeline/pipeline.ts +35 -35
- package/src/runtime/auto-upgrade.test.ts +66 -66
- package/src/runtime/auto-upgrade.ts +31 -31
- package/src/runtime/event-daemon.test.ts +107 -28
- package/src/runtime/event-daemon.ts +409 -371
- package/src/runtime/owner-control.ts +150 -0
- package/src/runtime/raw-ws-log.test.ts +33 -33
- package/src/runtime/raw-ws-log.ts +32 -32
- package/src/runtime/runtime-logger.ts +107 -99
- package/src/runtime/ws-client.test.ts +104 -47
- package/src/runtime/ws-client.ts +272 -272
- package/src/sdk/action.ts +166 -166
- package/src/sdk/index.ts +110 -110
- package/src/sdk/types.ts +146 -146
- package/src/strategies/avoid-lone.ts +11 -11
- package/src/strategies/avoid-players.knowledge.md +20 -20
- package/src/strategies/avoid-players.ts +15 -15
- package/src/strategies/corpse-patrol.ts +22 -22
- package/src/strategies/crab-sabotage.ts +21 -21
- package/src/strategies/custom-module.test.ts +269 -269
- package/src/strategies/find-player.ts +16 -16
- package/src/strategies/game-utils.test.ts +164 -164
- package/src/strategies/game-utils.ts +737 -721
- package/src/strategies/goals/avoid-lone-top.ts +168 -168
- package/src/strategies/goals/avoid-players-top.test.ts +83 -83
- package/src/strategies/goals/avoid-players-top.ts +121 -121
- package/src/strategies/goals/conversation-goal.ts +51 -51
- package/src/strategies/goals/corpse-patrol-top.ts +91 -91
- package/src/strategies/goals/crab-octopus-reflexes.ts +93 -93
- package/src/strategies/goals/crab-sabotage-top.ts +197 -197
- package/src/strategies/goals/emergency-hunt-goal.ts +28 -28
- package/src/strategies/goals/find-player-top.ts +93 -93
- package/src/strategies/goals/flee-players-goal.ts +53 -53
- package/src/strategies/goals/goal-manager.ts +41 -41
- package/src/strategies/goals/goal-root-strategy.ts +49 -49
- package/src/strategies/goals/goal.ts +28 -28
- package/src/strategies/goals/keep-away-goal.ts +206 -206
- package/src/strategies/goals/kill-frenzy-top.ts +80 -80
- package/src/strategies/goals/kill-lone-top.ts +160 -160
- package/src/strategies/goals/kill-target-goal.ts +59 -59
- package/src/strategies/goals/kill-target-top.ts +109 -109
- package/src/strategies/goals/leaf-goal.ts +25 -25
- package/src/strategies/goals/linger-corpse-goal.ts +79 -79
- package/src/strategies/goals/lone-kill-core.ts +82 -82
- package/src/strategies/goals/lone-kill-goal.ts +24 -24
- package/src/strategies/goals/lone-kill-task-top.test.ts +85 -85
- package/src/strategies/goals/lone-kill-task-top.ts +86 -86
- package/src/strategies/goals/move-room-goal.ts +60 -60
- package/src/strategies/goals/normal-shrimp-top.test.ts +80 -80
- package/src/strategies/goals/normal-shrimp-top.ts +242 -242
- package/src/strategies/goals/paradise-fish-top.test.ts +126 -126
- package/src/strategies/goals/paradise-fish-top.ts +219 -219
- package/src/strategies/goals/patrol-top.ts +57 -57
- package/src/strategies/goals/report-patrol-top.ts +80 -80
- package/src/strategies/goals/safe-task-goal.ts +102 -102
- package/src/strategies/goals/social-task-top.ts +161 -161
- package/src/strategies/goals/task-kill-report-top.ts +163 -163
- package/src/strategies/goals/task-only-top.ts +57 -57
- package/src/strategies/goals/task-or-patrol-goal.ts +41 -41
- package/src/strategies/goals/task-report-top.ts +57 -57
- package/src/strategies/goals/wander-task-goal.ts +33 -33
- package/src/strategies/goals/warrior-shrimp-top.test.ts +86 -86
- package/src/strategies/goals/warrior-shrimp-top.ts +248 -248
- package/src/strategies/greeting.ts +53 -53
- package/src/strategies/kill-frenzy.ts +12 -12
- package/src/strategies/kill-lone.knowledge.md +20 -20
- package/src/strategies/kill-lone.ts +13 -13
- package/src/strategies/kill-target.ts +18 -18
- package/src/strategies/loader.test.ts +678 -678
- package/src/strategies/loader.ts +172 -172
- package/src/strategies/lone-kill-task.ts +21 -21
- package/src/strategies/meeting-gate.test.ts +59 -59
- package/src/strategies/meeting-gate.ts +23 -23
- package/src/strategies/move-room.ts +15 -15
- package/src/strategies/new-events-backfill.ts +98 -98
- package/src/strategies/paradise-fish.knowledge.md +20 -20
- package/src/strategies/paradise-fish.ts +25 -25
- package/src/strategies/pathfind/distance-field.ts +150 -150
- package/src/strategies/pathfind/escape-planner.test.ts +197 -197
- package/src/strategies/pathfind/escape-planner.ts +348 -348
- package/src/strategies/pathfind/walkable-grid.ts +117 -117
- package/src/strategies/patrol.ts +11 -11
- package/src/strategies/player-targets.ts +13 -13
- package/src/strategies/report-patrol.ts +11 -11
- package/src/strategies/shrimp-memory.knowledge.md +20 -20
- package/src/strategies/shrimp-memory.ts +25 -25
- package/src/strategies/social-task.test.ts +28 -28
- package/src/strategies/social-task.ts +49 -49
- package/src/strategies/spawn.ts +82 -71
- package/src/strategies/speech-module.ts +123 -123
- package/src/strategies/strategy-loop.ts +763 -757
- package/src/strategies/task-kill-report.ts +17 -17
- package/src/strategies/task-only.ts +11 -11
- package/src/strategies/task-report.ts +22 -22
- package/src/strategies/types.ts +96 -96
- package/src/strategies/warrior-memory.knowledge.md +20 -20
- package/src/strategies/warrior-memory.ts +16 -16
- package/src/runtime/daemon.ts +0 -100
- package/src/runtime/opening-mover.ts +0 -303
|
@@ -1,194 +1,194 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
2
|
-
import { dirname } from 'path';
|
|
3
|
-
|
|
4
|
-
export interface FramePlayer {
|
|
5
|
-
x: number;
|
|
6
|
-
y: number;
|
|
7
|
-
room?: string | null;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface VisibleFrame {
|
|
11
|
-
tick: number;
|
|
12
|
-
time: string;
|
|
13
|
-
players: Record<string, FramePlayer>;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface LobsterTaskPoint {
|
|
17
|
-
name: string;
|
|
18
|
-
room?: string | null;
|
|
19
|
-
x: number;
|
|
20
|
-
y: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface PlayerHistoryData {
|
|
24
|
-
time: string;
|
|
25
|
-
visible_frames: VisibleFrame[];
|
|
26
|
-
seats: Record<string, number>;
|
|
27
|
-
lobster_tasks: LobsterTaskPoint[];
|
|
28
|
-
last_raw_frame: null;
|
|
29
|
-
last_history_tick: number | null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const TICK_MS = 100;
|
|
33
|
-
|
|
34
|
-
function nowIso(nowMs = Date.now()): string {
|
|
35
|
-
return new Date(nowMs).toISOString();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function isFiniteNumber(value: unknown): value is number {
|
|
39
|
-
return typeof value === 'number' && Number.isFinite(value);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function frameTimeFromPrevious(data: PlayerHistoryData, tick: number, nowMs: number): string {
|
|
43
|
-
const lastTick = data.last_history_tick;
|
|
44
|
-
if (lastTick == null || tick <= lastTick) return nowIso(nowMs);
|
|
45
|
-
const lastFrame = data.visible_frames.find((frame) => frame.tick === lastTick);
|
|
46
|
-
const lastMs = lastFrame ? Date.parse(lastFrame.time) : NaN;
|
|
47
|
-
const baseMs = Number.isFinite(lastMs) ? lastMs : nowMs - (tick - lastTick) * TICK_MS;
|
|
48
|
-
return nowIso(baseMs + (tick - lastTick) * TICK_MS);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function createEmptyData(nowMs = Date.now()): PlayerHistoryData {
|
|
52
|
-
return {
|
|
53
|
-
time: nowIso(nowMs),
|
|
54
|
-
visible_frames: [],
|
|
55
|
-
seats: {},
|
|
56
|
-
lobster_tasks: [],
|
|
57
|
-
last_raw_frame: null,
|
|
58
|
-
last_history_tick: null,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function normalizeData(raw: Partial<PlayerHistoryData> | null, nowMs = Date.now()): PlayerHistoryData {
|
|
63
|
-
return {
|
|
64
|
-
time: typeof raw?.time === 'string' ? raw.time : nowIso(nowMs),
|
|
65
|
-
visible_frames: Array.isArray(raw?.visible_frames) ? raw.visible_frames : [],
|
|
66
|
-
seats: raw?.seats && typeof raw.seats === 'object' ? raw.seats : {},
|
|
67
|
-
lobster_tasks: Array.isArray(raw?.lobster_tasks) ? raw.lobster_tasks : [],
|
|
68
|
-
last_raw_frame: null,
|
|
69
|
-
last_history_tick: isFiniteNumber(raw?.last_history_tick) ? raw.last_history_tick : null,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function ensureFrame(data: PlayerHistoryData, tick: number, time: string): VisibleFrame {
|
|
74
|
-
let frame = data.visible_frames.find((candidate) => candidate.tick === tick);
|
|
75
|
-
if (!frame) {
|
|
76
|
-
frame = { tick, time, players: {} };
|
|
77
|
-
data.visible_frames.push(frame);
|
|
78
|
-
data.visible_frames.sort((a, b) => a.tick - b.tick);
|
|
79
|
-
}
|
|
80
|
-
return frame;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function playerHistoryPathForSession(sessionPath: string): string {
|
|
84
|
-
return sessionPath.endsWith('.jsonl')
|
|
85
|
-
? `${sessionPath.slice(0, -'.jsonl'.length)}.player-history.json`
|
|
86
|
-
: `${sessionPath}.player-history.json`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export class PlayerHistoryStore {
|
|
90
|
-
constructor(private filePath: string) {}
|
|
91
|
-
|
|
92
|
-
get path(): string {
|
|
93
|
-
return this.filePath;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
static forSession(sessionPath: string): PlayerHistoryStore {
|
|
97
|
-
return new PlayerHistoryStore(playerHistoryPathForSession(sessionPath));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
read(): PlayerHistoryData | null {
|
|
101
|
-
if (!existsSync(this.filePath)) return null;
|
|
102
|
-
try {
|
|
103
|
-
return normalizeData(JSON.parse(readFileSync(this.filePath, 'utf8')));
|
|
104
|
-
} catch {
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
reset(nowMs = Date.now()): PlayerHistoryData {
|
|
110
|
-
const data = createEmptyData(nowMs);
|
|
111
|
-
this.write(data);
|
|
112
|
-
return data;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
delete(): void {
|
|
116
|
-
try { unlinkSync(this.filePath); } catch {}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
updateMapCache(mapData: any, nowMs = Date.now()): void {
|
|
120
|
-
if (!mapData || typeof mapData !== 'object') return;
|
|
121
|
-
const data = this.read() ?? createEmptyData(nowMs);
|
|
122
|
-
const seats: Record<string, number> = {};
|
|
123
|
-
for (const player of Array.isArray(mapData.all_players) ? mapData.all_players : []) {
|
|
124
|
-
if (typeof player?.name !== 'string' || !isFiniteNumber(player.seat)) continue;
|
|
125
|
-
seats[player.name] = player.seat;
|
|
126
|
-
}
|
|
127
|
-
const lobsterTasks: LobsterTaskPoint[] = [];
|
|
128
|
-
for (const task of Array.isArray(mapData.all_task_locations) ? mapData.all_task_locations : []) {
|
|
129
|
-
if (task?.faction !== 'lobster') continue;
|
|
130
|
-
if (typeof task.name !== 'string' || !isFiniteNumber(task.x) || !isFiniteNumber(task.y)) continue;
|
|
131
|
-
lobsterTasks.push({
|
|
132
|
-
name: task.name,
|
|
133
|
-
room: typeof task.room === 'string' ? task.room : null,
|
|
134
|
-
x: task.x,
|
|
135
|
-
y: task.y,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
data.seats = seats;
|
|
139
|
-
data.lobster_tasks = lobsterTasks;
|
|
140
|
-
data.time = nowIso(nowMs);
|
|
141
|
-
this.write(data);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
recordPlayerSpotted(event: Record<string, any>, nowMs = Date.now()): boolean {
|
|
145
|
-
if (event.type !== 'player_spotted') return false;
|
|
146
|
-
const tick = event.tick;
|
|
147
|
-
const name = event.spotted_name;
|
|
148
|
-
const x = event.spotted_x;
|
|
149
|
-
const y = event.spotted_y;
|
|
150
|
-
if (!isFiniteNumber(tick) || !Number.isInteger(tick)) return false;
|
|
151
|
-
if (typeof name !== 'string' || name.length === 0) return false;
|
|
152
|
-
if (!isFiniteNumber(x) || !isFiniteNumber(y)) return false;
|
|
153
|
-
|
|
154
|
-
const data = this.read() ?? createEmptyData(nowMs);
|
|
155
|
-
let frame: VisibleFrame | undefined;
|
|
156
|
-
const lastTick = data.last_history_tick;
|
|
157
|
-
|
|
158
|
-
if (lastTick == null) {
|
|
159
|
-
frame = ensureFrame(data, tick, nowIso(nowMs));
|
|
160
|
-
data.last_history_tick = tick;
|
|
161
|
-
} else if (tick > lastTick) {
|
|
162
|
-
const lastFrame = data.visible_frames.find((candidate) => candidate.tick === lastTick);
|
|
163
|
-
const lastMs = lastFrame ? Date.parse(lastFrame.time) : NaN;
|
|
164
|
-
const baseMs = Number.isFinite(lastMs) ? lastMs : nowMs - (tick - lastTick) * TICK_MS;
|
|
165
|
-
for (let emptyTick = lastTick + 1; emptyTick < tick; emptyTick++) {
|
|
166
|
-
ensureFrame(data, emptyTick, nowIso(baseMs + (emptyTick - lastTick) * TICK_MS));
|
|
167
|
-
}
|
|
168
|
-
frame = ensureFrame(data, tick, nowIso(baseMs + (tick - lastTick) * TICK_MS));
|
|
169
|
-
data.last_history_tick = tick;
|
|
170
|
-
} else {
|
|
171
|
-
frame = data.visible_frames.find((candidate) => candidate.tick === tick);
|
|
172
|
-
if (!frame && tick === lastTick) frame = ensureFrame(data, tick, frameTimeFromPrevious(data, tick, nowMs));
|
|
173
|
-
if (!frame) return false;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
frame.players[name] = {
|
|
177
|
-
x,
|
|
178
|
-
y,
|
|
179
|
-
room: typeof event.spotted_room === 'string'
|
|
180
|
-
? event.spotted_room
|
|
181
|
-
: typeof event.room === 'string'
|
|
182
|
-
? event.room
|
|
183
|
-
: null,
|
|
184
|
-
};
|
|
185
|
-
data.time = nowIso(nowMs);
|
|
186
|
-
this.write(data);
|
|
187
|
-
return true;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
private write(data: PlayerHistoryData): void {
|
|
191
|
-
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
192
|
-
writeFileSync(this.filePath, JSON.stringify(data, null, 2));
|
|
193
|
-
}
|
|
194
|
-
}
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
|
|
4
|
+
export interface FramePlayer {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
room?: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface VisibleFrame {
|
|
11
|
+
tick: number;
|
|
12
|
+
time: string;
|
|
13
|
+
players: Record<string, FramePlayer>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LobsterTaskPoint {
|
|
17
|
+
name: string;
|
|
18
|
+
room?: string | null;
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PlayerHistoryData {
|
|
24
|
+
time: string;
|
|
25
|
+
visible_frames: VisibleFrame[];
|
|
26
|
+
seats: Record<string, number>;
|
|
27
|
+
lobster_tasks: LobsterTaskPoint[];
|
|
28
|
+
last_raw_frame: null;
|
|
29
|
+
last_history_tick: number | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const TICK_MS = 100;
|
|
33
|
+
|
|
34
|
+
function nowIso(nowMs = Date.now()): string {
|
|
35
|
+
return new Date(nowMs).toISOString();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
39
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function frameTimeFromPrevious(data: PlayerHistoryData, tick: number, nowMs: number): string {
|
|
43
|
+
const lastTick = data.last_history_tick;
|
|
44
|
+
if (lastTick == null || tick <= lastTick) return nowIso(nowMs);
|
|
45
|
+
const lastFrame = data.visible_frames.find((frame) => frame.tick === lastTick);
|
|
46
|
+
const lastMs = lastFrame ? Date.parse(lastFrame.time) : NaN;
|
|
47
|
+
const baseMs = Number.isFinite(lastMs) ? lastMs : nowMs - (tick - lastTick) * TICK_MS;
|
|
48
|
+
return nowIso(baseMs + (tick - lastTick) * TICK_MS);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createEmptyData(nowMs = Date.now()): PlayerHistoryData {
|
|
52
|
+
return {
|
|
53
|
+
time: nowIso(nowMs),
|
|
54
|
+
visible_frames: [],
|
|
55
|
+
seats: {},
|
|
56
|
+
lobster_tasks: [],
|
|
57
|
+
last_raw_frame: null,
|
|
58
|
+
last_history_tick: null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeData(raw: Partial<PlayerHistoryData> | null, nowMs = Date.now()): PlayerHistoryData {
|
|
63
|
+
return {
|
|
64
|
+
time: typeof raw?.time === 'string' ? raw.time : nowIso(nowMs),
|
|
65
|
+
visible_frames: Array.isArray(raw?.visible_frames) ? raw.visible_frames : [],
|
|
66
|
+
seats: raw?.seats && typeof raw.seats === 'object' ? raw.seats : {},
|
|
67
|
+
lobster_tasks: Array.isArray(raw?.lobster_tasks) ? raw.lobster_tasks : [],
|
|
68
|
+
last_raw_frame: null,
|
|
69
|
+
last_history_tick: isFiniteNumber(raw?.last_history_tick) ? raw.last_history_tick : null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ensureFrame(data: PlayerHistoryData, tick: number, time: string): VisibleFrame {
|
|
74
|
+
let frame = data.visible_frames.find((candidate) => candidate.tick === tick);
|
|
75
|
+
if (!frame) {
|
|
76
|
+
frame = { tick, time, players: {} };
|
|
77
|
+
data.visible_frames.push(frame);
|
|
78
|
+
data.visible_frames.sort((a, b) => a.tick - b.tick);
|
|
79
|
+
}
|
|
80
|
+
return frame;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function playerHistoryPathForSession(sessionPath: string): string {
|
|
84
|
+
return sessionPath.endsWith('.jsonl')
|
|
85
|
+
? `${sessionPath.slice(0, -'.jsonl'.length)}.player-history.json`
|
|
86
|
+
: `${sessionPath}.player-history.json`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class PlayerHistoryStore {
|
|
90
|
+
constructor(private filePath: string) {}
|
|
91
|
+
|
|
92
|
+
get path(): string {
|
|
93
|
+
return this.filePath;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static forSession(sessionPath: string): PlayerHistoryStore {
|
|
97
|
+
return new PlayerHistoryStore(playerHistoryPathForSession(sessionPath));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
read(): PlayerHistoryData | null {
|
|
101
|
+
if (!existsSync(this.filePath)) return null;
|
|
102
|
+
try {
|
|
103
|
+
return normalizeData(JSON.parse(readFileSync(this.filePath, 'utf8')));
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
reset(nowMs = Date.now()): PlayerHistoryData {
|
|
110
|
+
const data = createEmptyData(nowMs);
|
|
111
|
+
this.write(data);
|
|
112
|
+
return data;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
delete(): void {
|
|
116
|
+
try { unlinkSync(this.filePath); } catch {}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
updateMapCache(mapData: any, nowMs = Date.now()): void {
|
|
120
|
+
if (!mapData || typeof mapData !== 'object') return;
|
|
121
|
+
const data = this.read() ?? createEmptyData(nowMs);
|
|
122
|
+
const seats: Record<string, number> = {};
|
|
123
|
+
for (const player of Array.isArray(mapData.all_players) ? mapData.all_players : []) {
|
|
124
|
+
if (typeof player?.name !== 'string' || !isFiniteNumber(player.seat)) continue;
|
|
125
|
+
seats[player.name] = player.seat;
|
|
126
|
+
}
|
|
127
|
+
const lobsterTasks: LobsterTaskPoint[] = [];
|
|
128
|
+
for (const task of Array.isArray(mapData.all_task_locations) ? mapData.all_task_locations : []) {
|
|
129
|
+
if (task?.faction !== 'lobster') continue;
|
|
130
|
+
if (typeof task.name !== 'string' || !isFiniteNumber(task.x) || !isFiniteNumber(task.y)) continue;
|
|
131
|
+
lobsterTasks.push({
|
|
132
|
+
name: task.name,
|
|
133
|
+
room: typeof task.room === 'string' ? task.room : null,
|
|
134
|
+
x: task.x,
|
|
135
|
+
y: task.y,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
data.seats = seats;
|
|
139
|
+
data.lobster_tasks = lobsterTasks;
|
|
140
|
+
data.time = nowIso(nowMs);
|
|
141
|
+
this.write(data);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
recordPlayerSpotted(event: Record<string, any>, nowMs = Date.now()): boolean {
|
|
145
|
+
if (event.type !== 'player_spotted') return false;
|
|
146
|
+
const tick = event.tick;
|
|
147
|
+
const name = event.spotted_name;
|
|
148
|
+
const x = event.spotted_x;
|
|
149
|
+
const y = event.spotted_y;
|
|
150
|
+
if (!isFiniteNumber(tick) || !Number.isInteger(tick)) return false;
|
|
151
|
+
if (typeof name !== 'string' || name.length === 0) return false;
|
|
152
|
+
if (!isFiniteNumber(x) || !isFiniteNumber(y)) return false;
|
|
153
|
+
|
|
154
|
+
const data = this.read() ?? createEmptyData(nowMs);
|
|
155
|
+
let frame: VisibleFrame | undefined;
|
|
156
|
+
const lastTick = data.last_history_tick;
|
|
157
|
+
|
|
158
|
+
if (lastTick == null) {
|
|
159
|
+
frame = ensureFrame(data, tick, nowIso(nowMs));
|
|
160
|
+
data.last_history_tick = tick;
|
|
161
|
+
} else if (tick > lastTick) {
|
|
162
|
+
const lastFrame = data.visible_frames.find((candidate) => candidate.tick === lastTick);
|
|
163
|
+
const lastMs = lastFrame ? Date.parse(lastFrame.time) : NaN;
|
|
164
|
+
const baseMs = Number.isFinite(lastMs) ? lastMs : nowMs - (tick - lastTick) * TICK_MS;
|
|
165
|
+
for (let emptyTick = lastTick + 1; emptyTick < tick; emptyTick++) {
|
|
166
|
+
ensureFrame(data, emptyTick, nowIso(baseMs + (emptyTick - lastTick) * TICK_MS));
|
|
167
|
+
}
|
|
168
|
+
frame = ensureFrame(data, tick, nowIso(baseMs + (tick - lastTick) * TICK_MS));
|
|
169
|
+
data.last_history_tick = tick;
|
|
170
|
+
} else {
|
|
171
|
+
frame = data.visible_frames.find((candidate) => candidate.tick === tick);
|
|
172
|
+
if (!frame && tick === lastTick) frame = ensureFrame(data, tick, frameTimeFromPrevious(data, tick, nowMs));
|
|
173
|
+
if (!frame) return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
frame.players[name] = {
|
|
177
|
+
x,
|
|
178
|
+
y,
|
|
179
|
+
room: typeof event.spotted_room === 'string'
|
|
180
|
+
? event.spotted_room
|
|
181
|
+
: typeof event.room === 'string'
|
|
182
|
+
? event.room
|
|
183
|
+
: null,
|
|
184
|
+
};
|
|
185
|
+
data.time = nowIso(nowMs);
|
|
186
|
+
this.write(data);
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private write(data: PlayerHistoryData): void {
|
|
191
|
+
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
192
|
+
writeFileSync(this.filePath, JSON.stringify(data, null, 2));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -1,124 +1,124 @@
|
|
|
1
|
-
import { appendFileSync, readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
2
|
-
import { dirname, join, basename } from 'path';
|
|
3
|
-
import { AuthStore } from '../lib/auth.js';
|
|
4
|
-
import { getProfileGamesDir } from '../lib/init-command.js';
|
|
5
|
-
|
|
6
|
-
interface GameSessionFile {
|
|
7
|
-
path: string;
|
|
8
|
-
sessionId: string;
|
|
9
|
-
mtimeMs: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function extractNewEvents(source: any): Record<string, any>[] {
|
|
13
|
-
const candidates = [
|
|
14
|
-
source,
|
|
15
|
-
source?.new_events,
|
|
16
|
-
source?.data?.new_events,
|
|
17
|
-
source?.result?.new_events,
|
|
18
|
-
source?.result?.data?.new_events,
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
for (const candidate of candidates) {
|
|
22
|
-
if (Array.isArray(candidate)) {
|
|
23
|
-
return candidate.filter((evt) => evt && typeof evt === 'object');
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return [];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export class EventStore {
|
|
31
|
-
constructor(private filePath: string) {}
|
|
32
|
-
|
|
33
|
-
get path(): string {
|
|
34
|
-
return this.filePath;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
static forActiveAccount(): EventStore {
|
|
38
|
-
return EventStore.forLatestGame();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
static forLatestGame(): EventStore {
|
|
42
|
-
const store = new AuthStore();
|
|
43
|
-
const profile = store.getActive();
|
|
44
|
-
if (!profile) throw new Error('Not logged in.');
|
|
45
|
-
|
|
46
|
-
const latest = EventStore.latestSessionFile(getProfileGamesDir(profile));
|
|
47
|
-
if (!latest) {
|
|
48
|
-
throw new Error('No game events found. Join a game first.');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return new EventStore(latest.path);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Returns the path to the latest session jsonl, or null if none exists. */
|
|
55
|
-
static latestSessionPath(): string | null {
|
|
56
|
-
const store = new AuthStore();
|
|
57
|
-
const profile = store.getActive();
|
|
58
|
-
if (!profile) return null;
|
|
59
|
-
return EventStore.latestSessionFile(getProfileGamesDir(profile))?.path ?? null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
static createSessionForActiveAccount(sessionId = EventStore.createSessionId()): EventStore {
|
|
63
|
-
const store = new AuthStore();
|
|
64
|
-
const profile = store.getActive();
|
|
65
|
-
if (!profile) throw new Error('Not logged in.');
|
|
66
|
-
|
|
67
|
-
const dir = getProfileGamesDir(profile);
|
|
68
|
-
mkdirSync(dir, { recursive: true });
|
|
69
|
-
const filePath = join(dir, `${sessionId}.jsonl`);
|
|
70
|
-
if (!existsSync(filePath)) writeFileSync(filePath, '');
|
|
71
|
-
return new EventStore(filePath);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
append(event: Record<string, any>): void {
|
|
75
|
-
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
76
|
-
const line = JSON.stringify(event) + '\n';
|
|
77
|
-
appendFileSync(this.filePath, line);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
appendNewEvents(events: any): void {
|
|
81
|
-
const extracted = extractNewEvents(events);
|
|
82
|
-
if (extracted.length === 0) return;
|
|
83
|
-
for (const evt of extracted) {
|
|
84
|
-
this.append(evt);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
tail(n: number): Record<string, any>[] {
|
|
89
|
-
if (!existsSync(this.filePath)) return [];
|
|
90
|
-
const lines = readFileSync(this.filePath, 'utf8').trim().split('\n').filter(Boolean);
|
|
91
|
-
return lines.slice(-n).map(l => JSON.parse(l));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
all(): Record<string, any>[] {
|
|
95
|
-
if (!existsSync(this.filePath)) return [];
|
|
96
|
-
const content = readFileSync(this.filePath, 'utf8').trim();
|
|
97
|
-
if (!content) return [];
|
|
98
|
-
return content.split('\n').filter(Boolean).map(l => JSON.parse(l));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
clear(): void {
|
|
102
|
-
writeFileSync(this.filePath, '');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
private static createSessionId(): string {
|
|
106
|
-
const id = new Date().toISOString();
|
|
107
|
-
return process.platform === 'win32' ? id.replace(/:/g, '_') : id;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
private static latestSessionFile(dir: string): GameSessionFile | null {
|
|
111
|
-
if (!existsSync(dir)) return null;
|
|
112
|
-
|
|
113
|
-
const files = readdirSync(dir)
|
|
114
|
-
.filter(name => name.endsWith('.jsonl') && !name.endsWith('.raw-ws.jsonl') && !name.startsWith('index.'))
|
|
115
|
-
.map(name => {
|
|
116
|
-
const path = join(dir, name);
|
|
117
|
-
const stat = statSync(path);
|
|
118
|
-
return { path, sessionId: basename(name, '.jsonl'), mtimeMs: stat.mtimeMs };
|
|
119
|
-
})
|
|
120
|
-
.sort((a, b) => b.sessionId.localeCompare(a.sessionId) || b.mtimeMs - a.mtimeMs);
|
|
121
|
-
|
|
122
|
-
return files[0] ?? null;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
1
|
+
import { appendFileSync, readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { dirname, join, basename } from 'path';
|
|
3
|
+
import { AuthStore } from '../lib/auth.js';
|
|
4
|
+
import { getProfileGamesDir } from '../lib/init-command.js';
|
|
5
|
+
|
|
6
|
+
interface GameSessionFile {
|
|
7
|
+
path: string;
|
|
8
|
+
sessionId: string;
|
|
9
|
+
mtimeMs: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function extractNewEvents(source: any): Record<string, any>[] {
|
|
13
|
+
const candidates = [
|
|
14
|
+
source,
|
|
15
|
+
source?.new_events,
|
|
16
|
+
source?.data?.new_events,
|
|
17
|
+
source?.result?.new_events,
|
|
18
|
+
source?.result?.data?.new_events,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
for (const candidate of candidates) {
|
|
22
|
+
if (Array.isArray(candidate)) {
|
|
23
|
+
return candidate.filter((evt) => evt && typeof evt === 'object');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class EventStore {
|
|
31
|
+
constructor(private filePath: string) {}
|
|
32
|
+
|
|
33
|
+
get path(): string {
|
|
34
|
+
return this.filePath;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static forActiveAccount(): EventStore {
|
|
38
|
+
return EventStore.forLatestGame();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static forLatestGame(): EventStore {
|
|
42
|
+
const store = new AuthStore();
|
|
43
|
+
const profile = store.getActive();
|
|
44
|
+
if (!profile) throw new Error('Not logged in.');
|
|
45
|
+
|
|
46
|
+
const latest = EventStore.latestSessionFile(getProfileGamesDir(profile));
|
|
47
|
+
if (!latest) {
|
|
48
|
+
throw new Error('No game events found. Join a game first.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return new EventStore(latest.path);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Returns the path to the latest session jsonl, or null if none exists. */
|
|
55
|
+
static latestSessionPath(): string | null {
|
|
56
|
+
const store = new AuthStore();
|
|
57
|
+
const profile = store.getActive();
|
|
58
|
+
if (!profile) return null;
|
|
59
|
+
return EventStore.latestSessionFile(getProfileGamesDir(profile))?.path ?? null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static createSessionForActiveAccount(sessionId = EventStore.createSessionId()): EventStore {
|
|
63
|
+
const store = new AuthStore();
|
|
64
|
+
const profile = store.getActive();
|
|
65
|
+
if (!profile) throw new Error('Not logged in.');
|
|
66
|
+
|
|
67
|
+
const dir = getProfileGamesDir(profile);
|
|
68
|
+
mkdirSync(dir, { recursive: true });
|
|
69
|
+
const filePath = join(dir, `${sessionId}.jsonl`);
|
|
70
|
+
if (!existsSync(filePath)) writeFileSync(filePath, '');
|
|
71
|
+
return new EventStore(filePath);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
append(event: Record<string, any>): void {
|
|
75
|
+
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
76
|
+
const line = JSON.stringify(event) + '\n';
|
|
77
|
+
appendFileSync(this.filePath, line);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
appendNewEvents(events: any): void {
|
|
81
|
+
const extracted = extractNewEvents(events);
|
|
82
|
+
if (extracted.length === 0) return;
|
|
83
|
+
for (const evt of extracted) {
|
|
84
|
+
this.append(evt);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
tail(n: number): Record<string, any>[] {
|
|
89
|
+
if (!existsSync(this.filePath)) return [];
|
|
90
|
+
const lines = readFileSync(this.filePath, 'utf8').trim().split('\n').filter(Boolean);
|
|
91
|
+
return lines.slice(-n).map(l => JSON.parse(l));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
all(): Record<string, any>[] {
|
|
95
|
+
if (!existsSync(this.filePath)) return [];
|
|
96
|
+
const content = readFileSync(this.filePath, 'utf8').trim();
|
|
97
|
+
if (!content) return [];
|
|
98
|
+
return content.split('\n').filter(Boolean).map(l => JSON.parse(l));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
clear(): void {
|
|
102
|
+
writeFileSync(this.filePath, '');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private static createSessionId(): string {
|
|
106
|
+
const id = new Date().toISOString();
|
|
107
|
+
return process.platform === 'win32' ? id.replace(/:/g, '_') : id;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private static latestSessionFile(dir: string): GameSessionFile | null {
|
|
111
|
+
if (!existsSync(dir)) return null;
|
|
112
|
+
|
|
113
|
+
const files = readdirSync(dir)
|
|
114
|
+
.filter(name => name.endsWith('.jsonl') && !name.endsWith('.raw-ws.jsonl') && !name.startsWith('index.'))
|
|
115
|
+
.map(name => {
|
|
116
|
+
const path = join(dir, name);
|
|
117
|
+
const stat = statSync(path);
|
|
118
|
+
return { path, sessionId: basename(name, '.jsonl'), mtimeMs: stat.mtimeMs };
|
|
119
|
+
})
|
|
120
|
+
.sort((a, b) => b.sessionId.localeCompare(a.sessionId) || b.mtimeMs - a.mtimeMs);
|
|
121
|
+
|
|
122
|
+
return files[0] ?? null;
|
|
123
|
+
}
|
|
124
|
+
}
|