@myclaw163/clawclaw-cli 0.6.60 → 0.6.63

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 (206) hide show
  1. package/README.md +427 -440
  2. package/package.json +48 -48
  3. package/personas//347/220/206/346/231/272/346/270/251/345/222/214.md +23 -23
  4. package/personas//350/200/201/350/260/213/346/267/261/347/256/227.md +22 -22
  5. package/personas//350/257/232/346/201/263/347/233/264/347/216/207.md +22 -22
  6. package/personas//350/275/273/346/235/276/346/264/273/346/263/274.md +22 -22
  7. package/personas//351/207/216/346/200/247/345/217/233/351/200/206.md +23 -23
  8. package/scripts/find-hide-spots.py +157 -0
  9. package/scripts/postinstall.mjs +20 -20
  10. package/scripts/sync-bundled-skill.mjs +244 -244
  11. package/scripts/sync-bundled-skill.test.mjs +152 -152
  12. package/skills/clawclaw/SKILL.md +244 -244
  13. package/skills/clawclaw/references/CHATTERBOX.md +142 -142
  14. package/skills/clawclaw/references/COMMANDS.md +148 -129
  15. package/skills/clawclaw/references/GAME-MECHANICS.md +186 -186
  16. package/skills/clawclaw/references/HUB.md +48 -48
  17. package/skills/clawclaw/references/KNOWLEDGE.md +43 -43
  18. package/skills/clawclaw/references/STRATEGIES.md +57 -57
  19. package/skills/clawclaw/references/STREAM.md +92 -59
  20. package/skills/clawclaw/references/TACTICS.md +65 -65
  21. package/src/assets/clawclaw-ascii-map.txt +40 -40
  22. package/src/cli.ts +110 -110
  23. package/src/commands/_schema.ts +109 -109
  24. package/src/commands/account.ts +209 -209
  25. package/src/commands/config.ts +30 -30
  26. package/src/commands/do.test.ts +73 -73
  27. package/src/commands/do.ts +126 -126
  28. package/src/commands/events.test.ts +71 -0
  29. package/src/commands/events.ts +155 -22
  30. package/src/commands/game-map.test.ts +28 -28
  31. package/src/commands/game-start-plan.test.ts +84 -84
  32. package/src/commands/game.ts +1027 -1027
  33. package/src/commands/history-player.test.ts +102 -102
  34. package/src/commands/history.ts +573 -573
  35. package/src/commands/hub.test.ts +96 -96
  36. package/src/commands/hub.ts +234 -234
  37. package/src/commands/knowledge.test.ts +19 -19
  38. package/src/commands/knowledge.ts +168 -168
  39. package/src/commands/load.test.ts +51 -51
  40. package/src/commands/load.ts +13 -13
  41. package/src/commands/meeting-history.test.ts +106 -106
  42. package/src/commands/memory.ts +40 -40
  43. package/src/commands/peek.ts +45 -45
  44. package/src/commands/persona.ts +57 -57
  45. package/src/commands/setup/codex.ts +248 -248
  46. package/src/commands/setup/hermes.test.ts +96 -96
  47. package/src/commands/setup/hermes.ts +76 -76
  48. package/src/commands/setup/index.ts +13 -13
  49. package/src/commands/setup/openclaw.test.ts +114 -114
  50. package/src/commands/setup/openclaw.ts +147 -147
  51. package/src/commands/skill.ts +128 -128
  52. package/src/commands/state.ts +46 -46
  53. package/src/commands/strategy.test.ts +135 -135
  54. package/src/commands/strategy.ts +180 -180
  55. package/src/commands/tts.ts +128 -128
  56. package/src/commands/upgrade.test.ts +82 -82
  57. package/src/commands/upgrade.ts +148 -148
  58. package/src/commands/watch.test.ts +966 -969
  59. package/src/commands/watch.ts +659 -720
  60. package/src/lib/auth.test.ts +59 -59
  61. package/src/lib/auth.ts +186 -186
  62. package/src/lib/command-meta.ts +37 -37
  63. package/src/lib/game-client.ts +391 -391
  64. package/src/lib/host-config-patcher.test.ts +130 -130
  65. package/src/lib/host-config-patcher.ts +151 -151
  66. package/src/lib/http-keepalive.ts +15 -15
  67. package/src/lib/http-transport.test.ts +42 -42
  68. package/src/lib/http-transport.ts +113 -113
  69. package/src/lib/hub-client.test.ts +56 -56
  70. package/src/lib/hub-client.ts +88 -88
  71. package/src/lib/hub-install.test.ts +98 -98
  72. package/src/lib/hub-install.ts +121 -121
  73. package/src/lib/hub-reminder.ts +56 -56
  74. package/src/lib/hub-unzip.test.ts +69 -69
  75. package/src/lib/hub-unzip.ts +62 -62
  76. package/src/lib/init-command.test.ts +75 -75
  77. package/src/lib/init-command.ts +120 -120
  78. package/src/lib/knowledge-store.test.ts +180 -180
  79. package/src/lib/knowledge-store.ts +374 -374
  80. package/src/lib/load-context.test.ts +52 -52
  81. package/src/lib/load-context.ts +52 -52
  82. package/src/lib/match-state.test.ts +134 -134
  83. package/src/lib/match-state.ts +94 -94
  84. package/src/lib/netease-tts.ts +83 -83
  85. package/src/lib/normalize.ts +42 -42
  86. package/src/lib/persona.test.ts +41 -41
  87. package/src/lib/persona.ts +72 -72
  88. package/src/lib/server-registry.ts +152 -152
  89. package/src/lib/skill-version.test.ts +48 -48
  90. package/src/lib/skill-version.ts +19 -19
  91. package/src/lib/strategy-export.test.ts +232 -232
  92. package/src/lib/strategy-export.ts +242 -242
  93. package/src/lib/tts-keys.ts +7 -7
  94. package/src/lib/tts-speech.test.ts +63 -63
  95. package/src/lib/tts-speech.ts +76 -76
  96. package/src/lib/workspace-argv.test.ts +49 -49
  97. package/src/lib/workspace-argv.ts +44 -44
  98. package/src/perception/player-history-store.test.ts +87 -87
  99. package/src/perception/player-history-store.ts +194 -194
  100. package/src/pipeline/event-format.test.ts +135 -0
  101. package/src/pipeline/event-format.ts +376 -0
  102. package/src/pipeline/event-hints.ts +173 -0
  103. package/src/pipeline/event-store.test.ts +28 -0
  104. package/src/pipeline/event-store.ts +193 -124
  105. package/src/pipeline/pipeline.ts +35 -35
  106. package/src/runtime/auto-upgrade.test.ts +66 -66
  107. package/src/runtime/auto-upgrade.ts +31 -31
  108. package/src/runtime/event-daemon.test.ts +107 -107
  109. package/src/runtime/event-daemon.ts +409 -409
  110. package/src/runtime/owner-control.ts +150 -150
  111. package/src/runtime/raw-ws-log.test.ts +33 -33
  112. package/src/runtime/raw-ws-log.ts +32 -32
  113. package/src/runtime/runtime-logger.ts +107 -107
  114. package/src/runtime/ws-client.test.ts +104 -104
  115. package/src/runtime/ws-client.ts +272 -272
  116. package/src/sdk/action.ts +166 -166
  117. package/src/sdk/index.ts +111 -110
  118. package/src/sdk/types.ts +159 -146
  119. package/src/strategies/avoid-lone.ts +11 -11
  120. package/src/strategies/avoid-players.knowledge.md +20 -20
  121. package/src/strategies/avoid-players.ts +15 -15
  122. package/src/strategies/corpse-patrol.ts +22 -22
  123. package/src/strategies/crab-sabotage.ts +21 -21
  124. package/src/strategies/custom-module.test.ts +269 -269
  125. package/src/strategies/find-player.ts +16 -16
  126. package/src/strategies/game-utils.test.ts +190 -190
  127. package/src/strategies/game-utils.ts +782 -744
  128. package/src/strategies/goals/anchor-linger.ts +77 -0
  129. package/src/strategies/goals/avoid-lone-top.ts +168 -168
  130. package/src/strategies/goals/avoid-players-top.test.ts +83 -83
  131. package/src/strategies/goals/avoid-players-top.ts +121 -121
  132. package/src/strategies/goals/conversation-goal.ts +51 -51
  133. package/src/strategies/goals/corpse-patrol-top.ts +91 -91
  134. package/src/strategies/goals/crab-octopus-reflexes.ts +93 -93
  135. package/src/strategies/goals/crab-sabotage-top.ts +197 -197
  136. package/src/strategies/goals/emergency-hunt-goal.ts +28 -28
  137. package/src/strategies/goals/find-player-top.ts +93 -93
  138. package/src/strategies/goals/flee-players-goal.ts +53 -53
  139. package/src/strategies/goals/follow-companion-goal.ts +106 -0
  140. package/src/strategies/goals/goal-manager.ts +41 -41
  141. package/src/strategies/goals/goal-root-strategy.ts +49 -49
  142. package/src/strategies/goals/goal.ts +28 -28
  143. package/src/strategies/goals/hide-top.ts +197 -0
  144. package/src/strategies/goals/keep-away-goal.ts +209 -209
  145. package/src/strategies/goals/kill-frenzy-top.ts +80 -80
  146. package/src/strategies/goals/kill-lone-top.ts +160 -160
  147. package/src/strategies/goals/kill-target-goal.ts +59 -59
  148. package/src/strategies/goals/kill-target-top.ts +109 -109
  149. package/src/strategies/goals/leaf-goal.ts +25 -25
  150. package/src/strategies/goals/linger-corpse-goal.ts +35 -79
  151. package/src/strategies/goals/lone-kill-core.ts +82 -82
  152. package/src/strategies/goals/lone-kill-goal.ts +24 -24
  153. package/src/strategies/goals/lone-kill-task-top.test.ts +85 -85
  154. package/src/strategies/goals/lone-kill-task-top.ts +86 -86
  155. package/src/strategies/goals/move-room-goal.ts +60 -60
  156. package/src/strategies/goals/normal-shrimp-top.test.ts +80 -80
  157. package/src/strategies/goals/normal-shrimp-top.ts +242 -242
  158. package/src/strategies/goals/paradise-fish-top.test.ts +126 -126
  159. package/src/strategies/goals/paradise-fish-top.ts +207 -219
  160. package/src/strategies/goals/patrol-top.ts +57 -57
  161. package/src/strategies/goals/report-patrol-top.ts +80 -80
  162. package/src/strategies/goals/safe-task-goal.ts +102 -102
  163. package/src/strategies/goals/social-task-top.ts +161 -161
  164. package/src/strategies/goals/task-kill-report-top.ts +163 -163
  165. package/src/strategies/goals/task-only-top.ts +57 -57
  166. package/src/strategies/goals/task-or-patrol-goal.ts +41 -41
  167. package/src/strategies/goals/task-report-top.ts +57 -57
  168. package/src/strategies/goals/wander-task-goal.ts +33 -33
  169. package/src/strategies/goals/warrior-shrimp-top.test.ts +86 -86
  170. package/src/strategies/goals/warrior-shrimp-top.ts +248 -248
  171. package/src/strategies/greeting.ts +53 -53
  172. package/src/strategies/hide-spots.ts +123 -0
  173. package/src/strategies/hide.ts +23 -0
  174. package/src/strategies/kill-frenzy.ts +12 -12
  175. package/src/strategies/kill-lone.knowledge.md +20 -20
  176. package/src/strategies/kill-lone.ts +13 -13
  177. package/src/strategies/kill-target.ts +18 -18
  178. package/src/strategies/loader.test.ts +678 -678
  179. package/src/strategies/loader.ts +172 -172
  180. package/src/strategies/lone-kill-task.ts +21 -21
  181. package/src/strategies/meeting-gate.test.ts +59 -59
  182. package/src/strategies/meeting-gate.ts +23 -23
  183. package/src/strategies/move-room.ts +15 -15
  184. package/src/strategies/new-events-backfill.ts +98 -98
  185. package/src/strategies/paradise-fish.knowledge.md +20 -20
  186. package/src/strategies/paradise-fish.ts +25 -25
  187. package/src/strategies/pathfind/distance-field.ts +150 -150
  188. package/src/strategies/pathfind/escape-planner.test.ts +197 -197
  189. package/src/strategies/pathfind/escape-planner.ts +355 -355
  190. package/src/strategies/pathfind/walkable-grid.ts +117 -117
  191. package/src/strategies/patrol.ts +11 -11
  192. package/src/strategies/player-targets.ts +13 -13
  193. package/src/strategies/report-patrol.ts +11 -11
  194. package/src/strategies/shrimp-memory.knowledge.md +20 -20
  195. package/src/strategies/shrimp-memory.ts +25 -25
  196. package/src/strategies/social-task.test.ts +28 -28
  197. package/src/strategies/social-task.ts +49 -49
  198. package/src/strategies/spawn.ts +82 -82
  199. package/src/strategies/speech-module.ts +123 -123
  200. package/src/strategies/strategy-loop.ts +771 -763
  201. package/src/strategies/task-kill-report.ts +17 -17
  202. package/src/strategies/task-only.ts +11 -11
  203. package/src/strategies/task-report.ts +22 -22
  204. package/src/strategies/types.ts +102 -96
  205. package/src/strategies/warrior-memory.knowledge.md +20 -20
  206. package/src/strategies/warrior-memory.ts +16 -16
@@ -0,0 +1,197 @@
1
+ import type { GameState, PlayerInfo, Position } from '../../sdk/types.js';
2
+ import { Action } from '../../sdk/action.js';
3
+ import {
4
+ canUseKill,
5
+ dist,
6
+ killCommitRange,
7
+ nonTeammatesVisible,
8
+ PROGRESS_INTERVAL_MS,
9
+ SHRIMP_KILL_RANGE,
10
+ } from '../game-utils.js';
11
+ import { nearestSafeHideSpot, type HideSpot } from '../hide-spots.js';
12
+ import { planEscape, type EscapeOptions } from '../pathfind/escape-planner.js';
13
+ import type { BehaviorDecision, StrategyContext } from '../types.js';
14
+ import { Goal } from './goal.js';
15
+ import { KeepAwayGoal } from './keep-away-goal.js';
16
+
17
+ const SUB_PRIORITY = 0.5;
18
+ const FLEE_KEY = 'hide-flee';
19
+ /** 走到躲藏点这个距离内就算到位,原地潜伏不动。 */
20
+ const REACHED_DISTANCE = 40;
21
+ /** 威胁离开视野后,其位置仍在选点时被回避这么久——逃完不会立刻又选回威胁刚待过的点。 */
22
+ const THREAT_MEMORY_MS = 5_000;
23
+ /**
24
+ * 「无处可逃」用的逃跑推演参数:与 KeepAwayGoal 同款保守模型(killRange=160,按枪虾/武士虾兜底)。
25
+ * 理由是「连我自己用来逃跑的同一套推演都判定必被追上,逃就没意义,才出刀自保」,判据自洽。
26
+ */
27
+ const ESCAPE_OPTS: EscapeOptions = { killRange: SHRIMP_KILL_RANGE, steps: 7, beamWidth: 8, directions: 16, fieldRadius: 900 };
28
+
29
+ type ProgressTier = 'flee' | 'relocate' | 'hidden';
30
+ type ThreatPointResolver = () => Position[];
31
+
32
+ /**
33
+ * 躲藏点劳作循环:与 SafeTaskOrPatrolGoal 同构——planSpot 边逃边预重算(粘性、威胁硬排除),
34
+ * tick 执行(走向躲藏点;到位则原地不动)。没有可用点时不动。
35
+ */
36
+ class HideSpotGoal extends Goal {
37
+ private current: HideSpot | null = null;
38
+
39
+ constructor(private readonly threatPoints: ThreatPointResolver) {
40
+ super();
41
+ }
42
+
43
+ get currentSpot(): HideSpot | null {
44
+ return this.current;
45
+ }
46
+
47
+ reset(): void {
48
+ this.current = null;
49
+ }
50
+
51
+ /** 重算并记住安全躲藏点,不发移动指令(逃跑期间可安全调用)。 */
52
+ planSpot(state: GameState, ctx: StrategyContext): HideSpot | null {
53
+ const spot = nearestSafeHideSpot(
54
+ { x: state.you.x, y: state.you.y },
55
+ this.threatPoints(),
56
+ { stickyTo: this.current, blockedTarget: ctx.blockedMoveTarget },
57
+ );
58
+ this.current = spot;
59
+ return spot;
60
+ }
61
+
62
+ tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
63
+ const spot = this.planSpot(state, ctx);
64
+ if (!spot) return [];
65
+ if (dist(state.you.x, state.you.y, spot.x, spot.y) <= REACHED_DISTANCE) return []; // 到位,原地潜伏
66
+ return [{ action: Action.move({ x: spot.x, y: spot.y }).withThinking(`前往躲藏点${spot.room}潜伏`) }];
67
+ }
68
+ }
69
+
70
+ /**
71
+ * 躲藏策略顶层:在离线算好的藏身角落里选测地最近且不挨威胁的一个潜伏不动,直到有人进入视野。
72
+ * 有人来就 ① 用 KeepAwayGoal 保持距离甩开,② 边逃边重算排除该威胁的新躲藏点——与 shrimp-memory 的
73
+ * 「躲人 + 重选任务」同构,只是把任务换成躲藏点。蟹不躲队友(威胁统一用 nonTeammatesVisible 过滤队友)。
74
+ *
75
+ * killWhenCornered:被**单个**对手逼进出刀距离、且 planEscape 判定无路可逃时才出刀自保。
76
+ * null = 按角色默认:蟹 / 章鱼开,武士虾 / 枪虾(及无刀角色)关。
77
+ */
78
+ export class HideTop extends Goal {
79
+ private readonly spotGoal = new HideSpotGoal(() => this.threatPointsFor());
80
+ private readonly recentThreats = new Map<string, { x: number; y: number; until: number }>();
81
+
82
+ constructor(private readonly killWhenCornered: boolean | null = null) {
83
+ super();
84
+ }
85
+
86
+ onMeetingResume(): void {
87
+ this.spotGoal.reset();
88
+ this.recentThreats.clear();
89
+ if (this.subGoal) this.removeSubGoal();
90
+ }
91
+
92
+ tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
93
+ const visible = nonTeammatesVisible(state, ctx);
94
+ this.rememberThreats(visible);
95
+
96
+ // ── 被单个对手逼到死角且刀好 → 出刀自保(按角色 / 参数开关) ──
97
+ if (this.killEnabled(state) && canUseKill(state)) {
98
+ const victim = this.corneredVictim(state, visible);
99
+ if (victim) {
100
+ if (this.subGoal) this.removeSubGoal();
101
+ ctx.notifications.push(`被${victim.name}逼到无处可逃,出刀自保!`);
102
+ return [{ action: Action.kill(victim.name).withThinking(`被${victim.name}逼进出刀距离又逃不掉,先下手自保。`) }];
103
+ }
104
+ }
105
+
106
+ // ── 视野里有人 → 保持距离甩开 + 边逃边重算安全躲藏点 ──
107
+ if (visible.length > 0) {
108
+ this.spotGoal.planSpot(state, ctx); // 重算(排除威胁),逃完落到新点
109
+ this.setFlee();
110
+ this.emitProgress(state, ctx, visible, 'flee');
111
+ return [];
112
+ }
113
+
114
+ // ── 无人 → 走向最近安全躲藏点并潜伏 ──
115
+ if (this.subGoal) this.removeSubGoal();
116
+ const decisions = this.spotGoal.tick(state, ctx);
117
+ this.emitProgress(state, ctx, visible, decisions.length === 0 ? 'hidden' : 'relocate');
118
+ return decisions;
119
+ }
120
+
121
+ private killEnabled(state: GameState): boolean {
122
+ if (!roleHasKnife(state)) return false; // 无刀角色(普通虾)永不出刀,传 kill 参数也不行
123
+ return this.killWhenCornered ?? defaultKillWhenCornered(state);
124
+ }
125
+
126
+ /** 单个对手、进入出刀距离、且 planEscape 判定必被追上 → 返回该对手,否则 null。 */
127
+ private corneredVictim(state: GameState, visible: PlayerInfo[]): PlayerInfo | null {
128
+ // 只在单个追兵时自保:2 人以上当众动手仍会被另一人举报,逃跑(推演偏悲观,真人有视野盲区)反而更优。
129
+ if (visible.length !== 1) return null;
130
+ const target = visible[0];
131
+ const d = target.distance ?? dist(state.you.x, state.you.y, target.x, target.y);
132
+ if (d > killCommitRange(state.you.role)) return null;
133
+ const plan = planEscape({ x: state.you.x, y: state.you.y }, [{ x: target.x, y: target.y }], ESCAPE_OPTS);
134
+ return plan.caught ? target : null;
135
+ }
136
+
137
+ private setFlee(): void {
138
+ const worker = this.subGoal;
139
+ if (worker instanceof KeepAwayGoal && worker.key === FLEE_KEY) return;
140
+ if (this.subGoal) this.removeSubGoal();
141
+ this.setSubGoal(new KeepAwayGoal(FLEE_KEY, (s, c) => nonTeammatesVisible(s, c), {
142
+ threatRadius: Infinity,
143
+ noun: '靠近的人',
144
+ }), SUB_PRIORITY);
145
+ }
146
+
147
+ /** 记住近期见过的非队友位置(按名字去重 + TTL),供威胁离开视野后选点继续回避。 */
148
+ private rememberThreats(visible: PlayerInfo[]): void {
149
+ const now = Date.now();
150
+ for (const p of visible) {
151
+ const key = p.name || String(p.seat ?? '');
152
+ if (key) this.recentThreats.set(key, { x: p.x, y: p.y, until: now + THREAT_MEMORY_MS });
153
+ }
154
+ for (const [key, v] of this.recentThreats) if (v.until <= now) this.recentThreats.delete(key);
155
+ }
156
+
157
+ private threatPointsFor(): Position[] {
158
+ const now = Date.now();
159
+ const out: Position[] = [];
160
+ for (const v of this.recentThreats.values()) if (v.until > now) out.push({ x: v.x, y: v.y });
161
+ return out;
162
+ }
163
+
164
+ private emitProgress(state: GameState, ctx: StrategyContext, visible: PlayerInfo[], tier: ProgressTier): void {
165
+ const now = Date.now();
166
+ if (now - ctx.lastProgressNotifyAt < PROGRESS_INTERVAL_MS) return;
167
+ ctx.lastProgressNotifyAt = now;
168
+
169
+ const room = state.you.room ?? '未知';
170
+ const spot = this.spotGoal.currentSpot;
171
+ let msg = `[进度] 当前在${room}`;
172
+ if (tier === 'flee') {
173
+ const names = visible.map(p => p.name).join('、');
174
+ msg += `,${names}靠近,保持距离甩开${spot ? `,改藏到${spot.room}` : ''}。`;
175
+ } else if (tier === 'relocate') {
176
+ msg += spot ? `,前往躲藏点${spot.room}潜伏。` : ',寻找躲藏点。';
177
+ } else {
178
+ msg += spot ? `,已在${spot.room}潜伏,按兵不动。` : ',附近无可用躲藏点,原地待命。';
179
+ }
180
+ ctx.notifications.push(msg);
181
+ }
182
+ }
183
+
184
+ /** 是否带刀(有击杀能力):蟹 / 章鱼 / 武士虾 / 枪虾。普通虾无刀。 */
185
+ function roleHasKnife(state: GameState): boolean {
186
+ const role = state.you.role ?? '';
187
+ const faction = state.you.faction ?? '';
188
+ return faction === 'crab' || role.includes('crab') || role.includes('octopus')
189
+ || role === 'shrimp_warrior' || role === 'shrimp_pistol';
190
+ }
191
+
192
+ /** 角色默认是否「死角自保出刀」:蟹 / 章鱼开;武士虾 / 枪虾及其余关。 */
193
+ function defaultKillWhenCornered(state: GameState): boolean {
194
+ const role = state.you.role ?? '';
195
+ const faction = state.you.faction ?? '';
196
+ return faction === 'crab' || role.includes('crab') || role.includes('octopus');
197
+ }
@@ -1,209 +1,209 @@
1
- import { Action } from '../../sdk/action.js';
2
- import type { GameState, PlayerInfo, Position } from '../../sdk/types.js';
3
- import { dist, SHRIMP_KILL_RANGE } from '../game-utils.js';
4
- import { assessEscapeTarget, planEscape, type EscapeOptions } from '../pathfind/escape-planner.js';
5
- import { loadWalkableGrid } from '../pathfind/walkable-grid.js';
6
- import type { BehaviorDecision, StrategyContext } from '../types.js';
7
- import { Goal } from './goal.js';
8
-
9
- const PANIC_RADIUS = 190;
10
- const CRITICAL_RADIUS = 100;
11
- const REACHED_DISTANCE = 28;
12
- const MIN_USEFUL_TARGET_DISTANCE = 120;
13
- const MAX_THREATS = 4;
14
- const CRITICAL_REPLAN_INTERVAL_MS = 1600;
15
- const PANIC_REPLAN_INTERVAL_MS = 2500;
16
- const PANIC_REPLAN_TARGET_SHIFT = 220;
17
- const PLAN_OPTS: EscapeOptions = { steps: 7, beamWidth: 8, directions: 16, fieldRadius: 900 };
18
- const FALLBACK_DISTANCES = [330, 260, 200, 140];
19
- const FALLBACK_ANGLE_OFFSETS = [0, Math.PI / 6, -Math.PI / 6, Math.PI / 3, -Math.PI / 3, Math.PI / 2, -Math.PI / 2, Math.PI];
20
-
21
- export type ThreatResolver = (state: GameState, ctx: StrategyContext) => PlayerInfo[];
22
-
23
- export interface KeepAwayGoalOptions {
24
- /** 交战兼结束半径(px);Infinity = resolver 返回的全算威胁。所有调用点必须显式传,无默认值。 */
25
- threatRadius: number;
26
- /** 半径内无威胁时 isFinish=true,供父节点自然回收;独立 keep-away 策略作为顶层须传 false,否则每个安静 tick 被 GoalManager 清掉再重建、idle 通知刷屏。 */
27
- finishWhenClear?: boolean;
28
- /** 无威胁时的 idle/coast 通知;作为子目标默认关闭(父节点自有进度播报)。 */
29
- idleNotices?: boolean;
30
- /** 通知措辞里威胁的称呼:发现{noun}{names},{verb}到 (x, y)。 */
31
- noun?: string;
32
- /** 覆盖 planEscape 参数(与 PLAN_OPTS 合并)。 */
33
- planOpts?: EscapeOptions;
34
- }
35
-
36
- function targetKey(target: Position): string {
37
- return `${Math.round(target.x)},${Math.round(target.y)}`;
38
- }
39
-
40
- function fallbackEscapeTarget(you: Position, threats: PlayerInfo[]): Position | null {
41
- if (threats.length === 0) return null;
42
- let vx = 0;
43
- let vy = 0;
44
- for (const t of threats) {
45
- const dx = you.x - t.x;
46
- const dy = you.y - t.y;
47
- const d = Math.max(1, Math.hypot(dx, dy));
48
- const weight = 1 / Math.max(80, d);
49
- vx += (dx / d) * weight;
50
- vy += (dy / d) * weight;
51
- }
52
- if (Math.hypot(vx, vy) < 0.001) {
53
- vx = you.x - threats[0].x;
54
- vy = you.y - threats[0].y;
55
- }
56
- if (Math.hypot(vx, vy) < 0.001) vx = 1;
57
-
58
- const grid = loadWalkableGrid();
59
- const baseAngle = Math.atan2(vy, vx);
60
- const nearestBefore = Math.min(...threats.map(t => dist(you.x, you.y, t.x, t.y)));
61
- let best: { target: Position; score: number } | null = null;
62
-
63
- for (const distancePx of FALLBACK_DISTANCES) {
64
- for (const offset of FALLBACK_ANGLE_OFFSETS) {
65
- const angle = baseAngle + offset;
66
- const raw = {
67
- x: you.x + Math.cos(angle) * distancePx,
68
- y: you.y + Math.sin(angle) * distancePx,
69
- };
70
- const cell = grid.snapToWalkable(grid.worldToCell(raw.x, raw.y), 10);
71
- if (cell < 0) continue;
72
- const target = grid.cellToWorld(cell);
73
- const moveDistance = dist(you.x, you.y, target.x, target.y);
74
- if (moveDistance < MIN_USEFUL_TARGET_DISTANCE) continue;
75
- const nearestAfter = Math.min(...threats.map(t => dist(target.x, target.y, t.x, t.y)));
76
- const score = (nearestAfter - nearestBefore) * 4 + moveDistance - Math.abs(offset) * 80;
77
- if (!best || score > best.score) best = { target, score };
78
- }
79
- }
80
-
81
- return best?.target ?? null;
82
- }
83
-
84
- /**
85
- * 寻路躲避 Goal:用路径推演(planEscape)给出较远可走点并承诺该目标,移动中滑行,
86
- * 只有路径变危险、被贴近或卡路才重规划。威胁来源、交战半径、结束条件均由构造参数定制,
87
- * 供独立 keep-away 测试策略与 NormalShrimpTop 的 A/B 档共用。
88
- */
89
- export class KeepAwayGoal extends Goal {
90
- private committed: Position | null = null;
91
- private lastNoticeKey = '';
92
- private lastCommitAt = 0;
93
- private lastCommitWasPanic = false;
94
- private readonly threatRadius: number;
95
- private readonly planOpts: EscapeOptions;
96
-
97
- constructor(
98
- public readonly key: string,
99
- private readonly resolveThreats: ThreatResolver,
100
- private readonly options: KeepAwayGoalOptions,
101
- ) {
102
- super();
103
- this.threatRadius = options.threatRadius;
104
- // 逃跑推演对「会不会被追上」用保守击杀距离:威胁在本层只有坐标、没有角色(resolver 返回
105
- // 的是可见非队友的裸位置,本游戏看不到他人身份),无法逐威胁取 killRangeFor(role),故统一用
106
- // 武士虾/枪虾的 SHRIMP_KILL_RANGE(160) 兜底,宁可对蟹/章鱼多拉开距离也不对带刀好人少逃。
107
- // escape-planner 自身默认 80(服务端 base kill_range),此处刻意覆盖。
108
- this.planOpts = { killRange: SHRIMP_KILL_RANGE, ...PLAN_OPTS, ...options.planOpts };
109
- }
110
-
111
- tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
112
- const you = state.you;
113
- const threats = this.threatsOf(state, ctx);
114
-
115
- if (threats.length === 0) return this.handleNoThreat(you, ctx);
116
-
117
- const threatPoints = threats.map(p => ({ x: p.x, y: p.y }));
118
- const nearestThreat = dist(threats[0].x, threats[0].y, you.x, you.y);
119
- const shouldPanic = nearestThreat <= PANIC_RADIUS;
120
-
121
- if (this.committed && (
122
- dist(you.x, you.y, this.committed.x, this.committed.y) <= REACHED_DISTANCE
123
- || (ctx.blockedMoveTarget && dist(this.committed.x, this.committed.y, ctx.blockedMoveTarget.x, ctx.blockedMoveTarget.y) <= 24)
124
- )) {
125
- this.committed = null;
126
- }
127
-
128
- const committedAssessment = this.committed
129
- ? assessEscapeTarget(you, threatPoints, this.committed, this.planOpts)
130
- : null;
131
- const committedBad = committedAssessment
132
- ? committedAssessment.caught || committedAssessment.distanceToTarget === Infinity
133
- : false;
134
-
135
- if (this.committed && !shouldPanic && !committedBad) return [];
136
- if (this.committed && shouldPanic) {
137
- const elapsed = Date.now() - this.lastCommitAt;
138
- const isCritical = nearestThreat <= CRITICAL_RADIUS;
139
- if (!committedBad && isCritical && this.lastCommitWasPanic && elapsed < CRITICAL_REPLAN_INTERVAL_MS) return [];
140
- if (!committedBad && !isCritical && elapsed < PANIC_REPLAN_INTERVAL_MS) return [];
141
- }
142
-
143
- if (!this.committed || shouldPanic || committedBad) {
144
- const plan = planEscape(you, threatPoints, this.planOpts);
145
- const target = plan.target;
146
- const targetDistance = dist(you.x, you.y, target.x, target.y);
147
- const caughtDetail = plan.caught
148
- ? `规划已被追上,逃点${Math.round(targetDistance)}px,最近威胁${Math.round(nearestThreat)}px,最小安全距${Math.round(plan.minCrabDistance)}px。`
149
- : '';
150
- if (targetDistance < MIN_USEFUL_TARGET_DISTANCE) {
151
- const fallback = fallbackEscapeTarget(you, threats);
152
- const detail = `规划${plan.caught ? '已被追上' : '逃点太近'},逃点${Math.round(targetDistance)}px,最近威胁${Math.round(nearestThreat)}px,最小安全距${Math.round(plan.minCrabDistance)}px。`;
153
- if (!fallback) {
154
- this.notice(ctx, `planner-stuck:${Math.round(nearestThreat)}:${Math.round(targetDistance)}`, `躲避规划没有可用逃点:${detail}`);
155
- return [];
156
- }
157
- this.commit(ctx, fallback, threats, '兜底远离', true, detail);
158
- } else if (!this.committed) {
159
- this.commit(ctx, target, threats, plan.caught ? '挣脱' : shouldPanic ? '急避' : '远离', shouldPanic, caughtDetail);
160
- } else if (dist(target.x, target.y, this.committed.x, this.committed.y) > PANIC_REPLAN_TARGET_SHIFT) {
161
- this.commit(ctx, target, threats, plan.caught ? '改挣脱' : '改避', true, caughtDetail);
162
- } else {
163
- return [];
164
- }
165
- }
166
-
167
- const target = this.committed!;
168
- return [{ action: Action.move(target).withThinking(`躲避${threats.map(p => p.name).join('、')},前往 (${Math.round(target.x)}, ${Math.round(target.y)})`) }];
169
- }
170
-
171
- isFinish(state: GameState, ctx: StrategyContext): boolean {
172
- return (this.options.finishWhenClear ?? true) && this.threatsOf(state, ctx).length === 0;
173
- }
174
-
175
- private threatsOf(state: GameState, ctx: StrategyContext): PlayerInfo[] {
176
- const you = state.you;
177
- return this.resolveThreats(state, ctx)
178
- .filter(p => this.threatRadius === Infinity || dist(p.x, p.y, you.x, you.y) < this.threatRadius)
179
- .sort((a, b) => dist(a.x, a.y, you.x, you.y) - dist(b.x, b.y, you.x, you.y))
180
- .slice(0, MAX_THREATS);
181
- }
182
-
183
- private handleNoThreat(you: Position & { currently_moving?: boolean }, ctx: StrategyContext): BehaviorDecision[] {
184
- this.committed = null;
185
- if (this.options.idleNotices) {
186
- if (!you.currently_moving) this.notice(ctx, 'idle', `附近 ${this.threatRadius}px 内无人,原地待命。`);
187
- else this.notice(ctx, 'coast', '威胁解除,继续完成当前移动,不再补发停步指令。');
188
- }
189
- return [];
190
- }
191
-
192
- private commit(ctx: StrategyContext, target: Position, threats: PlayerInfo[], verb: string, panic = false, detail = ''): void {
193
- this.committed = target;
194
- this.lastCommitAt = Date.now();
195
- this.lastCommitWasPanic = panic;
196
- const names = threats.map(p => p.name).join('、');
197
- this.notice(
198
- ctx,
199
- `${verb}:${targetKey(target)}`,
200
- `发现${this.options.noun ?? ''}${names},${verb}到 (${Math.round(target.x)}, ${Math.round(target.y)})。${detail}`,
201
- );
202
- }
203
-
204
- private notice(ctx: StrategyContext, key: string, msg: string): void {
205
- if (key === this.lastNoticeKey) return;
206
- this.lastNoticeKey = key;
207
- ctx.notifications.push(msg);
208
- }
209
- }
1
+ import { Action } from '../../sdk/action.js';
2
+ import type { GameState, PlayerInfo, Position } from '../../sdk/types.js';
3
+ import { dist, SHRIMP_KILL_RANGE } from '../game-utils.js';
4
+ import { assessEscapeTarget, planEscape, type EscapeOptions } from '../pathfind/escape-planner.js';
5
+ import { loadWalkableGrid } from '../pathfind/walkable-grid.js';
6
+ import type { BehaviorDecision, StrategyContext } from '../types.js';
7
+ import { Goal } from './goal.js';
8
+
9
+ const PANIC_RADIUS = 190;
10
+ const CRITICAL_RADIUS = 100;
11
+ const REACHED_DISTANCE = 28;
12
+ const MIN_USEFUL_TARGET_DISTANCE = 120;
13
+ const MAX_THREATS = 4;
14
+ const CRITICAL_REPLAN_INTERVAL_MS = 1600;
15
+ const PANIC_REPLAN_INTERVAL_MS = 2500;
16
+ const PANIC_REPLAN_TARGET_SHIFT = 220;
17
+ const PLAN_OPTS: EscapeOptions = { steps: 7, beamWidth: 8, directions: 16, fieldRadius: 900 };
18
+ const FALLBACK_DISTANCES = [330, 260, 200, 140];
19
+ const FALLBACK_ANGLE_OFFSETS = [0, Math.PI / 6, -Math.PI / 6, Math.PI / 3, -Math.PI / 3, Math.PI / 2, -Math.PI / 2, Math.PI];
20
+
21
+ export type ThreatResolver = (state: GameState, ctx: StrategyContext) => PlayerInfo[];
22
+
23
+ export interface KeepAwayGoalOptions {
24
+ /** 交战兼结束半径(px);Infinity = resolver 返回的全算威胁。所有调用点必须显式传,无默认值。 */
25
+ threatRadius: number;
26
+ /** 半径内无威胁时 isFinish=true,供父节点自然回收;独立 keep-away 策略作为顶层须传 false,否则每个安静 tick 被 GoalManager 清掉再重建、idle 通知刷屏。 */
27
+ finishWhenClear?: boolean;
28
+ /** 无威胁时的 idle/coast 通知;作为子目标默认关闭(父节点自有进度播报)。 */
29
+ idleNotices?: boolean;
30
+ /** 通知措辞里威胁的称呼:发现{noun}{names},{verb}到 (x, y)。 */
31
+ noun?: string;
32
+ /** 覆盖 planEscape 参数(与 PLAN_OPTS 合并)。 */
33
+ planOpts?: EscapeOptions;
34
+ }
35
+
36
+ function targetKey(target: Position): string {
37
+ return `${Math.round(target.x)},${Math.round(target.y)}`;
38
+ }
39
+
40
+ function fallbackEscapeTarget(you: Position, threats: PlayerInfo[]): Position | null {
41
+ if (threats.length === 0) return null;
42
+ let vx = 0;
43
+ let vy = 0;
44
+ for (const t of threats) {
45
+ const dx = you.x - t.x;
46
+ const dy = you.y - t.y;
47
+ const d = Math.max(1, Math.hypot(dx, dy));
48
+ const weight = 1 / Math.max(80, d);
49
+ vx += (dx / d) * weight;
50
+ vy += (dy / d) * weight;
51
+ }
52
+ if (Math.hypot(vx, vy) < 0.001) {
53
+ vx = you.x - threats[0].x;
54
+ vy = you.y - threats[0].y;
55
+ }
56
+ if (Math.hypot(vx, vy) < 0.001) vx = 1;
57
+
58
+ const grid = loadWalkableGrid();
59
+ const baseAngle = Math.atan2(vy, vx);
60
+ const nearestBefore = Math.min(...threats.map(t => dist(you.x, you.y, t.x, t.y)));
61
+ let best: { target: Position; score: number } | null = null;
62
+
63
+ for (const distancePx of FALLBACK_DISTANCES) {
64
+ for (const offset of FALLBACK_ANGLE_OFFSETS) {
65
+ const angle = baseAngle + offset;
66
+ const raw = {
67
+ x: you.x + Math.cos(angle) * distancePx,
68
+ y: you.y + Math.sin(angle) * distancePx,
69
+ };
70
+ const cell = grid.snapToWalkable(grid.worldToCell(raw.x, raw.y), 10);
71
+ if (cell < 0) continue;
72
+ const target = grid.cellToWorld(cell);
73
+ const moveDistance = dist(you.x, you.y, target.x, target.y);
74
+ if (moveDistance < MIN_USEFUL_TARGET_DISTANCE) continue;
75
+ const nearestAfter = Math.min(...threats.map(t => dist(target.x, target.y, t.x, t.y)));
76
+ const score = (nearestAfter - nearestBefore) * 4 + moveDistance - Math.abs(offset) * 80;
77
+ if (!best || score > best.score) best = { target, score };
78
+ }
79
+ }
80
+
81
+ return best?.target ?? null;
82
+ }
83
+
84
+ /**
85
+ * 寻路躲避 Goal:用路径推演(planEscape)给出较远可走点并承诺该目标,移动中滑行,
86
+ * 只有路径变危险、被贴近或卡路才重规划。威胁来源、交战半径、结束条件均由构造参数定制,
87
+ * 供独立 keep-away 测试策略与 NormalShrimpTop 的 A/B 档共用。
88
+ */
89
+ export class KeepAwayGoal extends Goal {
90
+ private committed: Position | null = null;
91
+ private lastNoticeKey = '';
92
+ private lastCommitAt = 0;
93
+ private lastCommitWasPanic = false;
94
+ private readonly threatRadius: number;
95
+ private readonly planOpts: EscapeOptions;
96
+
97
+ constructor(
98
+ public readonly key: string,
99
+ private readonly resolveThreats: ThreatResolver,
100
+ private readonly options: KeepAwayGoalOptions,
101
+ ) {
102
+ super();
103
+ this.threatRadius = options.threatRadius;
104
+ // 逃跑推演对「会不会被追上」用保守击杀距离:威胁在本层只有坐标、没有角色(resolver 返回
105
+ // 的是可见非队友的裸位置,本游戏看不到他人身份),无法逐威胁取 killRangeFor(role),故统一用
106
+ // 武士虾/枪虾的 SHRIMP_KILL_RANGE(160) 兜底,宁可对蟹/章鱼多拉开距离也不对带刀好人少逃。
107
+ // escape-planner 自身默认 80(服务端 base kill_range),此处刻意覆盖。
108
+ this.planOpts = { killRange: SHRIMP_KILL_RANGE, ...PLAN_OPTS, ...options.planOpts };
109
+ }
110
+
111
+ tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
112
+ const you = state.you;
113
+ const threats = this.threatsOf(state, ctx);
114
+
115
+ if (threats.length === 0) return this.handleNoThreat(you, ctx);
116
+
117
+ const threatPoints = threats.map(p => ({ x: p.x, y: p.y }));
118
+ const nearestThreat = dist(threats[0].x, threats[0].y, you.x, you.y);
119
+ const shouldPanic = nearestThreat <= PANIC_RADIUS;
120
+
121
+ if (this.committed && (
122
+ dist(you.x, you.y, this.committed.x, this.committed.y) <= REACHED_DISTANCE
123
+ || (ctx.blockedMoveTarget && dist(this.committed.x, this.committed.y, ctx.blockedMoveTarget.x, ctx.blockedMoveTarget.y) <= 24)
124
+ )) {
125
+ this.committed = null;
126
+ }
127
+
128
+ const committedAssessment = this.committed
129
+ ? assessEscapeTarget(you, threatPoints, this.committed, this.planOpts)
130
+ : null;
131
+ const committedBad = committedAssessment
132
+ ? committedAssessment.caught || committedAssessment.distanceToTarget === Infinity
133
+ : false;
134
+
135
+ if (this.committed && !shouldPanic && !committedBad) return [];
136
+ if (this.committed && shouldPanic) {
137
+ const elapsed = Date.now() - this.lastCommitAt;
138
+ const isCritical = nearestThreat <= CRITICAL_RADIUS;
139
+ if (!committedBad && isCritical && this.lastCommitWasPanic && elapsed < CRITICAL_REPLAN_INTERVAL_MS) return [];
140
+ if (!committedBad && !isCritical && elapsed < PANIC_REPLAN_INTERVAL_MS) return [];
141
+ }
142
+
143
+ if (!this.committed || shouldPanic || committedBad) {
144
+ const plan = planEscape(you, threatPoints, this.planOpts);
145
+ const target = plan.target;
146
+ const targetDistance = dist(you.x, you.y, target.x, target.y);
147
+ const caughtDetail = plan.caught
148
+ ? `规划已被追上,逃点${Math.round(targetDistance)}px,最近威胁${Math.round(nearestThreat)}px,最小安全距${Math.round(plan.minCrabDistance)}px。`
149
+ : '';
150
+ if (targetDistance < MIN_USEFUL_TARGET_DISTANCE) {
151
+ const fallback = fallbackEscapeTarget(you, threats);
152
+ const detail = `规划${plan.caught ? '已被追上' : '逃点太近'},逃点${Math.round(targetDistance)}px,最近威胁${Math.round(nearestThreat)}px,最小安全距${Math.round(plan.minCrabDistance)}px。`;
153
+ if (!fallback) {
154
+ this.notice(ctx, `planner-stuck:${Math.round(nearestThreat)}:${Math.round(targetDistance)}`, `躲避规划没有可用逃点:${detail}`);
155
+ return [];
156
+ }
157
+ this.commit(ctx, fallback, threats, '兜底远离', true, detail);
158
+ } else if (!this.committed) {
159
+ this.commit(ctx, target, threats, plan.caught ? '挣脱' : shouldPanic ? '急避' : '远离', shouldPanic, caughtDetail);
160
+ } else if (dist(target.x, target.y, this.committed.x, this.committed.y) > PANIC_REPLAN_TARGET_SHIFT) {
161
+ this.commit(ctx, target, threats, plan.caught ? '改挣脱' : '改避', true, caughtDetail);
162
+ } else {
163
+ return [];
164
+ }
165
+ }
166
+
167
+ const target = this.committed!;
168
+ return [{ action: Action.move(target).withThinking(`躲避${threats.map(p => p.name).join('、')},拉开距离`) }];
169
+ }
170
+
171
+ isFinish(state: GameState, ctx: StrategyContext): boolean {
172
+ return (this.options.finishWhenClear ?? true) && this.threatsOf(state, ctx).length === 0;
173
+ }
174
+
175
+ private threatsOf(state: GameState, ctx: StrategyContext): PlayerInfo[] {
176
+ const you = state.you;
177
+ return this.resolveThreats(state, ctx)
178
+ .filter(p => this.threatRadius === Infinity || dist(p.x, p.y, you.x, you.y) < this.threatRadius)
179
+ .sort((a, b) => dist(a.x, a.y, you.x, you.y) - dist(b.x, b.y, you.x, you.y))
180
+ .slice(0, MAX_THREATS);
181
+ }
182
+
183
+ private handleNoThreat(you: Position & { currently_moving?: boolean }, ctx: StrategyContext): BehaviorDecision[] {
184
+ this.committed = null;
185
+ if (this.options.idleNotices) {
186
+ if (!you.currently_moving) this.notice(ctx, 'idle', `附近 ${this.threatRadius}px 内无人,原地待命。`);
187
+ else this.notice(ctx, 'coast', '威胁解除,继续完成当前移动,不再补发停步指令。');
188
+ }
189
+ return [];
190
+ }
191
+
192
+ private commit(ctx: StrategyContext, target: Position, threats: PlayerInfo[], verb: string, panic = false, detail = ''): void {
193
+ this.committed = target;
194
+ this.lastCommitAt = Date.now();
195
+ this.lastCommitWasPanic = panic;
196
+ const names = threats.map(p => p.name).join('、');
197
+ this.notice(
198
+ ctx,
199
+ `${verb}:${targetKey(target)}`,
200
+ `发现${this.options.noun ?? ''}${names},${verb}到 (${Math.round(target.x)}, ${Math.round(target.y)})。${detail}`,
201
+ );
202
+ }
203
+
204
+ private notice(ctx: StrategyContext, key: string, msg: string): void {
205
+ if (key === this.lastNoticeKey) return;
206
+ this.lastNoticeKey = key;
207
+ ctx.notifications.push(msg);
208
+ }
209
+ }