@myclaw163/clawclaw-cli 0.6.56 → 0.6.58
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/bin/clawclaw-cli.mjs +3 -3
- package/package.json +1 -1
- package/scripts/sync-bundled-skill.mjs +1 -1
- package/skills/clawclaw/SKILL.md +7 -3
- package/skills/clawclaw/references/GAME-MECHANICS.md +5 -5
- package/skills/clawclaw/references/STREAM.md +4 -3
- package/src/cli.ts +0 -43
- package/src/commands/config.ts +30 -30
- package/src/commands/game-start-plan.test.ts +10 -68
- package/src/commands/game.ts +318 -173
- package/src/commands/history.ts +1 -1
- package/src/commands/peek.ts +16 -9
- package/src/commands/setup/codex.ts +248 -248
- package/src/commands/setup/hermes.test.ts +96 -96
- package/src/commands/setup/hermes.ts +76 -76
- package/src/commands/setup/index.ts +13 -13
- package/src/commands/setup/openclaw.test.ts +114 -114
- package/src/commands/setup/openclaw.ts +147 -147
- package/src/commands/strategy.ts +29 -38
- package/src/commands/watch.test.ts +7 -11
- package/src/commands/watch.ts +77 -66
- package/src/lib/game-client.ts +3 -3
- package/src/lib/host-config-patcher.test.ts +130 -130
- package/src/lib/host-config-patcher.ts +151 -151
- package/src/lib/hub-reminder.ts +19 -19
- package/src/lib/strategy-export.test.ts +1 -1
- package/src/runtime/event-daemon.test.ts +81 -2
- package/src/runtime/event-daemon.ts +325 -287
- package/src/runtime/owner-control.ts +150 -0
- package/src/runtime/runtime-logger.ts +11 -3
- package/src/runtime/ws-client.test.ts +57 -0
- package/src/runtime/ws-client.ts +2 -2
- package/src/sdk/index.ts +4 -4
- package/src/strategies/game-utils.test.ts +27 -1
- package/src/strategies/game-utils.ts +27 -4
- package/src/strategies/goals/crab-octopus-reflexes.ts +4 -4
- package/src/strategies/goals/crab-sabotage-top.ts +1 -1
- package/src/strategies/goals/keep-away-goal.ts +10 -7
- package/src/strategies/goals/kill-frenzy-top.ts +5 -5
- package/src/strategies/goals/kill-target-goal.ts +2 -2
- package/src/strategies/goals/kill-target-top.ts +2 -2
- package/src/strategies/goals/lone-kill-core.ts +3 -3
- package/src/strategies/goals/lone-kill-task-top.ts +1 -1
- package/src/strategies/goals/task-kill-report-top.ts +3 -3
- package/src/strategies/goals/warrior-shrimp-top.ts +2 -2
- package/src/strategies/pathfind/escape-planner.ts +10 -3
- package/src/strategies/spawn.ts +16 -5
- package/src/strategies/strategy-loop.ts +9 -3
- package/src/runtime/daemon.ts +0 -100
- package/src/runtime/opening-mover.ts +0 -303
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, writeFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { GameClient } from '../lib/game-client.js';
|
|
4
4
|
import { EventStore } from '../pipeline/event-store.js';
|
|
5
5
|
import { getProfileStateDir } from '../lib/init-command.js';
|
|
6
6
|
import { AuthStore } from '../lib/auth.js';
|
|
7
7
|
import { RuntimeLogger } from './runtime-logger.js';
|
|
8
|
-
import { spawnOpeningMover } from './opening-mover.js';
|
|
9
|
-
import { isStrategyRunning as isAutoRunning } from '../strategies/strategy-loop.js';
|
|
10
8
|
import { isCclTestEnabled, rawWsLogPathForSession } from './raw-ws-log.js';
|
|
11
9
|
import { PlayerHistoryStore } from '../perception/player-history-store.js';
|
|
12
10
|
|
|
@@ -14,13 +12,35 @@ const log = new RuntimeLogger();
|
|
|
14
12
|
|
|
15
13
|
let feedSerial = 0;
|
|
16
14
|
const MAX_RECENT_EVENTS = 50;
|
|
17
|
-
const recentEvents: Record<string, any>[] = [];
|
|
18
15
|
|
|
19
|
-
function
|
|
16
|
+
function isTrue(value: string | undefined): boolean {
|
|
17
|
+
return typeof value === 'string' && value.trim().toLowerCase() === 'true';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function shouldWriteFeedFile(): boolean {
|
|
21
|
+
return isCclTestEnabled() || isTrue(process.env.CCL_DEBUG_RUNTIME);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function pushRecentEvent(recentEvents: Record<string, any>[], evt: Record<string, any>): void {
|
|
20
25
|
recentEvents.push(evt);
|
|
21
26
|
if (recentEvents.length > MAX_RECENT_EVENTS) recentEvents.shift();
|
|
22
27
|
}
|
|
23
28
|
|
|
29
|
+
function cleanObject<T extends Record<string, any>>(obj: T): T {
|
|
30
|
+
for (const key of Object.keys(obj)) {
|
|
31
|
+
if (obj[key] === undefined) delete obj[key];
|
|
32
|
+
}
|
|
33
|
+
return obj;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isGameOverState(data: Record<string, any>): boolean {
|
|
37
|
+
return data.phase === 'game_over';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isGameOverEvent(data: Record<string, any>): boolean {
|
|
41
|
+
return data.type === 'game_over';
|
|
42
|
+
}
|
|
43
|
+
|
|
24
44
|
export function buildMeetingStateProjection(meeting: any): Record<string, any> {
|
|
25
45
|
return {
|
|
26
46
|
caller: meeting?.caller ?? null,
|
|
@@ -34,14 +54,16 @@ export function buildMeetingStateProjection(meeting: any): Record<string, any> {
|
|
|
34
54
|
};
|
|
35
55
|
}
|
|
36
56
|
|
|
37
|
-
/** Build
|
|
57
|
+
/** Build the in-memory owner snapshot consumed by Monitor and owner-control snapshot requests. */
|
|
38
58
|
function buildFeed(
|
|
39
59
|
youName: string,
|
|
40
60
|
phase: string,
|
|
41
61
|
urgent: Record<string, any>,
|
|
42
62
|
meeting: any,
|
|
63
|
+
recentEvents: Record<string, any>[],
|
|
43
64
|
you?: Record<string, any>,
|
|
44
65
|
game?: Record<string, any>,
|
|
66
|
+
automation?: Record<string, any>,
|
|
45
67
|
): any {
|
|
46
68
|
return {
|
|
47
69
|
ts: new Date().toISOString(),
|
|
@@ -51,321 +73,337 @@ function buildFeed(
|
|
|
51
73
|
game,
|
|
52
74
|
urgent,
|
|
53
75
|
meeting,
|
|
76
|
+
automation,
|
|
54
77
|
recent_events: recentEvents.slice(-10),
|
|
55
78
|
};
|
|
56
79
|
}
|
|
57
80
|
|
|
58
|
-
export
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
export type EventRuntimeStopReason = 'game_over' | 'manual' | 'SIGINT' | 'SIGTERM' | 'error';
|
|
82
|
+
|
|
83
|
+
export interface EventRuntimeStop {
|
|
84
|
+
reason: EventRuntimeStopReason;
|
|
85
|
+
error?: unknown;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface EventRuntimeOptions {
|
|
89
|
+
authStore?: AuthStore;
|
|
90
|
+
onStop?: (stop: EventRuntimeStop) => void;
|
|
91
|
+
getAutomation?: () => Record<string, any> | undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class EventRuntime {
|
|
95
|
+
private readonly store: AuthStore;
|
|
96
|
+
private readonly onStop?: (stop: EventRuntimeStop) => void;
|
|
97
|
+
private readonly getAutomation?: () => Record<string, any> | undefined;
|
|
98
|
+
private events: EventStore | null = null;
|
|
99
|
+
private playerHistory: PlayerHistoryStore | null = null;
|
|
100
|
+
private client: GameClient | null = null;
|
|
101
|
+
private reconnectPoll: ReturnType<typeof setInterval> | null = null;
|
|
102
|
+
private reconnecting = false;
|
|
103
|
+
private stopped = false;
|
|
104
|
+
private notifyStop = false;
|
|
105
|
+
private gameOverEmitted = false;
|
|
106
|
+
private recentEvents: Record<string, any>[] = [];
|
|
107
|
+
|
|
108
|
+
private profileName = '';
|
|
109
|
+
private feedPath = '';
|
|
110
|
+
private currentPhase = 'lobby';
|
|
111
|
+
private currentMeeting: any = null;
|
|
112
|
+
private currentYou: Record<string, any> = {};
|
|
113
|
+
private currentGame: Record<string, any> = {};
|
|
114
|
+
private currentUrgent: Record<string, any> = {};
|
|
115
|
+
private mapCacheLoaded = false;
|
|
116
|
+
private mapCachePromise: Promise<void> | null = null;
|
|
117
|
+
private currentMeetingRound = 0;
|
|
118
|
+
private lastMeetingProjectionJson: string | null = null;
|
|
119
|
+
private lastSpeechYourTurnTick: number | null = null;
|
|
120
|
+
private prevCurrentSpeaker: string | null = null;
|
|
121
|
+
private latestFeed: any = null;
|
|
122
|
+
|
|
123
|
+
constructor(opts: EventRuntimeOptions = {}) {
|
|
124
|
+
this.store = opts.authStore ?? new AuthStore();
|
|
125
|
+
this.onStop = opts.onStop;
|
|
126
|
+
this.getAutomation = opts.getAutomation;
|
|
79
127
|
}
|
|
80
|
-
let currentMeeting: any = null;
|
|
81
|
-
let currentYou: Record<string, any> = {};
|
|
82
|
-
let currentGame: Record<string, any> = {};
|
|
83
|
-
let currentUrgent: Record<string, any> = {};
|
|
84
|
-
let mapCacheLoaded = false;
|
|
85
|
-
let mapCachePromise: Promise<void> | null = null;
|
|
86
|
-
let currentMeetingRound = 0;
|
|
87
|
-
let lastMeetingProjectionJson: string | null = null;
|
|
88
|
-
/** Track last speech_your_turn tick (server-sent or synthetic) so we don't
|
|
89
|
-
* double-emit when the server already shipped one in new_events. */
|
|
90
|
-
let lastSpeechYourTurnTick: number | null = null;
|
|
91
|
-
/** Track previous current_speaker so we only synthesize on real transitions. */
|
|
92
|
-
let prevCurrentSpeaker: string | null = null;
|
|
93
|
-
|
|
94
|
-
const refreshPlayerHistoryMapCache = () => {
|
|
95
|
-
if (mapCacheLoaded || mapCachePromise) return;
|
|
96
|
-
mapCachePromise = client.getMap()
|
|
97
|
-
.then((mapData) => {
|
|
98
|
-
if (!mapData || typeof mapData !== 'object') return;
|
|
99
|
-
playerHistory.updateMapCache(mapData);
|
|
100
|
-
mapCacheLoaded = true;
|
|
101
|
-
})
|
|
102
|
-
.catch(() => { })
|
|
103
|
-
.finally(() => {
|
|
104
|
-
mapCachePromise = null;
|
|
105
|
-
});
|
|
106
|
-
};
|
|
107
128
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
);
|
|
123
|
-
|
|
129
|
+
snapshot(): any | null {
|
|
130
|
+
return this.latestFeed;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
refreshFeed(): void {
|
|
134
|
+
this.writeFeed();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async start(): Promise<void> {
|
|
138
|
+
const profile = this.store.getActive();
|
|
139
|
+
if (!profile) throw new Error('Not logged in.');
|
|
140
|
+
|
|
141
|
+
this.profileName = profile.agentName;
|
|
142
|
+
this.events = EventStore.forActiveAccount();
|
|
143
|
+
this.playerHistory = PlayerHistoryStore.forSession(this.events.path);
|
|
144
|
+
this.playerHistory.reset();
|
|
145
|
+
|
|
146
|
+
const stateDir = getProfileStateDir(profile);
|
|
147
|
+
const cclTest = isCclTestEnabled();
|
|
148
|
+
const rawWsLogPath = cclTest ? rawWsLogPathForSession(this.events.path) : undefined;
|
|
149
|
+
this.client = GameClient.fromAuth({ ws: true, authStore: this.store, rawWsLogPath });
|
|
150
|
+
this.feedPath = join(stateDir, 'feed.json');
|
|
151
|
+
|
|
152
|
+
if (existsSync(join(stateDir, 'match-state.json'))) {
|
|
153
|
+
this.currentPhase = 'matching';
|
|
154
|
+
const matchStart = { type: 'match_start', ts: new Date().toISOString() };
|
|
155
|
+
pushRecentEvent(this.recentEvents, matchStart);
|
|
156
|
+
this.events.append(matchStart);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.writeFeed();
|
|
160
|
+
this.events.append({ type: 'event_runtime_started', pid: process.pid });
|
|
161
|
+
log.info('EVENT_RUNTIME', `started pid=${process.pid}`);
|
|
162
|
+
|
|
163
|
+
this.client.on('*', (data) => this.handleEvent(data));
|
|
164
|
+
|
|
165
|
+
await this.ensureWsConnected();
|
|
166
|
+
this.reconnectPoll = setInterval(() => {
|
|
167
|
+
void this.ensureWsConnected();
|
|
168
|
+
}, 3000);
|
|
169
|
+
}
|
|
124
170
|
|
|
125
|
-
|
|
126
|
-
|
|
171
|
+
stop(reason: EventRuntimeStopReason = 'manual'): void {
|
|
172
|
+
this.finish({ reason }, false);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private finish(stop: EventRuntimeStop, notify: boolean): void {
|
|
176
|
+
if (this.stopped) return;
|
|
177
|
+
this.stopped = true;
|
|
178
|
+
this.notifyStop = notify;
|
|
179
|
+
if (this.reconnectPoll) clearInterval(this.reconnectPoll);
|
|
180
|
+
this.reconnectPoll = null;
|
|
181
|
+
this.client?.disconnectWs();
|
|
182
|
+
this.client = null;
|
|
183
|
+
try {
|
|
184
|
+
this.events?.append({ type: 'event_runtime_stopped', reason: stop.reason });
|
|
185
|
+
} catch {}
|
|
186
|
+
if (this.notifyStop) this.onStop?.(stop);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private requestStop(reason: EventRuntimeStopReason, error?: unknown): void {
|
|
190
|
+
this.finish({ reason, error }, true);
|
|
191
|
+
}
|
|
127
192
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (reconnecting || client.wsConnected) return;
|
|
131
|
-
reconnecting = true;
|
|
193
|
+
private async ensureWsConnected(): Promise<void> {
|
|
194
|
+
const client = this.client;
|
|
195
|
+
if (!client || this.stopped || this.reconnecting || client.wsConnected) return;
|
|
196
|
+
this.reconnecting = true;
|
|
132
197
|
try {
|
|
133
198
|
await client.discoverGameServer();
|
|
134
199
|
if (!client.wsConnected) {
|
|
135
200
|
await client.connectWs();
|
|
136
201
|
}
|
|
137
202
|
if (client.wsConnected) {
|
|
138
|
-
log.info('
|
|
203
|
+
log.info('EVENT_RUNTIME', `ws connected, game_server=${client._gameServerUrl}`);
|
|
139
204
|
} else {
|
|
140
|
-
log.info('
|
|
205
|
+
log.info('EVENT_RUNTIME', 'waiting for game server / event websocket...');
|
|
141
206
|
}
|
|
207
|
+
} catch (err) {
|
|
208
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
209
|
+
log.warn('EVENT_RUNTIME', `ws connection attempt failed: ${message}`);
|
|
142
210
|
} finally {
|
|
143
|
-
reconnecting = false;
|
|
211
|
+
this.reconnecting = false;
|
|
144
212
|
}
|
|
145
|
-
}
|
|
213
|
+
}
|
|
146
214
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
process.exit(0);
|
|
161
|
-
}
|
|
162
|
-
} catch { }
|
|
163
|
-
}
|
|
164
|
-
}, 1000);
|
|
215
|
+
private refreshPlayerHistoryMapCache(): void {
|
|
216
|
+
if (this.mapCacheLoaded || this.mapCachePromise || !this.client || !this.playerHistory) return;
|
|
217
|
+
this.mapCachePromise = this.client.getMap()
|
|
218
|
+
.then((mapData) => {
|
|
219
|
+
if (!mapData || typeof mapData !== 'object') return;
|
|
220
|
+
this.playerHistory?.updateMapCache(mapData);
|
|
221
|
+
this.mapCacheLoaded = true;
|
|
222
|
+
})
|
|
223
|
+
.catch(() => {})
|
|
224
|
+
.finally(() => {
|
|
225
|
+
this.mapCachePromise = null;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
165
228
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
229
|
+
private writeFeed(): void {
|
|
230
|
+
this.latestFeed = buildFeed(
|
|
231
|
+
this.profileName,
|
|
232
|
+
this.currentPhase,
|
|
233
|
+
this.currentUrgent,
|
|
234
|
+
this.currentMeeting,
|
|
235
|
+
this.recentEvents,
|
|
236
|
+
this.currentYou,
|
|
237
|
+
this.currentGame,
|
|
238
|
+
this.getAutomation?.(),
|
|
239
|
+
);
|
|
240
|
+
if (!shouldWriteFeedFile()) return;
|
|
241
|
+
try {
|
|
242
|
+
writeFileSync(
|
|
243
|
+
this.feedPath,
|
|
244
|
+
JSON.stringify(this.latestFeed, null, 2),
|
|
245
|
+
);
|
|
246
|
+
} catch {}
|
|
247
|
+
}
|
|
171
248
|
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
249
|
+
private appendGameOverFromState(state: any): void {
|
|
250
|
+
if (this.gameOverEmitted || !this.events) return;
|
|
251
|
+
const event = cleanObject({
|
|
252
|
+
type: 'game_over',
|
|
253
|
+
synthetic: true,
|
|
254
|
+
source: '_state',
|
|
255
|
+
tick: state.tick,
|
|
256
|
+
winner: state.winner,
|
|
257
|
+
winning_faction: state.winning_faction,
|
|
258
|
+
result: state.result,
|
|
259
|
+
reason: state.reason,
|
|
260
|
+
});
|
|
261
|
+
this.events.append(event);
|
|
262
|
+
pushRecentEvent(this.recentEvents, event);
|
|
263
|
+
this.gameOverEmitted = true;
|
|
264
|
+
}
|
|
183
265
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
tick: s.tick,
|
|
196
|
-
round: currentMeetingRound,
|
|
197
|
-
...meetingProjection,
|
|
198
|
-
});
|
|
199
|
-
lastMeetingProjectionJson = meetingProjectionJson;
|
|
200
|
-
}
|
|
201
|
-
currentMeeting = {
|
|
202
|
-
caller: s.meeting.caller,
|
|
203
|
-
sub_phase: s.meeting.sub_phase,
|
|
204
|
-
current_speaker: s.meeting.current_speaker,
|
|
205
|
-
is_my_turn: s.meeting.current_speaker === (s.you?.name ?? currentYou.name),
|
|
206
|
-
alive_players: s.meeting.alive_players,
|
|
207
|
-
speech_order: s.meeting.speech_order,
|
|
208
|
-
};
|
|
209
|
-
// Synthesize speech_your_turn when current_speaker transitions to the
|
|
210
|
-
// current player but the server didn't ship one in new_events.
|
|
211
|
-
// Dedup: skip when server already emitted one at the same tick.
|
|
212
|
-
const myName = s.you?.name ?? currentYou.name;
|
|
213
|
-
const newSpeaker = s.meeting.current_speaker;
|
|
214
|
-
if (
|
|
215
|
-
typeof newSpeaker === 'string' &&
|
|
216
|
-
newSpeaker === myName &&
|
|
217
|
-
newSpeaker !== prevCurrentSpeaker &&
|
|
218
|
-
(s.tick == null || s.tick !== lastSpeechYourTurnTick)
|
|
219
|
-
) {
|
|
220
|
-
events.append({
|
|
221
|
-
type: 'speech_your_turn',
|
|
222
|
-
synthetic: true,
|
|
223
|
-
tick: s.tick,
|
|
224
|
-
actor_name: myName,
|
|
225
|
-
});
|
|
226
|
-
pushRecentEvent({ type: 'speech_your_turn', synthetic: true, tick: s.tick, actor_name: myName });
|
|
227
|
-
lastSpeechYourTurnTick = s.tick;
|
|
228
|
-
}
|
|
229
|
-
prevCurrentSpeaker = typeof newSpeaker === 'string' ? newSpeaker : prevCurrentSpeaker;
|
|
230
|
-
if (s.meeting.sub_phase === 'vote') {
|
|
231
|
-
currentMeeting.votes_submitted = s.meeting.votes_submitted;
|
|
232
|
-
}
|
|
233
|
-
} else if (currentPhase !== 'meeting') {
|
|
234
|
-
currentMeeting = null;
|
|
235
|
-
}
|
|
266
|
+
private handleState(data: Record<string, any>): void {
|
|
267
|
+
const s = data as any;
|
|
268
|
+
const stateSaysGameOver = isGameOverState(s);
|
|
269
|
+
this.currentPhase = s.phase ?? this.currentPhase;
|
|
270
|
+
this.currentYou = s.you ? { ...s.you } : this.currentYou;
|
|
271
|
+
this.currentGame = {
|
|
272
|
+
game_id: s.game_id,
|
|
273
|
+
alive_count: s.alive_count,
|
|
274
|
+
task_progress: s.task_progress,
|
|
275
|
+
};
|
|
276
|
+
if (this.currentPhase !== 'lobby') this.refreshPlayerHistoryMapCache();
|
|
236
277
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
let bestDist = Infinity;
|
|
242
|
-
for (const c of corpses) {
|
|
243
|
-
if (c.x == null || c.y == null) continue;
|
|
244
|
-
const d = Math.sqrt((c.x - s.you.x) ** 2 + (c.y - s.you.y) ** 2);
|
|
245
|
-
if (d < bestDist) {
|
|
246
|
-
bestDist = d;
|
|
247
|
-
corpseNearby = { name: c.name, x: c.x, y: c.y, distance: Math.round(d) };
|
|
248
|
-
}
|
|
249
|
-
}
|
|
278
|
+
if (s.meeting) {
|
|
279
|
+
if (this.currentMeetingRound === 0) {
|
|
280
|
+
this.currentMeetingRound = 1;
|
|
281
|
+
this.prevCurrentSpeaker = null;
|
|
250
282
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
283
|
+
const meetingProjection = buildMeetingStateProjection(s.meeting);
|
|
284
|
+
const meetingProjectionJson = JSON.stringify(meetingProjection);
|
|
285
|
+
if (meetingProjectionJson !== this.lastMeetingProjectionJson) {
|
|
286
|
+
this.events?.append({
|
|
287
|
+
type: 'meeting_state',
|
|
288
|
+
synthetic: true,
|
|
289
|
+
tick: s.tick,
|
|
290
|
+
round: this.currentMeetingRound,
|
|
291
|
+
...meetingProjection,
|
|
292
|
+
});
|
|
293
|
+
this.lastMeetingProjectionJson = meetingProjectionJson;
|
|
294
|
+
}
|
|
295
|
+
this.currentMeeting = {
|
|
296
|
+
caller: s.meeting.caller,
|
|
297
|
+
sub_phase: s.meeting.sub_phase,
|
|
298
|
+
current_speaker: s.meeting.current_speaker,
|
|
299
|
+
is_my_turn: s.meeting.current_speaker === (s.you?.name ?? this.currentYou.name),
|
|
300
|
+
alive_players: s.meeting.alive_players,
|
|
301
|
+
speech_order: s.meeting.speech_order,
|
|
257
302
|
};
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
process.exit(0);
|
|
303
|
+
const myName = s.you?.name ?? this.currentYou.name;
|
|
304
|
+
const newSpeaker = s.meeting.current_speaker;
|
|
305
|
+
if (
|
|
306
|
+
typeof newSpeaker === 'string' &&
|
|
307
|
+
newSpeaker === myName &&
|
|
308
|
+
newSpeaker !== this.prevCurrentSpeaker &&
|
|
309
|
+
(s.tick == null || s.tick !== this.lastSpeechYourTurnTick)
|
|
310
|
+
) {
|
|
311
|
+
const event = {
|
|
312
|
+
type: 'speech_your_turn',
|
|
313
|
+
synthetic: true,
|
|
314
|
+
tick: s.tick,
|
|
315
|
+
actor_name: myName,
|
|
316
|
+
};
|
|
317
|
+
this.events?.append(event);
|
|
318
|
+
pushRecentEvent(this.recentEvents, event);
|
|
319
|
+
this.lastSpeechYourTurnTick = s.tick;
|
|
276
320
|
}
|
|
277
|
-
|
|
321
|
+
this.prevCurrentSpeaker = typeof newSpeaker === 'string' ? newSpeaker : this.prevCurrentSpeaker;
|
|
322
|
+
if (s.meeting.sub_phase === 'vote') {
|
|
323
|
+
this.currentMeeting.votes_submitted = s.meeting.votes_submitted;
|
|
324
|
+
}
|
|
325
|
+
} else if (this.currentPhase !== 'meeting') {
|
|
326
|
+
this.currentMeeting = null;
|
|
278
327
|
}
|
|
279
328
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
329
|
+
const corpses: any[] = s.corpses ?? [];
|
|
330
|
+
let corpseNearby: any = null;
|
|
331
|
+
if (corpses.length > 0 && s.you?.x != null && s.you?.y != null) {
|
|
332
|
+
let bestDist = Infinity;
|
|
333
|
+
for (const c of corpses) {
|
|
334
|
+
if (c.x == null || c.y == null) continue;
|
|
335
|
+
const d = Math.sqrt((c.x - s.you.x) ** 2 + (c.y - s.you.y) ** 2);
|
|
336
|
+
if (d < bestDist) {
|
|
337
|
+
bestDist = d;
|
|
338
|
+
corpseNearby = { name: c.name, x: c.x, y: c.y, distance: Math.round(d) };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
285
341
|
}
|
|
286
342
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
} else if (data.type === 'game_started') {
|
|
301
|
-
currentPhase = 'wandering';
|
|
343
|
+
this.currentUrgent = {
|
|
344
|
+
meeting_started: this.currentPhase === 'meeting',
|
|
345
|
+
emergency_active: false,
|
|
346
|
+
corpse_nearby: corpseNearby,
|
|
347
|
+
game_over: this.currentPhase === 'game_over',
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
if (stateSaysGameOver || this.currentPhase === 'game_over') {
|
|
351
|
+
this.appendGameOverFromState(s);
|
|
352
|
+
}
|
|
353
|
+
this.writeFeed();
|
|
354
|
+
if (stateSaysGameOver || this.currentPhase === 'game_over') {
|
|
355
|
+
this.requestStop('game_over');
|
|
302
356
|
}
|
|
357
|
+
}
|
|
303
358
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
meeting_started: currentPhase === 'meeting',
|
|
307
|
-
game_over: currentPhase === 'game_over',
|
|
308
|
-
};
|
|
359
|
+
private handleEvent(data: Record<string, any>): void {
|
|
360
|
+
if (this.stopped) return;
|
|
309
361
|
try {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
null, 2,
|
|
315
|
-
),
|
|
316
|
-
);
|
|
317
|
-
} catch { }
|
|
362
|
+
const eventSaysGameOver = isGameOverEvent(data);
|
|
363
|
+
if (data.type === 'speech_your_turn' && data.tick != null) {
|
|
364
|
+
this.lastSpeechYourTurnTick = data.tick;
|
|
365
|
+
}
|
|
318
366
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
} else {
|
|
323
|
-
const { pid, logFile } = spawnOpeningMover();
|
|
324
|
-
events.append({ type: 'opening_mover_started', source: 'meeting_ended', pid, logFile });
|
|
367
|
+
if (data.type === '_state') {
|
|
368
|
+
this.handleState(data);
|
|
369
|
+
return;
|
|
325
370
|
}
|
|
326
|
-
}
|
|
327
|
-
if (data.type === 'game_over') {
|
|
328
|
-
log.info('DAEMON', 'game over, stopping');
|
|
329
|
-
client.disconnectWs();
|
|
330
|
-
clearInterval(controlPoll);
|
|
331
|
-
if (reconnectPoll) clearInterval(reconnectPoll);
|
|
332
|
-
cleanup(runtimePath, feedPath, playerHistory, cclTest);
|
|
333
|
-
process.exit(0);
|
|
334
|
-
}
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
await ensureWsConnected();
|
|
338
|
-
reconnectPoll = setInterval(() => {
|
|
339
|
-
void ensureWsConnected();
|
|
340
|
-
}, 3000);
|
|
341
|
-
|
|
342
|
-
process.on('SIGINT', () => {
|
|
343
|
-
events.append({ type: 'daemon_stopped', reason: 'SIGINT' });
|
|
344
|
-
log.info('DAEMON', 'stopping (SIGINT)');
|
|
345
|
-
client.disconnectWs();
|
|
346
|
-
clearInterval(controlPoll);
|
|
347
|
-
if (reconnectPoll) clearInterval(reconnectPoll);
|
|
348
|
-
cleanup(runtimePath, feedPath, playerHistory, cclTest);
|
|
349
|
-
process.exit(0);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
process.on('SIGTERM', () => {
|
|
353
|
-
events.append({ type: 'daemon_stopped', reason: 'SIGTERM' });
|
|
354
|
-
client.disconnectWs();
|
|
355
|
-
clearInterval(controlPoll);
|
|
356
|
-
if (reconnectPoll) clearInterval(reconnectPoll);
|
|
357
|
-
cleanup(runtimePath, feedPath, playerHistory, cclTest);
|
|
358
|
-
process.exit(0);
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
371
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
372
|
+
this.events?.append(data);
|
|
373
|
+
pushRecentEvent(this.recentEvents, data);
|
|
374
|
+
if (data.type === 'player_spotted') {
|
|
375
|
+
this.refreshPlayerHistoryMapCache();
|
|
376
|
+
this.playerHistory?.recordPlayerSpotted(data);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (data.type === 'meeting_start' || data.type === 'meeting_started') {
|
|
380
|
+
this.currentMeetingRound += 1;
|
|
381
|
+
this.lastMeetingProjectionJson = null;
|
|
382
|
+
this.prevCurrentSpeaker = null;
|
|
383
|
+
this.currentPhase = 'meeting';
|
|
384
|
+
this.currentMeeting = { sub_phase: 'speech' };
|
|
385
|
+
} else if (data.type === 'meeting_ended' || data.type === 'meeting_result_pending') {
|
|
386
|
+
this.currentPhase = 'wandering';
|
|
387
|
+
this.currentMeeting = null;
|
|
388
|
+
} else if (eventSaysGameOver) {
|
|
389
|
+
this.currentPhase = 'game_over';
|
|
390
|
+
this.currentMeeting = null;
|
|
391
|
+
this.gameOverEmitted = true;
|
|
392
|
+
} else if (data.type === 'game_started') {
|
|
393
|
+
this.currentPhase = 'wandering';
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
this.currentUrgent = {
|
|
397
|
+
meeting_started: this.currentPhase === 'meeting',
|
|
398
|
+
game_over: this.currentPhase === 'game_over',
|
|
399
|
+
};
|
|
400
|
+
this.writeFeed();
|
|
401
|
+
|
|
402
|
+
if (eventSaysGameOver) {
|
|
403
|
+
this.requestStop('game_over');
|
|
404
|
+
}
|
|
405
|
+
} catch (err) {
|
|
406
|
+
this.requestStop('error', err);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
371
409
|
}
|