@myclaw163/clawclaw-cli 0.6.76 → 0.6.77
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 +387 -387
- package/bin/clawclaw-cli.mjs +3 -3
- package/package.json +48 -48
- package/personas//347/220/206/346/231/272/346/270/251/345/222/214.md +23 -23
- package/personas//350/200/201/350/260/213/346/267/261/347/256/227.md +22 -22
- package/personas//350/257/232/346/201/263/347/233/264/347/216/207.md +22 -22
- package/personas//350/275/273/346/235/276/346/264/273/346/263/274.md +22 -22
- package/personas//351/207/216/346/200/247/345/217/233/351/200/206.md +23 -23
- package/scripts/check-skill-command-surface.mjs +116 -116
- package/scripts/find-hide-spots.py +157 -157
- package/scripts/postinstall.mjs +20 -20
- package/scripts/sync-bundled-skill.mjs +254 -245
- package/scripts/sync-bundled-skill.test.mjs +152 -152
- package/skills/clawclaw/SKILL.md +248 -248
- package/skills/clawclaw/references/CHATTERBOX.md +141 -141
- package/skills/clawclaw/references/COMMANDS.md +160 -160
- package/skills/clawclaw/references/GAME-MECHANICS.md +188 -188
- package/skills/clawclaw/references/HUB.md +48 -48
- package/skills/clawclaw/references/KNOWLEDGE.md +42 -42
- package/skills/clawclaw/references/STRATEGIES.md +59 -59
- package/skills/clawclaw/references/STREAM.md +93 -93
- package/skills/clawclaw/references/TACTICS.md +65 -65
- package/src/assets/clawclaw-ascii-map.txt +40 -40
- package/src/cli.ts +112 -112
- package/src/commands/_schema.ts +124 -124
- package/src/commands/account.ts +209 -209
- package/src/commands/data.test.ts +33 -33
- package/src/commands/data.ts +22 -22
- package/src/commands/do.test.ts +84 -84
- package/src/commands/do.ts +130 -130
- package/src/commands/events.test.ts +100 -100
- package/src/commands/events.ts +250 -250
- package/src/commands/game-map.test.ts +28 -28
- package/src/commands/game-start-plan.test.ts +84 -84
- package/src/commands/game.ts +1113 -1113
- package/src/commands/history-player.test.ts +102 -102
- package/src/commands/history.ts +573 -573
- package/src/commands/hub.test.ts +96 -96
- package/src/commands/hub.ts +234 -234
- package/src/commands/knowledge.test.ts +13 -13
- package/src/commands/knowledge.ts +139 -139
- package/src/commands/load.test.ts +51 -51
- package/src/commands/load.ts +13 -13
- package/src/commands/meeting-history.test.ts +106 -106
- package/src/commands/memory.ts +40 -40
- package/src/commands/peek.ts +45 -45
- package/src/commands/persona.ts +57 -57
- package/src/commands/setup/codex.ts +266 -266
- package/src/commands/skill.ts +128 -128
- package/src/commands/state.ts +46 -46
- package/src/commands/strategy.test.ts +153 -153
- package/src/commands/strategy.ts +183 -183
- package/src/commands/tts.ts +128 -128
- package/src/commands/upgrade.test.ts +82 -82
- package/src/commands/upgrade.ts +148 -148
- package/src/commands/watch.test.ts +999 -999
- package/src/commands/watch.ts +660 -660
- package/src/lib/auth.test.ts +86 -86
- package/src/lib/auth.ts +223 -223
- package/src/lib/command-meta.ts +37 -37
- package/src/lib/game-client.ts +403 -403
- package/src/lib/game-context.ts +92 -92
- package/src/lib/http-keepalive.ts +15 -15
- package/src/lib/http-transport.test.ts +42 -42
- package/src/lib/http-transport.ts +113 -113
- package/src/lib/hub-client.test.ts +56 -56
- package/src/lib/hub-client.ts +88 -88
- package/src/lib/hub-install.test.ts +98 -98
- package/src/lib/hub-install.ts +160 -160
- package/src/lib/hub-reminder.ts +78 -78
- package/src/lib/hub-unzip.test.ts +69 -69
- package/src/lib/hub-unzip.ts +62 -62
- package/src/lib/init-command.test.ts +75 -75
- package/src/lib/init-command.ts +130 -130
- package/src/lib/knowledge-store.test.ts +170 -170
- package/src/lib/knowledge-store.ts +369 -369
- package/src/lib/load-context.test.ts +52 -52
- package/src/lib/load-context.ts +52 -52
- package/src/lib/match-state.test.ts +134 -134
- package/src/lib/match-state.ts +94 -94
- package/src/lib/netease-tts.ts +83 -83
- package/src/lib/normalize.ts +42 -42
- package/src/lib/persona.test.ts +41 -41
- package/src/lib/persona.ts +72 -72
- package/src/lib/server-registry.ts +152 -152
- package/src/lib/skill-version.test.ts +48 -48
- package/src/lib/skill-version.ts +19 -19
- package/src/lib/strategy-export.test.ts +240 -240
- package/src/lib/strategy-export.ts +247 -247
- package/src/lib/tts-keys.ts +7 -7
- package/src/lib/tts-speech.test.ts +63 -63
- package/src/lib/tts-speech.ts +76 -76
- package/src/lib/user-data.test.ts +96 -96
- package/src/lib/user-data.ts +400 -400
- package/src/lib/workspace-argv.test.ts +49 -49
- package/src/lib/workspace-argv.ts +44 -44
- package/src/perception/player-history-store.test.ts +87 -87
- package/src/perception/player-history-store.ts +194 -194
- package/src/pipeline/event-format.test.ts +243 -243
- package/src/pipeline/event-format.ts +501 -501
- package/src/pipeline/event-hints.ts +195 -195
- package/src/pipeline/event-store.test.ts +28 -28
- package/src/pipeline/event-store.ts +193 -193
- package/src/pipeline/pipeline.ts +35 -35
- package/src/pipeline/player-projection.test.ts +168 -168
- package/src/pipeline/player-projection.ts +370 -370
- package/src/runtime/auto-upgrade.test.ts +66 -66
- package/src/runtime/auto-upgrade.ts +31 -31
- package/src/runtime/event-daemon.test.ts +209 -209
- package/src/runtime/event-daemon.ts +519 -519
- package/src/runtime/owner-control.ts +150 -150
- package/src/runtime/raw-ws-log.test.ts +33 -33
- package/src/runtime/raw-ws-log.ts +32 -32
- package/src/runtime/runtime-logger.ts +107 -107
- package/src/runtime/ws-client.test.ts +125 -125
- package/src/runtime/ws-client.ts +287 -287
- package/src/sdk/action.ts +166 -166
- package/src/sdk/index.ts +110 -110
- package/src/sdk/types.ts +161 -161
- package/src/strategies/avoid-lone.ts +12 -12
- package/src/strategies/avoid-players.knowledge.md +19 -19
- package/src/strategies/avoid-players.ts +16 -16
- package/src/strategies/corpse-patrol.ts +23 -23
- package/src/strategies/crab-sabotage.ts +22 -22
- package/src/strategies/custom-module.test.ts +270 -270
- package/src/strategies/find-player.ts +17 -17
- package/src/strategies/game-utils.test.ts +242 -242
- package/src/strategies/game-utils.ts +846 -846
- package/src/strategies/goals/anchor-linger.ts +77 -77
- package/src/strategies/goals/avoid-lone-top.ts +168 -168
- package/src/strategies/goals/avoid-players-top.test.ts +83 -83
- package/src/strategies/goals/avoid-players-top.ts +121 -121
- package/src/strategies/goals/conversation-goal.ts +51 -51
- package/src/strategies/goals/corpse-patrol-top.ts +113 -113
- package/src/strategies/goals/crab-octopus-reflexes.ts +101 -101
- package/src/strategies/goals/crab-sabotage-top.ts +197 -197
- package/src/strategies/goals/emergency-hunt-goal.ts +28 -28
- package/src/strategies/goals/find-player-top.ts +93 -93
- package/src/strategies/goals/flee-players-goal.ts +53 -53
- package/src/strategies/goals/follow-companion-goal.ts +106 -106
- package/src/strategies/goals/goal-manager.ts +41 -41
- package/src/strategies/goals/goal-root-strategy.ts +49 -49
- package/src/strategies/goals/goal.ts +28 -28
- package/src/strategies/goals/hide-top.ts +197 -197
- package/src/strategies/goals/keep-away-goal.ts +221 -221
- package/src/strategies/goals/kill-frenzy-top.ts +80 -80
- package/src/strategies/goals/kill-lone-top.ts +160 -160
- package/src/strategies/goals/kill-target-goal.ts +59 -59
- package/src/strategies/goals/kill-target-top.ts +109 -109
- package/src/strategies/goals/leaf-goal.ts +27 -27
- package/src/strategies/goals/linger-corpse-goal.ts +35 -35
- package/src/strategies/goals/lone-kill-core.ts +82 -82
- package/src/strategies/goals/lone-kill-goal.ts +24 -24
- package/src/strategies/goals/lone-kill-task-top.test.ts +85 -85
- package/src/strategies/goals/lone-kill-task-top.ts +133 -133
- package/src/strategies/goals/move-room-goal.ts +60 -60
- package/src/strategies/goals/normal-shrimp-top.test.ts +80 -80
- package/src/strategies/goals/normal-shrimp-top.ts +242 -242
- package/src/strategies/goals/paradise-fish-top.test.ts +126 -126
- package/src/strategies/goals/paradise-fish-top.ts +224 -224
- package/src/strategies/goals/patrol-top.ts +57 -57
- package/src/strategies/goals/report-patrol-top.ts +80 -80
- package/src/strategies/goals/safe-task-goal.ts +102 -102
- package/src/strategies/goals/social-task-top.ts +161 -161
- package/src/strategies/goals/task-kill-report-top.ts +163 -163
- package/src/strategies/goals/task-only-top.ts +57 -57
- package/src/strategies/goals/task-or-patrol-goal.ts +41 -41
- package/src/strategies/goals/task-report-top.ts +57 -57
- package/src/strategies/goals/wander-task-goal.ts +33 -33
- package/src/strategies/goals/warrior-shrimp-top.test.ts +87 -87
- package/src/strategies/goals/warrior-shrimp-top.ts +267 -267
- package/src/strategies/greeting.ts +53 -53
- package/src/strategies/hide-spots.ts +59 -59
- package/src/strategies/hide.ts +24 -24
- package/src/strategies/kill-frenzy.ts +13 -13
- package/src/strategies/kill-lone.knowledge.md +17 -17
- package/src/strategies/kill-lone.ts +14 -14
- package/src/strategies/kill-target.ts +19 -19
- package/src/strategies/loader.test.ts +678 -678
- package/src/strategies/loader.ts +181 -181
- package/src/strategies/lone-kill-task.ts +22 -22
- package/src/strategies/meeting-gate.test.ts +59 -59
- package/src/strategies/meeting-gate.ts +23 -23
- package/src/strategies/move-room.ts +16 -16
- package/src/strategies/new-events-backfill.ts +98 -98
- package/src/strategies/off-route-points.ts +105 -105
- package/src/strategies/paradise-fish.knowledge.md +19 -19
- package/src/strategies/paradise-fish.ts +26 -26
- package/src/strategies/pathfind/distance-field.ts +150 -150
- package/src/strategies/pathfind/escape-planner.test.ts +197 -197
- package/src/strategies/pathfind/escape-planner.ts +355 -355
- package/src/strategies/pathfind/walkable-grid.ts +117 -117
- package/src/strategies/patrol.ts +12 -12
- package/src/strategies/player-targets.ts +13 -13
- package/src/strategies/report-patrol.ts +12 -12
- package/src/strategies/shrimp-memory.knowledge.md +19 -19
- package/src/strategies/shrimp-memory.ts +26 -26
- package/src/strategies/social-task.test.ts +28 -28
- package/src/strategies/social-task.ts +50 -50
- package/src/strategies/spawn.ts +82 -82
- package/src/strategies/speech-module.ts +123 -123
- package/src/strategies/strategy-loop.test.ts +15 -15
- package/src/strategies/strategy-loop.ts +776 -776
- package/src/strategies/task-kill-report.ts +18 -18
- package/src/strategies/task-only.ts +12 -12
- package/src/strategies/task-report.ts +23 -23
- package/src/strategies/types.ts +109 -109
- package/src/strategies/warrior-memory.knowledge.md +21 -21
- package/src/strategies/warrior-memory.ts +17 -17
|
@@ -1,355 +1,355 @@
|
|
|
1
|
-
import type { Position } from '../../sdk/types.js';
|
|
2
|
-
import { COST_DIAG, COST_STRAIGHT, FieldCalculator, PX_PER_UNIT, UNITS_PER_PX, type FieldResult } from './distance-field.js';
|
|
3
|
-
import { loadWalkableGrid, type WalkableGrid } from './walkable-grid.js';
|
|
4
|
-
|
|
5
|
-
// Escape planning against pure-pursuit crabs, in lockstep time steps where
|
|
6
|
-
// every crab walks its shortest walkable path toward the shrimp's position at
|
|
7
|
-
// the start of each step (same speed as the shrimp):
|
|
8
|
-
// - planEscape: beam-search the shrimp move that stays uncaught and maximizes
|
|
9
|
-
// the final geodesic distance to the nearest crab.
|
|
10
|
-
// - assessEscapeTarget: rate an already-committed target by rolling out the
|
|
11
|
-
// fixed walk toward it, so callers can keep a target until it turns bad
|
|
12
|
-
// instead of re-targeting every tick.
|
|
13
|
-
//
|
|
14
|
-
// Model approximations: octile 5/7 geodesics overestimate the server's
|
|
15
|
-
// smoothed paths by up to ~8% (symmetrically for both sides), and the
|
|
16
|
-
// pure-pursuit crab is more pessimistic than real crabs with vision limits.
|
|
17
|
-
|
|
18
|
-
export interface EscapeOptions {
|
|
19
|
-
/** Planning horizon in time steps. Default 6. */
|
|
20
|
-
steps?: number;
|
|
21
|
-
/** Distance each side covers per step, px. Default 144 (120px/s x 1.2s tick). */
|
|
22
|
-
stepDist?: number;
|
|
23
|
-
/** Geodesic distance at or under which the shrimp counts as caught, px. Default 80 (server kill_range). */
|
|
24
|
-
killRange?: number;
|
|
25
|
-
/** Beam width. Default 8. */
|
|
26
|
-
beamWidth?: number;
|
|
27
|
-
/** Distance field radius, px; crabs beyond it are frozen and scored as this far. Default 900. */
|
|
28
|
-
fieldRadius?: number;
|
|
29
|
-
/** Number of angle buckets for shrimp move candidates. Default 16. */
|
|
30
|
-
directions?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface EscapePlan {
|
|
34
|
-
/** Position after the final step (best struggle endpoint when caught). */
|
|
35
|
-
target: Position;
|
|
36
|
-
/** Geodesic distance from target to the nearest simulated crab, px; capped at fieldRadius; Infinity with no crabs. */
|
|
37
|
-
minCrabDistance: number;
|
|
38
|
-
/** One waypoint per survived step (tile centers in world coords). */
|
|
39
|
-
path: Position[];
|
|
40
|
-
/** True when every branch gets caught within the horizon. */
|
|
41
|
-
caught: boolean;
|
|
42
|
-
survivedSteps: number;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface TargetAssessment {
|
|
46
|
-
/** Geodesic distance from the shrimp's current position to the target, px; Infinity if unreachable. */
|
|
47
|
-
distanceToTarget: number;
|
|
48
|
-
/** Geodesic distance to the nearest crab when the rollout ends (arrival, horizon, or last safe step when caught), px; capped at fieldRadius; Infinity with no crabs. */
|
|
49
|
-
minCrabDistance: number;
|
|
50
|
-
/** True when walking the fixed path to the target gets caught within the horizon. */
|
|
51
|
-
caught: boolean;
|
|
52
|
-
survivedSteps: number;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface Node {
|
|
56
|
-
cell: number;
|
|
57
|
-
crabCells: number[];
|
|
58
|
-
depth: number;
|
|
59
|
-
parent: Node | null;
|
|
60
|
-
/** Lower bound on the min crab distance, px (beam ranking only). */
|
|
61
|
-
proxy: number;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
interface ResolvedOptions {
|
|
65
|
-
steps: number;
|
|
66
|
-
killRange: number;
|
|
67
|
-
beamWidth: number;
|
|
68
|
-
directions: number;
|
|
69
|
-
stepUnits: number;
|
|
70
|
-
killUnits: number;
|
|
71
|
-
radiusUnits: number;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function resolveOptions(opts: EscapeOptions): ResolvedOptions {
|
|
75
|
-
const steps = opts.steps ?? 6;
|
|
76
|
-
const stepDist = opts.stepDist ?? 144;
|
|
77
|
-
const killRange = opts.killRange ?? 80;
|
|
78
|
-
const beamWidth = opts.beamWidth ?? 8;
|
|
79
|
-
const fieldRadius = opts.fieldRadius ?? 900;
|
|
80
|
-
const directions = opts.directions ?? 16;
|
|
81
|
-
return {
|
|
82
|
-
steps,
|
|
83
|
-
killRange,
|
|
84
|
-
beamWidth,
|
|
85
|
-
directions,
|
|
86
|
-
stepUnits: Math.round(stepDist * UNITS_PER_PX),
|
|
87
|
-
killUnits: Math.round(killRange * UNITS_PER_PX),
|
|
88
|
-
radiusUnits: Math.round(fieldRadius * UNITS_PER_PX),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
interface Planner {
|
|
93
|
-
grid: WalkableGrid;
|
|
94
|
-
calc: FieldCalculator;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
let planner: Planner | null = null;
|
|
98
|
-
|
|
99
|
-
function getPlanner(): Planner {
|
|
100
|
-
if (!planner) {
|
|
101
|
-
const grid = loadWalkableGrid();
|
|
102
|
-
planner = { grid, calc: new FieldCalculator(grid) };
|
|
103
|
-
}
|
|
104
|
-
return planner;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function snapCell(grid: WalkableGrid, pos: Position): number {
|
|
108
|
-
return grid.snapToWalkable(grid.worldToCell(pos.x, pos.y));
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function snapCrabCells(grid: WalkableGrid, crabs: Position[]): number[] {
|
|
112
|
-
return crabs.map(crab => snapCell(grid, crab)).filter(cell => cell >= 0);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function euclidPx(grid: WalkableGrid, a: number, b: number): number {
|
|
116
|
-
const pa = grid.cellToWorld(a);
|
|
117
|
-
const pb = grid.cellToWorld(b);
|
|
118
|
-
return Math.hypot(pa.x - pb.x, pa.y - pb.y);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Both sides traverse their step path in the same time window; sample
|
|
122
|
-
// matching fractions and flag any moment within kill range (euclidean,
|
|
123
|
-
// ignoring walls — slightly conservative). Catches head-on swaps that the
|
|
124
|
-
// endpoint check misses. t=0 is skipped: step-start safety is already
|
|
125
|
-
// established geodesically, and a thin wall can make it euclid-close.
|
|
126
|
-
function pathsCross(grid: WalkableGrid, killRange: number, shrimpPath: number[], crabPath: number[]): boolean {
|
|
127
|
-
for (let k = 1; k <= 8; k++) {
|
|
128
|
-
const t = k / 8;
|
|
129
|
-
const a = shrimpPath[Math.round(t * (shrimpPath.length - 1))];
|
|
130
|
-
const b = crabPath[Math.round(t * (crabPath.length - 1))];
|
|
131
|
-
if (euclidPx(grid, a, b) <= killRange) return true;
|
|
132
|
-
}
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** One pursuit step: every crab descends the latest field (source = shrimp) by stepUnits. */
|
|
137
|
-
function pursuitStep(calc: FieldCalculator, crabCells: number[], stepUnits: number): { end: number; cells: number[] }[] {
|
|
138
|
-
return crabCells.map(cell => calc.descend(cell, stepUnits));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function planEscape(shrimp: Position, crabs: Position[], opts: EscapeOptions = {}): EscapePlan {
|
|
142
|
-
const { grid, calc } = getPlanner();
|
|
143
|
-
const o = resolveOptions(opts);
|
|
144
|
-
|
|
145
|
-
const start = snapCell(grid, shrimp);
|
|
146
|
-
if (start < 0) {
|
|
147
|
-
return { target: shrimp, minCrabDistance: Infinity, path: [], caught: false, survivedSteps: 0 };
|
|
148
|
-
}
|
|
149
|
-
const crabCells = snapCrabCells(grid, crabs);
|
|
150
|
-
|
|
151
|
-
let frontier: Node[] = [{ cell: start, crabCells, depth: 0, parent: null, proxy: 0 }];
|
|
152
|
-
let bestLeaf: { node: Node; minUnits: number } | null = null;
|
|
153
|
-
let struggle: { node: Node; minUnits: number } | null = null;
|
|
154
|
-
|
|
155
|
-
while (frontier.length > 0) {
|
|
156
|
-
const children: Node[] = [];
|
|
157
|
-
for (const node of frontier) {
|
|
158
|
-
const field: FieldResult = calc.compute(node.cell, o.radiusUnits, o.stepUnits, o.directions);
|
|
159
|
-
const crabDists = node.crabCells.map(cell => Math.min(field.distOf(cell), o.radiusUnits));
|
|
160
|
-
const minUnits = crabDists.length > 0 ? Math.min(...crabDists) : Infinity;
|
|
161
|
-
|
|
162
|
-
// End-of-step catch check, deferred to expansion: this field's source is
|
|
163
|
-
// the shrimp position and the crabs have already taken last step's move.
|
|
164
|
-
if (node.depth > 0 && minUnits <= o.killUnits) continue;
|
|
165
|
-
if (
|
|
166
|
-
!struggle ||
|
|
167
|
-
node.depth > struggle.node.depth ||
|
|
168
|
-
(node.depth === struggle.node.depth && minUnits > struggle.minUnits)
|
|
169
|
-
) {
|
|
170
|
-
struggle = { node, minUnits };
|
|
171
|
-
}
|
|
172
|
-
if (node.depth === o.steps) {
|
|
173
|
-
if (!bestLeaf || minUnits > bestLeaf.minUnits) bestLeaf = { node, minUnits };
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Pure pursuit: each crab descends this field toward the shrimp's
|
|
178
|
-
// step-start position.
|
|
179
|
-
const crabMoves = pursuitStep(calc, node.crabCells, o.stepUnits);
|
|
180
|
-
const newCrabCells = crabMoves.map(move => move.end);
|
|
181
|
-
const dangerous: number[] = [];
|
|
182
|
-
crabDists.forEach((d, i) => {
|
|
183
|
-
if (d <= o.killUnits + 2 * o.stepUnits) dangerous.push(i);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const candidates: number[] = [node.cell];
|
|
187
|
-
for (const cell of field.ringCandidates) if (cell >= 0) candidates.push(cell);
|
|
188
|
-
for (const cand of candidates) {
|
|
189
|
-
const shrimpPath = cand === node.cell ? [node.cell] : calc.pathToSource(cand);
|
|
190
|
-
if (dangerous.some(i => pathsCross(grid, o.killRange, shrimpPath, crabMoves[i].cells))) continue;
|
|
191
|
-
const candUnits = field.distOf(cand);
|
|
192
|
-
let proxy = Infinity;
|
|
193
|
-
for (const crab of newCrabCells) {
|
|
194
|
-
const crabUnits = field.distOf(crab);
|
|
195
|
-
const triangle = crabUnits === Infinity ? o.radiusUnits : crabUnits - candUnits;
|
|
196
|
-
proxy = Math.min(proxy, Math.max(euclidPx(grid, cand, crab), triangle * PX_PER_UNIT));
|
|
197
|
-
}
|
|
198
|
-
children.push({ cell: cand, crabCells: newCrabCells, depth: node.depth + 1, parent: node, proxy });
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
if (children.length === 0) break;
|
|
202
|
-
const byCell = new Map<number, Node>();
|
|
203
|
-
for (const child of children) {
|
|
204
|
-
const seen = byCell.get(child.cell);
|
|
205
|
-
if (!seen || child.proxy > seen.proxy) byCell.set(child.cell, child);
|
|
206
|
-
}
|
|
207
|
-
frontier = [...byCell.values()].sort((a, b) => b.proxy - a.proxy).slice(0, o.beamWidth);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const result = bestLeaf ?? struggle!;
|
|
211
|
-
const path: Position[] = [];
|
|
212
|
-
for (let n: Node | null = result.node; n && n.depth > 0; n = n.parent) {
|
|
213
|
-
path.unshift(grid.cellToWorld(n.cell));
|
|
214
|
-
}
|
|
215
|
-
return {
|
|
216
|
-
target: result.node.depth > 0 ? grid.cellToWorld(result.node.cell) : grid.cellToWorld(start),
|
|
217
|
-
minCrabDistance: result.minUnits * PX_PER_UNIT,
|
|
218
|
-
path,
|
|
219
|
-
caught: !bestLeaf,
|
|
220
|
-
survivedSteps: result.node.depth,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Fixed-policy rollout: the shrimp walks its initial geodesic path toward the
|
|
226
|
-
* target (mirroring the server's single A* at move submission) while the crabs
|
|
227
|
-
* pure-pursue each step, with the same catch semantics as planEscape. The
|
|
228
|
-
* rollout ends on arrival, at the horizon, or when caught.
|
|
229
|
-
*/
|
|
230
|
-
export function assessEscapeTarget(shrimp: Position, crabs: Position[], target: Position, opts: EscapeOptions = {}): TargetAssessment {
|
|
231
|
-
const { grid, calc } = getPlanner();
|
|
232
|
-
const o = resolveOptions(opts);
|
|
233
|
-
|
|
234
|
-
const start = snapCell(grid, shrimp);
|
|
235
|
-
const targetCell = start >= 0 ? snapCell(grid, target) : -1;
|
|
236
|
-
if (start < 0 || targetCell < 0) {
|
|
237
|
-
return { distanceToTarget: Infinity, minCrabDistance: Infinity, caught: false, survivedSteps: 0 };
|
|
238
|
-
}
|
|
239
|
-
let crabCells = snapCrabCells(grid, crabs);
|
|
240
|
-
|
|
241
|
-
// The walk path is fixed up front; extract it (and the target distance)
|
|
242
|
-
// before the per-step fields invalidate this one.
|
|
243
|
-
const reach = calc.compute(start, o.radiusUnits + o.steps * o.stepUnits, 0);
|
|
244
|
-
const targetUnits = reach.distOf(targetCell);
|
|
245
|
-
if (targetUnits === Infinity) {
|
|
246
|
-
return { distanceToTarget: Infinity, minCrabDistance: Infinity, caught: false, survivedSteps: 0 };
|
|
247
|
-
}
|
|
248
|
-
const walkPath = calc.pathToSource(targetCell);
|
|
249
|
-
const W = grid.width;
|
|
250
|
-
const cumUnits = new Array<number>(walkPath.length);
|
|
251
|
-
cumUnits[0] = 0;
|
|
252
|
-
for (let i = 1; i < walkPath.length; i++) {
|
|
253
|
-
const a = walkPath[i - 1];
|
|
254
|
-
const b = walkPath[i];
|
|
255
|
-
const diagonal = a % W !== b % W && Math.floor(a / W) !== Math.floor(b / W);
|
|
256
|
-
cumUnits[i] = cumUnits[i - 1] + (diagonal ? COST_DIAG : COST_STRAIGHT);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
let shrimpIdx = 0;
|
|
260
|
-
let caught = false;
|
|
261
|
-
let survivedSteps = 0;
|
|
262
|
-
let minUnitsAtEnd = Infinity;
|
|
263
|
-
|
|
264
|
-
for (let depth = 0; ; depth++) {
|
|
265
|
-
const cell = walkPath[shrimpIdx];
|
|
266
|
-
const field = calc.compute(cell, o.radiusUnits, 0);
|
|
267
|
-
const crabDists = crabCells.map(c => Math.min(field.distOf(c), o.radiusUnits));
|
|
268
|
-
const minUnits = crabDists.length > 0 ? Math.min(...crabDists) : Infinity;
|
|
269
|
-
|
|
270
|
-
// Same end-of-step semantics as planEscape: at depth > 0 the crabs have
|
|
271
|
-
// already taken last step's move toward the previous shrimp position.
|
|
272
|
-
if (depth > 0 && minUnits <= o.killUnits) {
|
|
273
|
-
caught = true;
|
|
274
|
-
break;
|
|
275
|
-
}
|
|
276
|
-
survivedSteps = depth;
|
|
277
|
-
minUnitsAtEnd = minUnits;
|
|
278
|
-
if (depth === o.steps || shrimpIdx >= walkPath.length - 1) break;
|
|
279
|
-
|
|
280
|
-
const crabMoves = pursuitStep(calc, crabCells, o.stepUnits);
|
|
281
|
-
let nextIdx = shrimpIdx;
|
|
282
|
-
while (nextIdx < walkPath.length - 1 && cumUnits[nextIdx + 1] - cumUnits[shrimpIdx] <= o.stepUnits) nextIdx++;
|
|
283
|
-
|
|
284
|
-
const dangerous: number[] = [];
|
|
285
|
-
crabDists.forEach((d, i) => {
|
|
286
|
-
if (d <= o.killUnits + 2 * o.stepUnits) dangerous.push(i);
|
|
287
|
-
});
|
|
288
|
-
const segment = walkPath.slice(shrimpIdx, nextIdx + 1);
|
|
289
|
-
if (dangerous.some(i => pathsCross(grid, o.killRange, segment, crabMoves[i].cells))) {
|
|
290
|
-
caught = true;
|
|
291
|
-
break;
|
|
292
|
-
}
|
|
293
|
-
crabCells = crabMoves.map(move => move.end);
|
|
294
|
-
shrimpIdx = nextIdx;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
return {
|
|
298
|
-
distanceToTarget: targetUnits * PX_PER_UNIT,
|
|
299
|
-
minCrabDistance: minUnitsAtEnd * PX_PER_UNIT,
|
|
300
|
-
caught,
|
|
301
|
-
survivedSteps,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
export interface RouteInfo {
|
|
306
|
-
/** 测地距离 px;maxRadiusPx 之外或不可达 = Infinity。 */
|
|
307
|
-
distancePx: number;
|
|
308
|
-
/** 测地路径是否经过任一威胁点 threatRadiusPx 内(不可达时恒为 false)。 */
|
|
309
|
-
nearThreat: boolean;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* 沿路径每 4 格(16px)采样一次离威胁点的欧氏距离,含末端格;半径 ~200px 下足够密。
|
|
314
|
-
* 豁免起点 from 的 radiusPx 邻域:逃跑那刻起点必然紧贴威胁,这一采样点不该把
|
|
315
|
-
* 「背向威胁、越走越远」的整条路线判危;真正夹在你与目的地之间、离起点超过
|
|
316
|
-
* radiusPx 的威胁仍会命中。对齐同文件 pathsCross 跳过 step 起点的语义。
|
|
317
|
-
*/
|
|
318
|
-
function pathNearAny(grid: WalkableGrid, path: number[], threats: Position[], radiusPx: number, from: Position): boolean {
|
|
319
|
-
for (let i = 0; i < path.length; i += 4) {
|
|
320
|
-
const idx = Math.min(i, path.length - 1);
|
|
321
|
-
const w = grid.cellToWorld(path[idx]);
|
|
322
|
-
if (Math.hypot(w.x - from.x, w.y - from.y) <= radiusPx) continue;
|
|
323
|
-
if (threats.some(t => Math.hypot(w.x - t.x, w.y - t.y) <= radiusPx)) return true;
|
|
324
|
-
}
|
|
325
|
-
const end = grid.cellToWorld(path[path.length - 1]);
|
|
326
|
-
if (Math.hypot(end.x - from.x, end.y - from.y) <= radiusPx) return false;
|
|
327
|
-
return threats.some(t => Math.hypot(end.x - t.x, end.y - t.y) <= radiusPx);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* 从 from 到各点的测地距离与「必经之路是否有危险」,一次距离场扫描 + 逐点回溯
|
|
332
|
-
* 梯度路径查表。threats 为空时只算距离不查路径。from 无法吸附到可走网格时返回
|
|
333
|
-
* null,调用方退回欧氏距离(且无路径信息)。
|
|
334
|
-
*/
|
|
335
|
-
export function assessRoutes(
|
|
336
|
-
from: Position,
|
|
337
|
-
points: Position[],
|
|
338
|
-
threats: Position[],
|
|
339
|
-
threatRadiusPx: number,
|
|
340
|
-
maxRadiusPx = 3000,
|
|
341
|
-
): RouteInfo[] | null {
|
|
342
|
-
const { grid, calc } = getPlanner();
|
|
343
|
-
const start = snapCell(grid, from);
|
|
344
|
-
if (start < 0) return null;
|
|
345
|
-
const field = calc.compute(start, Math.round(maxRadiusPx * UNITS_PER_PX), 0);
|
|
346
|
-
return points.map(p => {
|
|
347
|
-
const cell = snapCell(grid, p);
|
|
348
|
-
const units = cell >= 0 ? field.distOf(cell) : Infinity;
|
|
349
|
-
if (units === Infinity) return { distancePx: Infinity, nearThreat: false };
|
|
350
|
-
return {
|
|
351
|
-
distancePx: units * PX_PER_UNIT,
|
|
352
|
-
nearThreat: threats.length > 0 && pathNearAny(grid, calc.pathToSource(cell), threats, threatRadiusPx, from),
|
|
353
|
-
};
|
|
354
|
-
});
|
|
355
|
-
}
|
|
1
|
+
import type { Position } from '../../sdk/types.js';
|
|
2
|
+
import { COST_DIAG, COST_STRAIGHT, FieldCalculator, PX_PER_UNIT, UNITS_PER_PX, type FieldResult } from './distance-field.js';
|
|
3
|
+
import { loadWalkableGrid, type WalkableGrid } from './walkable-grid.js';
|
|
4
|
+
|
|
5
|
+
// Escape planning against pure-pursuit crabs, in lockstep time steps where
|
|
6
|
+
// every crab walks its shortest walkable path toward the shrimp's position at
|
|
7
|
+
// the start of each step (same speed as the shrimp):
|
|
8
|
+
// - planEscape: beam-search the shrimp move that stays uncaught and maximizes
|
|
9
|
+
// the final geodesic distance to the nearest crab.
|
|
10
|
+
// - assessEscapeTarget: rate an already-committed target by rolling out the
|
|
11
|
+
// fixed walk toward it, so callers can keep a target until it turns bad
|
|
12
|
+
// instead of re-targeting every tick.
|
|
13
|
+
//
|
|
14
|
+
// Model approximations: octile 5/7 geodesics overestimate the server's
|
|
15
|
+
// smoothed paths by up to ~8% (symmetrically for both sides), and the
|
|
16
|
+
// pure-pursuit crab is more pessimistic than real crabs with vision limits.
|
|
17
|
+
|
|
18
|
+
export interface EscapeOptions {
|
|
19
|
+
/** Planning horizon in time steps. Default 6. */
|
|
20
|
+
steps?: number;
|
|
21
|
+
/** Distance each side covers per step, px. Default 144 (120px/s x 1.2s tick). */
|
|
22
|
+
stepDist?: number;
|
|
23
|
+
/** Geodesic distance at or under which the shrimp counts as caught, px. Default 80 (server kill_range). */
|
|
24
|
+
killRange?: number;
|
|
25
|
+
/** Beam width. Default 8. */
|
|
26
|
+
beamWidth?: number;
|
|
27
|
+
/** Distance field radius, px; crabs beyond it are frozen and scored as this far. Default 900. */
|
|
28
|
+
fieldRadius?: number;
|
|
29
|
+
/** Number of angle buckets for shrimp move candidates. Default 16. */
|
|
30
|
+
directions?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface EscapePlan {
|
|
34
|
+
/** Position after the final step (best struggle endpoint when caught). */
|
|
35
|
+
target: Position;
|
|
36
|
+
/** Geodesic distance from target to the nearest simulated crab, px; capped at fieldRadius; Infinity with no crabs. */
|
|
37
|
+
minCrabDistance: number;
|
|
38
|
+
/** One waypoint per survived step (tile centers in world coords). */
|
|
39
|
+
path: Position[];
|
|
40
|
+
/** True when every branch gets caught within the horizon. */
|
|
41
|
+
caught: boolean;
|
|
42
|
+
survivedSteps: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TargetAssessment {
|
|
46
|
+
/** Geodesic distance from the shrimp's current position to the target, px; Infinity if unreachable. */
|
|
47
|
+
distanceToTarget: number;
|
|
48
|
+
/** Geodesic distance to the nearest crab when the rollout ends (arrival, horizon, or last safe step when caught), px; capped at fieldRadius; Infinity with no crabs. */
|
|
49
|
+
minCrabDistance: number;
|
|
50
|
+
/** True when walking the fixed path to the target gets caught within the horizon. */
|
|
51
|
+
caught: boolean;
|
|
52
|
+
survivedSteps: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface Node {
|
|
56
|
+
cell: number;
|
|
57
|
+
crabCells: number[];
|
|
58
|
+
depth: number;
|
|
59
|
+
parent: Node | null;
|
|
60
|
+
/** Lower bound on the min crab distance, px (beam ranking only). */
|
|
61
|
+
proxy: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface ResolvedOptions {
|
|
65
|
+
steps: number;
|
|
66
|
+
killRange: number;
|
|
67
|
+
beamWidth: number;
|
|
68
|
+
directions: number;
|
|
69
|
+
stepUnits: number;
|
|
70
|
+
killUnits: number;
|
|
71
|
+
radiusUnits: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveOptions(opts: EscapeOptions): ResolvedOptions {
|
|
75
|
+
const steps = opts.steps ?? 6;
|
|
76
|
+
const stepDist = opts.stepDist ?? 144;
|
|
77
|
+
const killRange = opts.killRange ?? 80;
|
|
78
|
+
const beamWidth = opts.beamWidth ?? 8;
|
|
79
|
+
const fieldRadius = opts.fieldRadius ?? 900;
|
|
80
|
+
const directions = opts.directions ?? 16;
|
|
81
|
+
return {
|
|
82
|
+
steps,
|
|
83
|
+
killRange,
|
|
84
|
+
beamWidth,
|
|
85
|
+
directions,
|
|
86
|
+
stepUnits: Math.round(stepDist * UNITS_PER_PX),
|
|
87
|
+
killUnits: Math.round(killRange * UNITS_PER_PX),
|
|
88
|
+
radiusUnits: Math.round(fieldRadius * UNITS_PER_PX),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface Planner {
|
|
93
|
+
grid: WalkableGrid;
|
|
94
|
+
calc: FieldCalculator;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let planner: Planner | null = null;
|
|
98
|
+
|
|
99
|
+
function getPlanner(): Planner {
|
|
100
|
+
if (!planner) {
|
|
101
|
+
const grid = loadWalkableGrid();
|
|
102
|
+
planner = { grid, calc: new FieldCalculator(grid) };
|
|
103
|
+
}
|
|
104
|
+
return planner;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function snapCell(grid: WalkableGrid, pos: Position): number {
|
|
108
|
+
return grid.snapToWalkable(grid.worldToCell(pos.x, pos.y));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function snapCrabCells(grid: WalkableGrid, crabs: Position[]): number[] {
|
|
112
|
+
return crabs.map(crab => snapCell(grid, crab)).filter(cell => cell >= 0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function euclidPx(grid: WalkableGrid, a: number, b: number): number {
|
|
116
|
+
const pa = grid.cellToWorld(a);
|
|
117
|
+
const pb = grid.cellToWorld(b);
|
|
118
|
+
return Math.hypot(pa.x - pb.x, pa.y - pb.y);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Both sides traverse their step path in the same time window; sample
|
|
122
|
+
// matching fractions and flag any moment within kill range (euclidean,
|
|
123
|
+
// ignoring walls — slightly conservative). Catches head-on swaps that the
|
|
124
|
+
// endpoint check misses. t=0 is skipped: step-start safety is already
|
|
125
|
+
// established geodesically, and a thin wall can make it euclid-close.
|
|
126
|
+
function pathsCross(grid: WalkableGrid, killRange: number, shrimpPath: number[], crabPath: number[]): boolean {
|
|
127
|
+
for (let k = 1; k <= 8; k++) {
|
|
128
|
+
const t = k / 8;
|
|
129
|
+
const a = shrimpPath[Math.round(t * (shrimpPath.length - 1))];
|
|
130
|
+
const b = crabPath[Math.round(t * (crabPath.length - 1))];
|
|
131
|
+
if (euclidPx(grid, a, b) <= killRange) return true;
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** One pursuit step: every crab descends the latest field (source = shrimp) by stepUnits. */
|
|
137
|
+
function pursuitStep(calc: FieldCalculator, crabCells: number[], stepUnits: number): { end: number; cells: number[] }[] {
|
|
138
|
+
return crabCells.map(cell => calc.descend(cell, stepUnits));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function planEscape(shrimp: Position, crabs: Position[], opts: EscapeOptions = {}): EscapePlan {
|
|
142
|
+
const { grid, calc } = getPlanner();
|
|
143
|
+
const o = resolveOptions(opts);
|
|
144
|
+
|
|
145
|
+
const start = snapCell(grid, shrimp);
|
|
146
|
+
if (start < 0) {
|
|
147
|
+
return { target: shrimp, minCrabDistance: Infinity, path: [], caught: false, survivedSteps: 0 };
|
|
148
|
+
}
|
|
149
|
+
const crabCells = snapCrabCells(grid, crabs);
|
|
150
|
+
|
|
151
|
+
let frontier: Node[] = [{ cell: start, crabCells, depth: 0, parent: null, proxy: 0 }];
|
|
152
|
+
let bestLeaf: { node: Node; minUnits: number } | null = null;
|
|
153
|
+
let struggle: { node: Node; minUnits: number } | null = null;
|
|
154
|
+
|
|
155
|
+
while (frontier.length > 0) {
|
|
156
|
+
const children: Node[] = [];
|
|
157
|
+
for (const node of frontier) {
|
|
158
|
+
const field: FieldResult = calc.compute(node.cell, o.radiusUnits, o.stepUnits, o.directions);
|
|
159
|
+
const crabDists = node.crabCells.map(cell => Math.min(field.distOf(cell), o.radiusUnits));
|
|
160
|
+
const minUnits = crabDists.length > 0 ? Math.min(...crabDists) : Infinity;
|
|
161
|
+
|
|
162
|
+
// End-of-step catch check, deferred to expansion: this field's source is
|
|
163
|
+
// the shrimp position and the crabs have already taken last step's move.
|
|
164
|
+
if (node.depth > 0 && minUnits <= o.killUnits) continue;
|
|
165
|
+
if (
|
|
166
|
+
!struggle ||
|
|
167
|
+
node.depth > struggle.node.depth ||
|
|
168
|
+
(node.depth === struggle.node.depth && minUnits > struggle.minUnits)
|
|
169
|
+
) {
|
|
170
|
+
struggle = { node, minUnits };
|
|
171
|
+
}
|
|
172
|
+
if (node.depth === o.steps) {
|
|
173
|
+
if (!bestLeaf || minUnits > bestLeaf.minUnits) bestLeaf = { node, minUnits };
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Pure pursuit: each crab descends this field toward the shrimp's
|
|
178
|
+
// step-start position.
|
|
179
|
+
const crabMoves = pursuitStep(calc, node.crabCells, o.stepUnits);
|
|
180
|
+
const newCrabCells = crabMoves.map(move => move.end);
|
|
181
|
+
const dangerous: number[] = [];
|
|
182
|
+
crabDists.forEach((d, i) => {
|
|
183
|
+
if (d <= o.killUnits + 2 * o.stepUnits) dangerous.push(i);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const candidates: number[] = [node.cell];
|
|
187
|
+
for (const cell of field.ringCandidates) if (cell >= 0) candidates.push(cell);
|
|
188
|
+
for (const cand of candidates) {
|
|
189
|
+
const shrimpPath = cand === node.cell ? [node.cell] : calc.pathToSource(cand);
|
|
190
|
+
if (dangerous.some(i => pathsCross(grid, o.killRange, shrimpPath, crabMoves[i].cells))) continue;
|
|
191
|
+
const candUnits = field.distOf(cand);
|
|
192
|
+
let proxy = Infinity;
|
|
193
|
+
for (const crab of newCrabCells) {
|
|
194
|
+
const crabUnits = field.distOf(crab);
|
|
195
|
+
const triangle = crabUnits === Infinity ? o.radiusUnits : crabUnits - candUnits;
|
|
196
|
+
proxy = Math.min(proxy, Math.max(euclidPx(grid, cand, crab), triangle * PX_PER_UNIT));
|
|
197
|
+
}
|
|
198
|
+
children.push({ cell: cand, crabCells: newCrabCells, depth: node.depth + 1, parent: node, proxy });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (children.length === 0) break;
|
|
202
|
+
const byCell = new Map<number, Node>();
|
|
203
|
+
for (const child of children) {
|
|
204
|
+
const seen = byCell.get(child.cell);
|
|
205
|
+
if (!seen || child.proxy > seen.proxy) byCell.set(child.cell, child);
|
|
206
|
+
}
|
|
207
|
+
frontier = [...byCell.values()].sort((a, b) => b.proxy - a.proxy).slice(0, o.beamWidth);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const result = bestLeaf ?? struggle!;
|
|
211
|
+
const path: Position[] = [];
|
|
212
|
+
for (let n: Node | null = result.node; n && n.depth > 0; n = n.parent) {
|
|
213
|
+
path.unshift(grid.cellToWorld(n.cell));
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
target: result.node.depth > 0 ? grid.cellToWorld(result.node.cell) : grid.cellToWorld(start),
|
|
217
|
+
minCrabDistance: result.minUnits * PX_PER_UNIT,
|
|
218
|
+
path,
|
|
219
|
+
caught: !bestLeaf,
|
|
220
|
+
survivedSteps: result.node.depth,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Fixed-policy rollout: the shrimp walks its initial geodesic path toward the
|
|
226
|
+
* target (mirroring the server's single A* at move submission) while the crabs
|
|
227
|
+
* pure-pursue each step, with the same catch semantics as planEscape. The
|
|
228
|
+
* rollout ends on arrival, at the horizon, or when caught.
|
|
229
|
+
*/
|
|
230
|
+
export function assessEscapeTarget(shrimp: Position, crabs: Position[], target: Position, opts: EscapeOptions = {}): TargetAssessment {
|
|
231
|
+
const { grid, calc } = getPlanner();
|
|
232
|
+
const o = resolveOptions(opts);
|
|
233
|
+
|
|
234
|
+
const start = snapCell(grid, shrimp);
|
|
235
|
+
const targetCell = start >= 0 ? snapCell(grid, target) : -1;
|
|
236
|
+
if (start < 0 || targetCell < 0) {
|
|
237
|
+
return { distanceToTarget: Infinity, minCrabDistance: Infinity, caught: false, survivedSteps: 0 };
|
|
238
|
+
}
|
|
239
|
+
let crabCells = snapCrabCells(grid, crabs);
|
|
240
|
+
|
|
241
|
+
// The walk path is fixed up front; extract it (and the target distance)
|
|
242
|
+
// before the per-step fields invalidate this one.
|
|
243
|
+
const reach = calc.compute(start, o.radiusUnits + o.steps * o.stepUnits, 0);
|
|
244
|
+
const targetUnits = reach.distOf(targetCell);
|
|
245
|
+
if (targetUnits === Infinity) {
|
|
246
|
+
return { distanceToTarget: Infinity, minCrabDistance: Infinity, caught: false, survivedSteps: 0 };
|
|
247
|
+
}
|
|
248
|
+
const walkPath = calc.pathToSource(targetCell);
|
|
249
|
+
const W = grid.width;
|
|
250
|
+
const cumUnits = new Array<number>(walkPath.length);
|
|
251
|
+
cumUnits[0] = 0;
|
|
252
|
+
for (let i = 1; i < walkPath.length; i++) {
|
|
253
|
+
const a = walkPath[i - 1];
|
|
254
|
+
const b = walkPath[i];
|
|
255
|
+
const diagonal = a % W !== b % W && Math.floor(a / W) !== Math.floor(b / W);
|
|
256
|
+
cumUnits[i] = cumUnits[i - 1] + (diagonal ? COST_DIAG : COST_STRAIGHT);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let shrimpIdx = 0;
|
|
260
|
+
let caught = false;
|
|
261
|
+
let survivedSteps = 0;
|
|
262
|
+
let minUnitsAtEnd = Infinity;
|
|
263
|
+
|
|
264
|
+
for (let depth = 0; ; depth++) {
|
|
265
|
+
const cell = walkPath[shrimpIdx];
|
|
266
|
+
const field = calc.compute(cell, o.radiusUnits, 0);
|
|
267
|
+
const crabDists = crabCells.map(c => Math.min(field.distOf(c), o.radiusUnits));
|
|
268
|
+
const minUnits = crabDists.length > 0 ? Math.min(...crabDists) : Infinity;
|
|
269
|
+
|
|
270
|
+
// Same end-of-step semantics as planEscape: at depth > 0 the crabs have
|
|
271
|
+
// already taken last step's move toward the previous shrimp position.
|
|
272
|
+
if (depth > 0 && minUnits <= o.killUnits) {
|
|
273
|
+
caught = true;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
survivedSteps = depth;
|
|
277
|
+
minUnitsAtEnd = minUnits;
|
|
278
|
+
if (depth === o.steps || shrimpIdx >= walkPath.length - 1) break;
|
|
279
|
+
|
|
280
|
+
const crabMoves = pursuitStep(calc, crabCells, o.stepUnits);
|
|
281
|
+
let nextIdx = shrimpIdx;
|
|
282
|
+
while (nextIdx < walkPath.length - 1 && cumUnits[nextIdx + 1] - cumUnits[shrimpIdx] <= o.stepUnits) nextIdx++;
|
|
283
|
+
|
|
284
|
+
const dangerous: number[] = [];
|
|
285
|
+
crabDists.forEach((d, i) => {
|
|
286
|
+
if (d <= o.killUnits + 2 * o.stepUnits) dangerous.push(i);
|
|
287
|
+
});
|
|
288
|
+
const segment = walkPath.slice(shrimpIdx, nextIdx + 1);
|
|
289
|
+
if (dangerous.some(i => pathsCross(grid, o.killRange, segment, crabMoves[i].cells))) {
|
|
290
|
+
caught = true;
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
crabCells = crabMoves.map(move => move.end);
|
|
294
|
+
shrimpIdx = nextIdx;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
distanceToTarget: targetUnits * PX_PER_UNIT,
|
|
299
|
+
minCrabDistance: minUnitsAtEnd * PX_PER_UNIT,
|
|
300
|
+
caught,
|
|
301
|
+
survivedSteps,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export interface RouteInfo {
|
|
306
|
+
/** 测地距离 px;maxRadiusPx 之外或不可达 = Infinity。 */
|
|
307
|
+
distancePx: number;
|
|
308
|
+
/** 测地路径是否经过任一威胁点 threatRadiusPx 内(不可达时恒为 false)。 */
|
|
309
|
+
nearThreat: boolean;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* 沿路径每 4 格(16px)采样一次离威胁点的欧氏距离,含末端格;半径 ~200px 下足够密。
|
|
314
|
+
* 豁免起点 from 的 radiusPx 邻域:逃跑那刻起点必然紧贴威胁,这一采样点不该把
|
|
315
|
+
* 「背向威胁、越走越远」的整条路线判危;真正夹在你与目的地之间、离起点超过
|
|
316
|
+
* radiusPx 的威胁仍会命中。对齐同文件 pathsCross 跳过 step 起点的语义。
|
|
317
|
+
*/
|
|
318
|
+
function pathNearAny(grid: WalkableGrid, path: number[], threats: Position[], radiusPx: number, from: Position): boolean {
|
|
319
|
+
for (let i = 0; i < path.length; i += 4) {
|
|
320
|
+
const idx = Math.min(i, path.length - 1);
|
|
321
|
+
const w = grid.cellToWorld(path[idx]);
|
|
322
|
+
if (Math.hypot(w.x - from.x, w.y - from.y) <= radiusPx) continue;
|
|
323
|
+
if (threats.some(t => Math.hypot(w.x - t.x, w.y - t.y) <= radiusPx)) return true;
|
|
324
|
+
}
|
|
325
|
+
const end = grid.cellToWorld(path[path.length - 1]);
|
|
326
|
+
if (Math.hypot(end.x - from.x, end.y - from.y) <= radiusPx) return false;
|
|
327
|
+
return threats.some(t => Math.hypot(end.x - t.x, end.y - t.y) <= radiusPx);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* 从 from 到各点的测地距离与「必经之路是否有危险」,一次距离场扫描 + 逐点回溯
|
|
332
|
+
* 梯度路径查表。threats 为空时只算距离不查路径。from 无法吸附到可走网格时返回
|
|
333
|
+
* null,调用方退回欧氏距离(且无路径信息)。
|
|
334
|
+
*/
|
|
335
|
+
export function assessRoutes(
|
|
336
|
+
from: Position,
|
|
337
|
+
points: Position[],
|
|
338
|
+
threats: Position[],
|
|
339
|
+
threatRadiusPx: number,
|
|
340
|
+
maxRadiusPx = 3000,
|
|
341
|
+
): RouteInfo[] | null {
|
|
342
|
+
const { grid, calc } = getPlanner();
|
|
343
|
+
const start = snapCell(grid, from);
|
|
344
|
+
if (start < 0) return null;
|
|
345
|
+
const field = calc.compute(start, Math.round(maxRadiusPx * UNITS_PER_PX), 0);
|
|
346
|
+
return points.map(p => {
|
|
347
|
+
const cell = snapCell(grid, p);
|
|
348
|
+
const units = cell >= 0 ? field.distOf(cell) : Infinity;
|
|
349
|
+
if (units === Infinity) return { distancePx: Infinity, nearThreat: false };
|
|
350
|
+
return {
|
|
351
|
+
distancePx: units * PX_PER_UNIT,
|
|
352
|
+
nearThreat: threats.length > 0 && pathNearAny(grid, calc.pathToSource(cell), threats, threatRadiusPx, from),
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
}
|