@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.
- package/README.md +440 -0
- package/bin/clawclaw-cli.mjs +4 -0
- package/package.json +48 -0
- package/personas//347/220/206/346/231/272/346/270/251/345/222/214.md +23 -0
- package/personas//350/200/201/350/260/213/346/267/261/347/256/227.md +22 -0
- package/personas//350/257/232/346/201/263/347/233/264/347/216/207.md +22 -0
- package/personas//350/275/273/346/235/276/346/264/273/346/263/274.md +22 -0
- package/personas//351/207/216/346/200/247/345/217/233/351/200/206.md +23 -0
- package/scripts/postinstall.mjs +20 -0
- package/scripts/sync-bundled-skill.mjs +245 -0
- package/scripts/sync-bundled-skill.test.mjs +152 -0
- package/skills/clawclaw/SKILL.md +240 -0
- package/skills/clawclaw/references/CHATTERBOX.md +142 -0
- package/skills/clawclaw/references/COMMANDS.md +132 -0
- package/skills/clawclaw/references/GAME-MECHANICS.md +186 -0
- package/skills/clawclaw/references/HUB.md +48 -0
- package/skills/clawclaw/references/KNOWLEDGE.md +43 -0
- package/skills/clawclaw/references/STRATEGIES.md +57 -0
- package/skills/clawclaw/references/STREAM.md +58 -0
- package/skills/clawclaw/references/TACTICS.md +65 -0
- package/src/assets/clawclaw-ascii-map.txt +40 -0
- package/src/cli.ts +153 -0
- package/src/commands/_schema.ts +109 -0
- package/src/commands/account.ts +209 -0
- package/src/commands/config.ts +30 -0
- package/src/commands/do.test.ts +37 -0
- package/src/commands/do.ts +95 -0
- package/src/commands/events.ts +22 -0
- package/src/commands/game-map.test.ts +28 -0
- package/src/commands/game-start-plan.test.ts +142 -0
- package/src/commands/game.ts +882 -0
- package/src/commands/history-player.test.ts +102 -0
- package/src/commands/history.ts +573 -0
- package/src/commands/hub.test.ts +96 -0
- package/src/commands/hub.ts +234 -0
- package/src/commands/knowledge.test.ts +19 -0
- package/src/commands/knowledge.ts +168 -0
- package/src/commands/load.test.ts +51 -0
- package/src/commands/load.ts +13 -0
- package/src/commands/meeting-history.test.ts +106 -0
- package/src/commands/memory.ts +40 -0
- package/src/commands/peek.ts +38 -0
- package/src/commands/persona.ts +57 -0
- package/src/commands/setup/codex.ts +248 -0
- package/src/commands/setup/hermes.test.ts +96 -0
- package/src/commands/setup/hermes.ts +76 -0
- package/src/commands/setup/index.ts +13 -0
- package/src/commands/setup/openclaw.test.ts +114 -0
- package/src/commands/setup/openclaw.ts +147 -0
- package/src/commands/skill.ts +128 -0
- package/src/commands/state.ts +46 -0
- package/src/commands/strategy.test.ts +135 -0
- package/src/commands/strategy.ts +189 -0
- package/src/commands/tts.ts +128 -0
- package/src/commands/upgrade.test.ts +91 -0
- package/src/commands/upgrade.ts +154 -0
- package/src/commands/watch.test.ts +973 -0
- package/src/commands/watch.ts +709 -0
- package/src/lib/auth.test.ts +59 -0
- package/src/lib/auth.ts +186 -0
- package/src/lib/command-meta.ts +37 -0
- package/src/lib/game-client.ts +391 -0
- package/src/lib/host-config-patcher.test.ts +130 -0
- package/src/lib/host-config-patcher.ts +151 -0
- package/src/lib/http-keepalive.ts +15 -0
- package/src/lib/http-transport.test.ts +42 -0
- package/src/lib/http-transport.ts +113 -0
- package/src/lib/hub-client.test.ts +56 -0
- package/src/lib/hub-client.ts +88 -0
- package/src/lib/hub-install.test.ts +98 -0
- package/src/lib/hub-install.ts +121 -0
- package/src/lib/hub-reminder.ts +75 -0
- package/src/lib/hub-unzip.test.ts +69 -0
- package/src/lib/hub-unzip.ts +62 -0
- package/src/lib/init-command.test.ts +75 -0
- package/src/lib/init-command.ts +120 -0
- package/src/lib/knowledge-store.test.ts +180 -0
- package/src/lib/knowledge-store.ts +374 -0
- package/src/lib/load-context.test.ts +52 -0
- package/src/lib/load-context.ts +52 -0
- package/src/lib/match-state.test.ts +134 -0
- package/src/lib/match-state.ts +94 -0
- package/src/lib/netease-tts.ts +83 -0
- package/src/lib/normalize.ts +42 -0
- package/src/lib/persona.test.ts +41 -0
- package/src/lib/persona.ts +72 -0
- package/src/lib/server-registry.ts +152 -0
- package/src/lib/skill-version.test.ts +48 -0
- package/src/lib/skill-version.ts +19 -0
- package/src/lib/strategy-export.test.ts +232 -0
- package/src/lib/strategy-export.ts +242 -0
- package/src/lib/tts-keys.ts +7 -0
- package/src/lib/tts-speech.test.ts +63 -0
- package/src/lib/tts-speech.ts +76 -0
- package/src/lib/workspace-argv.test.ts +49 -0
- package/src/lib/workspace-argv.ts +44 -0
- package/src/perception/player-history-store.test.ts +87 -0
- package/src/perception/player-history-store.ts +194 -0
- package/src/pipeline/event-store.ts +124 -0
- package/src/pipeline/pipeline.ts +35 -0
- package/src/runtime/auto-upgrade.test.ts +66 -0
- package/src/runtime/auto-upgrade.ts +31 -0
- package/src/runtime/daemon.ts +100 -0
- package/src/runtime/event-daemon.test.ts +28 -0
- package/src/runtime/event-daemon.ts +371 -0
- package/src/runtime/opening-mover.ts +303 -0
- package/src/runtime/raw-ws-log.test.ts +33 -0
- package/src/runtime/raw-ws-log.ts +32 -0
- package/src/runtime/runtime-logger.ts +99 -0
- package/src/runtime/ws-client.test.ts +47 -0
- package/src/runtime/ws-client.ts +272 -0
- package/src/sdk/action.ts +166 -0
- package/src/sdk/index.ts +110 -0
- package/src/sdk/types.ts +146 -0
- package/src/strategies/avoid-lone.ts +11 -0
- package/src/strategies/avoid-players.knowledge.md +20 -0
- package/src/strategies/avoid-players.ts +15 -0
- package/src/strategies/corpse-patrol.ts +22 -0
- package/src/strategies/crab-sabotage.ts +21 -0
- package/src/strategies/custom-module.test.ts +269 -0
- package/src/strategies/find-player.ts +16 -0
- package/src/strategies/game-utils.test.ts +164 -0
- package/src/strategies/game-utils.ts +721 -0
- package/src/strategies/goals/avoid-lone-top.ts +168 -0
- package/src/strategies/goals/avoid-players-top.test.ts +83 -0
- package/src/strategies/goals/avoid-players-top.ts +121 -0
- package/src/strategies/goals/conversation-goal.ts +51 -0
- package/src/strategies/goals/corpse-patrol-top.ts +91 -0
- package/src/strategies/goals/crab-octopus-reflexes.ts +93 -0
- package/src/strategies/goals/crab-sabotage-top.ts +197 -0
- package/src/strategies/goals/emergency-hunt-goal.ts +28 -0
- package/src/strategies/goals/find-player-top.ts +93 -0
- package/src/strategies/goals/flee-players-goal.ts +53 -0
- package/src/strategies/goals/goal-manager.ts +41 -0
- package/src/strategies/goals/goal-root-strategy.ts +49 -0
- package/src/strategies/goals/goal.ts +28 -0
- package/src/strategies/goals/keep-away-goal.ts +206 -0
- package/src/strategies/goals/kill-frenzy-top.ts +80 -0
- package/src/strategies/goals/kill-lone-top.ts +160 -0
- package/src/strategies/goals/kill-target-goal.ts +59 -0
- package/src/strategies/goals/kill-target-top.ts +109 -0
- package/src/strategies/goals/leaf-goal.ts +25 -0
- package/src/strategies/goals/linger-corpse-goal.ts +79 -0
- package/src/strategies/goals/lone-kill-core.ts +82 -0
- package/src/strategies/goals/lone-kill-goal.ts +24 -0
- package/src/strategies/goals/lone-kill-task-top.test.ts +85 -0
- package/src/strategies/goals/lone-kill-task-top.ts +86 -0
- package/src/strategies/goals/move-room-goal.ts +60 -0
- package/src/strategies/goals/normal-shrimp-top.test.ts +80 -0
- package/src/strategies/goals/normal-shrimp-top.ts +242 -0
- package/src/strategies/goals/paradise-fish-top.test.ts +126 -0
- package/src/strategies/goals/paradise-fish-top.ts +219 -0
- package/src/strategies/goals/patrol-top.ts +57 -0
- package/src/strategies/goals/report-patrol-top.ts +80 -0
- package/src/strategies/goals/safe-task-goal.ts +102 -0
- package/src/strategies/goals/social-task-top.ts +161 -0
- package/src/strategies/goals/task-kill-report-top.ts +163 -0
- package/src/strategies/goals/task-only-top.ts +57 -0
- package/src/strategies/goals/task-or-patrol-goal.ts +41 -0
- package/src/strategies/goals/task-report-top.ts +57 -0
- package/src/strategies/goals/wander-task-goal.ts +33 -0
- package/src/strategies/goals/warrior-shrimp-top.test.ts +86 -0
- package/src/strategies/goals/warrior-shrimp-top.ts +248 -0
- package/src/strategies/greeting.ts +53 -0
- package/src/strategies/kill-frenzy.ts +12 -0
- package/src/strategies/kill-lone.knowledge.md +20 -0
- package/src/strategies/kill-lone.ts +13 -0
- package/src/strategies/kill-target.ts +18 -0
- package/src/strategies/loader.test.ts +678 -0
- package/src/strategies/loader.ts +172 -0
- package/src/strategies/lone-kill-task.ts +21 -0
- package/src/strategies/meeting-gate.test.ts +59 -0
- package/src/strategies/meeting-gate.ts +23 -0
- package/src/strategies/move-room.ts +15 -0
- package/src/strategies/new-events-backfill.ts +98 -0
- package/src/strategies/paradise-fish.knowledge.md +20 -0
- package/src/strategies/paradise-fish.ts +25 -0
- package/src/strategies/pathfind/clawclaw-walkable.bin +0 -0
- package/src/strategies/pathfind/distance-field.ts +150 -0
- package/src/strategies/pathfind/escape-planner.test.ts +197 -0
- package/src/strategies/pathfind/escape-planner.ts +348 -0
- package/src/strategies/pathfind/walkable-grid.ts +117 -0
- package/src/strategies/patrol.ts +11 -0
- package/src/strategies/player-targets.ts +13 -0
- package/src/strategies/report-patrol.ts +11 -0
- package/src/strategies/shrimp-memory.knowledge.md +20 -0
- package/src/strategies/shrimp-memory.ts +25 -0
- package/src/strategies/social-task.test.ts +28 -0
- package/src/strategies/social-task.ts +49 -0
- package/src/strategies/spawn.ts +71 -0
- package/src/strategies/speech-module.ts +123 -0
- package/src/strategies/strategy-loop.ts +757 -0
- package/src/strategies/task-kill-report.ts +17 -0
- package/src/strategies/task-only.ts +11 -0
- package/src/strategies/task-report.ts +22 -0
- package/src/strategies/types.ts +96 -0
- package/src/strategies/warrior-memory.knowledge.md +20 -0
- package/src/strategies/warrior-memory.ts +16 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
import type { GameState, PlayerInfo, Position, TaskInfo, EmergencyInfo, CorpseInfo } from '../sdk/types.js';
|
|
2
|
+
import { Action } from '../sdk/action.js';
|
|
3
|
+
import type { BehaviorDecision, CorpseTarget, RoomTarget, StrategyContext } from './types.js';
|
|
4
|
+
import { emptyKnowledgeView } from '../lib/knowledge-store.js';
|
|
5
|
+
import { assessRoutes } from './pathfind/escape-planner.js';
|
|
6
|
+
|
|
7
|
+
export const TASK_SUBMIT_RADIUS = 12;
|
|
8
|
+
export const KILL_RANGE = 50;
|
|
9
|
+
export const REPORT_RANGE = 150;
|
|
10
|
+
export const SHRIMP_VISION_RANGE = 270;
|
|
11
|
+
export const SHRIMP_VISION_EXIT_BUFFER = 30;
|
|
12
|
+
export const SHRIMP_VISION_RELEASE_RANGE = SHRIMP_VISION_RANGE + SHRIMP_VISION_EXIT_BUFFER;
|
|
13
|
+
export const FOLLOW_RANGE = 300;
|
|
14
|
+
export const PATROL_REACHED_DISTANCE = 150;
|
|
15
|
+
export const PROGRESS_INTERVAL_MS = 30_000;
|
|
16
|
+
export const LONE_IGNORE_MS = 10_000;
|
|
17
|
+
export const LONE_FOLLOW_DISTANCE = 10;
|
|
18
|
+
/** 任务点离任一可见尸体多近就算「在尸体旁」,会做任务伪装的坏人据此跳过该任务、避免站在命案现场做任务。 */
|
|
19
|
+
export const CORPSE_TASK_AVOID_RANGE = 100;
|
|
20
|
+
/** 判「同一具尸体」的极近半径:尸体不会移动,跨 tick 只有坐标取整抖动,远小于两具不同尸体的间距。 */
|
|
21
|
+
export const CORPSE_SAME_BODY_RADIUS = 20;
|
|
22
|
+
/** corpse_spotted 事件坐标离自己多近才算「人就在命案现场」——超出视为别处尸体的迟到/补发事件,不触发现场反射。 */
|
|
23
|
+
export const CORPSE_SCENE_RANGE = REPORT_RANGE;
|
|
24
|
+
|
|
25
|
+
export function killCooldownSecs(state: GameState): number {
|
|
26
|
+
return state.you.kill_cooldown_secs ?? 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function hasKillUseRemaining(state: GameState): boolean {
|
|
30
|
+
return state.you.kills_remaining == null || state.you.kills_remaining > 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function canUseKill(state: GameState): boolean {
|
|
34
|
+
return hasKillUseRemaining(state) && killCooldownSecs(state) <= 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function dist(x1: number, y1: number, x2: number, y2: number): number {
|
|
38
|
+
return Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function firstAvailableTask(
|
|
42
|
+
tasks: TaskInfo[],
|
|
43
|
+
predicate: (task: TaskInfo) => boolean,
|
|
44
|
+
emergency?: EmergencyInfo | null,
|
|
45
|
+
blockedTarget?: { x: number; y: number } | null,
|
|
46
|
+
avoidCorpses?: CorpseInfo[] | null,
|
|
47
|
+
): TaskInfo | null {
|
|
48
|
+
const nearCorpse = (x: number, y: number): boolean =>
|
|
49
|
+
!!avoidCorpses && avoidCorpses.some(c => c.x != null && c.y != null && dist(x, y, c.x, c.y) <= CORPSE_TASK_AVOID_RANGE);
|
|
50
|
+
|
|
51
|
+
// 紧急任务(维修点)始终自动优先,不做尸体回避:大家都会涌向维修点,在那儿做任务并不可疑,
|
|
52
|
+
// 且紧急维修有倒计时、时间敏感。尸体回避只针对下面的常规伪装任务。
|
|
53
|
+
if (emergency && emergency.status !== 'completed' && emergency.x != null && emergency.y != null && predicate(emergency)) {
|
|
54
|
+
if (!blockedTarget || dist(emergency.x, emergency.y, blockedTarget.x, blockedTarget.y) > 10) {
|
|
55
|
+
return emergency;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return tasks.find(t => {
|
|
59
|
+
if (t.status === 'completed' || t.status === 'in_progress') return false;
|
|
60
|
+
if (t.x == null || t.y == null) return false;
|
|
61
|
+
if (blockedTarget && dist(t.x, t.y, blockedTarget.x, blockedTarget.y) <= 10) return false;
|
|
62
|
+
if (nearCorpse(t.x, t.y)) return false;
|
|
63
|
+
return predicate(t);
|
|
64
|
+
}) ?? null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 任务点离任一威胁点多近就算「在威胁旁」而被硬排除(欧氏即可——隔薄墙的威胁照样危险)。 */
|
|
68
|
+
export const TASK_THREAT_EXCLUDE_RANGE = 500;
|
|
69
|
+
/** 测地路径离任一威胁点多近就算「必经之路有危险」而被硬排除(治路上有人把守时的来回逡巡)。 */
|
|
70
|
+
export const TASK_PATH_THREAT_RADIUS = 350;
|
|
71
|
+
|
|
72
|
+
export interface SafeTaskOptions {
|
|
73
|
+
/** 硬排除:这些点 threatExcludeRadius 内的任务直接丢弃。 */
|
|
74
|
+
threatPoints?: Position[];
|
|
75
|
+
threatExcludeRadius?: number;
|
|
76
|
+
/** 路径硬排除半径:去任务点的测地路径经过威胁点这个距离内也丢弃,默认 TASK_PATH_THREAT_RADIUS。 */
|
|
77
|
+
pathThreatRadius?: number;
|
|
78
|
+
/** 粘性:该任务仍是合法候选就直接返回它,不重新比距离,杜绝等距摇摆。 */
|
|
79
|
+
stickyTaskName?: string | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SafePatrolOptions {
|
|
83
|
+
threatPoints?: Position[];
|
|
84
|
+
threatExcludeRadius?: number;
|
|
85
|
+
pathThreatRadius?: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* firstAvailableTask 的进阶版:同样的紧急任务优先与候选过滤,但常规任务改选
|
|
90
|
+
* 测地最近(distance-field 一次扫描),并支持威胁硬排除(任务点旁有威胁,或
|
|
91
|
+
* 必经之路经过威胁附近)与当前任务粘性。粘性任务同样要过路径检查——路被人
|
|
92
|
+
* 把守时直接换任务,而不是「后撤→粘回原任务→再走近→再后撤」地振荡。
|
|
93
|
+
* 测地不可达的候选按欧氏距离垫底(仍可选,避免全图无任务可做)。
|
|
94
|
+
*/
|
|
95
|
+
export function nearestSafeTask(
|
|
96
|
+
state: GameState,
|
|
97
|
+
tasks: TaskInfo[],
|
|
98
|
+
predicate: (task: TaskInfo) => boolean,
|
|
99
|
+
emergency?: EmergencyInfo | null,
|
|
100
|
+
blockedTarget?: { x: number; y: number } | null,
|
|
101
|
+
avoidCorpses?: CorpseInfo[] | null,
|
|
102
|
+
opts: SafeTaskOptions = {},
|
|
103
|
+
): TaskInfo | null {
|
|
104
|
+
// 紧急任务语义与 firstAvailableTask 一致:时间敏感且人人都涌过去,不做尸体/威胁回避。
|
|
105
|
+
if (emergency && emergency.status !== 'completed' && emergency.x != null && emergency.y != null && predicate(emergency)) {
|
|
106
|
+
if (!blockedTarget || dist(emergency.x, emergency.y, blockedTarget.x, blockedTarget.y) > 10) {
|
|
107
|
+
return emergency;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const threatPoints = opts.threatPoints ?? [];
|
|
112
|
+
const threatRadius = opts.threatExcludeRadius ?? TASK_THREAT_EXCLUDE_RANGE;
|
|
113
|
+
const nearCorpse = (x: number, y: number): boolean =>
|
|
114
|
+
!!avoidCorpses && avoidCorpses.some(c => c.x != null && c.y != null && dist(x, y, c.x, c.y) <= CORPSE_TASK_AVOID_RANGE);
|
|
115
|
+
|
|
116
|
+
const candidates = tasks.filter(t => {
|
|
117
|
+
if (t.status === 'completed' || t.status === 'in_progress') return false;
|
|
118
|
+
if (t.x == null || t.y == null) return false;
|
|
119
|
+
if (blockedTarget && dist(t.x, t.y, blockedTarget.x, blockedTarget.y) <= 10) return false;
|
|
120
|
+
if (nearCorpse(t.x, t.y)) return false;
|
|
121
|
+
if (threatPoints.some(p => dist(t.x!, t.y!, p.x, p.y) <= threatRadius)) return false;
|
|
122
|
+
return predicate(t);
|
|
123
|
+
});
|
|
124
|
+
if (candidates.length === 0) return null;
|
|
125
|
+
|
|
126
|
+
// 无威胁时粘性可以零成本短路(不必为路径检查扫距离场)。
|
|
127
|
+
if (opts.stickyTaskName && threatPoints.length === 0) {
|
|
128
|
+
const sticky = candidates.find(t => t.task_name === opts.stickyTaskName);
|
|
129
|
+
if (sticky) return sticky;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const routes = assessRoutes(
|
|
133
|
+
{ x: state.you.x, y: state.you.y },
|
|
134
|
+
candidates.map(t => ({ x: t.x!, y: t.y! })),
|
|
135
|
+
threatPoints,
|
|
136
|
+
opts.pathThreatRadius ?? TASK_PATH_THREAT_RADIUS,
|
|
137
|
+
);
|
|
138
|
+
// routes 为 null(自己不在可走格上,极少见)时退回欧氏排序,无路径排除。
|
|
139
|
+
const viable = routes == null
|
|
140
|
+
? candidates.map((t, i) => ({ t, i }))
|
|
141
|
+
: candidates.map((t, i) => ({ t, i })).filter(({ i }) => !routes[i].nearThreat);
|
|
142
|
+
if (viable.length === 0) return null;
|
|
143
|
+
|
|
144
|
+
if (opts.stickyTaskName) {
|
|
145
|
+
const sticky = viable.find(({ t }) => t.task_name === opts.stickyTaskName);
|
|
146
|
+
if (sticky) return sticky.t;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let best = viable[0].t;
|
|
150
|
+
let bestScore = Infinity;
|
|
151
|
+
for (const { t, i } of viable) {
|
|
152
|
+
const euclid = dist(state.you.x, state.you.y, t.x!, t.y!);
|
|
153
|
+
const geoPx = routes?.[i].distancePx;
|
|
154
|
+
const score = geoPx == null ? euclid : geoPx !== Infinity ? geoPx : euclid + 1e6;
|
|
155
|
+
if (score < bestScore) {
|
|
156
|
+
bestScore = score;
|
|
157
|
+
best = t;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return best;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 巡逻一步:朝当前巡逻房间移动,已到达就推进到下一个房间再走。rooms 为空返回 [](无处可去)。
|
|
165
|
+
* stopOnPlayer 控制路上撞见玩家是否立刻停下(猎杀型策略=true,借此撞见落单猎物即停、下一 tick 出刀)。
|
|
166
|
+
* 抽自各 *-top 里重复的 patrolDecision。
|
|
167
|
+
*/
|
|
168
|
+
export function patrolStep(
|
|
169
|
+
state: GameState,
|
|
170
|
+
ctx: StrategyContext,
|
|
171
|
+
patrol: PatrolState,
|
|
172
|
+
stopOnPlayer = true,
|
|
173
|
+
): BehaviorDecision[] {
|
|
174
|
+
let room = patrol.nextRoom(ctx);
|
|
175
|
+
if (!room) return [];
|
|
176
|
+
if (dist(state.you.x, state.you.y, room.x, room.y) <= PATROL_REACHED_DISTANCE) {
|
|
177
|
+
patrol.advance(ctx);
|
|
178
|
+
const next = patrol.nextRoom(ctx);
|
|
179
|
+
if (next) room = next;
|
|
180
|
+
}
|
|
181
|
+
return [{ action: Action.move({ x: room.x, y: room.y }, stopOnPlayer) }];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function safePatrolStep(
|
|
185
|
+
state: GameState,
|
|
186
|
+
ctx: StrategyContext,
|
|
187
|
+
patrol: PatrolState,
|
|
188
|
+
stopOnPlayer = true,
|
|
189
|
+
opts: SafePatrolOptions = {},
|
|
190
|
+
): BehaviorDecision[] {
|
|
191
|
+
const room = nextSafePatrolRoom(state, ctx, patrol, opts);
|
|
192
|
+
return room ? [{ action: Action.move({ x: room.x, y: room.y }, stopOnPlayer) }] : [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function nextSafePatrolRoom(
|
|
196
|
+
state: GameState,
|
|
197
|
+
ctx: StrategyContext,
|
|
198
|
+
patrol: PatrolState,
|
|
199
|
+
opts: SafePatrolOptions,
|
|
200
|
+
): RoomTarget | null {
|
|
201
|
+
let current = patrol.nextRoom(ctx);
|
|
202
|
+
if (!current) return null;
|
|
203
|
+
if (dist(state.you.x, state.you.y, current.x, current.y) <= PATROL_REACHED_DISTANCE) {
|
|
204
|
+
patrol.advance(ctx);
|
|
205
|
+
current = patrol.nextRoom(ctx);
|
|
206
|
+
if (!current) return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const ordered = orderedPatrolRooms(ctx, current);
|
|
210
|
+
if (ordered.length === 0) return null;
|
|
211
|
+
const threatPoints = opts.threatPoints ?? [];
|
|
212
|
+
const notBlocked = ordered.filter(room =>
|
|
213
|
+
!ctx.blockedMoveTarget || dist(room.x, room.y, ctx.blockedMoveTarget.x, ctx.blockedMoveTarget.y) > 10);
|
|
214
|
+
const candidates = notBlocked.length > 0 ? notBlocked : ordered;
|
|
215
|
+
if (threatPoints.length === 0) {
|
|
216
|
+
movePatrolToRoom(ctx, patrol, current, candidates[0]);
|
|
217
|
+
return candidates[0];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const threatRadius = opts.threatExcludeRadius ?? TASK_THREAT_EXCLUDE_RANGE;
|
|
221
|
+
const endpointSafe = candidates.filter(room =>
|
|
222
|
+
!threatPoints.some(p => dist(room.x, room.y, p.x, p.y) <= threatRadius));
|
|
223
|
+
if (endpointSafe.length === 0) return null;
|
|
224
|
+
|
|
225
|
+
const routes = assessRoutes(
|
|
226
|
+
{ x: state.you.x, y: state.you.y },
|
|
227
|
+
endpointSafe.map(room => ({ x: room.x, y: room.y })),
|
|
228
|
+
threatPoints,
|
|
229
|
+
opts.pathThreatRadius ?? TASK_PATH_THREAT_RADIUS,
|
|
230
|
+
);
|
|
231
|
+
const viable = routes == null
|
|
232
|
+
? endpointSafe
|
|
233
|
+
: endpointSafe.filter((_room, i) => !routes[i].nearThreat);
|
|
234
|
+
const selected = viable[0] ?? null;
|
|
235
|
+
if (!selected) return null;
|
|
236
|
+
movePatrolToRoom(ctx, patrol, current, selected);
|
|
237
|
+
return selected;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function orderedPatrolRooms(ctx: StrategyContext, current: RoomTarget): RoomTarget[] {
|
|
241
|
+
if (ctx.rooms.length === 0) return [];
|
|
242
|
+
const start = Math.max(0, ctx.rooms.findIndex(room => room.name === current.name));
|
|
243
|
+
return ctx.rooms.map((_room, offset) => ctx.rooms[(start + offset) % ctx.rooms.length]);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function movePatrolToRoom(ctx: StrategyContext, patrol: PatrolState, current: RoomTarget, selected: RoomTarget): void {
|
|
247
|
+
if (selected.name === current.name || ctx.rooms.length === 0) return;
|
|
248
|
+
for (let i = 0; i < ctx.rooms.length; i += 1) {
|
|
249
|
+
const room = patrol.nextRoom(ctx);
|
|
250
|
+
if (!room || room.name === selected.name) return;
|
|
251
|
+
patrol.advance(ctx);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 靠近 → 到点提交单个任务;到点但本地仍被冷却(not_at_task_location 等)返回 null,让上层兜底巡逻、
|
|
257
|
+
* 避免死盯一个刚到却提交失败的点。供「会做任务伪装」的坏人策略(crab-sabotage / lone-kill-task)复用。
|
|
258
|
+
*/
|
|
259
|
+
export function approachOrDoTaskStep(
|
|
260
|
+
state: GameState,
|
|
261
|
+
ctx: StrategyContext,
|
|
262
|
+
task: TaskInfo,
|
|
263
|
+
stopOnPlayer = false,
|
|
264
|
+
): BehaviorDecision[] | null {
|
|
265
|
+
const d = dist(state.you.x, state.you.y, task.x!, task.y!);
|
|
266
|
+
if (d <= TASK_SUBMIT_RADIUS) {
|
|
267
|
+
if (Date.now() >= ctx.taskLocalBlockedUntil) return [{ action: Action.doTask(task.task_name) }];
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
return [{ action: Action.move({ x: task.x!, y: task.y! }, stopOnPlayer) }];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** 单个任务的「靠近 → 到点提交」决策:在提交半径内且本地未被冷却就 doTask,否则移动过去。 */
|
|
274
|
+
export function taskMoveDecision(state: GameState, ctx: StrategyContext, task: TaskInfo): BehaviorDecision {
|
|
275
|
+
const d = dist(state.you.x, state.you.y, task.x!, task.y!);
|
|
276
|
+
if (d <= TASK_SUBMIT_RADIUS && Date.now() >= ctx.taskLocalBlockedUntil) {
|
|
277
|
+
return { action: Action.doTask(task.task_name) };
|
|
278
|
+
}
|
|
279
|
+
return { action: Action.move({ x: task.x!, y: task.y! }) };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function nearestReportableCorpse(state: GameState): GameState['corpses'][number] | null {
|
|
283
|
+
const nearby = state.corpses
|
|
284
|
+
.map(corpse => {
|
|
285
|
+
const distance = corpse.distance ?? (
|
|
286
|
+
corpse.x != null && corpse.y != null
|
|
287
|
+
? dist(state.you.x, state.you.y, corpse.x, corpse.y)
|
|
288
|
+
: Infinity
|
|
289
|
+
);
|
|
290
|
+
return { corpse, distance };
|
|
291
|
+
})
|
|
292
|
+
.filter(({ distance }) => distance <= REPORT_RANGE)
|
|
293
|
+
.sort((a, b) => a.distance - b.distance);
|
|
294
|
+
return nearby[0]?.corpse ?? null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** 已知尸体中离自己最近、且有坐标可导航的一具(含已离开视野、之前记下的)。供接近/徘徊用。 */
|
|
298
|
+
export function nearestKnownCorpse(state: GameState, ctx: StrategyContext): CorpseTarget | null {
|
|
299
|
+
const candidates = (ctx.knownCorpses ?? []).filter(c => c.x != null && c.y != null);
|
|
300
|
+
if (candidates.length === 0) return null;
|
|
301
|
+
const corpse = candidates.reduce((best, c) =>
|
|
302
|
+
dist(state.you.x, state.you.y, c.x!, c.y!) < dist(state.you.x, state.you.y, best.x!, best.y!) ? c : best,
|
|
303
|
+
);
|
|
304
|
+
return { x: corpse.x!, y: corpse.y!, name: corpse.name, room: corpse.room };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 可导航的尸体目标:优先返回带坐标的最近尸体;若已知尸体都还没拿到坐标(state.corpses 只给 distance,
|
|
309
|
+
* 坐标得等 corpse_spotted 事件补上),退化为「尸体所在房间」的锚点,先把 bot 带进房间——人进视野后坐标
|
|
310
|
+
* 自然补全,避免「明知有尸体却因暂时没坐标而巡逻走开」。两者皆无才 null。接近/徘徊类**导航**用,区别于
|
|
311
|
+
* 严格要坐标的 nearestKnownCorpse(报尸等必须站在尸体上的场景仍用后者)。
|
|
312
|
+
*/
|
|
313
|
+
export function nearestKnownCorpseNav(state: GameState, ctx: StrategyContext): CorpseTarget | null {
|
|
314
|
+
const withCoords = nearestKnownCorpse(state, ctx);
|
|
315
|
+
if (withCoords) return withCoords;
|
|
316
|
+
for (const c of ctx.knownCorpses ?? []) {
|
|
317
|
+
if (!c.room) continue;
|
|
318
|
+
const room = ctx.rooms.find(r => r.name === c.room);
|
|
319
|
+
if (room) return { x: room.x, y: room.y, name: c.name, room: c.room };
|
|
320
|
+
}
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** 是否已知任何尸体(含看见过、已离开视野的)。开会清场后由 strategy-loop 清空。供「该不该去报尸/回避」类**导航**判断用。 */
|
|
325
|
+
export function hasKnownCorpse(ctx: StrategyContext): boolean {
|
|
326
|
+
return (ctx.knownCorpses?.length ?? 0) > 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 当前这一帧我就**站在命案现场**(≤CORPSE_SCENE_RANGE,150px)——视野里有尸体或本 tick 刚冒出
|
|
331
|
+
* `corpse_spotted`,且尸体确实落在现场半径内。贴脸灭口目击者 / 报尸自证 / 标注尸体旁目击者这类
|
|
332
|
+
* **现场反射**必须用它,**不能**用 `hasKnownCorpse`:后者只表示「记得地图某处有尸体」,会把「记得有
|
|
333
|
+
* 尸体」误当成「人就在尸体旁」,导致随便撞见一个人就灭口 / 误把后来遇见者当目击者 / 生成错误的栽赃简报。
|
|
334
|
+
*/
|
|
335
|
+
export function corpseAtScene(state: GameState): boolean {
|
|
336
|
+
// state.corpses 按「视野半径」(SHRIMP_VISION_RANGE)过滤,比命案现场半径(CORPSE_SCENE_RANGE=150)大得多——视野里有
|
|
337
|
+
// 尸体 ≠ 人就站在尸体旁。不加距离门控会让 300px 外的尸体也触发灭口/目击者标注/栽赃,故同样按现场半径筛。
|
|
338
|
+
const corpseInScene = state.corpses.some(c => {
|
|
339
|
+
const d = c.distance ?? (
|
|
340
|
+
c.x != null && c.y != null ? dist(state.you.x, state.you.y, c.x, c.y) : Infinity
|
|
341
|
+
);
|
|
342
|
+
return d <= CORPSE_SCENE_RANGE;
|
|
343
|
+
});
|
|
344
|
+
if (corpseInScene) return true;
|
|
345
|
+
// corpse_spotted 事件可能被 backfill 补入、或在本轮移动开始后才到达,单看「增量里有该事件」会把地图别处
|
|
346
|
+
// 的尸体误当成脚下的命案现场。尸体不会移动,故只认坐标确实落在现场半径内的事件。
|
|
347
|
+
return spottedCorpsesFromEvents(state).some(c =>
|
|
348
|
+
dist(state.you.x, state.you.y, c.x, c.y) <= CORPSE_SCENE_RANGE);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** 本 tick 增量里**全部** corpse_spotted 事件(按到达顺序)。同批多具尸体不能只留最后一具——
|
|
352
|
+
* server 的 state.corpses 没坐标,漏掉的尸体无法再补回。 */
|
|
353
|
+
export function spottedCorpsesFromEvents(state: GameState): CorpseTarget[] {
|
|
354
|
+
return spottedCorpsesFromEventList(state.new_events);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** corpse_spotted 坐标提取的底层版:可作用于任意事件数组(如 action 结果里的 new_events,绕开主循环游标竞态)。 */
|
|
358
|
+
export function spottedCorpsesFromEventList(events: any[] | undefined): CorpseTarget[] {
|
|
359
|
+
const out: CorpseTarget[] = [];
|
|
360
|
+
for (const evt of events ?? []) {
|
|
361
|
+
if (evt?.type !== 'corpse_spotted') continue;
|
|
362
|
+
const x = Number(evt.corpse_x);
|
|
363
|
+
const y = Number(evt.corpse_y);
|
|
364
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
|
|
365
|
+
out.push({
|
|
366
|
+
x,
|
|
367
|
+
y,
|
|
368
|
+
name: typeof evt.corpse_name === 'string' ? evt.corpse_name : undefined,
|
|
369
|
+
room: typeof evt.corpse_room === 'string' ? evt.corpse_room : undefined,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return out;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* 已知尸体的跨 tick 记忆,全局唯一一份由 strategy-loop 持有,每 tick `observe` 后写入 `ctx.knownCorpses`,
|
|
377
|
+
* 供所有策略统一读取(检测 hasKnownCorpse、接近 nearestKnownCorpse、任务回避 firstAvailableTask/nearestSafeTask、
|
|
378
|
+
* 死亡确认按名/座位匹配)。`state.corpses` 只含当前视野(nearby)尸体,`corpse_spotted` 事件又只在尸体出现的
|
|
379
|
+
* 那一 tick 进增量——两者都没有持久性,尸体一离开视野下一 tick 就会丢。CorpseMemory 把见过的尸体按身份累积
|
|
380
|
+
* 去重,使「看见过、已离开视野」的尸体仍持续可见。尸体在游戏里出现后一直存在到开会才被清场,故 strategy-loop
|
|
381
|
+
* 在 `onMeetingResume` 时 `reset` 并清空 `ctx.knownCorpses`。报尸本身仍按近距触发(nearestReportableCorpse
|
|
382
|
+
* 读 state.corpses),记忆负责把 bot 带回尸体旁。
|
|
383
|
+
*/
|
|
384
|
+
export class CorpseMemory {
|
|
385
|
+
private readonly seen: CorpseInfo[] = [];
|
|
386
|
+
private readonly lastSeenPlayers = new Map<string, PlayerInfo>();
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* 每 tick 调用:缓存可见玩家位置,并入已确认击杀、当前视野尸体与本 tick 全部 corpse_spotted 事件。
|
|
390
|
+
* 服务端可能直到杀手再次移动才生成 corpse_spotted,因此成功击杀先用目标最后目击位置兜底。
|
|
391
|
+
*/
|
|
392
|
+
observe(state: GameState, recentlyKilledTargets?: Map<string, number>): void {
|
|
393
|
+
for (const player of state.players) this.rememberPlayer(player);
|
|
394
|
+
for (const evt of state.new_events ?? []) {
|
|
395
|
+
if (evt?.type !== 'player_spotted') continue;
|
|
396
|
+
const name = typeof evt.spotted_name === 'string' ? evt.spotted_name : '';
|
|
397
|
+
const x = Number(evt.spotted_x);
|
|
398
|
+
const y = Number(evt.spotted_y);
|
|
399
|
+
if (!name || !Number.isFinite(x) || !Number.isFinite(y)) continue;
|
|
400
|
+
this.rememberPlayer({
|
|
401
|
+
name,
|
|
402
|
+
room: typeof evt.spotted_room === 'string' ? evt.spotted_room : '',
|
|
403
|
+
x,
|
|
404
|
+
y,
|
|
405
|
+
seat: typeof evt.spotted_seat === 'number' ? evt.spotted_seat : undefined,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const now = Date.now();
|
|
410
|
+
for (const [target, until] of recentlyKilledTargets ?? []) {
|
|
411
|
+
if (until <= now) continue;
|
|
412
|
+
const player = this.lastSeenPlayers.get(target.trim().toLowerCase());
|
|
413
|
+
if (player) this.remember(player);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// state.corpses 只含当前视野尸体,且可能无坐标(server 仅给 distance);仍记下来,使无坐标尸体
|
|
417
|
+
// 至少能被「存在性检测」(hasKnownCorpse) 与「按名/座位死亡确认」命中,坐标随后由 corpse_spotted /
|
|
418
|
+
// 击杀回填补全。这样 knownCorpses 成为 state.corpses 的严格超集,跨 tick 持久、开会才清空。
|
|
419
|
+
for (const c of state.corpses) this.remember(c);
|
|
420
|
+
for (const spotted of spottedCorpsesFromEvents(state)) this.remember(spotted);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* 从任意事件数组补全 corpse_spotted 坐标——供 action 结果里的 new_events 用,绕开主循环 state.new_events
|
|
425
|
+
* 的共享游标竞态(daemon 可能先通过 WS 消费掉该事件,strategy 的 HTTP 轮询就再也看不到,尸体坐标永远进
|
|
426
|
+
* 不了记忆,导航类策略只能巡逻走开)。
|
|
427
|
+
*/
|
|
428
|
+
ingestEvents(events: any[] | undefined): void {
|
|
429
|
+
for (const spotted of spottedCorpsesFromEventList(events)) this.remember(spotted);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** 累积的已知尸体(含已离开视野的)。供 firstAvailableTask / nearestSafeTask 的尸体回避参数。 */
|
|
433
|
+
list(): CorpseInfo[] {
|
|
434
|
+
return this.seen;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** 会议后尸体清场 → 清空记忆。 */
|
|
438
|
+
reset(): void {
|
|
439
|
+
this.seen.length = 0;
|
|
440
|
+
this.lastSeenPlayers.clear();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private rememberPlayer(player: PlayerInfo): void {
|
|
444
|
+
const key = player.name.trim().toLowerCase();
|
|
445
|
+
if (key) this.lastSeenPlayers.set(key, player);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private remember(corpse: { x?: number; y?: number; name?: string; room?: string; seat?: number }): void {
|
|
449
|
+
// 身份去重,不能用任务回避半径(100px)——那会把相距 90px 的两具不同尸体并成一具,
|
|
450
|
+
// 漏掉第二具另一侧的任务回避。优先按尸体名判同,无名时退化为极近坐标(CORPSE_SAME_BODY_RADIUS)。
|
|
451
|
+
const name = corpse.name ?? '';
|
|
452
|
+
const existing = this.seen.find(c => this.isSameBody(c, corpse, name));
|
|
453
|
+
if (!existing) {
|
|
454
|
+
this.seen.push({ name, room: corpse.room ?? '', x: corpse.x, y: corpse.y, seat: corpse.seat });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
// 同一具尸体的后续观测:补全先前缺失的坐标 / 座位(首见常是无坐标的 state.corpses 条目)。
|
|
458
|
+
if (existing.x == null && corpse.x != null) { existing.x = corpse.x; existing.y = corpse.y; }
|
|
459
|
+
if (existing.seat == null && corpse.seat != null) existing.seat = corpse.seat;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private isSameBody(c: CorpseInfo, other: { x?: number; y?: number }, name: string): boolean {
|
|
463
|
+
if (name && c.name) return c.name === name; // 两边都有名:同名才是同一具
|
|
464
|
+
if (c.x != null && c.y != null && other.x != null && other.y != null) {
|
|
465
|
+
return dist(c.x, c.y, other.x, other.y) <= CORPSE_SAME_BODY_RADIUS;
|
|
466
|
+
}
|
|
467
|
+
return false; // 无名且坐标不全:无法判同,按新尸体处理(极退化,几乎不出现)
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function reportCorpseDecision(
|
|
472
|
+
state: GameState,
|
|
473
|
+
ctx: StrategyContext,
|
|
474
|
+
opts: { respectBlock?: boolean } = {},
|
|
475
|
+
): BehaviorDecision | null {
|
|
476
|
+
const reportBlocked = opts.respectBlock && Date.now() < ctx.reportBlockedUntil;
|
|
477
|
+
|
|
478
|
+
const reportable = nearestReportableCorpse(state);
|
|
479
|
+
if (reportable && !reportBlocked) {
|
|
480
|
+
ctx.reportCorpseTarget = null;
|
|
481
|
+
ctx.notifications.push(`发现尸体,正在报告!`);
|
|
482
|
+
return { action: Action.report(reportable.name, reportable.room) };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const corpse = nearestKnownCorpse(state, ctx);
|
|
486
|
+
if (corpse) {
|
|
487
|
+
if (!ctx.reportCorpseTarget) {
|
|
488
|
+
ctx.notifications.push(`发现尸体,正在靠近准备报告。`);
|
|
489
|
+
}
|
|
490
|
+
ctx.reportCorpseTarget = corpse;
|
|
491
|
+
return { action: Action.move({ x: corpse.x, y: corpse.y }) };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
ctx.reportCorpseTarget = null;
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function nonTeammatesVisible(state: GameState, ctx: StrategyContext): PlayerInfo[] {
|
|
499
|
+
const players = visibleLivePlayers(state, ctx);
|
|
500
|
+
return ctx.teammates.size > 0
|
|
501
|
+
? players.filter(p => !ctx.teammates.has(p.name))
|
|
502
|
+
: players;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function matchesTarget(
|
|
506
|
+
target: string,
|
|
507
|
+
player: { name?: string; seat?: number },
|
|
508
|
+
ctx: StrategyContext,
|
|
509
|
+
): boolean {
|
|
510
|
+
const normalized = target.trim().toLowerCase();
|
|
511
|
+
if (player.name && player.name.toLowerCase() === normalized) return true;
|
|
512
|
+
if (/^\d+$/.test(target.trim())) {
|
|
513
|
+
const nameFromSeat = ctx.playerNamesBySeat[target.trim()];
|
|
514
|
+
if (nameFromSeat && player.name && player.name.toLowerCase() === nameFromSeat.toLowerCase()) return true;
|
|
515
|
+
if (player.seat != null && String(player.seat) === target.trim()) return true;
|
|
516
|
+
}
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function matchesAnyTarget(
|
|
521
|
+
player: { name?: string; seat?: number },
|
|
522
|
+
targets: string[],
|
|
523
|
+
ctx: StrategyContext,
|
|
524
|
+
): boolean {
|
|
525
|
+
return targets.some(target => matchesTarget(target, player, ctx));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function isTargetAlive(
|
|
529
|
+
state: GameState,
|
|
530
|
+
target: string,
|
|
531
|
+
ctx: StrategyContext,
|
|
532
|
+
): boolean {
|
|
533
|
+
if (isRecentlyKilledTarget(target, ctx)) return false;
|
|
534
|
+
const player = state.all_players?.find(p => matchesTarget(target, p, ctx));
|
|
535
|
+
return player ? player.is_alive !== false : true;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function isRecentlyKilledTarget(target: string, ctx: StrategyContext): boolean {
|
|
539
|
+
const names = targetNames(target, ctx);
|
|
540
|
+
for (const name of names) {
|
|
541
|
+
const until = ctx.recentlyKilledTargets?.get(name);
|
|
542
|
+
if (!until) continue;
|
|
543
|
+
if (until > Date.now()) return true;
|
|
544
|
+
ctx.recentlyKilledTargets?.delete(name);
|
|
545
|
+
}
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function targetNames(target: string, ctx: StrategyContext): string[] {
|
|
550
|
+
const normalized = target.trim().toLowerCase();
|
|
551
|
+
if (!normalized) return [];
|
|
552
|
+
const names = [normalized];
|
|
553
|
+
if (/^\d+$/.test(target.trim())) {
|
|
554
|
+
const nameFromSeat = ctx.playerNamesBySeat[target.trim()];
|
|
555
|
+
if (nameFromSeat) names.push(nameFromSeat.toLowerCase());
|
|
556
|
+
}
|
|
557
|
+
return names;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export function visibleLivePlayers(state: GameState, ctx: StrategyContext): PlayerInfo[] {
|
|
561
|
+
return state.players.filter(p => isTargetAlive(state, p.name, ctx));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function nearestPlayerByDistance(state: GameState, players: PlayerInfo[]): PlayerInfo | null {
|
|
565
|
+
return players
|
|
566
|
+
.map(p => ({ p, d: p.distance ?? dist(state.you.x, state.you.y, p.x, p.y) }))
|
|
567
|
+
.sort((a, b) => a.d - b.d)[0]?.p ?? null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export function nearestVisibleTarget(
|
|
571
|
+
state: GameState,
|
|
572
|
+
ctx: StrategyContext,
|
|
573
|
+
targets: string[],
|
|
574
|
+
): PlayerInfo | null {
|
|
575
|
+
return nearestPlayerByDistance(
|
|
576
|
+
state,
|
|
577
|
+
visibleLivePlayers(state, ctx).filter(p => targets.some(t => matchesTarget(t, p, ctx))),
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export function pursueVisibleTarget(
|
|
582
|
+
state: GameState,
|
|
583
|
+
target: PlayerInfo,
|
|
584
|
+
opts: { kill: boolean; followDistance: number },
|
|
585
|
+
): BehaviorDecision | null {
|
|
586
|
+
const d = target.distance ?? dist(state.you.x, state.you.y, target.x, target.y);
|
|
587
|
+
if (opts.kill && !hasKillUseRemaining(state)) return null;
|
|
588
|
+
if (opts.kill && d <= KILL_RANGE && canUseKill(state)) {
|
|
589
|
+
return { action: Action.kill(target.name) };
|
|
590
|
+
}
|
|
591
|
+
if (d > opts.followDistance) {
|
|
592
|
+
return { action: Action.move({ x: target.x, y: target.y }) };
|
|
593
|
+
}
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ── 知识系统语义助手 ────────────────────────────────────────────────
|
|
598
|
+
// 把 ctx.knowledge 的查询统一成「躲/杀/护」三种意图,供内置与用户策略复用。
|
|
599
|
+
// 见 docs/策略知识系统-by-claude.md。
|
|
600
|
+
|
|
601
|
+
type PlayerRef = { name?: string; seat?: number };
|
|
602
|
+
const FALLBACK_KNOWLEDGE = emptyKnowledgeView();
|
|
603
|
+
|
|
604
|
+
function knowledgeOf(ctx: StrategyContext) {
|
|
605
|
+
return ctx.knowledge ?? FALLBACK_KNOWLEDGE;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** hostile:带刀策略可主动攻击;无刀策略应回避。 */
|
|
609
|
+
export function isKnowledgeHostile(p: PlayerRef, ctx: StrategyContext, minConfidence?: number): boolean {
|
|
610
|
+
return knowledgeOf(ctx).isHostile(p, minConfidence);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** suspect 或 hostile:任何生存型策略都应视为威胁并回避。 */
|
|
614
|
+
export function isKnowledgeThreat(p: PlayerRef, ctx: StrategyContext, minConfidence?: number): boolean {
|
|
615
|
+
const mark = knowledgeOf(ctx).markOf(p, minConfidence);
|
|
616
|
+
return mark === 'suspect' || mark === 'hostile';
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** trusted:可信同伴,永远优先于回避与击杀。 */
|
|
620
|
+
export function isKnowledgeTrusted(p: PlayerRef, ctx: StrategyContext, minConfidence?: number): boolean {
|
|
621
|
+
return knowledgeOf(ctx).isTrusted(p, minConfidence);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** @deprecated Use isKnowledgeHostile. */
|
|
625
|
+
export const isKnowledgeKillTarget = isKnowledgeHostile;
|
|
626
|
+
/** @deprecated Use isKnowledgeThreat. */
|
|
627
|
+
export const isKnowledgeAvoid = isKnowledgeThreat;
|
|
628
|
+
/** @deprecated Use isKnowledgeTrusted. */
|
|
629
|
+
export const isKnowledgeProtected = isKnowledgeTrusted;
|
|
630
|
+
|
|
631
|
+
export function resolveRoom(name: string, ctx: StrategyContext): RoomTarget | null {
|
|
632
|
+
const folded = name.trim().toLowerCase();
|
|
633
|
+
if (!folded) return null;
|
|
634
|
+
return ctx.rooms.find(room => room.name.toLowerCase() === folded) ?? null;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export function isInRoom(name: string, state: GameState): boolean {
|
|
638
|
+
const room = state.you.room;
|
|
639
|
+
if (!room) return false;
|
|
640
|
+
return room.toLowerCase() === name.trim().toLowerCase();
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export function hasReachedRoomTarget(state: GameState, target: RoomTarget): boolean {
|
|
644
|
+
return dist(state.you.x, state.you.y, target.x, target.y) <= PATROL_REACHED_DISTANCE;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export function roomAwayFromPlayers(
|
|
648
|
+
state: GameState,
|
|
649
|
+
ctx: StrategyContext,
|
|
650
|
+
players: PlayerInfo[],
|
|
651
|
+
): RoomTarget | null {
|
|
652
|
+
if (ctx.rooms.length === 0 || players.length === 0) return null;
|
|
653
|
+
const playerRooms = new Set(players.map(player => player.room?.toLowerCase()).filter(Boolean));
|
|
654
|
+
|
|
655
|
+
const nearestPlayer = players
|
|
656
|
+
.map(player => ({
|
|
657
|
+
player,
|
|
658
|
+
distance: player.distance ?? dist(state.you.x, state.you.y, player.x, player.y),
|
|
659
|
+
}))
|
|
660
|
+
.sort((a, b) => a.distance - b.distance)[0]?.player;
|
|
661
|
+
if (!nearestPlayer) return null;
|
|
662
|
+
|
|
663
|
+
const awayX = state.you.x - nearestPlayer.x;
|
|
664
|
+
const awayY = state.you.y - nearestPlayer.y;
|
|
665
|
+
const awayLen = Math.hypot(awayX, awayY);
|
|
666
|
+
|
|
667
|
+
const scored = ctx.rooms.map(room => {
|
|
668
|
+
const toRoomX = room.x - state.you.x;
|
|
669
|
+
const toRoomY = room.y - state.you.y;
|
|
670
|
+
const toRoomLen = Math.hypot(toRoomX, toRoomY);
|
|
671
|
+
const direction = awayLen > 0 && toRoomLen > 0
|
|
672
|
+
? (toRoomX * awayX + toRoomY * awayY) / (toRoomLen * awayLen)
|
|
673
|
+
: 0;
|
|
674
|
+
const nearestDistance = Math.min(...players.map(player => dist(room.x, room.y, player.x, player.y)));
|
|
675
|
+
return {
|
|
676
|
+
room,
|
|
677
|
+
direction,
|
|
678
|
+
nearestDistance,
|
|
679
|
+
selfDistance: dist(state.you.x, state.you.y, room.x, room.y),
|
|
680
|
+
};
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
const viable = scored.filter(item => {
|
|
684
|
+
if (playerRooms.has(item.room.name.toLowerCase())) return false;
|
|
685
|
+
if (ctx.blockedMoveTarget && dist(item.room.x, item.room.y, ctx.blockedMoveTarget.x, ctx.blockedMoveTarget.y) <= 10) return false;
|
|
686
|
+
return true;
|
|
687
|
+
});
|
|
688
|
+
const base = viable.length > 0 ? viable : scored;
|
|
689
|
+
const oppositeRooms = base.filter(item => item.direction > 0.15);
|
|
690
|
+
const candidates = oppositeRooms.length > 0 ? oppositeRooms : base;
|
|
691
|
+
candidates.sort((a, b) => {
|
|
692
|
+
const distanceDiff = b.nearestDistance - a.nearestDistance;
|
|
693
|
+
if (Math.abs(distanceDiff) > 1) return distanceDiff;
|
|
694
|
+
const directionDiff = b.direction - a.direction;
|
|
695
|
+
if (Math.abs(directionDiff) > 0.01) return directionDiff;
|
|
696
|
+
return a.selfDistance - b.selfDistance;
|
|
697
|
+
});
|
|
698
|
+
return candidates[0]?.room ?? null;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export class PatrolState {
|
|
702
|
+
private roomIndex = 0;
|
|
703
|
+
|
|
704
|
+
nextRoom(ctx: StrategyContext): RoomTarget | null {
|
|
705
|
+
if (ctx.rooms.length === 0) return null;
|
|
706
|
+
if (ctx.forcePatrolAdvance) {
|
|
707
|
+
ctx.forcePatrolAdvance = false;
|
|
708
|
+
this.roomIndex = (this.roomIndex + 1) % ctx.rooms.length;
|
|
709
|
+
}
|
|
710
|
+
return ctx.rooms[this.roomIndex % ctx.rooms.length];
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
advance(ctx: StrategyContext): void {
|
|
714
|
+
if (ctx.rooms.length === 0) return;
|
|
715
|
+
this.roomIndex = (this.roomIndex + 1) % ctx.rooms.length;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
reset(): void {
|
|
719
|
+
this.roomIndex = 0;
|
|
720
|
+
}
|
|
721
|
+
}
|