@myclaw163/clawclaw-cli 0.6.68 → 0.6.70
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/package.json +1 -1
- package/skills/clawclaw/references/KNOWLEDGE.md +1 -1
- package/skills/clawclaw/references/STRATEGIES.md +5 -3
- package/src/commands/game.ts +15 -0
- package/src/commands/strategy.test.ts +10 -0
- package/src/commands/strategy.ts +11 -10
- package/src/commands/watch.test.ts +11 -0
- package/src/commands/watch.ts +2 -3
- package/src/lib/auth.test.ts +15 -0
- package/src/pipeline/event-format.test.ts +82 -2
- package/src/pipeline/event-format.ts +114 -5
- package/src/pipeline/event-hints.ts +20 -3
- package/src/runtime/event-daemon.test.ts +34 -0
- package/src/runtime/event-daemon.ts +51 -3
- package/src/sdk/index.ts +1 -1
- package/src/strategies/avoid-lone.ts +1 -0
- package/src/strategies/avoid-players.ts +1 -0
- package/src/strategies/corpse-patrol.ts +1 -0
- package/src/strategies/crab-sabotage.ts +1 -0
- package/src/strategies/custom-module.test.ts +1 -0
- package/src/strategies/find-player.ts +1 -0
- package/src/strategies/game-utils.test.ts +53 -1
- package/src/strategies/game-utils.ts +69 -17
- package/src/strategies/goals/crab-octopus-reflexes.ts +11 -3
- package/src/strategies/goals/keep-away-goal.ts +9 -5
- package/src/strategies/goals/lone-kill-task-top.ts +25 -9
- package/src/strategies/goals/warrior-shrimp-top.ts +13 -306
- package/src/strategies/hide-spots.ts +11 -75
- package/src/strategies/hide.ts +1 -0
- package/src/strategies/kill-frenzy.ts +1 -0
- package/src/strategies/kill-lone.ts +1 -0
- package/src/strategies/kill-target.ts +1 -0
- package/src/strategies/loader.ts +9 -2
- package/src/strategies/lone-kill-task.ts +1 -0
- package/src/strategies/move-room.ts +1 -0
- package/src/strategies/off-route-points.ts +105 -0
- package/src/strategies/paradise-fish.ts +1 -0
- package/src/strategies/patrol.ts +1 -0
- package/src/strategies/report-patrol.ts +1 -0
- package/src/strategies/shrimp-memory.ts +1 -0
- package/src/strategies/social-task.ts +1 -0
- package/src/strategies/task-kill-report.ts +1 -0
- package/src/strategies/task-only.ts +1 -0
- package/src/strategies/task-report.ts +1 -0
- package/src/strategies/types.ts +7 -0
- package/src/strategies/warrior-memory.knowledge.md +2 -2
- package/src/strategies/warrior-memory.ts +2 -1
|
@@ -33,6 +33,26 @@ function cleanObject<T extends Record<string, any>>(obj: T): T {
|
|
|
33
33
|
return obj;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
37
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function playersFromMap(mapData: any): Array<{ name: string; seat: number }> {
|
|
41
|
+
if (!mapData || typeof mapData !== 'object' || !Array.isArray(mapData.all_players)) return [];
|
|
42
|
+
const players: Array<{ name: string; seat: number }> = [];
|
|
43
|
+
for (const player of mapData.all_players) {
|
|
44
|
+
if (typeof player?.name !== 'string' || !isFiniteNumber(player.seat)) continue;
|
|
45
|
+
players.push({ name: player.name, seat: player.seat });
|
|
46
|
+
}
|
|
47
|
+
return players;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function seatMapFromPlayers(players: Array<{ name: string; seat: number }>): Record<string, number> {
|
|
51
|
+
const seats: Record<string, number> = {};
|
|
52
|
+
for (const player of players) seats[player.name] = player.seat;
|
|
53
|
+
return seats;
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
function isGameOverState(data: Record<string, any>): boolean {
|
|
37
57
|
return data.phase === 'game_over';
|
|
38
58
|
}
|
|
@@ -112,6 +132,8 @@ export class EventRuntime {
|
|
|
112
132
|
private currentYou: Record<string, any> = {};
|
|
113
133
|
private currentGame: Record<string, any> = {};
|
|
114
134
|
private currentUrgent: Record<string, any> = {};
|
|
135
|
+
private currentAllPlayers: Array<{ name: string; seat: number }> = [];
|
|
136
|
+
private currentSeatMap: Record<string, number> = {};
|
|
115
137
|
private mapCacheLoaded = false;
|
|
116
138
|
private mapCachePromise: Promise<void> | null = null;
|
|
117
139
|
private currentMeetingRound = 0;
|
|
@@ -217,8 +239,24 @@ export class EventRuntime {
|
|
|
217
239
|
this.mapCachePromise = this.client.getMap()
|
|
218
240
|
.then((mapData) => {
|
|
219
241
|
if (!mapData || typeof mapData !== 'object') return;
|
|
242
|
+
const players = playersFromMap(mapData);
|
|
243
|
+
if (players.length > 0) {
|
|
244
|
+
this.currentAllPlayers = players;
|
|
245
|
+
this.currentSeatMap = seatMapFromPlayers(players);
|
|
246
|
+
this.currentGame = cleanObject({
|
|
247
|
+
...this.currentGame,
|
|
248
|
+
all_players: this.currentAllPlayers,
|
|
249
|
+
all_seats: this.currentSeatMap,
|
|
250
|
+
});
|
|
251
|
+
const myName = this.currentYou.name ?? this.profileName;
|
|
252
|
+
const mySeat = typeof myName === 'string' ? this.currentSeatMap[myName] : undefined;
|
|
253
|
+
if (mySeat !== undefined) {
|
|
254
|
+
this.currentYou = { ...this.currentYou, seat: this.currentYou.seat ?? mySeat };
|
|
255
|
+
}
|
|
256
|
+
this.writeFeed();
|
|
257
|
+
}
|
|
220
258
|
this.playerHistory?.updateMapCache(mapData);
|
|
221
|
-
this.mapCacheLoaded = true;
|
|
259
|
+
if (players.length > 0) this.mapCacheLoaded = true;
|
|
222
260
|
})
|
|
223
261
|
.catch(() => {})
|
|
224
262
|
.finally(() => {
|
|
@@ -268,11 +306,18 @@ export class EventRuntime {
|
|
|
268
306
|
const stateSaysGameOver = isGameOverState(s);
|
|
269
307
|
this.currentPhase = s.phase ?? this.currentPhase;
|
|
270
308
|
this.currentYou = s.you ? { ...s.you } : this.currentYou;
|
|
271
|
-
this.
|
|
309
|
+
const myName = this.currentYou.name ?? this.profileName;
|
|
310
|
+
const mySeat = typeof myName === 'string' ? this.currentSeatMap[myName] : undefined;
|
|
311
|
+
if (this.currentYou.seat === undefined && mySeat !== undefined) {
|
|
312
|
+
this.currentYou = { ...this.currentYou, seat: mySeat };
|
|
313
|
+
}
|
|
314
|
+
this.currentGame = cleanObject({
|
|
272
315
|
game_id: s.game_id,
|
|
273
316
|
alive_count: s.alive_count,
|
|
274
317
|
task_progress: s.task_progress,
|
|
275
|
-
|
|
318
|
+
all_players: this.currentAllPlayers.length > 0 ? this.currentAllPlayers : undefined,
|
|
319
|
+
all_seats: Object.keys(this.currentSeatMap).length > 0 ? this.currentSeatMap : undefined,
|
|
320
|
+
});
|
|
276
321
|
if (this.currentPhase !== 'lobby') this.refreshPlayerHistoryMapCache();
|
|
277
322
|
|
|
278
323
|
if (s.meeting) {
|
|
@@ -375,6 +420,9 @@ export class EventRuntime {
|
|
|
375
420
|
this.refreshPlayerHistoryMapCache();
|
|
376
421
|
this.playerHistory?.recordPlayerSpotted(data);
|
|
377
422
|
}
|
|
423
|
+
if (data.type === 'role_assigned' || data.type === 'game_started' || data.type === 'crab_teammates') {
|
|
424
|
+
this.refreshPlayerHistoryMapCache();
|
|
425
|
+
}
|
|
378
426
|
|
|
379
427
|
if (data.type === 'meeting_start' || data.type === 'meeting_started') {
|
|
380
428
|
this.currentMeetingRound += 1;
|
package/src/sdk/index.ts
CHANGED
|
@@ -52,7 +52,7 @@ export type {
|
|
|
52
52
|
// Game utilities (for user strategies)
|
|
53
53
|
export {
|
|
54
54
|
dist, firstAvailableTask, nearestKnownCorpse, nearestKnownCorpseNav, hasKnownCorpse, corpseAtScene, nearestReportableCorpse,
|
|
55
|
-
emergencyRushDecision, EMERGENCY_RUSH_ALIVE_THRESHOLD,
|
|
55
|
+
emergencyRushDecision, activeEmergencyRushTask, killYieldsToEmergencyRepair, EMERGENCY_RUSH_ALIVE_THRESHOLD,
|
|
56
56
|
safePatrolStep,
|
|
57
57
|
nonTeammatesVisible, matchesTarget, isTargetAlive,
|
|
58
58
|
nearestVisibleTarget, pursueVisibleTarget,
|
|
@@ -4,6 +4,7 @@ import { AvoidLoneTop } from './goals/avoid-lone-top.js';
|
|
|
4
4
|
|
|
5
5
|
export const strategy: StrategyEntry = {
|
|
6
6
|
id: 'avoid-lone',
|
|
7
|
+
name: '结伴',
|
|
7
8
|
description: '避免落单。视野里只有一个人时寻路躲避主动避开(推演逃点、持续远离);视野里有两人以上时跟住次近的玩家、把可见人数维持在 ≥2(最近的人本就贴着不会丢,次近的人才是跌回一对一的临界);持续无人或没有当前目标时巡逻。',
|
|
8
9
|
create() {
|
|
9
10
|
return new GoalRootStrategy('avoid-lone', () => new AvoidLoneTop(), { resetOnMeetingResume: false });
|
|
@@ -5,6 +5,7 @@ import { parseTargetArgs } from './player-targets.js';
|
|
|
5
5
|
|
|
6
6
|
export const strategy: StrategyEntry = {
|
|
7
7
|
id: 'avoid-players',
|
|
8
|
+
name: '避人',
|
|
8
9
|
description: '回避玩家:按房间顺序巡逻,视野里出现回避目标就寻路躲避(推演逃点、持续远离),威胁消失后恢复巡逻。回避名单 = 启动参数(座位号或名字,可多人,可省略)∪ 知识库中标记为 hostile(坏人)的玩家;trusted 与未标记的被怀疑者都不自动回避(要躲就标 hostile 或用启动参数点名)。Agent 可用 `ccl knowledge mark` 动态改判,免重启。',
|
|
9
10
|
create(args?: string[]) {
|
|
10
11
|
const targets = args ? parseTargetArgs(args) : [];
|
|
@@ -15,6 +15,7 @@ class CorpsePatrolStrategy extends GoalRootStrategy {
|
|
|
15
15
|
|
|
16
16
|
export const strategy: StrategyEntry = {
|
|
17
17
|
id: 'corpse-patrol',
|
|
18
|
+
name: '守尸',
|
|
18
19
|
description: '发现尸体就在附近40至100距离内随机游荡,但不报告——故意在尸体旁出没制造嫌疑感。没有尸体时巡逻各房间。传入打招呼话术时,视野内出现人就随机发送一条,之后120秒内不再发言。不做任务,不杀人;但残局(已知存活≤6)出现紧急维修任务时例外,会最高优先级抢着去做,阻止蟹靠破坏倒计时取胜。(天堂鱼默认;进阶版见 paradise-fish)参数:可选:1~3 条打招呼话术。',
|
|
19
20
|
create(args?: string[]) {
|
|
20
21
|
return new CorpsePatrolStrategy(parseGreetingArgs(args, 'corpse-patrol'));
|
|
@@ -6,6 +6,7 @@ import { SpeechModule, getSpeechConfigForRole } from './speech-module.js';
|
|
|
6
6
|
|
|
7
7
|
export const strategy: StrategyEntry = {
|
|
8
8
|
id: 'crab-sabotage',
|
|
9
|
+
name: '破坏蟹',
|
|
9
10
|
description: '优先完成蟹的真实破坏任务(破坏点紧挨已知尸体时会跳过);做完立刻触发紧急警报;警报期照样会被举报、不乱杀,只清真正落单的目标。有紧急事件时跳过破坏流程。视野里只有一个非队友且冷却可用就靠近,进入50距离就立刻出刀,即使正在做破坏任务;刀在冷却时不追不等、改做低优先级任务伪装,冷却好且目标仍落单可见再出刀;正在做任务时不会撇下任务去追猎(贴脸且刀好仍会立刻出刀)。发现尸体且附近有非队友、但当前不能原地出刀时,会靠近并报告尸体。视野里出现两人以上时,暂停攻击10秒并做任务伪装,先做真实任务再做虾的伪装任务;选任务时会跳过紧挨已知尸体(含看见过、已离开视野的)的任务。没有可做任务时按房间巡逻,绝不停在原地。(普通蟹默认)',
|
|
10
11
|
create(args?: string[]) {
|
|
11
12
|
const role = args?.[0] ?? 'crab_generic';
|
|
@@ -73,6 +73,7 @@ class TestCustomModuleStrategy implements Strategy {
|
|
|
73
73
|
|
|
74
74
|
const testEntry: StrategyEntry = {
|
|
75
75
|
id: 'test-custom-module',
|
|
76
|
+
name: 'test-custom-module',
|
|
76
77
|
description: 'Internal test strategy for CustomModule verification.',
|
|
77
78
|
create() {
|
|
78
79
|
return new TestCustomModuleStrategy();
|
|
@@ -5,6 +5,7 @@ import { parseTargetArgs } from './player-targets.js';
|
|
|
5
5
|
|
|
6
6
|
export const strategy: StrategyEntry = {
|
|
7
7
|
id: 'find-player',
|
|
8
|
+
name: '盯人',
|
|
8
9
|
description: '全力寻找并贴身跟踪一个指定目标(座位号或名字,单个)。看到就贴上去近距离尾随(约40距离);跟丢就先奔向最后出现的位置,再按房间顺序不停巡逻直到目标重新出现,绝不放弃。只跟踪、不出刀、不做任务、不报告;目标死亡即停。',
|
|
9
10
|
create(args?: string[]) {
|
|
10
11
|
const targets = args ? parseTargetArgs(args) : [];
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import type { GameState, TaskInfo } from '../sdk/types.js';
|
|
3
3
|
import type { StrategyContext } from './types.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
CorpseMemory,
|
|
6
|
+
emergencyRushDecision,
|
|
7
|
+
firstAvailableTask,
|
|
8
|
+
killYieldsToEmergencyRepair,
|
|
9
|
+
nearestSafeTask,
|
|
10
|
+
PatrolState,
|
|
11
|
+
safePatrolStep,
|
|
12
|
+
} from './game-utils.js';
|
|
5
13
|
|
|
6
14
|
function state(overrides: Partial<GameState> = {}): GameState {
|
|
7
15
|
return {
|
|
@@ -163,6 +171,50 @@ describe('safePatrolStep', () => {
|
|
|
163
171
|
});
|
|
164
172
|
});
|
|
165
173
|
|
|
174
|
+
describe('emergency repair kill yielding', () => {
|
|
175
|
+
function emergencyContext(overrides: Partial<StrategyContext> = {}): StrategyContext {
|
|
176
|
+
return context({
|
|
177
|
+
emergency: {
|
|
178
|
+
task_id: 'repair',
|
|
179
|
+
task_name: 'repair',
|
|
180
|
+
room: 'control',
|
|
181
|
+
status: 'emergency',
|
|
182
|
+
x: 100,
|
|
183
|
+
y: 0,
|
|
184
|
+
remaining_secs: 45,
|
|
185
|
+
},
|
|
186
|
+
...overrides,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
it('yields near the repair task at one player above the rush threshold', () => {
|
|
191
|
+
const repairState = state({ alive_count: 7 });
|
|
192
|
+
const ctx = emergencyContext();
|
|
193
|
+
|
|
194
|
+
expect(killYieldsToEmergencyRepair(
|
|
195
|
+
repairState,
|
|
196
|
+
ctx,
|
|
197
|
+
{ room: 'control', x: 130, y: 0 },
|
|
198
|
+
)).toBe(true);
|
|
199
|
+
expect(emergencyRushDecision(repairState, ctx)).toBeNull();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('does not yield before the one-player buffer', () => {
|
|
203
|
+
expect(killYieldsToEmergencyRepair(
|
|
204
|
+
state({ alive_count: 8 }),
|
|
205
|
+
emergencyContext(),
|
|
206
|
+
{ room: 'control', x: 130, y: 0 },
|
|
207
|
+
)).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('still rushes the repair task at the endgame threshold', () => {
|
|
211
|
+
const decision = emergencyRushDecision(state({ alive_count: 6 }), emergencyContext());
|
|
212
|
+
|
|
213
|
+
expect(decision?.action.type).toBe('move');
|
|
214
|
+
expect(decision?.action.payload).toMatchObject({ target_x: 100, target_y: 0 });
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
166
218
|
describe('nearestSafeTask', () => {
|
|
167
219
|
// 威胁贴在起点旁(视野内 ~52px),唯一安全任务远在反方向、离威胁 556px(> 端点
|
|
168
220
|
// 排除半径 500)。修复前 pathNearAny 会因起点采样紧贴威胁把整条「背向威胁」的路线判危,
|
|
@@ -65,23 +65,33 @@ function normalizeRoom(room?: string): string {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
69
|
-
* 1.
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
|
|
68
|
+
* 两点是否「紧挨」——命中其一即算:
|
|
69
|
+
* 1. **同房间**:两者在同一个(非走廊)房间——房间级判据,自动随房间尺寸缩放。
|
|
70
|
+
* 2. **距离兜底**:欧氏距离 ≤ range——覆盖贴门跨到隔壁房间、或缺房间标签(走廊、坐标未回填)的情形。
|
|
71
|
+
* 供「避开尸体做任务」「残局维修点附近不杀人」等「某点是否紧挨某参照点」的判断统一复用。
|
|
72
|
+
*/
|
|
73
|
+
export function sameRoomOrWithinRange(
|
|
74
|
+
a: { x?: number; y?: number; room?: string },
|
|
75
|
+
b: { x?: number; y?: number; room?: string },
|
|
76
|
+
range: number,
|
|
77
|
+
): boolean {
|
|
78
|
+
const ar = normalizeRoom(a.room);
|
|
79
|
+
if (ar && ar === normalizeRoom(b.room)) return true;
|
|
80
|
+
return a.x != null && a.y != null && b.x != null && b.y != null
|
|
81
|
+
&& dist(a.x, a.y, b.x, b.y) <= range;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 任务点是否落在「尸体回避区」,供做任务伪装的角色跳过命案房间的任务。命中其一即算(同房间 / 欧氏
|
|
86
|
+
* 距离 ≤ CORPSE_TASK_AVOID_RANGE,见 sameRoomOrWithinRange):这样坏人杀完不会回到陈尸的那个房间里
|
|
87
|
+
* 做任务,哪怕任务点离尸体有大半个房间远;距离兜底覆盖尸体贴门跨房、或尸体/任务缺房间标签的情形。
|
|
73
88
|
*/
|
|
74
89
|
export function taskNearKnownCorpse(
|
|
75
90
|
task: { x?: number; y?: number; room?: string },
|
|
76
91
|
avoidCorpses?: CorpseInfo[] | null,
|
|
77
92
|
): boolean {
|
|
78
93
|
if (!avoidCorpses || avoidCorpses.length === 0) return false;
|
|
79
|
-
|
|
80
|
-
return avoidCorpses.some(c => {
|
|
81
|
-
if (taskRoom && normalizeRoom(c.room) === taskRoom) return true;
|
|
82
|
-
return c.x != null && c.y != null && task.x != null && task.y != null
|
|
83
|
-
&& dist(task.x, task.y, c.x, c.y) <= CORPSE_TASK_AVOID_RANGE;
|
|
84
|
-
});
|
|
94
|
+
return avoidCorpses.some(c => sameRoomOrWithinRange(task, c, CORPSE_TASK_AVOID_RANGE));
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
export function firstAvailableTask(
|
|
@@ -330,21 +340,63 @@ export function taskMoveDecision(state: GameState, ctx: StrategyContext, task: T
|
|
|
330
340
|
/** 已知存活人数 ≤ 此值才触发「残局抢修紧急任务」。 */
|
|
331
341
|
export const EMERGENCY_RUSH_ALIVE_THRESHOLD = 6;
|
|
332
342
|
|
|
343
|
+
/** 当前是否有「进行中、带坐标」的紧急维修任务(不看存活人数门槛)。 */
|
|
344
|
+
function pendingEmergencyRepair(ctx: StrategyContext): EmergencyInfo | null {
|
|
345
|
+
const e = ctx.emergency;
|
|
346
|
+
if (!e || e.status === 'completed' || e.x == null || e.y == null) return null;
|
|
347
|
+
return e;
|
|
348
|
+
}
|
|
349
|
+
|
|
333
350
|
/**
|
|
334
|
-
*
|
|
335
|
-
*
|
|
351
|
+
* 残局抢修是否「该生效」:已知存活人数(state.alive_count,含本玩家尚未目睹其死亡者)≤
|
|
352
|
+
* EMERGENCY_RUSH_ALIVE_THRESHOLD 且当前有进行中、带坐标的紧急维修任务时返回该任务,否则 null。
|
|
353
|
+
* 这是 emergencyRushDecision「奔去维修」的门槛;「维修点附近不杀人」(killYieldsToEmergencyRepair)
|
|
354
|
+
* 共用同一个紧急任务判定,但存活门槛比这里高一人,原因见该函数注释。
|
|
355
|
+
*/
|
|
356
|
+
export function activeEmergencyRushTask(state: GameState, ctx: StrategyContext): EmergencyInfo | null {
|
|
357
|
+
if (typeof state.alive_count !== 'number' || state.alive_count > EMERGENCY_RUSH_ALIVE_THRESHOLD) return null;
|
|
358
|
+
return pendingEmergencyRepair(ctx);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* 残局抢修紧急维修任务的「靠近 → 到点提交」单步决策:activeEmergencyRushTask 生效时返回该决策,否则 null
|
|
336
363
|
* (行为不触发,调用方按自身原优先级继续)。
|
|
337
364
|
*
|
|
338
365
|
* 背景:紧急任务超时则蟹阵营直接获胜(后端倒计时),而中立的章鱼/天堂鱼要靠活到最后取胜,因此残局应抢着
|
|
339
366
|
* 把维修做掉、掐断蟹的破坏取胜线。服务端已放开中立/蟹阵营提交紧急任务。紧急任务被任意玩家完成 / 开会 /
|
|
340
|
-
* 超时后,服务端会清空 state.emergency(→ ctx.emergency 为 null
|
|
367
|
+
* 超时后,服务端会清空 state.emergency(→ ctx.emergency 为 null),activeEmergencyRushTask 随之自动返回 null。
|
|
341
368
|
*/
|
|
342
369
|
export function emergencyRushDecision(state: GameState, ctx: StrategyContext): BehaviorDecision | null {
|
|
343
|
-
|
|
370
|
+
const task = activeEmergencyRushTask(state, ctx);
|
|
371
|
+
if (!task) return null;
|
|
372
|
+
// 维修点恰是上一帧被挡的移动目标 → 本帧让出(与旧 firstAvailableTask 的 blocked 兜底一致),别对着堵点空撞。
|
|
373
|
+
if (ctx.blockedMoveTarget && dist(task.x!, task.y!, ctx.blockedMoveTarget.x, ctx.blockedMoveTarget.y) <= 10) {
|
|
344
374
|
return null;
|
|
345
375
|
}
|
|
346
|
-
|
|
347
|
-
|
|
376
|
+
return taskMoveDecision(state, ctx, task);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 残局抢修期,落单目标是否「紧挨紧急维修点」而该让位于维修、不该追杀。
|
|
381
|
+
*
|
|
382
|
+
* 存活门槛比抢修本身(activeEmergencyRushTask,≤EMERGENCY_RUSH_ALIVE_THRESHOLD)**高一人**,因为出一刀
|
|
383
|
+
* 会让存活−1:alive_count == THRESHOLD+1 时此刻把这个落单目标杀掉,下一帧就跌进抢修档、又奔回这同一个维修点,
|
|
384
|
+
* 正好踩在自己刚造的尸体 + 涌来的虾群里,自证清白极难。所以 alive_count ≤ THRESHOLD+1 就让位。注意:把抢修
|
|
385
|
+
* 门槛整体抬到 7 治不了这个——边界只会平移到 8→7 复现同样的「杀完立刻奔回去维修」;真正的修法是让位档恒比
|
|
386
|
+
* 抢修档高 1。
|
|
387
|
+
*
|
|
388
|
+
* 命中还需有进行中带坐标的紧急维修、且目标与维修点同房间或在 CORPSE_TASK_AVOID_RANGE 内(复用做任务伪装时
|
|
389
|
+
* 「避开尸体」的同房间+距离判据)。远离维修点的落单目标不受影响,照常猎杀(抢修仍低于猎杀的设定不变)。
|
|
390
|
+
*/
|
|
391
|
+
export function killYieldsToEmergencyRepair(
|
|
392
|
+
state: GameState,
|
|
393
|
+
ctx: StrategyContext,
|
|
394
|
+
target: { x?: number; y?: number; room?: string },
|
|
395
|
+
): boolean {
|
|
396
|
+
if (typeof state.alive_count !== 'number' || state.alive_count > EMERGENCY_RUSH_ALIVE_THRESHOLD + 1) return false;
|
|
397
|
+
const e = pendingEmergencyRepair(ctx);
|
|
398
|
+
if (!e) return false;
|
|
399
|
+
return sameRoomOrWithinRange(target, { x: e.x, y: e.y, room: e.room }, CORPSE_TASK_AVOID_RANGE);
|
|
348
400
|
}
|
|
349
401
|
|
|
350
402
|
export function nearestReportableCorpse(state: GameState): GameState['corpses'][number] | null {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GameState } from '../../sdk/types.js';
|
|
1
|
+
import type { GameState, PlayerInfo } from '../../sdk/types.js';
|
|
2
2
|
import { Action } from '../../sdk/action.js';
|
|
3
3
|
import {
|
|
4
4
|
canUseKill,
|
|
@@ -58,8 +58,15 @@ export function corpseReportWithNonTeammate(state: GameState, ctx: StrategyConte
|
|
|
58
58
|
}];
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/**
|
|
62
|
-
|
|
61
|
+
/**
|
|
62
|
+
* 唯一一个落单非队友贴脸(出刀范围内)且刀好 → 立刻出刀;否则 null。无视暂停窗口、不管在做什么任务。
|
|
63
|
+
* 可选 skipTarget:返回 true 的目标本帧不出刀(章鱼残局抢修期用它放过紧挨紧急维修点的目标)。
|
|
64
|
+
*/
|
|
65
|
+
export function immediateLoneKillDecision(
|
|
66
|
+
state: GameState,
|
|
67
|
+
ctx: StrategyContext,
|
|
68
|
+
skipTarget?: (target: PlayerInfo) => boolean,
|
|
69
|
+
): BehaviorDecision[] | null {
|
|
63
70
|
if (!canUseKill(state)) return null;
|
|
64
71
|
|
|
65
72
|
const targets = nonTeammatesVisible(state, ctx);
|
|
@@ -68,6 +75,7 @@ export function immediateLoneKillDecision(state: GameState, ctx: StrategyContext
|
|
|
68
75
|
const target = targets[0];
|
|
69
76
|
const distance = target.distance ?? dist(state.you.x, state.you.y, target.x, target.y);
|
|
70
77
|
if (distance > killCommitRange(state.you.role)) return null;
|
|
78
|
+
if (skipTarget?.(target)) return null;
|
|
71
79
|
|
|
72
80
|
ctx.notifications.push(`发现落单目标${target.name}就在身边,立刻出刀!`);
|
|
73
81
|
return [{ action: Action.kill(target.name) }];
|
|
@@ -18,7 +18,7 @@ const PANIC_REPLAN_TARGET_SHIFT = 220;
|
|
|
18
18
|
* keep-away 逃跑推演基线参数。各档(FLEE 不覆盖;陌生人覆盖 steps:4)在此基础上合并 options.planOpts。
|
|
19
19
|
* killRange 取 SHRIMP_KILL_RANGE(160) 而非 planner 默认 80:本层威胁只有坐标、没有角色(resolver
|
|
20
20
|
* 返回可见非队友的裸位置,本游戏看不到他人身份),无法逐威胁取 killRangeFor,故统一用带刀虾兜底,
|
|
21
|
-
*
|
|
21
|
+
* 宁可对蟹/章鱼多拉开也不对带刀好人少逃;带刀好人遇到未确认对象也只复用这一层做回避,不再自卫出刀。
|
|
22
22
|
*/
|
|
23
23
|
export const KEEP_AWAY_BASE_PLAN_OPTS: EscapeOptions = {
|
|
24
24
|
killRange: SHRIMP_KILL_RANGE,
|
|
@@ -39,8 +39,9 @@ export interface KeepAwayGoalOptions {
|
|
|
39
39
|
finishWhenClear?: boolean;
|
|
40
40
|
/** 无威胁时的 idle/coast 通知;作为子目标默认关闭(父节点自有进度播报)。 */
|
|
41
41
|
idleNotices?: boolean;
|
|
42
|
-
/** 通知措辞里威胁的称呼:发现{noun}{names},{verb}到 (x, y)
|
|
43
|
-
|
|
42
|
+
/** 通知措辞里威胁的称呼:发现{noun}{names},{verb}到 (x, y)。传字符串则所有威胁共用一个称呼;传函数则
|
|
43
|
+
* 逐个威胁按其真实身份标注(如坏人/被怀疑者分别措辞),避免把混在一起躲的被怀疑者也一律叫成坏人。 */
|
|
44
|
+
noun?: string | ((p: PlayerInfo, ctx: StrategyContext) => string);
|
|
44
45
|
/** 覆盖 planEscape 参数(与 PLAN_OPTS 合并)。 */
|
|
45
46
|
planOpts?: EscapeOptions;
|
|
46
47
|
}
|
|
@@ -201,11 +202,14 @@ export class KeepAwayGoal extends Goal {
|
|
|
201
202
|
this.committed = target;
|
|
202
203
|
this.lastCommitAt = Date.now();
|
|
203
204
|
this.lastCommitWasPanic = panic;
|
|
204
|
-
const
|
|
205
|
+
const noun = this.options.noun;
|
|
206
|
+
const body = typeof noun === 'function'
|
|
207
|
+
? threats.map(p => `${noun(p, ctx)}${p.name}`).join('、')
|
|
208
|
+
: `${noun ?? ''}${threats.map(p => p.name).join('、')}`;
|
|
205
209
|
this.notice(
|
|
206
210
|
ctx,
|
|
207
211
|
`${verb}:${targetKey(target)}`,
|
|
208
|
-
`发现${
|
|
212
|
+
`发现${body},${verb}到 (${Math.round(target.x)}, ${Math.round(target.y)})。${detail}`,
|
|
209
213
|
);
|
|
210
214
|
}
|
|
211
215
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { GameState } from '../../sdk/types.js';
|
|
1
|
+
import type { GameState, PlayerInfo } from '../../sdk/types.js';
|
|
2
2
|
import {
|
|
3
3
|
emergencyRushDecision,
|
|
4
4
|
firstAvailableTask,
|
|
5
5
|
killCooldownSecs,
|
|
6
|
+
killYieldsToEmergencyRepair,
|
|
6
7
|
nonTeammatesVisible,
|
|
7
8
|
PatrolState,
|
|
8
9
|
PROGRESS_INTERVAL_MS,
|
|
@@ -22,7 +23,9 @@ import { corpseReportWithNonTeammate, immediateLoneKillDecision } from './crab-o
|
|
|
22
23
|
* 0. 反射(crab-octopus-reflexes,URGENT,抢在落单猎杀编排之前):落单非队友贴脸(出刀范围内)且刀好 →
|
|
23
24
|
* 立刻出刀;否则有尸体且附近有非队友 → 报警/靠近报警(不管尸体是谁造成的)。
|
|
24
25
|
* 1. 落单猎杀(LoneKillGoal,URGENT):core.assess() 判 hunt(刀好且有落单目标)→ 叶子 pursue() 靠近 / 出刀。
|
|
25
|
-
* 刀在冷却时 assess 返回 idle
|
|
26
|
+
* 刀在冷却时 assess 返回 idle,不追不等,直接下沉到任务伪装。**残局抢修期例外**:落单目标紧挨紧急维修点
|
|
27
|
+
* (同房间或避尸距离内,killYieldsToEmergencyRepair)时跳过反射刀/落单猎杀,让位给第 2 步去修——虾群都
|
|
28
|
+
* 涌向维修点,在那儿动手必被围观、还会撇下维修把破坏倒计时取胜线让给蟹;远离维修点的目标照常猎杀。
|
|
26
29
|
* 2. 残局抢修紧急任务(EMERGENCY,仅已知存活≤6):低于落单猎杀(击杀优先),高于伪装任务——
|
|
27
30
|
* 紧急任务超时则蟹阵营直接获胜,中立章鱼靠活到最后取胜,故残局抢着把维修做掉、掐断蟹的破坏取胜线。
|
|
28
31
|
* 3. 任务伪装 / 巡逻(WanderTaskGoal,WANDER):做已分配的伪装任务(排除当前紧急任务,那个走第 2 步),
|
|
@@ -50,7 +53,12 @@ export class LoneKillTaskTop extends Goal {
|
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
|
|
53
|
-
|
|
56
|
+
// 残局抢修期:紧急维修点附近不杀人。虾群都涌向维修点,在那儿动手必被围观、还会撇下维修把破坏
|
|
57
|
+
// 倒计时取胜线让给蟹。落单目标紧挨维修点(同房间或避尸距离内)时跳过反射刀与落单猎杀,让位给下面
|
|
58
|
+
// 的抢修分支;远离维修点的目标照常猎杀(抢修仍低于猎杀的设定不变)。
|
|
59
|
+
const yieldsToRepair = (t: PlayerInfo) => killYieldsToEmergencyRepair(state, ctx, t);
|
|
60
|
+
|
|
61
|
+
const immediateKill = immediateLoneKillDecision(state, ctx, yieldsToRepair);
|
|
54
62
|
if (immediateKill) {
|
|
55
63
|
this.emitProgress(state, ctx, false);
|
|
56
64
|
return emitLeaf(this, immediateKill, URGENT_GOAL_PRIORITY);
|
|
@@ -63,7 +71,8 @@ export class LoneKillTaskTop extends Goal {
|
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
const target = this.killCore.assess(state, ctx);
|
|
66
|
-
|
|
74
|
+
const sparedNearRepair = target.kind === 'hunt' && yieldsToRepair(target.target);
|
|
75
|
+
if (target.kind === 'hunt' && !sparedNearRepair) {
|
|
67
76
|
this.emitProgress(state, ctx, false);
|
|
68
77
|
this.loneKillGoal.setTarget(target.target);
|
|
69
78
|
return setBehavior(this, this.loneKillGoal, URGENT_GOAL_PRIORITY);
|
|
@@ -72,7 +81,7 @@ export class LoneKillTaskTop extends Goal {
|
|
|
72
81
|
// 残局抢修紧急任务(已知存活≤6):高于伪装任务,低于上面的落单猎杀/反射。
|
|
73
82
|
const emergency = emergencyRushDecision(state, ctx);
|
|
74
83
|
if (emergency) {
|
|
75
|
-
this.emitProgress(state, ctx, true);
|
|
84
|
+
this.emitProgress(state, ctx, true, sparedNearRepair ? target.target.name : null);
|
|
76
85
|
return emitLeaf(this, [emergency], EMERGENCY_GOAL_PRIORITY);
|
|
77
86
|
}
|
|
78
87
|
|
|
@@ -80,7 +89,12 @@ export class LoneKillTaskTop extends Goal {
|
|
|
80
89
|
return setBehavior(this, this.wanderGoal, WANDER_GOAL_PRIORITY);
|
|
81
90
|
}
|
|
82
91
|
|
|
83
|
-
private emitProgress(
|
|
92
|
+
private emitProgress(
|
|
93
|
+
state: GameState,
|
|
94
|
+
ctx: StrategyContext,
|
|
95
|
+
emergencyRush: boolean,
|
|
96
|
+
sparedNearRepair: string | null = null,
|
|
97
|
+
): void {
|
|
84
98
|
const now = Date.now();
|
|
85
99
|
if (now - ctx.lastProgressNotifyAt < PROGRESS_INTERVAL_MS) return;
|
|
86
100
|
ctx.lastProgressNotifyAt = now;
|
|
@@ -88,9 +102,11 @@ export class LoneKillTaskTop extends Goal {
|
|
|
88
102
|
const room = state.you.room ?? '未知';
|
|
89
103
|
if (emergencyRush) {
|
|
90
104
|
const name = ctx.emergency?.task_name ?? '紧急维修';
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
105
|
+
let msg = `[进度] 当前在${room},残局仅剩${state.alive_count}人,抢着去做紧急任务「${name}」,阻止蟹靠破坏倒计时取胜。`;
|
|
106
|
+
if (sparedNearRepair) {
|
|
107
|
+
msg += ` 落单目标${sparedNearRepair}就在维修点附近,先修不杀,免得在虾群涌入的维修点暴露自己。`;
|
|
108
|
+
}
|
|
109
|
+
ctx.notifications.push(msg);
|
|
94
110
|
return;
|
|
95
111
|
}
|
|
96
112
|
const cd = killCooldownSecs(state);
|