@myclaw163/clawclaw-cli 0.6.67 → 0.6.69

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 (54) hide show
  1. package/bin/clawclaw-cli.mjs +3 -3
  2. package/package.json +1 -1
  3. package/scripts/sync-bundled-skill.mjs +1 -1
  4. package/skills/clawclaw/references/COMMANDS.md +4 -4
  5. package/skills/clawclaw/references/KNOWLEDGE.md +14 -12
  6. package/src/commands/config.ts +30 -30
  7. package/src/commands/game.ts +15 -0
  8. package/src/commands/knowledge.test.ts +4 -10
  9. package/src/commands/knowledge.ts +10 -39
  10. package/src/commands/setup/hermes.test.ts +96 -96
  11. package/src/commands/setup/hermes.ts +76 -76
  12. package/src/commands/setup/index.ts +13 -13
  13. package/src/commands/setup/openclaw.test.ts +114 -114
  14. package/src/commands/setup/openclaw.ts +147 -147
  15. package/src/commands/watch.test.ts +11 -0
  16. package/src/commands/watch.ts +2 -3
  17. package/src/lib/auth.test.ts +15 -0
  18. package/src/lib/host-config-patcher.test.ts +130 -130
  19. package/src/lib/host-config-patcher.ts +151 -151
  20. package/src/lib/hub-reminder.ts +19 -19
  21. package/src/lib/knowledge-store.test.ts +28 -38
  22. package/src/lib/knowledge-store.ts +52 -57
  23. package/src/pipeline/event-format.test.ts +82 -2
  24. package/src/pipeline/event-format.ts +114 -5
  25. package/src/pipeline/event-hints.ts +20 -3
  26. package/src/runtime/event-daemon.test.ts +34 -0
  27. package/src/runtime/event-daemon.ts +51 -3
  28. package/src/sdk/index.ts +2 -3
  29. package/src/sdk/types.ts +2 -0
  30. package/src/strategies/avoid-players.knowledge.md +7 -8
  31. package/src/strategies/avoid-players.ts +1 -1
  32. package/src/strategies/corpse-patrol.ts +1 -1
  33. package/src/strategies/game-utils.test.ts +53 -1
  34. package/src/strategies/game-utils.ts +92 -28
  35. package/src/strategies/goals/avoid-players-top.ts +3 -3
  36. package/src/strategies/goals/corpse-patrol-top.ts +23 -1
  37. package/src/strategies/goals/crab-octopus-reflexes.ts +11 -3
  38. package/src/strategies/goals/keep-away-goal.ts +9 -5
  39. package/src/strategies/goals/leaf-goal.ts +2 -0
  40. package/src/strategies/goals/lone-kill-task-top.ts +58 -11
  41. package/src/strategies/goals/normal-shrimp-top.ts +11 -11
  42. package/src/strategies/goals/paradise-fish-top.ts +32 -15
  43. package/src/strategies/goals/warrior-shrimp-top.test.ts +4 -3
  44. package/src/strategies/goals/warrior-shrimp-top.ts +62 -295
  45. package/src/strategies/hide-spots.ts +11 -75
  46. package/src/strategies/kill-lone.knowledge.md +6 -9
  47. package/src/strategies/lone-kill-task.ts +1 -1
  48. package/src/strategies/off-route-points.ts +105 -0
  49. package/src/strategies/paradise-fish.knowledge.md +7 -8
  50. package/src/strategies/paradise-fish.ts +1 -1
  51. package/src/strategies/shrimp-memory.knowledge.md +7 -8
  52. package/src/strategies/shrimp-memory.ts +1 -1
  53. package/src/strategies/warrior-memory.knowledge.md +9 -10
  54. package/src/strategies/warrior-memory.ts +1 -1
package/src/sdk/index.ts CHANGED
@@ -21,8 +21,7 @@
21
21
  * hasKnownCorpse / corpseAtScene / nearestReportableCorpse / safePatrolStep /
22
22
  * nonTeammatesVisible / matchesTarget / isTargetAlive / nearestVisibleTarget /
23
23
  * pursueVisibleTarget / killCooldownSecs / hasKillUseRemaining / canUseKill / killRangeFor / killCommitRange /
24
- * isKnowledgeHostile / isKnowledgeThreat / isKnowledgeTrusted
25
- * (含 @deprecated 别名 isKnowledgeKillTarget / isKnowledgeAvoid / isKnowledgeProtected) /
24
+ * isKnowledgeHostile(坏人)/ isKnowledgeThreat(非 trusted = 坏人+被怀疑)/ isKnowledgeTrusted(好人)/
26
25
  * PatrolState / SafeTaskOptions / SafePatrolOptions + 常量(TASK_SUBMIT_RADIUS /
27
26
  * SHRIMP_VISION_RANGE / SHRIMP_VISION_EXIT_BUFFER / SHRIMP_VISION_RELEASE_RANGE /
28
27
  * FOLLOW_RANGE / PATROL_REACHED_DISTANCE / PROGRESS_INTERVAL_MS)
@@ -53,12 +52,12 @@ export type {
53
52
  // Game utilities (for user strategies)
54
53
  export {
55
54
  dist, firstAvailableTask, nearestKnownCorpse, nearestKnownCorpseNav, hasKnownCorpse, corpseAtScene, nearestReportableCorpse,
55
+ emergencyRushDecision, activeEmergencyRushTask, killYieldsToEmergencyRepair, EMERGENCY_RUSH_ALIVE_THRESHOLD,
56
56
  safePatrolStep,
57
57
  nonTeammatesVisible, matchesTarget, isTargetAlive,
58
58
  nearestVisibleTarget, pursueVisibleTarget,
59
59
  killCooldownSecs, hasKillUseRemaining, canUseKill, killRangeFor, killCommitRange,
60
60
  isKnowledgeHostile, isKnowledgeThreat, isKnowledgeTrusted,
61
- isKnowledgeKillTarget, isKnowledgeAvoid, isKnowledgeProtected,
62
61
  PatrolState,
63
62
  TASK_SUBMIT_RADIUS, SHRIMP_VISION_RANGE, SHRIMP_VISION_EXIT_BUFFER, SHRIMP_VISION_RELEASE_RANGE,
64
63
  FOLLOW_RANGE, PATROL_REACHED_DISTANCE,
package/src/sdk/types.ts CHANGED
@@ -130,6 +130,8 @@ export interface GameState {
130
130
  players: PlayerInfo[];
131
131
  corpses: CorpseInfo[];
132
132
  emergency?: EmergencyInfo;
133
+ /** 服务端下发的「已知存活人数」:存活者 + 本玩家尚未目睹其死亡者之和。残局抢修紧急任务的门槛。 */
134
+ alive_count?: number;
133
135
  task_progress?: { completed: number; goal: number };
134
136
  new_events?: Array<Record<string, any>>;
135
137
  stale: boolean;
@@ -1,20 +1,19 @@
1
1
  # avoid-players 知识契约
2
2
 
3
- `avoid-players` 读取统一的玩家三档标记:
3
+ `avoid-players` 读取两档玩家标记,**未标记的一律默认「被怀疑」**:
4
4
 
5
5
  | 标记 | 行为 |
6
6
  |------|------|
7
- | `suspect` | 加入回避名单,进入视野便逃离 |
8
- | `hostile` | 加入回避名单,进入视野便逃离 |
9
- | `trusted` | 不因知识标记而回避 |
7
+ | `hostile`(坏人) | 加入回避名单,进入视野便逃离 |
8
+ | `trusted`(好人) | 不因知识标记而回避 |
9
+ | 未标记(被怀疑) | 不自动回避——照常按房间巡逻(需要躲谁就把它标 hostile,或用启动参数点名) |
10
10
 
11
- 回避名单 = 启动参数(座位号/名字)∪ suspect/hostile 标记。威胁消失后恢复巡逻。
11
+ 回避名单 = 启动参数(座位号/名字)∪ 知识库 hostile 标记。威胁消失后恢复巡逻。
12
12
 
13
13
  ```bash
14
- ccl knowledge mark 4 suspect --confidence 0.7 --note "贴脸尾随"
15
- ccl knowledge mark 4 hostile --confidence 0.9 --note "确认危险"
14
+ ccl knowledge mark 4 hostile --note "确认危险"
16
15
  ccl knowledge mark 4 trusted --note "已澄清"
17
- ccl knowledge del player 4
16
+ ccl knowledge del player 4 # 取消标记 → 回到默认「被怀疑」(不自动回避)
18
17
  ```
19
18
 
20
19
  `role` 是纯身份事实,不直接控制策略。
@@ -5,7 +5,7 @@ import { parseTargetArgs } from './player-targets.js';
5
5
 
6
6
  export const strategy: StrategyEntry = {
7
7
  id: 'avoid-players',
8
- description: '回避玩家:按房间顺序巡逻,视野里出现回避目标就寻路躲避(推演逃点、持续远离),威胁消失后恢复巡逻。回避名单 = 启动参数(座位号或名字,可多人,可省略)∪ 知识库中标记为 suspect/hostile 的玩家;trusted 不回避。Agent 可用 `ccl knowledge mark` 动态改判,免重启。',
8
+ description: '回避玩家:按房间顺序巡逻,视野里出现回避目标就寻路躲避(推演逃点、持续远离),威胁消失后恢复巡逻。回避名单 = 启动参数(座位号或名字,可多人,可省略)∪ 知识库中标记为 hostile(坏人)的玩家;trusted 与未标记的被怀疑者都不自动回避(要躲就标 hostile 或用启动参数点名)。Agent 可用 `ccl knowledge mark` 动态改判,免重启。',
9
9
  create(args?: string[]) {
10
10
  const targets = args ? parseTargetArgs(args) : [];
11
11
  return new GoalRootStrategy('avoid-players', () => new AvoidPlayersTop(targets), {
@@ -15,7 +15,7 @@ class CorpsePatrolStrategy extends GoalRootStrategy {
15
15
 
16
16
  export const strategy: StrategyEntry = {
17
17
  id: 'corpse-patrol',
18
- description: '发现尸体就在附近40至100距离内随机游荡,但不报告——故意在尸体旁出没制造嫌疑感。没有尸体时巡逻各房间。传入打招呼话术时,视野内出现人就随机发送一条,之后120秒内不再发言。不做任务,不杀人。(天堂鱼默认;进阶版见 paradise-fish)参数:可选:1~3 条打招呼话术。',
18
+ description: '发现尸体就在附近40至100距离内随机游荡,但不报告——故意在尸体旁出没制造嫌疑感。没有尸体时巡逻各房间。传入打招呼话术时,视野内出现人就随机发送一条,之后120秒内不再发言。不做任务,不杀人;但残局(已知存活≤6)出现紧急维修任务时例外,会最高优先级抢着去做,阻止蟹靠破坏倒计时取胜。(天堂鱼默认;进阶版见 paradise-fish)参数:可选:1~3 条打招呼话术。',
19
19
  create(args?: string[]) {
20
20
  return new CorpsePatrolStrategy(parseGreetingArgs(args, 'corpse-patrol'));
21
21
  },
@@ -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 { CorpseMemory, firstAvailableTask, nearestSafeTask, PatrolState, safePatrolStep } from './game-utils.js';
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
- * 2. **距离兜底**:欧氏距离 ≤ CORPSE_TASK_AVOID_RANGE——覆盖尸体贴门跨到隔壁房间、或尸体/任务
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
- const taskRoom = normalizeRoom(task.room);
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(
@@ -327,6 +337,68 @@ export function taskMoveDecision(state: GameState, ctx: StrategyContext, task: T
327
337
  return { action: Action.move({ x: task.x!, y: task.y! }) };
328
338
  }
329
339
 
340
+ /** 已知存活人数 ≤ 此值才触发「残局抢修紧急任务」。 */
341
+ export const EMERGENCY_RUSH_ALIVE_THRESHOLD = 6;
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
+
350
+ /**
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
363
+ * (行为不触发,调用方按自身原优先级继续)。
364
+ *
365
+ * 背景:紧急任务超时则蟹阵营直接获胜(后端倒计时),而中立的章鱼/天堂鱼要靠活到最后取胜,因此残局应抢着
366
+ * 把维修做掉、掐断蟹的破坏取胜线。服务端已放开中立/蟹阵营提交紧急任务。紧急任务被任意玩家完成 / 开会 /
367
+ * 超时后,服务端会清空 state.emergency(→ ctx.emergency 为 null),activeEmergencyRushTask 随之自动返回 null。
368
+ */
369
+ export function emergencyRushDecision(state: GameState, ctx: StrategyContext): BehaviorDecision | null {
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) {
374
+ return null;
375
+ }
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);
400
+ }
401
+
330
402
  export function nearestReportableCorpse(state: GameState): GameState['corpses'][number] | null {
331
403
  const nearby = state.corpses
332
404
  .map(corpse => {
@@ -666,29 +738,21 @@ function knowledgeOf(ctx: StrategyContext) {
666
738
  return ctx.knowledge ?? FALLBACK_KNOWLEDGE;
667
739
  }
668
740
 
669
- /** hostile:带刀策略可主动攻击;无刀策略应回避。 */
670
- export function isKnowledgeHostile(p: PlayerRef, ctx: StrategyContext, minConfidence?: number): boolean {
671
- return knowledgeOf(ctx).isHostile(p, minConfidence);
741
+ /** hostile(坏人):明确敌对。带刀策略刀好时可主动攻击;无刀策略硬回避。 */
742
+ export function isKnowledgeHostile(p: PlayerRef, ctx: StrategyContext): boolean {
743
+ return knowledgeOf(ctx).isHostile(p);
672
744
  }
673
745
 
674
- /** suspect hostile:任何生存型策略都应视为威胁并回避。 */
675
- export function isKnowledgeThreat(p: PlayerRef, ctx: StrategyContext, minConfidence?: number): boolean {
676
- const mark = knowledgeOf(ctx).markOf(p, minConfidence);
677
- return mark === 'suspect' || mark === 'hostile';
746
+ /** 威胁 = trusted(坏人 + 被怀疑)。未标记 trusted 的人一律按需警惕——「除好人外都值得提防」。 */
747
+ export function isKnowledgeThreat(p: PlayerRef, ctx: StrategyContext): boolean {
748
+ return !knowledgeOf(ctx).isTrusted(p);
678
749
  }
679
750
 
680
- /** trusted:可信同伴,永远优先于回避与击杀。 */
681
- export function isKnowledgeTrusted(p: PlayerRef, ctx: StrategyContext, minConfidence?: number): boolean {
682
- return knowledgeOf(ctx).isTrusted(p, minConfidence);
751
+ /** trusted(好人):可信同伴,永远优先于回避与击杀。 */
752
+ export function isKnowledgeTrusted(p: PlayerRef, ctx: StrategyContext): boolean {
753
+ return knowledgeOf(ctx).isTrusted(p);
683
754
  }
684
755
 
685
- /** @deprecated Use isKnowledgeHostile. */
686
- export const isKnowledgeKillTarget = isKnowledgeHostile;
687
- /** @deprecated Use isKnowledgeThreat. */
688
- export const isKnowledgeAvoid = isKnowledgeThreat;
689
- /** @deprecated Use isKnowledgeTrusted. */
690
- export const isKnowledgeProtected = isKnowledgeTrusted;
691
-
692
756
  export function resolveRoom(name: string, ctx: StrategyContext): RoomTarget | null {
693
757
  const folded = name.trim().toLowerCase();
694
758
  if (!folded) return null;
@@ -1,5 +1,5 @@
1
1
  import type { GameState, PlayerInfo } from '../../sdk/types.js';
2
- import { hasReachedRoomTarget, isKnowledgeThreat, isTargetAlive, matchesAnyTarget, PatrolState, PROGRESS_INTERVAL_MS } from '../game-utils.js';
2
+ import { hasReachedRoomTarget, isKnowledgeHostile, isTargetAlive, matchesAnyTarget, PatrolState, PROGRESS_INTERVAL_MS } from '../game-utils.js';
3
3
  import type { BehaviorDecision, RoomTarget, StrategyContext } from '../types.js';
4
4
  import { Goal } from './goal.js';
5
5
  import { KeepAwayGoal, type KeepAwayGoalOptions, type ThreatResolver } from './keep-away-goal.js';
@@ -62,10 +62,10 @@ export class AvoidPlayersTop extends Goal {
62
62
  this.moveMode = 'flee';
63
63
  }
64
64
 
65
- /** args 回避名单(活着的)∪ 知识里标记 suspect/hostile 的玩家。 */
65
+ /** args 回避名单(活着的)∪ 知识库标记 hostile(坏人)的玩家。被怀疑者(未标记)不自动回避——照常按房间巡逻。 */
66
66
  private threats(state: GameState, ctx: StrategyContext): PlayerInfo[] {
67
67
  const validTargets = this.targets.filter(target => isTargetAlive(state, target, ctx));
68
- return state.players.filter(p => matchesAnyTarget(p, validTargets, ctx) || isKnowledgeThreat(p, ctx));
68
+ return state.players.filter(p => matchesAnyTarget(p, validTargets, ctx) || isKnowledgeHostile(p, ctx));
69
69
  }
70
70
 
71
71
  private setMoveGoal(room: RoomTarget, priority: number, mode: 'patrol' | 'flee', stopOnPlayer: boolean): void {
@@ -2,6 +2,7 @@ import type { GameState } from '../../sdk/types.js';
2
2
  import { Action } from '../../sdk/action.js';
3
3
  import {
4
4
  corpseAtScene,
5
+ emergencyRushDecision,
5
6
  nearestKnownCorpseNav,
6
7
  patrolStep,
7
8
  PatrolState,
@@ -24,6 +25,14 @@ export class CorpsePatrolTop extends Goal {
24
25
  }
25
26
 
26
27
  tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
28
+ // 残局抢修紧急任务(已知存活≤6):天堂鱼最高优先级,先于贴尸/巡逻/打招呼——
29
+ // 紧急任务超时则蟹直接获胜,天堂鱼靠活到最后取胜,故残局抢着把维修做掉、掐断蟹的破坏取胜线。
30
+ const emergency = emergencyRushDecision(state, ctx);
31
+ if (emergency) {
32
+ this.emitProgress(state, ctx, null, true);
33
+ return [emergency];
34
+ }
35
+
27
36
  const decisions: BehaviorDecision[] = [];
28
37
 
29
38
  const greetingDecision = this.tryGreeting(state);
@@ -63,13 +72,26 @@ export class CorpsePatrolTop extends Goal {
63
72
  return { action: Action.speech(text) };
64
73
  }
65
74
 
66
- private emitProgress(state: GameState, ctx: StrategyContext, corpse: CorpseTarget | null): void {
75
+ private emitProgress(
76
+ state: GameState,
77
+ ctx: StrategyContext,
78
+ corpse: CorpseTarget | null,
79
+ emergencyRush = false,
80
+ ): void {
67
81
  const now = Date.now();
68
82
  if (now - ctx.lastProgressNotifyAt < PROGRESS_INTERVAL_MS) return;
69
83
  ctx.lastProgressNotifyAt = now;
70
84
 
71
85
  const room = state.you.room ?? '未知';
72
86
 
87
+ if (emergencyRush) {
88
+ const name = ctx.emergency?.task_name ?? '紧急维修';
89
+ ctx.notifications.push(
90
+ `[进度] 当前在${room},残局仅剩${state.alive_count}人,抢着去做紧急任务「${name}」,阻止蟹靠破坏倒计时取胜。`,
91
+ );
92
+ return;
93
+ }
94
+
73
95
  let msg = `[进度] 当前在${room}`;
74
96
  if (corpse) {
75
97
  const label = corpse.name ?? '未知';
@@ -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
- /** 唯一一个落单非队友贴脸(出刀范围内)且刀好 → 立刻出刀;否则 null。无视暂停窗口、不管在做什么任务。 */
62
- export function immediateLoneKillDecision(state: GameState, ctx: StrategyContext): BehaviorDecision[] | null {
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
- * 宁可对蟹/章鱼多拉开也不对带刀好人少逃。武士虾自卫判定(warrior-shrimp-top)复用它,保证「避不开」口径一致。
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
- noun?: string;
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 names = threats.map(p => p.name).join('、');
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
- `发现${this.options.noun ?? ''}${names},${verb}到 (${Math.round(target.x)}, ${Math.round(target.y)})。${detail}`,
212
+ `发现${body},${verb}到 (${Math.round(target.x)}, ${Math.round(target.y)})。${detail}`,
209
213
  );
210
214
  }
211
215
 
@@ -2,6 +2,8 @@ import type { BehaviorDecision } from '../types.js';
2
2
  import { Goal } from './goal.js';
3
3
 
4
4
  export const URGENT_GOAL_PRIORITY = 1;
5
+ /** 残局抢修紧急任务:高于游走任务、低于落单猎杀/反射(URGENT),故对发言仍可被打断(< URGENT)。 */
6
+ export const EMERGENCY_GOAL_PRIORITY = 0.6;
5
7
  export const WANDER_GOAL_PRIORITY = 0.2;
6
8
 
7
9
  export class LeafGoal extends Goal {
@@ -1,7 +1,9 @@
1
- import type { GameState } from '../../sdk/types.js';
1
+ import type { GameState, PlayerInfo } from '../../sdk/types.js';
2
2
  import {
3
+ emergencyRushDecision,
3
4
  firstAvailableTask,
4
5
  killCooldownSecs,
6
+ killYieldsToEmergencyRepair,
5
7
  nonTeammatesVisible,
6
8
  PatrolState,
7
9
  PROGRESS_INTERVAL_MS,
@@ -11,7 +13,7 @@ import { Goal } from './goal.js';
11
13
  import { LoneKillCore } from './lone-kill-core.js';
12
14
  import { LoneKillGoal } from './lone-kill-goal.js';
13
15
  import { WanderTaskGoal } from './wander-task-goal.js';
14
- import { emitLeaf, setBehavior, URGENT_GOAL_PRIORITY, WANDER_GOAL_PRIORITY } from './leaf-goal.js';
16
+ import { emitLeaf, EMERGENCY_GOAL_PRIORITY, setBehavior, URGENT_GOAL_PRIORITY, WANDER_GOAL_PRIORITY } from './leaf-goal.js';
15
17
  import { corpseReportWithNonTeammate, immediateLoneKillDecision } from './crab-octopus-reflexes.js';
16
18
 
17
19
  /**
@@ -21,8 +23,12 @@ import { corpseReportWithNonTeammate, immediateLoneKillDecision } from './crab-o
21
23
  * 0. 反射(crab-octopus-reflexes,URGENT,抢在落单猎杀编排之前):落单非队友贴脸(出刀范围内)且刀好 →
22
24
  * 立刻出刀;否则有尸体且附近有非队友 → 报警/靠近报警(不管尸体是谁造成的)。
23
25
  * 1. 落单猎杀(LoneKillGoal,URGENT):core.assess() 判 hunt(刀好且有落单目标)→ 叶子 pursue() 靠近 / 出刀。
24
- * 刀在冷却时 assess 返回 idle,不追不等,直接下沉到任务伪装。
25
- * 2. 任务伪装 / 巡逻(WanderTaskGoal,WANDER):做已分配的伪装任务;章鱼不能提交紧急维修任务,
26
+ * 刀在冷却时 assess 返回 idle,不追不等,直接下沉到任务伪装。**残局抢修期例外**:落单目标紧挨紧急维修点
27
+ * (同房间或避尸距离内,killYieldsToEmergencyRepair)时跳过反射刀/落单猎杀,让位给第 2 步去修——虾群都
28
+ * 涌向维修点,在那儿动手必被围观、还会撇下维修把破坏倒计时取胜线让给蟹;远离维修点的目标照常猎杀。
29
+ * 2. 残局抢修紧急任务(EMERGENCY,仅已知存活≤6):低于落单猎杀(击杀优先),高于伪装任务——
30
+ * 紧急任务超时则蟹阵营直接获胜,中立章鱼靠活到最后取胜,故残局抢着把维修做掉、掐断蟹的破坏取胜线。
31
+ * 3. 任务伪装 / 巡逻(WanderTaskGoal,WANDER):做已分配的伪装任务(排除当前紧急任务,那个走第 2 步),
26
32
  * 跳过紧挨已知尸体(含看见过、已离开视野的)的任务,没任务就按房间巡逻,绝不停在原地。
27
33
  */
28
34
  export class LoneKillTaskTop extends Goal {
@@ -30,7 +36,14 @@ export class LoneKillTaskTop extends Goal {
30
36
  private readonly patrol = new PatrolState();
31
37
  private readonly loneKillGoal = new LoneKillGoal(this.killCore);
32
38
  private readonly wanderGoal = new WanderTaskGoal(
33
- (state, ctx) => firstAvailableTask(ctx.taskData, () => true, undefined, ctx.blockedMoveTarget, ctx.knownCorpses),
39
+ // 排除当前紧急任务:它由 tick 里的残局抢修分支按 EMERGENCY 优先级处理,不在此当普通伪装任务做。
40
+ (state, ctx) => firstAvailableTask(
41
+ ctx.taskData,
42
+ t => t.task_name !== ctx.emergency?.task_name,
43
+ undefined,
44
+ ctx.blockedMoveTarget,
45
+ ctx.knownCorpses,
46
+ ),
34
47
  () => this.killCore.patrolStopOnPlayer(),
35
48
  this.patrol,
36
49
  );
@@ -40,28 +53,62 @@ export class LoneKillTaskTop extends Goal {
40
53
  }
41
54
 
42
55
  tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
43
- this.emitProgress(state, ctx);
56
+ // 残局抢修期:紧急维修点附近不杀人。虾群都涌向维修点,在那儿动手必被围观、还会撇下维修把破坏
57
+ // 倒计时取胜线让给蟹。落单目标紧挨维修点(同房间或避尸距离内)时跳过反射刀与落单猎杀,让位给下面
58
+ // 的抢修分支;远离维修点的目标照常猎杀(抢修仍低于猎杀的设定不变)。
59
+ const yieldsToRepair = (t: PlayerInfo) => killYieldsToEmergencyRepair(state, ctx, t);
44
60
 
45
- const immediateKill = immediateLoneKillDecision(state, ctx);
46
- if (immediateKill) return emitLeaf(this, immediateKill, URGENT_GOAL_PRIORITY);
61
+ const immediateKill = immediateLoneKillDecision(state, ctx, yieldsToRepair);
62
+ if (immediateKill) {
63
+ this.emitProgress(state, ctx, false);
64
+ return emitLeaf(this, immediateKill, URGENT_GOAL_PRIORITY);
65
+ }
47
66
 
48
67
  const corpseReport = corpseReportWithNonTeammate(state, ctx);
49
- if (corpseReport) return emitLeaf(this, corpseReport, URGENT_GOAL_PRIORITY);
68
+ if (corpseReport) {
69
+ this.emitProgress(state, ctx, false);
70
+ return emitLeaf(this, corpseReport, URGENT_GOAL_PRIORITY);
71
+ }
50
72
 
51
73
  const target = this.killCore.assess(state, ctx);
52
- if (target.kind === 'hunt') {
74
+ const sparedNearRepair = target.kind === 'hunt' && yieldsToRepair(target.target);
75
+ if (target.kind === 'hunt' && !sparedNearRepair) {
76
+ this.emitProgress(state, ctx, false);
53
77
  this.loneKillGoal.setTarget(target.target);
54
78
  return setBehavior(this, this.loneKillGoal, URGENT_GOAL_PRIORITY);
55
79
  }
80
+
81
+ // 残局抢修紧急任务(已知存活≤6):高于伪装任务,低于上面的落单猎杀/反射。
82
+ const emergency = emergencyRushDecision(state, ctx);
83
+ if (emergency) {
84
+ this.emitProgress(state, ctx, true, sparedNearRepair ? target.target.name : null);
85
+ return emitLeaf(this, [emergency], EMERGENCY_GOAL_PRIORITY);
86
+ }
87
+
88
+ this.emitProgress(state, ctx, false);
56
89
  return setBehavior(this, this.wanderGoal, WANDER_GOAL_PRIORITY);
57
90
  }
58
91
 
59
- private emitProgress(state: GameState, ctx: StrategyContext): void {
92
+ private emitProgress(
93
+ state: GameState,
94
+ ctx: StrategyContext,
95
+ emergencyRush: boolean,
96
+ sparedNearRepair: string | null = null,
97
+ ): void {
60
98
  const now = Date.now();
61
99
  if (now - ctx.lastProgressNotifyAt < PROGRESS_INTERVAL_MS) return;
62
100
  ctx.lastProgressNotifyAt = now;
63
101
 
64
102
  const room = state.you.room ?? '未知';
103
+ if (emergencyRush) {
104
+ const name = ctx.emergency?.task_name ?? '紧急维修';
105
+ let msg = `[进度] 当前在${room},残局仅剩${state.alive_count}人,抢着去做紧急任务「${name}」,阻止蟹靠破坏倒计时取胜。`;
106
+ if (sparedNearRepair) {
107
+ msg += ` 落单目标${sparedNearRepair}就在维修点附近,先修不杀,免得在虾群涌入的维修点暴露自己。`;
108
+ }
109
+ ctx.notifications.push(msg);
110
+ return;
111
+ }
65
112
  const cd = killCooldownSecs(state);
66
113
  const killsRemaining = state.you.kills_remaining;
67
114
  const nonTeammates = nonTeammatesVisible(state, ctx);
@@ -4,7 +4,7 @@ import {
4
4
  dist,
5
5
  firstAvailableTask,
6
6
  hasKnownCorpse,
7
- isKnowledgeThreat,
7
+ isKnowledgeHostile,
8
8
  isKnowledgeTrusted,
9
9
  nonTeammatesVisible,
10
10
  PROGRESS_INTERVAL_MS,
@@ -39,8 +39,8 @@ type Tier = 'flee-bad' | 'keep-distance' | 'task';
39
39
  *
40
40
  * - P0 发现尸体:一律报警(reportCorpseDecision),必要时提示 Agent 更新尸体旁玩家记忆。
41
41
  * - P1 规避危险:
42
- * · A 视野里有威胁(知识库 suspect/hostile)→ 寻路躲避(KeepAwayGoal,不限距离)。
43
- * · B 无可信同伴且陌生人贴近(≤270px)→ 先拉开距离(KeepAwayGoal,拉开到 300px 即回任务)。
42
+ * · A 视野里有坏人(知识库 hostile)→ 寻路躲避(KeepAwayGoal,不限距离)。
43
+ * · B 无可信同伴且被怀疑者(未标记 = 默认被怀疑)贴近(≤270px)→ 先拉开距离(KeepAwayGoal,拉开到 300px 即回任务)。
44
44
  * 目击者豁免:当前见到 ≥2 个陌生人时不触发后撤(蟹不敢当众动手,独处才危险);
45
45
  * 进行中的后撤也会重新判定,只有仍是单陌生人危险时才拉满到 300px。豁免期间陌生人也不算任务威胁点。
46
46
  * - P2 紧急任务:无危险时优先处理。
@@ -54,7 +54,7 @@ export class NormalShrimpTop extends Goal {
54
54
  // 豁免期间陌生人不算威胁点(与 B 档不触发保持视野半径对齐),认定坏人始终算。
55
55
  (s, c) => nonTeammatesVisible(s, c)
56
56
  .filter(p => !this.isTrusted(p, c))
57
- .filter(p => this.isThreat(p, c) || !this.witnessExempt)
57
+ .filter(p => this.isHostile(p, c) || !this.witnessExempt)
58
58
  .map(p => ({ x: p.x, y: p.y })),
59
59
  );
60
60
  private readonly strangerSeenAt = new Map<string, number>();
@@ -91,10 +91,10 @@ export class NormalShrimpTop extends Goal {
91
91
  }
92
92
 
93
93
  // ── P1-A 视野里有认定坏人 → 寻路躲避 ──
94
- const threats = visible.filter(p => this.isThreat(p, ctx));
94
+ const threats = visible.filter(p => this.isHostile(p, ctx));
95
95
  if (threats.length > 0) {
96
96
  this.taskGoal.planTask(state, ctx, { holdUnsafeCurrentForMs: TASK_RETURN_INERTIA_MS });
97
- this.setKeepAway(FLEE_BAD_KEY, (s, c) => nonTeammatesVisible(s, c).filter(p => this.isThreat(p, c)), {
97
+ this.setKeepAway(FLEE_BAD_KEY, (s, c) => nonTeammatesVisible(s, c).filter(p => this.isHostile(p, c)), {
98
98
  threatRadius: Infinity,
99
99
  noun: '认定坏人',
100
100
  });
@@ -153,9 +153,9 @@ export class NormalShrimpTop extends Goal {
153
153
  this.witnessExempt = this.strangerSeenAt.size >= WITNESS_EXEMPT_COUNT;
154
154
  }
155
155
 
156
- /** 威胁:知识库 suspect/hostile,且未被标记为 trusted。 */
157
- private isThreat(p: PlayerInfo, ctx: StrategyContext): boolean {
158
- return isKnowledgeThreat(p, ctx) && !this.isTrusted(p, ctx);
156
+ /** 坏人:知识库 hostile(明确敌对)。被怀疑者(未标记)不在此列,走 B 档保持距离。 */
157
+ private isHostile(p: PlayerInfo, ctx: StrategyContext): boolean {
158
+ return isKnowledgeHostile(p, ctx);
159
159
  }
160
160
 
161
161
  /** 可信同伴:知识库 trusted。 */
@@ -185,7 +185,7 @@ export class NormalShrimpTop extends Goal {
185
185
  if (!key || this.corpseWitnessNotified.has(key)) continue;
186
186
  this.corpseWitnessNotified.add(key);
187
187
 
188
- if (this.isThreat(player, ctx)) bad.push(player.name);
188
+ if (this.isHostile(player, ctx)) bad.push(player.name);
189
189
  else if (this.isKnownParadiseFish(player, ctx)) paradise.push(player.name);
190
190
  else if (this.isTrusted(player, ctx)) trusted.push(player.name);
191
191
  else unknown.push(player.name);
@@ -217,7 +217,7 @@ export class NormalShrimpTop extends Goal {
217
217
 
218
218
  let msg = `[进度] 当前在${room}`;
219
219
  if (tier === 'flee-bad') {
220
- const names = visible.filter(p => this.isThreat(p, ctx)).map(p => p.name).join('、');
220
+ const names = visible.filter(p => this.isHostile(p, ctx)).map(p => p.name).join('、');
221
221
  msg += `,发现认定坏人${names},正在远离。`;
222
222
  } else if (tier === 'keep-distance') {
223
223
  msg += `,陌生人靠得太近,先拉开距离再做任务。`;