@myclaw163/clawclaw-cli 0.6.54

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 (198) hide show
  1. package/README.md +440 -0
  2. package/bin/clawclaw-cli.mjs +4 -0
  3. package/package.json +48 -0
  4. package/personas//347/220/206/346/231/272/346/270/251/345/222/214.md +23 -0
  5. package/personas//350/200/201/350/260/213/346/267/261/347/256/227.md +22 -0
  6. package/personas//350/257/232/346/201/263/347/233/264/347/216/207.md +22 -0
  7. package/personas//350/275/273/346/235/276/346/264/273/346/263/274.md +22 -0
  8. package/personas//351/207/216/346/200/247/345/217/233/351/200/206.md +23 -0
  9. package/scripts/postinstall.mjs +20 -0
  10. package/scripts/sync-bundled-skill.mjs +245 -0
  11. package/scripts/sync-bundled-skill.test.mjs +152 -0
  12. package/skills/clawclaw/SKILL.md +240 -0
  13. package/skills/clawclaw/references/CHATTERBOX.md +142 -0
  14. package/skills/clawclaw/references/COMMANDS.md +132 -0
  15. package/skills/clawclaw/references/GAME-MECHANICS.md +186 -0
  16. package/skills/clawclaw/references/HUB.md +48 -0
  17. package/skills/clawclaw/references/KNOWLEDGE.md +43 -0
  18. package/skills/clawclaw/references/STRATEGIES.md +57 -0
  19. package/skills/clawclaw/references/STREAM.md +58 -0
  20. package/skills/clawclaw/references/TACTICS.md +65 -0
  21. package/src/assets/clawclaw-ascii-map.txt +40 -0
  22. package/src/cli.ts +153 -0
  23. package/src/commands/_schema.ts +109 -0
  24. package/src/commands/account.ts +209 -0
  25. package/src/commands/config.ts +30 -0
  26. package/src/commands/do.test.ts +37 -0
  27. package/src/commands/do.ts +95 -0
  28. package/src/commands/events.ts +22 -0
  29. package/src/commands/game-map.test.ts +28 -0
  30. package/src/commands/game-start-plan.test.ts +142 -0
  31. package/src/commands/game.ts +882 -0
  32. package/src/commands/history-player.test.ts +102 -0
  33. package/src/commands/history.ts +573 -0
  34. package/src/commands/hub.test.ts +96 -0
  35. package/src/commands/hub.ts +234 -0
  36. package/src/commands/knowledge.test.ts +19 -0
  37. package/src/commands/knowledge.ts +168 -0
  38. package/src/commands/load.test.ts +51 -0
  39. package/src/commands/load.ts +13 -0
  40. package/src/commands/meeting-history.test.ts +106 -0
  41. package/src/commands/memory.ts +40 -0
  42. package/src/commands/peek.ts +38 -0
  43. package/src/commands/persona.ts +57 -0
  44. package/src/commands/setup/codex.ts +248 -0
  45. package/src/commands/setup/hermes.test.ts +96 -0
  46. package/src/commands/setup/hermes.ts +76 -0
  47. package/src/commands/setup/index.ts +13 -0
  48. package/src/commands/setup/openclaw.test.ts +114 -0
  49. package/src/commands/setup/openclaw.ts +147 -0
  50. package/src/commands/skill.ts +128 -0
  51. package/src/commands/state.ts +46 -0
  52. package/src/commands/strategy.test.ts +135 -0
  53. package/src/commands/strategy.ts +189 -0
  54. package/src/commands/tts.ts +128 -0
  55. package/src/commands/upgrade.test.ts +91 -0
  56. package/src/commands/upgrade.ts +154 -0
  57. package/src/commands/watch.test.ts +973 -0
  58. package/src/commands/watch.ts +709 -0
  59. package/src/lib/auth.test.ts +59 -0
  60. package/src/lib/auth.ts +186 -0
  61. package/src/lib/command-meta.ts +37 -0
  62. package/src/lib/game-client.ts +391 -0
  63. package/src/lib/host-config-patcher.test.ts +130 -0
  64. package/src/lib/host-config-patcher.ts +151 -0
  65. package/src/lib/http-keepalive.ts +15 -0
  66. package/src/lib/http-transport.test.ts +42 -0
  67. package/src/lib/http-transport.ts +113 -0
  68. package/src/lib/hub-client.test.ts +56 -0
  69. package/src/lib/hub-client.ts +88 -0
  70. package/src/lib/hub-install.test.ts +98 -0
  71. package/src/lib/hub-install.ts +121 -0
  72. package/src/lib/hub-reminder.ts +75 -0
  73. package/src/lib/hub-unzip.test.ts +69 -0
  74. package/src/lib/hub-unzip.ts +62 -0
  75. package/src/lib/init-command.test.ts +75 -0
  76. package/src/lib/init-command.ts +120 -0
  77. package/src/lib/knowledge-store.test.ts +180 -0
  78. package/src/lib/knowledge-store.ts +374 -0
  79. package/src/lib/load-context.test.ts +52 -0
  80. package/src/lib/load-context.ts +52 -0
  81. package/src/lib/match-state.test.ts +134 -0
  82. package/src/lib/match-state.ts +94 -0
  83. package/src/lib/netease-tts.ts +83 -0
  84. package/src/lib/normalize.ts +42 -0
  85. package/src/lib/persona.test.ts +41 -0
  86. package/src/lib/persona.ts +72 -0
  87. package/src/lib/server-registry.ts +152 -0
  88. package/src/lib/skill-version.test.ts +48 -0
  89. package/src/lib/skill-version.ts +19 -0
  90. package/src/lib/strategy-export.test.ts +232 -0
  91. package/src/lib/strategy-export.ts +242 -0
  92. package/src/lib/tts-keys.ts +7 -0
  93. package/src/lib/tts-speech.test.ts +63 -0
  94. package/src/lib/tts-speech.ts +76 -0
  95. package/src/lib/workspace-argv.test.ts +49 -0
  96. package/src/lib/workspace-argv.ts +44 -0
  97. package/src/perception/player-history-store.test.ts +87 -0
  98. package/src/perception/player-history-store.ts +194 -0
  99. package/src/pipeline/event-store.ts +124 -0
  100. package/src/pipeline/pipeline.ts +35 -0
  101. package/src/runtime/auto-upgrade.test.ts +66 -0
  102. package/src/runtime/auto-upgrade.ts +31 -0
  103. package/src/runtime/daemon.ts +100 -0
  104. package/src/runtime/event-daemon.test.ts +28 -0
  105. package/src/runtime/event-daemon.ts +371 -0
  106. package/src/runtime/opening-mover.ts +303 -0
  107. package/src/runtime/raw-ws-log.test.ts +33 -0
  108. package/src/runtime/raw-ws-log.ts +32 -0
  109. package/src/runtime/runtime-logger.ts +99 -0
  110. package/src/runtime/ws-client.test.ts +47 -0
  111. package/src/runtime/ws-client.ts +272 -0
  112. package/src/sdk/action.ts +166 -0
  113. package/src/sdk/index.ts +110 -0
  114. package/src/sdk/types.ts +146 -0
  115. package/src/strategies/avoid-lone.ts +11 -0
  116. package/src/strategies/avoid-players.knowledge.md +20 -0
  117. package/src/strategies/avoid-players.ts +15 -0
  118. package/src/strategies/corpse-patrol.ts +22 -0
  119. package/src/strategies/crab-sabotage.ts +21 -0
  120. package/src/strategies/custom-module.test.ts +269 -0
  121. package/src/strategies/find-player.ts +16 -0
  122. package/src/strategies/game-utils.test.ts +164 -0
  123. package/src/strategies/game-utils.ts +721 -0
  124. package/src/strategies/goals/avoid-lone-top.ts +168 -0
  125. package/src/strategies/goals/avoid-players-top.test.ts +83 -0
  126. package/src/strategies/goals/avoid-players-top.ts +121 -0
  127. package/src/strategies/goals/conversation-goal.ts +51 -0
  128. package/src/strategies/goals/corpse-patrol-top.ts +91 -0
  129. package/src/strategies/goals/crab-octopus-reflexes.ts +93 -0
  130. package/src/strategies/goals/crab-sabotage-top.ts +197 -0
  131. package/src/strategies/goals/emergency-hunt-goal.ts +28 -0
  132. package/src/strategies/goals/find-player-top.ts +93 -0
  133. package/src/strategies/goals/flee-players-goal.ts +53 -0
  134. package/src/strategies/goals/goal-manager.ts +41 -0
  135. package/src/strategies/goals/goal-root-strategy.ts +49 -0
  136. package/src/strategies/goals/goal.ts +28 -0
  137. package/src/strategies/goals/keep-away-goal.ts +206 -0
  138. package/src/strategies/goals/kill-frenzy-top.ts +80 -0
  139. package/src/strategies/goals/kill-lone-top.ts +160 -0
  140. package/src/strategies/goals/kill-target-goal.ts +59 -0
  141. package/src/strategies/goals/kill-target-top.ts +109 -0
  142. package/src/strategies/goals/leaf-goal.ts +25 -0
  143. package/src/strategies/goals/linger-corpse-goal.ts +79 -0
  144. package/src/strategies/goals/lone-kill-core.ts +82 -0
  145. package/src/strategies/goals/lone-kill-goal.ts +24 -0
  146. package/src/strategies/goals/lone-kill-task-top.test.ts +85 -0
  147. package/src/strategies/goals/lone-kill-task-top.ts +86 -0
  148. package/src/strategies/goals/move-room-goal.ts +60 -0
  149. package/src/strategies/goals/normal-shrimp-top.test.ts +80 -0
  150. package/src/strategies/goals/normal-shrimp-top.ts +242 -0
  151. package/src/strategies/goals/paradise-fish-top.test.ts +126 -0
  152. package/src/strategies/goals/paradise-fish-top.ts +219 -0
  153. package/src/strategies/goals/patrol-top.ts +57 -0
  154. package/src/strategies/goals/report-patrol-top.ts +80 -0
  155. package/src/strategies/goals/safe-task-goal.ts +102 -0
  156. package/src/strategies/goals/social-task-top.ts +161 -0
  157. package/src/strategies/goals/task-kill-report-top.ts +163 -0
  158. package/src/strategies/goals/task-only-top.ts +57 -0
  159. package/src/strategies/goals/task-or-patrol-goal.ts +41 -0
  160. package/src/strategies/goals/task-report-top.ts +57 -0
  161. package/src/strategies/goals/wander-task-goal.ts +33 -0
  162. package/src/strategies/goals/warrior-shrimp-top.test.ts +86 -0
  163. package/src/strategies/goals/warrior-shrimp-top.ts +248 -0
  164. package/src/strategies/greeting.ts +53 -0
  165. package/src/strategies/kill-frenzy.ts +12 -0
  166. package/src/strategies/kill-lone.knowledge.md +20 -0
  167. package/src/strategies/kill-lone.ts +13 -0
  168. package/src/strategies/kill-target.ts +18 -0
  169. package/src/strategies/loader.test.ts +678 -0
  170. package/src/strategies/loader.ts +172 -0
  171. package/src/strategies/lone-kill-task.ts +21 -0
  172. package/src/strategies/meeting-gate.test.ts +59 -0
  173. package/src/strategies/meeting-gate.ts +23 -0
  174. package/src/strategies/move-room.ts +15 -0
  175. package/src/strategies/new-events-backfill.ts +98 -0
  176. package/src/strategies/paradise-fish.knowledge.md +20 -0
  177. package/src/strategies/paradise-fish.ts +25 -0
  178. package/src/strategies/pathfind/clawclaw-walkable.bin +0 -0
  179. package/src/strategies/pathfind/distance-field.ts +150 -0
  180. package/src/strategies/pathfind/escape-planner.test.ts +197 -0
  181. package/src/strategies/pathfind/escape-planner.ts +348 -0
  182. package/src/strategies/pathfind/walkable-grid.ts +117 -0
  183. package/src/strategies/patrol.ts +11 -0
  184. package/src/strategies/player-targets.ts +13 -0
  185. package/src/strategies/report-patrol.ts +11 -0
  186. package/src/strategies/shrimp-memory.knowledge.md +20 -0
  187. package/src/strategies/shrimp-memory.ts +25 -0
  188. package/src/strategies/social-task.test.ts +28 -0
  189. package/src/strategies/social-task.ts +49 -0
  190. package/src/strategies/spawn.ts +71 -0
  191. package/src/strategies/speech-module.ts +123 -0
  192. package/src/strategies/strategy-loop.ts +757 -0
  193. package/src/strategies/task-kill-report.ts +17 -0
  194. package/src/strategies/task-only.ts +11 -0
  195. package/src/strategies/task-report.ts +22 -0
  196. package/src/strategies/types.ts +96 -0
  197. package/src/strategies/warrior-memory.knowledge.md +20 -0
  198. package/src/strategies/warrior-memory.ts +16 -0
@@ -0,0 +1,168 @@
1
+ import type { GameState, PlayerInfo } from '../../sdk/types.js';
2
+ import { dist, hasReachedRoomTarget, PatrolState, PROGRESS_INTERVAL_MS, pursueVisibleTarget } from '../game-utils.js';
3
+ import type { BehaviorDecision, RoomTarget, StrategyContext } from '../types.js';
4
+ import { Goal } from './goal.js';
5
+ import { KeepAwayGoal, type ThreatResolver } from './keep-away-goal.js';
6
+ import { MoveRoomGoal, type RoomArrival } from './move-room-goal.js';
7
+
8
+ const PATROL_PRIORITY = 0.2;
9
+ const MOVE_PRIORITY = 0.5;
10
+ const EMPTY_GRACE_MS = 8_000;
11
+ // 次近玩家落在这个范围内就原地待着;超出才迈步靠近。须 < 视野半径,留边缘余量。
12
+ const GATHER_FOLLOW_DISTANCE = 200;
13
+
14
+ type MoveMode = 'patrol' | 'flee' | 'gather';
15
+
16
+ export class AvoidLoneTop extends Goal {
17
+ private readonly patrol = new PatrolState();
18
+ private emptySince: number | null = null;
19
+ private moveMode: MoveMode | null = null;
20
+ private lastNoticeKey = '';
21
+
22
+ tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
23
+ const now = Date.now();
24
+ const visible = state.players;
25
+
26
+ if (visible.length === 0) {
27
+ if (this.emptySince == null) this.emptySince = now;
28
+ // 有「上一条移动」要守护时就尊重 8 秒空视野宽限:flee 靠子目标续命(KeepAwayGoal),
29
+ // gather 是内联返回的跟随 move(无子目标),靠 moveMode==='gather' 标记守护——
30
+ // 否则人短暂出视野会被立刻改判巡逻、覆盖掉刚发出的跟随移动。
31
+ const hasMoveToHold = this.subGoal != null || this.moveMode === 'gather';
32
+ if (!hasMoveToHold || now - this.emptySince >= EMPTY_GRACE_MS) {
33
+ this.setPatrolGoal(state, ctx);
34
+ this.emitProgress(state, ctx, 'patrol', null, visible);
35
+ } else {
36
+ this.emitProgress(state, ctx, this.moveMode, null, visible); // 宽限期内维持原目标
37
+ }
38
+ return [];
39
+ }
40
+
41
+ this.emptySince = null;
42
+
43
+ if (visible.length === 1) {
44
+ const lone = visible[0].name;
45
+ this.setKeepAway(`single:${lone}`,
46
+ currentState => currentState.players.filter(player => player.name === lone));
47
+ if (!this.emitNotice(ctx, `flee:${lone}`,
48
+ `视野内只有${lone},正在寻路避免落单。`))
49
+ this.emitProgress(state, ctx, 'flee', null, visible);
50
+ return [];
51
+ }
52
+
53
+ // 2+ 人:跟住「次近」的人。最近的人本就贴着、最不会掉出视野;真正卡在
54
+ // 「≥2 人 / 跌回 1 人独处」临界点上的是第二近的人,盯住他、保持在视野舒适范围内即可维持人数。
55
+ this.clearGoal();
56
+ this.moveMode = 'gather';
57
+ const anchor = [...visible].sort((a, b) => this.distOf(state, a) - this.distOf(state, b))[1]; // length>=2 必存在
58
+ const decision = pursueVisibleTarget(state, anchor, { kill: false, followDistance: GATHER_FOLLOW_DISTANCE });
59
+ if (decision) {
60
+ if (!this.emitNotice(ctx, `gather:follow:${anchor.name}`,
61
+ `视野内${visible.length}人,正在靠近${anchor.name}维持人数。`))
62
+ this.emitProgress(state, ctx, 'gather', null, visible);
63
+ return [{
64
+ action: decision.action.withThinking(
65
+ `跟住次近的${anchor.name},维持视野内至少两人,避免落单。`,
66
+ ),
67
+ }];
68
+ }
69
+ if (!this.emitNotice(ctx, `gather:hold:${visible.length}`,
70
+ `视野内${visible.length}人,已和大家在一起保持安全。`))
71
+ this.emitProgress(state, ctx, 'gather', null, visible);
72
+ return [];
73
+ }
74
+
75
+ private distOf(state: GameState, p: PlayerInfo): number {
76
+ return p.distance ?? dist(state.you.x, state.you.y, p.x, p.y);
77
+ }
78
+
79
+ private setPatrolGoal(state: GameState, ctx: StrategyContext): void {
80
+ let room = this.patrol.nextRoom(ctx);
81
+ if (!room) {
82
+ this.clearGoal();
83
+ return;
84
+ }
85
+
86
+ if (hasReachedRoomTarget(state, room)) {
87
+ this.patrol.advance(ctx);
88
+ room = this.patrol.nextRoom(ctx);
89
+ if (!room) {
90
+ this.clearGoal();
91
+ return;
92
+ }
93
+ }
94
+
95
+ this.setMoveGoal(room, 'patrol', true, PATROL_PRIORITY);
96
+ this.lastNoticeKey = '';
97
+ }
98
+
99
+ /**
100
+ * 寻路躲避:用 KeepAwayGoal 推演逃点远离这个唯一可见的人(不限距离,避免落单)。
101
+ * finishWhenClear:false——人短暂丢失视野时子目标不自我结束,交由父层 8 秒空视野宽限
102
+ * 决定何时转巡逻,否则一丢视野就同轮被清、下一 tick 立刻 patrol,宽限只剩一帧会来回摇摆。
103
+ */
104
+ private setKeepAway(key: string, resolveThreats: ThreatResolver): void {
105
+ const worker = this.subGoal;
106
+ if (worker instanceof KeepAwayGoal && worker.key === key) {
107
+ this.moveMode = 'flee';
108
+ return;
109
+ }
110
+ if (this.subGoal) this.removeSubGoal();
111
+ this.setSubGoal(new KeepAwayGoal(key, resolveThreats, {
112
+ threatRadius: Infinity,
113
+ finishWhenClear: false,
114
+ noun: '',
115
+ }), MOVE_PRIORITY);
116
+ this.moveMode = 'flee';
117
+ }
118
+
119
+ private setMoveGoal(
120
+ room: RoomTarget,
121
+ mode: MoveMode,
122
+ stopOnPlayer: boolean,
123
+ priority = MOVE_PRIORITY,
124
+ hasArrived: RoomArrival = hasReachedRoomTarget,
125
+ ): void {
126
+ const worker = this.subGoal;
127
+ if (worker instanceof MoveRoomGoal && worker.room === room.name && this.moveMode === mode) return;
128
+ if (this.subGoal) this.removeSubGoal();
129
+ this.setSubGoal(new MoveRoomGoal(room.name, hasArrived, {
130
+ emitProgress: false,
131
+ stopOnPlayer,
132
+ }), priority);
133
+ this.moveMode = mode;
134
+ }
135
+
136
+ private clearGoal(): void {
137
+ if (this.subGoal) this.removeSubGoal();
138
+ this.moveMode = null;
139
+ }
140
+
141
+ private emitNotice(ctx: StrategyContext, key: string, message: string): boolean {
142
+ if (key === this.lastNoticeKey) return false;
143
+ this.lastNoticeKey = key;
144
+ ctx.notifications.push(message);
145
+ return true;
146
+ }
147
+
148
+ private emitProgress(
149
+ state: GameState,
150
+ ctx: StrategyContext,
151
+ mode: MoveMode | null,
152
+ target: RoomTarget | null,
153
+ visible: PlayerInfo[],
154
+ ): void {
155
+ const now = Date.now();
156
+ if (now - ctx.lastProgressNotifyAt < PROGRESS_INTERVAL_MS) return;
157
+ ctx.lastProgressNotifyAt = now;
158
+
159
+ const room = state.you.room ?? '未知';
160
+ let msg = `[进度] 当前在${room},视野内${visible.length}人`;
161
+ if (mode === 'gather') msg += ',正在跟随人群';
162
+ else if (mode === 'flee') msg += ',正在避免落单风险';
163
+ else msg += ',正在巡逻寻找人群';
164
+ if (target) msg += `,前往${target.name}`;
165
+ msg += '。';
166
+ ctx.notifications.push(msg);
167
+ }
168
+ }
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { GameState } from '../../sdk/types.js';
3
+ import type { StrategyContext } from '../types.js';
4
+ import { AvoidPlayersTop } from './avoid-players-top.js';
5
+
6
+ function state(overrides: Partial<GameState> = {}): GameState {
7
+ return {
8
+ phase: 'wandering',
9
+ tick: 1,
10
+ you: {
11
+ name: 'self',
12
+ x: 0,
13
+ y: 0,
14
+ room: 'start',
15
+ role: 'neutral_octopus',
16
+ faction: 'neutral',
17
+ is_alive: true,
18
+ },
19
+ your_tasks: [],
20
+ players: [],
21
+ corpses: [],
22
+ stale: false,
23
+ ...overrides,
24
+ };
25
+ }
26
+
27
+ function context(overrides: Partial<StrategyContext> = {}): StrategyContext {
28
+ return {
29
+ taskData: [],
30
+ emergency: null,
31
+ taskLocalBlockedUntil: 0,
32
+ reportCorpseTarget: null,
33
+ reportBlockedUntil: 0,
34
+ notifications: [],
35
+ lastProgressNotifyAt: Date.now(),
36
+ teammates: new Set(),
37
+ alarmDone: false,
38
+ rooms: [{ name: 'patrol', x: 500, y: 0 }],
39
+ playerNamesBySeat: {},
40
+ forcePatrolAdvance: false,
41
+ blockedMoveTarget: null,
42
+ mySeat: 1,
43
+ speechNotifications: [],
44
+ agentAlerts: [],
45
+ ...overrides,
46
+ };
47
+ }
48
+
49
+ describe('AvoidPlayersTop', () => {
50
+ it('patrols without stop_on_player when the avoid list has no active threat', () => {
51
+ const top = new AvoidPlayersTop([]);
52
+ const ctx = context();
53
+ const current = state({
54
+ players: [{ name: 'roadie', x: 200, y: 0, room: 'start' }],
55
+ });
56
+
57
+ top.tick(current, ctx);
58
+ const decisions = top.subGoal?.tick(current, ctx) ?? [];
59
+
60
+ expect(decisions).toHaveLength(1);
61
+ expect(decisions[0].action.type).toBe('move');
62
+ expect(decisions[0].action.payload).toMatchObject({ target_x: 500, target_y: 0 });
63
+ expect(decisions[0].action.payload).not.toHaveProperty('stop_on_player');
64
+ });
65
+
66
+ it('patrols without stop_on_player when the configured avoid target is dead', () => {
67
+ const top = new AvoidPlayersTop(['dead-target']);
68
+ const ctx = context();
69
+ const current = state({
70
+ players: [{ name: 'roadie', x: 200, y: 0, room: 'start' }],
71
+ all_players: [
72
+ { name: 'dead-target', role: 'lobster', is_alive: false, seat: 2 },
73
+ { name: 'roadie', role: 'lobster', is_alive: true, seat: 3 },
74
+ ],
75
+ });
76
+
77
+ top.tick(current, ctx);
78
+ const decisions = top.subGoal?.tick(current, ctx) ?? [];
79
+
80
+ expect(decisions).toHaveLength(1);
81
+ expect(decisions[0].action.payload).not.toHaveProperty('stop_on_player');
82
+ });
83
+ });
@@ -0,0 +1,121 @@
1
+ import type { GameState, PlayerInfo } from '../../sdk/types.js';
2
+ import { hasReachedRoomTarget, isKnowledgeThreat, isTargetAlive, matchesAnyTarget, PatrolState, PROGRESS_INTERVAL_MS } from '../game-utils.js';
3
+ import type { BehaviorDecision, RoomTarget, StrategyContext } from '../types.js';
4
+ import { Goal } from './goal.js';
5
+ import { KeepAwayGoal, type KeepAwayGoalOptions, type ThreatResolver } from './keep-away-goal.js';
6
+ import { MoveRoomGoal } from './move-room-goal.js';
7
+
8
+ const PATROL_PRIORITY = 0.2;
9
+ const FLEE_PRIORITY = 0.5;
10
+
11
+ export class AvoidPlayersTop extends Goal {
12
+ private readonly patrol = new PatrolState();
13
+ private moveMode: 'patrol' | 'flee' | null = null;
14
+ private lastAvoidNoticeKey = '';
15
+
16
+ constructor(private readonly targets: string[]) {
17
+ super();
18
+ }
19
+
20
+ tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
21
+ const threats = this.threats(state, ctx);
22
+
23
+ if (threats.length > 0) {
24
+ this.setKeepAway();
25
+ if (!this.emitAvoidNotice(ctx, threats)) this.emitAvoidProgress(state, ctx, threats);
26
+ return [];
27
+ }
28
+
29
+ this.lastAvoidNoticeKey = '';
30
+ let room = this.patrol.nextRoom(ctx);
31
+ if (!room) {
32
+ this.emitPatrolProgress(state, ctx, null);
33
+ return [];
34
+ }
35
+
36
+ if (hasReachedRoomTarget(state, room)) {
37
+ this.patrol.advance(ctx);
38
+ room = this.patrol.nextRoom(ctx);
39
+ if (!room) {
40
+ this.emitPatrolProgress(state, ctx, null);
41
+ return [];
42
+ }
43
+ }
44
+
45
+ this.setMoveGoal(room, PATROL_PRIORITY, 'patrol', false);
46
+ this.emitPatrolProgress(state, ctx, room);
47
+ return [];
48
+ }
49
+
50
+ /** 寻路躲避:用 KeepAwayGoal 推演逃点远离回避目标(不限距离,名单上的人始终要躲)。 */
51
+ private setKeepAway(): void {
52
+ const key = this.targets.length > 0 ? this.targets.join('|') : 'avoid-knowledge';
53
+ const resolve: ThreatResolver = (state, ctx) => this.threats(state, ctx);
54
+ const options: KeepAwayGoalOptions = { threatRadius: Infinity, noun: '回避目标' };
55
+ const worker = this.subGoal;
56
+ if (worker instanceof KeepAwayGoal && worker.key === key) {
57
+ this.moveMode = 'flee';
58
+ return;
59
+ }
60
+ if (this.subGoal) this.removeSubGoal();
61
+ this.setSubGoal(new KeepAwayGoal(key, resolve, options), FLEE_PRIORITY);
62
+ this.moveMode = 'flee';
63
+ }
64
+
65
+ /** args 回避名单(活着的)∪ 知识里标记 suspect/hostile 的玩家。 */
66
+ private threats(state: GameState, ctx: StrategyContext): PlayerInfo[] {
67
+ const validTargets = this.targets.filter(target => isTargetAlive(state, target, ctx));
68
+ return state.players.filter(p => matchesAnyTarget(p, validTargets, ctx) || isKnowledgeThreat(p, ctx));
69
+ }
70
+
71
+ private setMoveGoal(room: RoomTarget, priority: number, mode: 'patrol' | 'flee', stopOnPlayer: boolean): void {
72
+ const worker = this.subGoal;
73
+ if (worker instanceof MoveRoomGoal && worker.room === room.name && this.moveMode === mode) return;
74
+ if (this.subGoal) this.removeSubGoal();
75
+ this.setSubGoal(new MoveRoomGoal(room.name, hasReachedRoomTarget, {
76
+ emitProgress: false,
77
+ stopOnPlayer,
78
+ }), priority);
79
+ this.moveMode = mode;
80
+ }
81
+
82
+ private emitAvoidNotice(ctx: StrategyContext, threats: PlayerInfo[]): boolean {
83
+ const names = threats.map(p => p.name).join('、');
84
+ const key = threats.map(p => p.name).sort().join('|');
85
+ if (key === this.lastAvoidNoticeKey) return false;
86
+ this.lastAvoidNoticeKey = key;
87
+
88
+ ctx.notifications.push(`发现回避目标${names},正在寻路躲避。`);
89
+ return true;
90
+ }
91
+
92
+ private emitPatrolProgress(state: GameState, ctx: StrategyContext, target: RoomTarget | null): void {
93
+ const now = Date.now();
94
+ if (now - ctx.lastProgressNotifyAt < PROGRESS_INTERVAL_MS) return;
95
+ ctx.lastProgressNotifyAt = now;
96
+
97
+ const room = state.you.room ?? '未知';
98
+ const targetNames = this.targets.length > 0 ? this.targets.join(', ') : '(依据知识库)';
99
+
100
+ let msg = `[进度] 当前在${room},巡逻中,回避名单: ${targetNames}`;
101
+ if (target) {
102
+ msg += `,正在前往${target.name}`;
103
+ }
104
+ msg += '。';
105
+ ctx.notifications.push(msg);
106
+ }
107
+
108
+ private emitAvoidProgress(
109
+ state: GameState,
110
+ ctx: StrategyContext,
111
+ threats: PlayerInfo[],
112
+ ): void {
113
+ const now = Date.now();
114
+ if (now - ctx.lastProgressNotifyAt < PROGRESS_INTERVAL_MS) return;
115
+ ctx.lastProgressNotifyAt = now;
116
+
117
+ const here = state.you.room ?? '未知';
118
+ const names = threats.map(p => p.name).join('、');
119
+ ctx.notifications.push(`[进度] 当前在${here},发现${names},正在寻路躲避。`);
120
+ }
121
+ }
@@ -0,0 +1,51 @@
1
+ import type { GameState } from '../../sdk/types.js';
2
+ import type { BehaviorDecision, StrategyContext } from '../types.js';
3
+ import { Goal } from './goal.js';
4
+ import { GoalManager } from './goal-manager.js';
5
+ import { URGENT_GOAL_PRIORITY } from './leaf-goal.js';
6
+ import type { SpeechModule } from '../speech-module.js';
7
+
8
+ export class ConversationGoal extends Goal {
9
+ private readonly innerMgr = new GoalManager();
10
+
11
+ constructor(
12
+ private readonly makeInner: () => Goal,
13
+ private readonly speech: SpeechModule,
14
+ ) {
15
+ super();
16
+ this.innerMgr.setTop(makeInner());
17
+ }
18
+
19
+ tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
20
+ if (this.innerMgr.getTop() === null) this.innerMgr.setTop(this.makeInner());
21
+ const innerDecisions = this.innerMgr.tick(state, ctx);
22
+
23
+ const interruptible = this.innerLeafPriority() < URGENT_GOAL_PRIORITY;
24
+ const primaryIsMove = innerDecisions[0]?.action.type === 'move';
25
+ const result = this.speech.evaluate(state, ctx, {
26
+ allowAutoGreet: interruptible && primaryIsMove,
27
+ });
28
+ for (const msg of result.notifications) ctx.speechNotifications.push(msg);
29
+
30
+ return innerDecisions;
31
+ }
32
+
33
+ onMeetingResume(): void {
34
+ this.speech.resetMeetingState();
35
+ this.innerMgr.getTop()?.onMeetingResume();
36
+ }
37
+
38
+ updateConfig(role: string): void {
39
+ this.speech.updateConfig(role);
40
+ }
41
+
42
+ private innerLeafPriority(): number {
43
+ let node: Goal | null = this.innerMgr.getTop();
44
+ let priority = 0;
45
+ while (node) {
46
+ priority = node.priority;
47
+ node = node.subGoal;
48
+ }
49
+ return priority;
50
+ }
51
+ }
@@ -0,0 +1,91 @@
1
+ import type { GameState } from '../../sdk/types.js';
2
+ import { Action } from '../../sdk/action.js';
3
+ import {
4
+ corpseAtScene,
5
+ nearestKnownCorpseNav,
6
+ patrolStep,
7
+ PatrolState,
8
+ PROGRESS_INTERVAL_MS,
9
+ } from '../game-utils.js';
10
+ import type { BehaviorDecision, CorpseTarget, StrategyContext } from '../types.js';
11
+ import { Goal } from './goal.js';
12
+ import { LingerCorpseGoal } from './linger-corpse-goal.js';
13
+
14
+ const GREETING_COOLDOWN_MS = 120_000;
15
+
16
+ export class CorpsePatrolTop extends Goal {
17
+ private readonly patrol = new PatrolState();
18
+ private readonly linger = new LingerCorpseGoal();
19
+ private lastCorpseNotified = false;
20
+ private lastGreetingAt: number | null = null;
21
+
22
+ constructor(private readonly greetingPhrases: string[]) {
23
+ super();
24
+ }
25
+
26
+ tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
27
+ const decisions: BehaviorDecision[] = [];
28
+
29
+ const greetingDecision = this.tryGreeting(state);
30
+ if (greetingDecision) decisions.push(greetingDecision);
31
+
32
+ const corpse = nearestKnownCorpseNav(state, ctx);
33
+ const canMove = !state.you.doing_task;
34
+
35
+ if (corpse) {
36
+ if (!this.lastCorpseNotified) {
37
+ this.lastCorpseNotified = true;
38
+ const label = corpse.name ? `${corpse.name}的尸体` : '尸体';
39
+ // corpseAtScene=已站在尸体旁(≤150px);仅凭记忆/房间锚点导航过去时人还没到,别谎报「在附近徘徊」。
40
+ ctx.notifications.push(
41
+ corpseAtScene(state) ? `发现${label}!在附近徘徊观察。` : `发现${label}!正前往查看。`,
42
+ );
43
+ }
44
+ if (canMove) decisions.push(...this.linger.tick(state, ctx));
45
+ } else {
46
+ this.lastCorpseNotified = false;
47
+ if (canMove) decisions.push(...patrolStep(state, ctx, this.patrol, false));
48
+ }
49
+
50
+ this.emitProgress(state, ctx, corpse);
51
+ return decisions;
52
+ }
53
+
54
+ private tryGreeting(state: GameState): BehaviorDecision | null {
55
+ if (this.greetingPhrases.length === 0) return null;
56
+ if (state.players.length === 0) return null;
57
+
58
+ const now = Date.now();
59
+ if (this.lastGreetingAt !== null && now - this.lastGreetingAt < GREETING_COOLDOWN_MS) return null;
60
+
61
+ const text = this.greetingPhrases[Math.floor(Math.random() * this.greetingPhrases.length)];
62
+ this.lastGreetingAt = now;
63
+ return { action: Action.speech(text) };
64
+ }
65
+
66
+ private emitProgress(state: GameState, ctx: StrategyContext, corpse: CorpseTarget | null): void {
67
+ const now = Date.now();
68
+ if (now - ctx.lastProgressNotifyAt < PROGRESS_INTERVAL_MS) return;
69
+ ctx.lastProgressNotifyAt = now;
70
+
71
+ const room = state.you.room ?? '未知';
72
+
73
+ let msg = `[进度] 当前在${room}`;
74
+ if (corpse) {
75
+ const label = corpse.name ?? '未知';
76
+ msg += corpseAtScene(state)
77
+ ? `,在${label}的尸体附近徘徊。`
78
+ : `,正前往${label}的尸体所在位置。`;
79
+ } else {
80
+ const target = this.patrol.nextRoom(ctx);
81
+ if (target) {
82
+ msg += `,巡逻中,正在前往${target.name}。`;
83
+ } else {
84
+ msg += `,巡逻中。`;
85
+ }
86
+ }
87
+ msg += ` 共${ctx.rooms.length}个房间。`;
88
+
89
+ ctx.notifications.push(msg);
90
+ }
91
+ }
@@ -0,0 +1,93 @@
1
+ import type { GameState } from '../../sdk/types.js';
2
+ import { Action } from '../../sdk/action.js';
3
+ import {
4
+ canUseKill,
5
+ corpseAtScene,
6
+ dist,
7
+ KILL_RANGE,
8
+ nearestReportableCorpse,
9
+ nonTeammatesVisible,
10
+ reportCorpseDecision,
11
+ } from '../game-utils.js';
12
+ import type { BehaviorDecision, StrategyContext } from '../types.js';
13
+
14
+ /**
15
+ * 蟹(坏人)/ 章鱼(中立带刀)默认策略共享的「反射动作」——由 CrabSabotageTop 与 LoneKillTaskTop
16
+ * 在各自 tick() 的**最前面**调用,抢在破坏/任务/落单猎杀编排之前先处理两类即时机会:
17
+ *
18
+ * - immediateLoneKillDecision:唯一一个落单非队友就贴在脸上(≤KILL_RANGE)且刀好 → 立刻出刀。它无视
19
+ * LoneKillCore 的「暂停窗口」,也不管自己是否正在做破坏任务——只要能原地一刀就先杀。
20
+ * - corpseReportWithNonTeammate:地上有尸体且附近有非队友 → 报警(够不着先靠近)。不管尸体是谁
21
+ * 造成的(含队友的杀),靠主动报警伪装好人 / 把嫌疑栽到在场者身上。
22
+ *
23
+ * 调用方按「能贴脸杀就杀,否则报警」的次序:先 immediateLoneKillDecision 再 corpseReportWithNonTeammate。
24
+ * 两者都**无记忆**(不读 ctx.knowledge),与默认蟹/章鱼对任意落单非队友照杀的设定一致;蟹/章鱼目前
25
+ * 没有记忆版策略,故没有「点名/保护」入口。
26
+ */
27
+
28
+ /** 有尸体且附近有非队友 → 报警/靠近报警;否则 null。respectBlock 防止 5s 内重复报。 */
29
+ export function corpseReportWithNonTeammate(state: GameState, ctx: StrategyContext): BehaviorDecision[] | null {
30
+ // 报尸自证/嫁祸是「此刻就在尸体旁、有人撞见」的现场反射,必须用当帧视野的尸体;用持久记忆会让
31
+ // 蟹/章鱼在地图别处随便撞见一个人就生成错误的栽赃简报。
32
+ if (!corpseAtScene(state)) return null;
33
+ const witnesses = nonTeammatesVisible(state, ctx);
34
+ if (witnesses.length === 0) return null;
35
+
36
+ const report = reportCorpseDecision(state, ctx, { respectBlock: true });
37
+ if (!report) return null;
38
+
39
+ const witnessNames = witnesses.map(p => p.name).join('、');
40
+ if (report.action.type === 'report') {
41
+ // 实时给 Agent 一条「为什么是我报的尸」简报:报尸=召集会议,是高风险自证动作。
42
+ // 若尸体很可能是自己刚才下的那一刀,必须显式提醒,免得 Agent 之后发言露馅。
43
+ const corpseName = nearestReportableCorpse(state)?.name ?? '';
44
+ ctx.agentAlerts.push(
45
+ isRecentOwnKill(ctx, corpseName)
46
+ ? `我刚下手的目标${corpseName ? `(${corpseName})` : ''}尸体被${witnessNames}撞见,我抢先报尸召集会议、把自己装成发现者并把嫌疑引向在场者。接下来发言/投票要顺着「我是主动报告的人」走,绝不能承认是自己动的手。`
47
+ : `尸体旁有${witnessNames}在场,我抢先报尸召集会议、伪装成主动报告的好人。发言时按「主动报告的发现者」立场走。`,
48
+ );
49
+ ctx.notifications.push(`尸体旁有${witnessNames}在场,立刻报告自证。`);
50
+ const base = report.action.thinkingContent ?? '发现尸体,报告召开会议';
51
+ return [{
52
+ action: report.action.withThinking(`${base};尸体旁有${witnessNames}在场,立刻报告自证。`),
53
+ }];
54
+ }
55
+ ctx.notifications.push(`尸体旁有${witnessNames}在场,靠近尸体准备报告自证。`);
56
+ return [{
57
+ action: report.action.withThinking(`尸体旁有${witnessNames}在场,靠近尸体准备报告自证。`),
58
+ }];
59
+ }
60
+
61
+ /** 唯一一个落单非队友贴脸(≤KILL_RANGE)且刀好 → 立刻出刀;否则 null。无视暂停窗口、不管在做什么任务。 */
62
+ export function immediateLoneKillDecision(state: GameState, ctx: StrategyContext): BehaviorDecision[] | null {
63
+ if (!canUseKill(state)) return null;
64
+
65
+ const targets = nonTeammatesVisible(state, ctx);
66
+ if (targets.length !== 1) return null;
67
+
68
+ const target = targets[0];
69
+ const distance = target.distance ?? dist(state.you.x, state.you.y, target.x, target.y);
70
+ if (distance > KILL_RANGE) return null;
71
+
72
+ ctx.notifications.push(`发现落单目标${target.name}就在身边,立刻出刀!`);
73
+ return [{ action: Action.kill(target.name) }];
74
+ }
75
+
76
+ /**
77
+ * 这具尸体是不是「自己刚才下的那一刀」。先按尸体名精确匹配 recentlyKilledTargets(刚杀过且未过期),
78
+ * 名字对不上但近期确实杀过人时,冒出的尸体大概率也是自己造成的,按 true 处理(宁可多提醒一次)。
79
+ */
80
+ function isRecentOwnKill(ctx: StrategyContext, corpseName: string): boolean {
81
+ const map = ctx.recentlyKilledTargets;
82
+ if (!map || map.size === 0) return false;
83
+ const now = Date.now();
84
+ const key = corpseName.trim().toLowerCase();
85
+ if (key) {
86
+ const until = map.get(key);
87
+ if (until != null && until > now) return true;
88
+ }
89
+ for (const until of map.values()) {
90
+ if (until > now) return true;
91
+ }
92
+ return false;
93
+ }