@myclaw163/clawclaw-cli 0.6.68 → 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.
@@ -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(
@@ -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
- * 残局抢修紧急维修任务的「靠近 → 到点提交」决策:已知存活人数(state.alive_count,含本玩家尚未目睹其
335
- * 死亡者)≤ EMERGENCY_RUSH_ALIVE_THRESHOLD 且当前有进行中的紧急任务时返回该单步决策,否则返回 null
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),本函数随之自动返回 null。
367
+ * 超时后,服务端会清空 state.emergency(→ ctx.emergency 为 null),activeEmergencyRushTask 随之自动返回 null。
341
368
  */
342
369
  export function emergencyRushDecision(state: GameState, ctx: StrategyContext): BehaviorDecision | null {
343
- if (typeof state.alive_count !== 'number' || state.alive_count > EMERGENCY_RUSH_ALIVE_THRESHOLD) {
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
- const emergency = firstAvailableTask([], () => true, ctx.emergency, ctx.blockedMoveTarget);
347
- return emergency ? taskMoveDecision(state, ctx, emergency) : null;
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
- /** 唯一一个落单非队友贴脸(出刀范围内)且刀好 → 立刻出刀;否则 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
 
@@ -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
- const immediateKill = immediateLoneKillDecision(state, ctx);
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
- if (target.kind === 'hunt') {
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(state: GameState, ctx: StrategyContext, emergencyRush: boolean): void {
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
- ctx.notifications.push(
92
- `[进度] 当前在${room},残局仅剩${state.alive_count}人,抢着去做紧急任务「${name}」,阻止蟹靠破坏倒计时取胜。`,
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);