@myclaw163/clawclaw-cli 0.6.56 → 0.6.58
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/bin/clawclaw-cli.mjs +3 -3
- package/package.json +1 -1
- package/scripts/sync-bundled-skill.mjs +1 -1
- package/skills/clawclaw/SKILL.md +7 -3
- package/skills/clawclaw/references/GAME-MECHANICS.md +5 -5
- package/skills/clawclaw/references/STREAM.md +4 -3
- package/src/cli.ts +0 -43
- package/src/commands/config.ts +30 -30
- package/src/commands/game-start-plan.test.ts +10 -68
- package/src/commands/game.ts +318 -173
- package/src/commands/history.ts +1 -1
- package/src/commands/peek.ts +16 -9
- package/src/commands/setup/codex.ts +248 -248
- package/src/commands/setup/hermes.test.ts +96 -96
- package/src/commands/setup/hermes.ts +76 -76
- package/src/commands/setup/index.ts +13 -13
- package/src/commands/setup/openclaw.test.ts +114 -114
- package/src/commands/setup/openclaw.ts +147 -147
- package/src/commands/strategy.ts +29 -38
- package/src/commands/watch.test.ts +7 -11
- package/src/commands/watch.ts +77 -66
- package/src/lib/game-client.ts +3 -3
- package/src/lib/host-config-patcher.test.ts +130 -130
- package/src/lib/host-config-patcher.ts +151 -151
- package/src/lib/hub-reminder.ts +19 -19
- package/src/lib/strategy-export.test.ts +1 -1
- package/src/runtime/event-daemon.test.ts +81 -2
- package/src/runtime/event-daemon.ts +325 -287
- package/src/runtime/owner-control.ts +150 -0
- package/src/runtime/runtime-logger.ts +11 -3
- package/src/runtime/ws-client.test.ts +57 -0
- package/src/runtime/ws-client.ts +2 -2
- package/src/sdk/index.ts +4 -4
- package/src/strategies/game-utils.test.ts +27 -1
- package/src/strategies/game-utils.ts +27 -4
- package/src/strategies/goals/crab-octopus-reflexes.ts +4 -4
- package/src/strategies/goals/crab-sabotage-top.ts +1 -1
- package/src/strategies/goals/keep-away-goal.ts +10 -7
- package/src/strategies/goals/kill-frenzy-top.ts +5 -5
- package/src/strategies/goals/kill-target-goal.ts +2 -2
- package/src/strategies/goals/kill-target-top.ts +2 -2
- package/src/strategies/goals/lone-kill-core.ts +3 -3
- package/src/strategies/goals/lone-kill-task-top.ts +1 -1
- package/src/strategies/goals/task-kill-report-top.ts +3 -3
- package/src/strategies/goals/warrior-shrimp-top.ts +2 -2
- package/src/strategies/pathfind/escape-planner.ts +10 -3
- package/src/strategies/spawn.ts +16 -5
- package/src/strategies/strategy-loop.ts +9 -3
- package/src/runtime/daemon.ts +0 -100
- package/src/runtime/opening-mover.ts +0 -303
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { connect, createServer, type Server } from 'net';
|
|
6
|
+
|
|
7
|
+
export const GAME_START_RUNTIME_FILE = 'game-start.json';
|
|
8
|
+
export const OWNER_CONTROL_TIMEOUT_MS = 2000;
|
|
9
|
+
|
|
10
|
+
export type OwnerControlType = 'stop' | 'quit' | 'leave' | 'switch_strategy' | 'stop_strategy' | 'snapshot';
|
|
11
|
+
|
|
12
|
+
export interface OwnerControlInfo {
|
|
13
|
+
kind: 'node-net-socket';
|
|
14
|
+
path: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface OwnerControlRequest {
|
|
18
|
+
token?: string;
|
|
19
|
+
type?: OwnerControlType;
|
|
20
|
+
strategy?: string;
|
|
21
|
+
args?: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface OwnerControlResponse {
|
|
25
|
+
ok: boolean;
|
|
26
|
+
type?: string;
|
|
27
|
+
error?: string;
|
|
28
|
+
message?: string;
|
|
29
|
+
[key: string]: any;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface OwnerControlServer {
|
|
33
|
+
control: OwnerControlInfo;
|
|
34
|
+
token: string;
|
|
35
|
+
close: () => Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function gameStartRuntimePath(stateDir: string): string {
|
|
39
|
+
return join(stateDir, GAME_START_RUNTIME_FILE);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function readGameStartRuntime(stateDir: string): Record<string, any> | null {
|
|
43
|
+
const runtimePath = gameStartRuntimePath(stateDir);
|
|
44
|
+
if (!existsSync(runtimePath)) return null;
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(readFileSync(runtimePath, 'utf8'));
|
|
47
|
+
} catch {}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ownerControlPath(stateDir: string, pid: number): string {
|
|
52
|
+
const hash = createHash('sha1').update(stateDir).digest('hex').slice(0, 12);
|
|
53
|
+
if (process.platform === 'win32') return `\\\\.\\pipe\\clawclaw-${hash}-${pid}`;
|
|
54
|
+
const dir = join(tmpdir(), 'clawclaw');
|
|
55
|
+
mkdirSync(dir, { recursive: true });
|
|
56
|
+
return join(dir, `${hash}-${pid}.sock`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function startOwnerControlServer(
|
|
60
|
+
stateDir: string,
|
|
61
|
+
onRequest: (request: OwnerControlRequest) => Promise<OwnerControlResponse> | OwnerControlResponse,
|
|
62
|
+
): Promise<OwnerControlServer> {
|
|
63
|
+
const token = randomBytes(24).toString('hex');
|
|
64
|
+
const path = ownerControlPath(stateDir, process.pid);
|
|
65
|
+
if (process.platform !== 'win32') {
|
|
66
|
+
try { unlinkSync(path); } catch {}
|
|
67
|
+
}
|
|
68
|
+
const server: Server = createServer((socket) => {
|
|
69
|
+
let buffer = '';
|
|
70
|
+
socket.setEncoding('utf8');
|
|
71
|
+
socket.on('data', (chunk) => {
|
|
72
|
+
buffer += chunk;
|
|
73
|
+
const nl = buffer.indexOf('\n');
|
|
74
|
+
if (nl < 0) return;
|
|
75
|
+
const line = buffer.slice(0, nl);
|
|
76
|
+
buffer = buffer.slice(nl + 1);
|
|
77
|
+
void (async () => {
|
|
78
|
+
try {
|
|
79
|
+
const request = JSON.parse(line) as OwnerControlRequest;
|
|
80
|
+
if (request.token !== token) {
|
|
81
|
+
socket.end(JSON.stringify({ ok: false, error: 'invalid_token' }) + '\n');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const response = await onRequest(request);
|
|
85
|
+
socket.end(JSON.stringify(response) + '\n');
|
|
86
|
+
} catch (err: any) {
|
|
87
|
+
socket.end(JSON.stringify({ ok: false, error: err?.message ?? String(err) }) + '\n');
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
await new Promise<void>((resolve, reject) => {
|
|
93
|
+
server.once('error', reject);
|
|
94
|
+
server.listen(path, () => {
|
|
95
|
+
server.off('error', reject);
|
|
96
|
+
resolve();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
control: { kind: 'node-net-socket', path },
|
|
101
|
+
token,
|
|
102
|
+
close: () => new Promise((resolve) => {
|
|
103
|
+
server.close(() => resolve());
|
|
104
|
+
}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function sendOwnerControlRequest(
|
|
109
|
+
stateDir: string,
|
|
110
|
+
type: OwnerControlType,
|
|
111
|
+
payload: Record<string, any> = {},
|
|
112
|
+
timeoutMs = OWNER_CONTROL_TIMEOUT_MS,
|
|
113
|
+
): Promise<OwnerControlResponse | null> {
|
|
114
|
+
const info = readGameStartRuntime(stateDir);
|
|
115
|
+
const path = typeof info?.control?.path === 'string' ? info.control.path : '';
|
|
116
|
+
const token = typeof info?.control_token === 'string' ? info.control_token : '';
|
|
117
|
+
if (!path || !token) return Promise.resolve(null);
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
const socket = connect(path);
|
|
120
|
+
let buffer = '';
|
|
121
|
+
const timer = setTimeout(() => {
|
|
122
|
+
socket.destroy();
|
|
123
|
+
reject(new Error('owner_control_timeout'));
|
|
124
|
+
}, timeoutMs);
|
|
125
|
+
socket.setEncoding('utf8');
|
|
126
|
+
socket.on('connect', () => {
|
|
127
|
+
socket.write(JSON.stringify({ token, type, ...payload }) + '\n');
|
|
128
|
+
});
|
|
129
|
+
socket.on('data', (chunk) => {
|
|
130
|
+
buffer += chunk;
|
|
131
|
+
const nl = buffer.indexOf('\n');
|
|
132
|
+
if (nl < 0) return;
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
socket.end();
|
|
135
|
+
try {
|
|
136
|
+
resolve(JSON.parse(buffer.slice(0, nl)));
|
|
137
|
+
} catch (err) {
|
|
138
|
+
reject(err);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
socket.on('error', (err) => {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
reject(err);
|
|
144
|
+
});
|
|
145
|
+
socket.on('close', () => {
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
if (!buffer) resolve(null);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime Logger — structured, leveled logging for the
|
|
2
|
+
* Runtime Logger — structured, leveled logging for the game runtime and CLI.
|
|
3
3
|
*
|
|
4
4
|
* Log format: [Runtime:<LEVEL>] <EVENT_TAG> <message>
|
|
5
5
|
*
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* ERROR — failures that need attention (map load failed, tick error)
|
|
11
11
|
*
|
|
12
12
|
* Control via env var CLAWCLAW_LOG_LEVEL (default: DEBUG).
|
|
13
|
+
* Console output is disabled by default so game runtime diagnostics do not
|
|
14
|
+
* leak into Monitor streams. Set CLAWCLAW_RUNTIME_LOG_TO_CONSOLE=1 to print.
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
export enum LogLevel {
|
|
@@ -36,11 +38,17 @@ function parseLogLevel(raw: string | undefined): LogLevel {
|
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
function isTrue(value: string | undefined): boolean {
|
|
42
|
+
return typeof value === 'string' && value.trim().toLowerCase() === 'true';
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
export class RuntimeLogger {
|
|
40
46
|
private minLevel: LogLevel;
|
|
47
|
+
private logToConsole: boolean;
|
|
41
48
|
|
|
42
49
|
constructor() {
|
|
43
50
|
this.minLevel = parseLogLevel(process.env.CLAWCLAW_LOG_LEVEL);
|
|
51
|
+
this.logToConsole = isTrue(process.env.CLAWCLAW_RUNTIME_LOG_TO_CONSOLE);
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
// ─── Core log methods ────────────────────────────────────────────────────
|
|
@@ -60,8 +68,7 @@ export class RuntimeLogger {
|
|
|
60
68
|
error(tag: string, msg: string, err?: unknown): void {
|
|
61
69
|
const suffix = err instanceof Error ? ` err=${err.message}` : (err ? ` err=${err}` : '');
|
|
62
70
|
this.log(LogLevel.ERROR, tag, msg + suffix);
|
|
63
|
-
|
|
64
|
-
if (err instanceof Error && err.stack) {
|
|
71
|
+
if (this.logToConsole && err instanceof Error && err.stack) {
|
|
65
72
|
console.error(err.stack);
|
|
66
73
|
}
|
|
67
74
|
}
|
|
@@ -82,6 +89,7 @@ export class RuntimeLogger {
|
|
|
82
89
|
|
|
83
90
|
private log(level: LogLevel, tag: string, msg: string): void {
|
|
84
91
|
if (level < this.minLevel) return;
|
|
92
|
+
if (!this.logToConsole) return;
|
|
85
93
|
const prefix = `[Runtime:${LEVEL_NAMES[level]}]`;
|
|
86
94
|
const line = `${prefix} ${tag} ${msg}`;
|
|
87
95
|
switch (level) {
|
|
@@ -44,4 +44,61 @@ describe('WsClient', () => {
|
|
|
44
44
|
wsMock.constructed[0].instance.onopen?.({});
|
|
45
45
|
await connected;
|
|
46
46
|
});
|
|
47
|
+
|
|
48
|
+
it('dispatches state.new_events before the _state snapshot', async () => {
|
|
49
|
+
const { WsClient } = await import('./ws-client.js');
|
|
50
|
+
const client = new WsClient({
|
|
51
|
+
url: 'wss://host.example/api/v1/game/stream',
|
|
52
|
+
apiKey: 'claw_secret',
|
|
53
|
+
});
|
|
54
|
+
const seen: Record<string, any>[] = [];
|
|
55
|
+
client.on('*', (event) => seen.push(event));
|
|
56
|
+
|
|
57
|
+
(client as any).handleMessage({
|
|
58
|
+
type: 'state',
|
|
59
|
+
data: {
|
|
60
|
+
phase: 'wandering',
|
|
61
|
+
tick: 20,
|
|
62
|
+
new_events: [{ type: 'game_over', tick: 20, winner: 'crab' }],
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(seen).toEqual([
|
|
67
|
+
{ type: 'game_over', tick: 20, winner: 'crab' },
|
|
68
|
+
{
|
|
69
|
+
type: '_state',
|
|
70
|
+
phase: 'wandering',
|
|
71
|
+
tick: 20,
|
|
72
|
+
new_events: [{ type: 'game_over', tick: 20, winner: 'crab' }],
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('dispatches a _state snapshot when phase is game_over without new_events', async () => {
|
|
78
|
+
const { WsClient } = await import('./ws-client.js');
|
|
79
|
+
const client = new WsClient({
|
|
80
|
+
url: 'wss://host.example/api/v1/game/stream',
|
|
81
|
+
apiKey: 'claw_secret',
|
|
82
|
+
});
|
|
83
|
+
const seen: Record<string, any>[] = [];
|
|
84
|
+
client.on('*', (event) => seen.push(event));
|
|
85
|
+
|
|
86
|
+
(client as any).handleMessage({
|
|
87
|
+
type: 'state',
|
|
88
|
+
data: {
|
|
89
|
+
phase: 'game_over',
|
|
90
|
+
tick: 21,
|
|
91
|
+
new_events: [],
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(seen).toEqual([
|
|
96
|
+
{
|
|
97
|
+
type: '_state',
|
|
98
|
+
phase: 'game_over',
|
|
99
|
+
tick: 21,
|
|
100
|
+
new_events: [],
|
|
101
|
+
},
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
47
104
|
});
|
package/src/runtime/ws-client.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/runtime/ws-client.ts
|
|
2
2
|
// Internal WebSocket client — NOT exported from index.ts
|
|
3
|
-
// Used only by
|
|
3
|
+
// Used only by runtime listeners internally.
|
|
4
4
|
|
|
5
5
|
import WebSocket from 'ws';
|
|
6
6
|
import { WsConnectionState } from '../sdk/types.js';
|
|
@@ -129,7 +129,7 @@ export class WsClient {
|
|
|
129
129
|
}
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
|
-
// state: always dispatch the full snapshot as _state for
|
|
132
|
+
// state: always dispatch the full snapshot as _state for runtime feed tracking
|
|
133
133
|
// (current_speaker, sub_phase, vote progress, etc. update without necessarily
|
|
134
134
|
// shipping a new event). Process new_events with tick-based dedup if present.
|
|
135
135
|
if (msg.type === 'state' && msg.data) {
|
package/src/sdk/index.ts
CHANGED
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
* · game-utils: dist / firstAvailableTask / nearestKnownCorpse / nearestKnownCorpseNav /
|
|
21
21
|
* hasKnownCorpse / corpseAtScene / nearestReportableCorpse / safePatrolStep /
|
|
22
22
|
* nonTeammatesVisible / matchesTarget / isTargetAlive / nearestVisibleTarget /
|
|
23
|
-
* pursueVisibleTarget / killCooldownSecs / hasKillUseRemaining / canUseKill /
|
|
23
|
+
* pursueVisibleTarget / killCooldownSecs / hasKillUseRemaining / canUseKill / killRangeFor / killCommitRange /
|
|
24
24
|
* isKnowledgeHostile / isKnowledgeThreat / isKnowledgeTrusted
|
|
25
25
|
* (含 @deprecated 别名 isKnowledgeKillTarget / isKnowledgeAvoid / isKnowledgeProtected) /
|
|
26
|
-
* PatrolState / SafeTaskOptions / SafePatrolOptions + 常量(TASK_SUBMIT_RADIUS /
|
|
26
|
+
* PatrolState / SafeTaskOptions / SafePatrolOptions + 常量(TASK_SUBMIT_RADIUS /
|
|
27
27
|
* SHRIMP_VISION_RANGE / SHRIMP_VISION_EXIT_BUFFER / SHRIMP_VISION_RELEASE_RANGE /
|
|
28
28
|
* FOLLOW_RANGE / PATROL_REACHED_DISTANCE / PROGRESS_INTERVAL_MS)
|
|
29
29
|
* · Knowledge (ctx.knowledge): KnowledgeView / SubjectEntry / FactMeta /
|
|
@@ -56,11 +56,11 @@ export {
|
|
|
56
56
|
safePatrolStep,
|
|
57
57
|
nonTeammatesVisible, matchesTarget, isTargetAlive,
|
|
58
58
|
nearestVisibleTarget, pursueVisibleTarget,
|
|
59
|
-
killCooldownSecs, hasKillUseRemaining, canUseKill,
|
|
59
|
+
killCooldownSecs, hasKillUseRemaining, canUseKill, killRangeFor, killCommitRange,
|
|
60
60
|
isKnowledgeHostile, isKnowledgeThreat, isKnowledgeTrusted,
|
|
61
61
|
isKnowledgeKillTarget, isKnowledgeAvoid, isKnowledgeProtected,
|
|
62
62
|
PatrolState,
|
|
63
|
-
TASK_SUBMIT_RADIUS,
|
|
63
|
+
TASK_SUBMIT_RADIUS, SHRIMP_VISION_RANGE, SHRIMP_VISION_EXIT_BUFFER, SHRIMP_VISION_RELEASE_RANGE,
|
|
64
64
|
FOLLOW_RANGE, PATROL_REACHED_DISTANCE,
|
|
65
65
|
PROGRESS_INTERVAL_MS,
|
|
66
66
|
} from '../strategies/game-utils.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import type { GameState, TaskInfo } from '../sdk/types.js';
|
|
3
3
|
import type { StrategyContext } from './types.js';
|
|
4
|
-
import { CorpseMemory, firstAvailableTask, PatrolState, safePatrolStep } from './game-utils.js';
|
|
4
|
+
import { CorpseMemory, firstAvailableTask, nearestSafeTask, PatrolState, safePatrolStep } from './game-utils.js';
|
|
5
5
|
|
|
6
6
|
function state(overrides: Partial<GameState> = {}): GameState {
|
|
7
7
|
return {
|
|
@@ -162,3 +162,29 @@ describe('safePatrolStep', () => {
|
|
|
162
162
|
expect(second[0].action.payload).toMatchObject({ target_x: 2602, target_y: 586 });
|
|
163
163
|
});
|
|
164
164
|
});
|
|
165
|
+
|
|
166
|
+
describe('nearestSafeTask', () => {
|
|
167
|
+
// 威胁贴在起点旁(视野内 ~52px),唯一安全任务远在反方向、离威胁 556px(> 端点
|
|
168
|
+
// 排除半径 500)。修复前 pathNearAny 会因起点采样紧贴威胁把整条「背向威胁」的路线判危,
|
|
169
|
+
// 返回 null(虚假僵住);修复后起点邻域被豁免,路线不再判危,应选中该任务。
|
|
170
|
+
// 坐标在真实可走网格上验证:start/threat/task 均吸附可走格、任务可达,
|
|
171
|
+
// 且去任务点的测地路径在起点 350px 邻域之外不再靠近威胁 350px 内。
|
|
172
|
+
it('returns a far safe task when a threat hugs the start', () => {
|
|
173
|
+
const routeState = state({ you: { ...state().you, x: 2514, y: 226 } });
|
|
174
|
+
const task = taskAt(2514, 730);
|
|
175
|
+
const threat = { x: 2514, y: 174 };
|
|
176
|
+
|
|
177
|
+
const result = nearestSafeTask(
|
|
178
|
+
routeState,
|
|
179
|
+
[task],
|
|
180
|
+
() => true,
|
|
181
|
+
null,
|
|
182
|
+
null,
|
|
183
|
+
null,
|
|
184
|
+
{ threatPoints: [threat] },
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(result).not.toBeNull();
|
|
188
|
+
expect(result).toMatchObject({ x: 2514, y: 730 });
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -5,7 +5,10 @@ import { emptyKnowledgeView } from '../lib/knowledge-store.js';
|
|
|
5
5
|
import { assessRoutes } from './pathfind/escape-planner.js';
|
|
6
6
|
|
|
7
7
|
export const TASK_SUBMIT_RADIUS = 12;
|
|
8
|
-
|
|
8
|
+
/** 蟹/章鱼/兜底角色的服务端击杀距离(= 后端全局 kill_range)。 */
|
|
9
|
+
export const BASE_KILL_RANGE = 80;
|
|
10
|
+
/** 武士虾/枪虾的服务端击杀距离(带刀好人,射程是蟹/章鱼的两倍)。 */
|
|
11
|
+
export const SHRIMP_KILL_RANGE = 160;
|
|
9
12
|
export const REPORT_RANGE = 150;
|
|
10
13
|
export const SHRIMP_VISION_RANGE = 270;
|
|
11
14
|
export const SHRIMP_VISION_EXIT_BUFFER = 30;
|
|
@@ -34,6 +37,19 @@ export function canUseKill(state: GameState): boolean {
|
|
|
34
37
|
return hasKillUseRemaining(state) && killCooldownSecs(state) <= 0;
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
const KILL_COMMIT_FACTOR = 0.9;
|
|
41
|
+
const LONG_RANGE_KILL_ROLES = new Set(['shrimp_warrior', 'shrimp_pistol']);
|
|
42
|
+
|
|
43
|
+
/** 角色的服务端实际击杀距离,镜像后端 effective_kill_range。武士虾/枪虾 160,其余 80。 */
|
|
44
|
+
export function killRangeFor(role: string | undefined): number {
|
|
45
|
+
return role && LONG_RANGE_KILL_ROLES.has(role) ? SHRIMP_KILL_RANGE : BASE_KILL_RANGE;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 机器人主动出刀的距离阈值:服务端击杀距离打 9 折(80→72,160→144),留余量避免目标在边界漂移导致空刀。 */
|
|
49
|
+
export function killCommitRange(role: string | undefined): number {
|
|
50
|
+
return killRangeFor(role) * KILL_COMMIT_FACTOR;
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
export function dist(x1: number, y1: number, x2: number, y2: number): number {
|
|
38
54
|
return Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);
|
|
39
55
|
}
|
|
@@ -64,6 +80,10 @@ export function firstAvailableTask(
|
|
|
64
80
|
}) ?? null;
|
|
65
81
|
}
|
|
66
82
|
|
|
83
|
+
// 路径半径可以 > 视野(270):pathNearAny 已豁免「起点 radiusPx 邻域」,逃跑那刻起点紧贴
|
|
84
|
+
// 威胁不再把整条背向路线判危,所以恢复成真正的「绕开把守者」避让距离。端点半径
|
|
85
|
+
// TASK_THREAT_EXCLUDE_RANGE 只看任务点离威胁多近、与起点采样无关,按玩法风险单独取值。
|
|
86
|
+
// 约束靠 game-utils.test.ts 的路线场景测试钉死(不加数值断言,因为旧的「< 视野」断言现在反而是错的)。
|
|
67
87
|
/** 任务点离任一威胁点多近就算「在威胁旁」而被硬排除(欧氏即可——隔薄墙的威胁照样危险)。 */
|
|
68
88
|
export const TASK_THREAT_EXCLUDE_RANGE = 500;
|
|
69
89
|
/** 测地路径离任一威胁点多近就算「必经之路有危险」而被硬排除(治路上有人把守时的来回逡巡)。 */
|
|
@@ -231,7 +251,10 @@ function nextSafePatrolRoom(
|
|
|
231
251
|
const viable = routes == null
|
|
232
252
|
? endpointSafe
|
|
233
253
|
: endpointSafe.filter((_room, i) => !routes[i].nearThreat);
|
|
234
|
-
|
|
254
|
+
// 所有路线被路径闸判危时退回端点已安全的 endpointSafe[0](端点远离威胁、仅路线稍擦威胁边,
|
|
255
|
+
// 对「游荡制造嫌疑」已足够),而非 null 触发上层静默僵住;绝不退回不带过滤的 patrolStep
|
|
256
|
+
//(会径直走向正在躲的陌生人)。
|
|
257
|
+
const selected = viable[0] ?? endpointSafe[0] ?? null;
|
|
235
258
|
if (!selected) return null;
|
|
236
259
|
movePatrolToRoom(ctx, patrol, current, selected);
|
|
237
260
|
return selected;
|
|
@@ -422,7 +445,7 @@ export class CorpseMemory {
|
|
|
422
445
|
|
|
423
446
|
/**
|
|
424
447
|
* 从任意事件数组补全 corpse_spotted 坐标——供 action 结果里的 new_events 用,绕开主循环 state.new_events
|
|
425
|
-
* 的共享游标竞态(
|
|
448
|
+
* 的共享游标竞态(runtime WS listener 可能先通过 WS 消费掉该事件,strategy 的 HTTP 轮询就再也看不到,尸体坐标永远进
|
|
426
449
|
* 不了记忆,导航类策略只能巡逻走开)。
|
|
427
450
|
*/
|
|
428
451
|
ingestEvents(events: any[] | undefined): void {
|
|
@@ -585,7 +608,7 @@ export function pursueVisibleTarget(
|
|
|
585
608
|
): BehaviorDecision | null {
|
|
586
609
|
const d = target.distance ?? dist(state.you.x, state.you.y, target.x, target.y);
|
|
587
610
|
if (opts.kill && !hasKillUseRemaining(state)) return null;
|
|
588
|
-
if (opts.kill && d <=
|
|
611
|
+
if (opts.kill && d <= killCommitRange(state.you.role) && canUseKill(state)) {
|
|
589
612
|
return { action: Action.kill(target.name) };
|
|
590
613
|
}
|
|
591
614
|
if (d > opts.followDistance) {
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
canUseKill,
|
|
5
5
|
corpseAtScene,
|
|
6
6
|
dist,
|
|
7
|
-
|
|
7
|
+
killCommitRange,
|
|
8
8
|
nearestReportableCorpse,
|
|
9
9
|
nonTeammatesVisible,
|
|
10
10
|
reportCorpseDecision,
|
|
@@ -15,7 +15,7 @@ import type { BehaviorDecision, StrategyContext } from '../types.js';
|
|
|
15
15
|
* 蟹(坏人)/ 章鱼(中立带刀)默认策略共享的「反射动作」——由 CrabSabotageTop 与 LoneKillTaskTop
|
|
16
16
|
* 在各自 tick() 的**最前面**调用,抢在破坏/任务/落单猎杀编排之前先处理两类即时机会:
|
|
17
17
|
*
|
|
18
|
-
* - immediateLoneKillDecision:唯一一个落单非队友就贴在脸上(
|
|
18
|
+
* - immediateLoneKillDecision:唯一一个落单非队友就贴在脸上(出刀范围内)且刀好 → 立刻出刀。它无视
|
|
19
19
|
* LoneKillCore 的「暂停窗口」,也不管自己是否正在做破坏任务——只要能原地一刀就先杀。
|
|
20
20
|
* - corpseReportWithNonTeammate:地上有尸体且附近有非队友 → 报警(够不着先靠近)。不管尸体是谁
|
|
21
21
|
* 造成的(含队友的杀),靠主动报警伪装好人 / 把嫌疑栽到在场者身上。
|
|
@@ -58,7 +58,7 @@ export function corpseReportWithNonTeammate(state: GameState, ctx: StrategyConte
|
|
|
58
58
|
}];
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/** 唯一一个落单非队友贴脸(
|
|
61
|
+
/** 唯一一个落单非队友贴脸(出刀范围内)且刀好 → 立刻出刀;否则 null。无视暂停窗口、不管在做什么任务。 */
|
|
62
62
|
export function immediateLoneKillDecision(state: GameState, ctx: StrategyContext): BehaviorDecision[] | null {
|
|
63
63
|
if (!canUseKill(state)) return null;
|
|
64
64
|
|
|
@@ -67,7 +67,7 @@ export function immediateLoneKillDecision(state: GameState, ctx: StrategyContext
|
|
|
67
67
|
|
|
68
68
|
const target = targets[0];
|
|
69
69
|
const distance = target.distance ?? dist(state.you.x, state.you.y, target.x, target.y);
|
|
70
|
-
if (distance >
|
|
70
|
+
if (distance > killCommitRange(state.you.role)) return null;
|
|
71
71
|
|
|
72
72
|
ctx.notifications.push(`发现落单目标${target.name}就在身边,立刻出刀!`);
|
|
73
73
|
return [{ action: Action.kill(target.name) }];
|
|
@@ -23,7 +23,7 @@ const SABOTAGE_ALARM_DEDUPE_MS = 90_000;
|
|
|
23
23
|
* 蟹的「破坏 + 落单猎杀 + 任务伪装」编排 top。本身不产决策,每 tick 自上而下判定该走哪条分支、
|
|
24
24
|
* 把对应**叶子子目标**挂为唯一子目标(产出动作的是叶子;URGENT/WANDER 优先级供 ConversationGoal 判断):
|
|
25
25
|
*
|
|
26
|
-
* 0a. 反射·立刻出刀(crab-octopus-reflexes,URGENT):落单非队友贴脸(
|
|
26
|
+
* 0a. 反射·立刻出刀(crab-octopus-reflexes,URGENT):落单非队友贴脸(出刀范围内)且刀好 → 立刻出刀,
|
|
27
27
|
* 无视暂停窗口、不管自己是否正在做破坏任务。
|
|
28
28
|
* 0b. 反射·报尸(crab-octopus-reflexes,URGENT):有尸体且附近有非队友 → 报警/靠近报警,不管尸体是谁造成的。
|
|
29
29
|
* 1a. 破坏完成 → 触发紧急警报(一次性动作,LeafGoal 承载,URGENT)。
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { Action } from '../../sdk/action.js';
|
|
2
2
|
import type { GameState, PlayerInfo, Position } from '../../sdk/types.js';
|
|
3
|
-
import { dist } from '../game-utils.js';
|
|
3
|
+
import { dist, SHRIMP_KILL_RANGE } from '../game-utils.js';
|
|
4
4
|
import { assessEscapeTarget, planEscape, type EscapeOptions } from '../pathfind/escape-planner.js';
|
|
5
5
|
import { loadWalkableGrid } from '../pathfind/walkable-grid.js';
|
|
6
6
|
import type { BehaviorDecision, StrategyContext } from '../types.js';
|
|
7
7
|
import { Goal } from './goal.js';
|
|
8
8
|
|
|
9
|
-
const DEFAULT_THREAT_RADIUS = 420;
|
|
10
9
|
const PANIC_RADIUS = 190;
|
|
11
10
|
const CRITICAL_RADIUS = 100;
|
|
12
11
|
const REACHED_DISTANCE = 28;
|
|
@@ -22,8 +21,8 @@ const FALLBACK_ANGLE_OFFSETS = [0, Math.PI / 6, -Math.PI / 6, Math.PI / 3, -Math
|
|
|
22
21
|
export type ThreatResolver = (state: GameState, ctx: StrategyContext) => PlayerInfo[];
|
|
23
22
|
|
|
24
23
|
export interface KeepAwayGoalOptions {
|
|
25
|
-
/** 交战兼结束半径(px
|
|
26
|
-
threatRadius
|
|
24
|
+
/** 交战兼结束半径(px);Infinity = resolver 返回的全算威胁。所有调用点必须显式传,无默认值。 */
|
|
25
|
+
threatRadius: number;
|
|
27
26
|
/** 半径内无威胁时 isFinish=true,供父节点自然回收;独立 keep-away 策略作为顶层须传 false,否则每个安静 tick 被 GoalManager 清掉再重建、idle 通知刷屏。 */
|
|
28
27
|
finishWhenClear?: boolean;
|
|
29
28
|
/** 无威胁时的 idle/coast 通知;作为子目标默认关闭(父节点自有进度播报)。 */
|
|
@@ -98,11 +97,15 @@ export class KeepAwayGoal extends Goal {
|
|
|
98
97
|
constructor(
|
|
99
98
|
public readonly key: string,
|
|
100
99
|
private readonly resolveThreats: ThreatResolver,
|
|
101
|
-
private readonly options: KeepAwayGoalOptions
|
|
100
|
+
private readonly options: KeepAwayGoalOptions,
|
|
102
101
|
) {
|
|
103
102
|
super();
|
|
104
|
-
this.threatRadius = options.threatRadius
|
|
105
|
-
|
|
103
|
+
this.threatRadius = options.threatRadius;
|
|
104
|
+
// 逃跑推演对「会不会被追上」用保守击杀距离:威胁在本层只有坐标、没有角色(resolver 返回
|
|
105
|
+
// 的是可见非队友的裸位置,本游戏看不到他人身份),无法逐威胁取 killRangeFor(role),故统一用
|
|
106
|
+
// 武士虾/枪虾的 SHRIMP_KILL_RANGE(160) 兜底,宁可对蟹/章鱼多拉开距离也不对带刀好人少逃。
|
|
107
|
+
// escape-planner 自身默认 80(服务端 base kill_range),此处刻意覆盖。
|
|
108
|
+
this.planOpts = { killRange: SHRIMP_KILL_RANGE, ...PLAN_OPTS, ...options.planOpts };
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
|
|
@@ -2,7 +2,7 @@ import type { GameState, PlayerInfo } from '../../sdk/types.js';
|
|
|
2
2
|
import {
|
|
3
3
|
dist,
|
|
4
4
|
killCooldownSecs,
|
|
5
|
-
|
|
5
|
+
killCommitRange,
|
|
6
6
|
LONE_FOLLOW_DISTANCE,
|
|
7
7
|
nearestPlayerByDistance,
|
|
8
8
|
nonTeammatesVisible,
|
|
@@ -19,9 +19,9 @@ import { Goal } from './goal.js';
|
|
|
19
19
|
* 与 kill-lone 不同——周围人多也照杀,绝不因人多而暂停。
|
|
20
20
|
*
|
|
21
21
|
* 逐 tick:
|
|
22
|
-
* - 视野内有非队友 →
|
|
22
|
+
* - 视野内有非队友 → 锁定最近那个,贴上去:出刀范围内且冷却好就出刀,
|
|
23
23
|
* 冷却中保持 LONE_FOLLOW_DISTANCE 贴身跟随等待。
|
|
24
|
-
* - 否则 →
|
|
24
|
+
* - 否则 → 按房间顺序巡逻找猎物。永不停手。
|
|
25
25
|
*
|
|
26
26
|
* 与 kill-target 复用同一套「锁定后追/砍/等冷却」体验(pursueVisibleTarget,
|
|
27
27
|
* kill=true、followDistance=LONE_FOLLOW_DISTANCE),仅目标选取规则不同:
|
|
@@ -38,14 +38,14 @@ export class KillFrenzyTop extends Goal {
|
|
|
38
38
|
ctx.notifications.push(`发现目标${target.name}在攻击范围内,出刀!`);
|
|
39
39
|
} else {
|
|
40
40
|
const d = target.distance ?? dist(state.you.x, state.you.y, target.x, target.y);
|
|
41
|
-
if (d >
|
|
41
|
+
if (d > killCommitRange(state.you.role)) ctx.notifications.push(`发现目标${target.name},正在靠近!`);
|
|
42
42
|
}
|
|
43
43
|
this.emitProgress(state, ctx);
|
|
44
44
|
return decision ? [decision] : [];
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
this.emitProgress(state, ctx);
|
|
48
|
-
return patrolStep(state, ctx, this.patrol,
|
|
48
|
+
return patrolStep(state, ctx, this.patrol, false);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
private nearestVictim(state: GameState, ctx: StrategyContext): PlayerInfo | null {
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
dist,
|
|
6
6
|
hasKillUseRemaining,
|
|
7
7
|
isKnowledgeTrusted,
|
|
8
|
-
|
|
8
|
+
killCommitRange,
|
|
9
9
|
LONE_FOLLOW_DISTANCE,
|
|
10
10
|
matchesAnyTarget,
|
|
11
11
|
nonTeammatesVisible,
|
|
@@ -29,7 +29,7 @@ export class KillVisibleTargetGoal extends Goal {
|
|
|
29
29
|
|
|
30
30
|
const d = target.distance ?? dist(state.you.x, state.you.y, target.x, target.y);
|
|
31
31
|
|
|
32
|
-
if (d <=
|
|
32
|
+
if (d <= killCommitRange(state.you.role)) {
|
|
33
33
|
if (canUseKill(state)) {
|
|
34
34
|
ctx.notifications.push(`发现追杀目标${target.name},出刀!`);
|
|
35
35
|
return [{ action: Action.kill(target.name) }];
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
killCooldownSecs,
|
|
5
5
|
isTargetAlive,
|
|
6
6
|
isRecentlyKilledTarget,
|
|
7
|
-
|
|
7
|
+
killCommitRange,
|
|
8
8
|
LONE_FOLLOW_DISTANCE,
|
|
9
9
|
LONE_IGNORE_MS,
|
|
10
10
|
matchesTarget,
|
|
@@ -54,7 +54,7 @@ export class KillTargetTop extends Goal {
|
|
|
54
54
|
ctx.notifications.push(`发现目标${target.name}在攻击范围内,出刀!`);
|
|
55
55
|
} else {
|
|
56
56
|
const d = target.distance ?? dist(state.you.x, state.you.y, target.x, target.y);
|
|
57
|
-
if (d >
|
|
57
|
+
if (d > killCommitRange(state.you.role)) ctx.notifications.push(`发现目标${target.name},正在靠近!`);
|
|
58
58
|
}
|
|
59
59
|
this.emitProgress(state, ctx);
|
|
60
60
|
return decision ? [decision] : [];
|
|
@@ -3,7 +3,7 @@ import { Action } from '../../sdk/action.js';
|
|
|
3
3
|
import {
|
|
4
4
|
canUseKill,
|
|
5
5
|
dist,
|
|
6
|
-
|
|
6
|
+
killCommitRange,
|
|
7
7
|
LONE_IGNORE_MS,
|
|
8
8
|
nonTeammatesVisible,
|
|
9
9
|
} from '../game-utils.js';
|
|
@@ -72,11 +72,11 @@ export class LoneKillCore {
|
|
|
72
72
|
|
|
73
73
|
pursue(state: GameState, ctx: StrategyContext, target: PlayerInfo): BehaviorDecision {
|
|
74
74
|
const d = target.distance ?? dist(state.you.x, state.you.y, target.x, target.y);
|
|
75
|
-
if (d <=
|
|
75
|
+
if (d <= killCommitRange(state.you.role) && canUseKill(state)) {
|
|
76
76
|
ctx.notifications.push(`发现落单目标${target.name},出刀!`);
|
|
77
77
|
return { action: Action.kill(target.name) };
|
|
78
78
|
}
|
|
79
|
-
if (d >
|
|
79
|
+
if (d > killCommitRange(state.you.role)) ctx.notifications.push(`发现落单目标${target.name},正在靠近!`);
|
|
80
80
|
return { action: Action.move({ x: target.x, y: target.y }) };
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -18,7 +18,7 @@ import { corpseReportWithNonTeammate, immediateLoneKillDecision } from './crab-o
|
|
|
18
18
|
* 章鱼(中立带刀)的「落单猎杀 + 任务伪装」编排 top。本身不产决策,每 tick 只判定该走哪个行为、
|
|
19
19
|
* 把对应**叶子子目标**挂为唯一子目标(产出动作的是叶子,优先级标注紧急/游走,供 ConversationGoal 判断):
|
|
20
20
|
*
|
|
21
|
-
* 0. 反射(crab-octopus-reflexes,URGENT,抢在落单猎杀编排之前):落单非队友贴脸(
|
|
21
|
+
* 0. 反射(crab-octopus-reflexes,URGENT,抢在落单猎杀编排之前):落单非队友贴脸(出刀范围内)且刀好 →
|
|
22
22
|
* 立刻出刀;否则有尸体且附近有非队友 → 报警/靠近报警(不管尸体是谁造成的)。
|
|
23
23
|
* 1. 落单猎杀(LoneKillGoal,URGENT):core.assess() 判 hunt(刀好且有落单目标)→ 叶子 pursue() 靠近 / 出刀。
|
|
24
24
|
* 刀在冷却时 assess 返回 idle,不追不等,直接下沉到任务伪装。
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
hasKillUseRemaining,
|
|
8
8
|
isTargetAlive,
|
|
9
9
|
killCooldownSecs,
|
|
10
|
-
|
|
10
|
+
killCommitRange,
|
|
11
11
|
LONE_FOLLOW_DISTANCE,
|
|
12
12
|
matchesTarget,
|
|
13
13
|
PROGRESS_INTERVAL_MS,
|
|
@@ -54,12 +54,12 @@ export class TaskKillReportTop extends Goal {
|
|
|
54
54
|
ctx.notifications.push(`发现嫌疑目标${nearest.player.name}!`);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
if (nearest.dist <=
|
|
57
|
+
if (nearest.dist <= killCommitRange(state.you.role) && killReady) {
|
|
58
58
|
ctx.notifications.push(`嫌疑目标${nearest.player.name}在攻击范围内,出刀!`);
|
|
59
59
|
return this.emitDecisions(state, ctx, [{ action: Action.kill(nearest.player.name) }], URGENT_GOAL_PRIORITY);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
if (nearest.dist >
|
|
62
|
+
if (nearest.dist > killCommitRange(state.you.role) && killReady) {
|
|
63
63
|
return this.emitDecisions(
|
|
64
64
|
state,
|
|
65
65
|
ctx,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
isKnowledgeThreat,
|
|
11
11
|
isKnowledgeTrusted,
|
|
12
12
|
killCooldownSecs,
|
|
13
|
-
|
|
13
|
+
killCommitRange,
|
|
14
14
|
matchesAnyTarget,
|
|
15
15
|
nonTeammatesVisible,
|
|
16
16
|
PROGRESS_INTERVAL_MS,
|
|
@@ -172,7 +172,7 @@ export class WarriorShrimpTop extends Goal {
|
|
|
172
172
|
return visible
|
|
173
173
|
.filter(p => !this.isTrusted(p, ctx))
|
|
174
174
|
.map(p => ({ p, d: p.distance ?? dist(state.you.x, state.you.y, p.x, p.y) }))
|
|
175
|
-
.filter(x => x.d <=
|
|
175
|
+
.filter(x => x.d <= killCommitRange(state.you.role))
|
|
176
176
|
.sort((a, b) => a.d - b.d)[0]?.p ?? null;
|
|
177
177
|
}
|
|
178
178
|
|