@myclaw163/clawclaw-cli 0.6.61 → 0.6.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,123 @@
1
+ import type { Position } from '../sdk/types.js';
2
+ import { dist } from './game-utils.js';
3
+ import { assessRoutes } from './pathfind/escape-planner.js';
4
+
5
+ export interface HideSpot {
6
+ /** 世界像素坐标,与 GameState / 可行走网格同坐标系。 */
7
+ x: number;
8
+ y: number;
9
+ /** 所在房间,仅供日志/调试。 */
10
+ room: string;
11
+ }
12
+
13
+ /**
14
+ * 躲藏点:离线从服务端烘焙地图(config/clawclaw/clawclaw.tmj.baked.npz)算出的「藏身角落」。筛选条件:
15
+ * - 所在房间有 ≥2 个出口(非死胡同,被追能逃);**走廊**(做任务的必经路线)一律排除;
16
+ * - 出口 ≥4 的枢纽房间排除(穿行人流太大,藏不住);
17
+ * - 取房间内「离各出口最深、且远离任务点/出生点」的可走格(缩在内侧角落,避开穿行线与刷新点)。
18
+ *
19
+ * 重新生成:scripts/find-hide-spots.py(用后端 venv 解释器跑,读 baked.npz,打印可粘贴的本数组)。
20
+ * 这是离线烘焙数据,和 pathfind 的可行走网格一样:运行时按需 snap 到最近可走格即可。
21
+ */
22
+ export const HIDE_SPOTS: readonly HideSpot[] = [
23
+ { x: 2882, y: 1362, room: '导航仓' },
24
+ { x: 2386, y: 1138, room: '健身房' },
25
+ { x: 1922, y: 1234, room: '酒吧' },
26
+ { x: 2418, y: 2130, room: '休闲会所' },
27
+ { x: 1538, y: 1826, room: '监控室' },
28
+ { x: 514, y: 2402, room: '动力监控室' },
29
+ { x: 1538, y: 2514, room: '自助餐厅' },
30
+ { x: 1314, y: 2674, room: '制氧舱' },
31
+ { x: 2514, y: 2882, room: '中央厨房' },
32
+ { x: 2882, y: 2994, room: '冷库' },
33
+ { x: 3858, y: 2434, room: '情报室' },
34
+ ];
35
+
36
+ /** 躲藏点离任一威胁多近就硬排除(与 nearestSafeTask 的端点排除同量级)。 */
37
+ export const HIDE_THREAT_EXCLUDE_RANGE = 500;
38
+ /** 去躲藏点的测地路径经过威胁这个距离内也排除(避免走向把守者)。 */
39
+ export const HIDE_PATH_THREAT_RADIUS = 350;
40
+
41
+ export interface NearestSafeHideOptions {
42
+ threatExcludeRadius?: number;
43
+ pathThreatRadius?: number;
44
+ /** 当前已选躲藏点:仍是合法候选就保持不变,杜绝等距摇摆。 */
45
+ stickyTo?: HideSpot | null;
46
+ /** 被标记为不可达的移动目标:在其附近的点跳过。 */
47
+ blockedTarget?: Position | null;
48
+ }
49
+
50
+ const BLOCKED_RADIUS = 12;
51
+
52
+ function sameSpot(a: HideSpot, b: HideSpot): boolean {
53
+ return a.x === b.x && a.y === b.y;
54
+ }
55
+
56
+ /** 所有点都被威胁封死时的兜底:离威胁最远的点(仍好过原地等死)。 */
57
+ function farthestFromThreats(threatPoints: Position[], blocked?: Position | null): HideSpot | null {
58
+ let best: HideSpot | null = null;
59
+ let bestMin = -Infinity;
60
+ for (const s of HIDE_SPOTS) {
61
+ if (blocked && dist(s.x, s.y, blocked.x, blocked.y) <= BLOCKED_RADIUS) continue;
62
+ const minD = threatPoints.length === 0 ? 0 : Math.min(...threatPoints.map(p => dist(s.x, s.y, p.x, p.y)));
63
+ if (minD > bestMin) {
64
+ bestMin = minD;
65
+ best = s;
66
+ }
67
+ }
68
+ return best;
69
+ }
70
+
71
+ /**
72
+ * 选「最近的安全躲藏点」:与 nearestSafeTask 同构——威胁旁的点硬排除、去路经过威胁附近的点排除,
73
+ * 余者取测地最近(distance-field 一次扫描;测地不可达按欧氏垫底),带粘性(当前点仍合法就不换)。
74
+ * 全部被威胁封死时退回「离威胁最远」的点。
75
+ */
76
+ export function nearestSafeHideSpot(
77
+ from: Position,
78
+ threatPoints: Position[] = [],
79
+ opts: NearestSafeHideOptions = {},
80
+ ): HideSpot | null {
81
+ const excludeR = opts.threatExcludeRadius ?? HIDE_THREAT_EXCLUDE_RANGE;
82
+ const blocked = opts.blockedTarget ?? null;
83
+
84
+ const candidates = HIDE_SPOTS.filter(s =>
85
+ (!blocked || dist(s.x, s.y, blocked.x, blocked.y) > BLOCKED_RADIUS)
86
+ && !threatPoints.some(p => dist(s.x, s.y, p.x, p.y) <= excludeR));
87
+
88
+ if (candidates.length === 0) return farthestFromThreats(threatPoints, blocked);
89
+
90
+ // 无威胁时粘性可零成本短路(不必为路径检查扫距离场)。
91
+ if (opts.stickyTo && threatPoints.length === 0) {
92
+ const sticky = candidates.find(c => sameSpot(c, opts.stickyTo!));
93
+ if (sticky) return sticky;
94
+ }
95
+
96
+ const routes = assessRoutes(
97
+ from,
98
+ candidates.map(c => ({ x: c.x, y: c.y })),
99
+ threatPoints,
100
+ opts.pathThreatRadius ?? HIDE_PATH_THREAT_RADIUS,
101
+ );
102
+ const indexed = candidates.map((c, i) => ({ c, i }));
103
+ const viable = routes == null ? indexed : indexed.filter(({ i }) => !routes[i].nearThreat);
104
+ const pool = viable.length > 0 ? viable : indexed;
105
+
106
+ if (opts.stickyTo) {
107
+ const sticky = pool.find(({ c }) => sameSpot(c, opts.stickyTo!));
108
+ if (sticky) return sticky.c;
109
+ }
110
+
111
+ let best = pool[0].c;
112
+ let bestScore = Infinity;
113
+ for (const { c, i } of pool) {
114
+ const euclid = dist(from.x, from.y, c.x, c.y);
115
+ const geo = routes?.[i].distancePx;
116
+ const score = geo == null ? euclid : geo !== Infinity ? geo : euclid + 1e6;
117
+ if (score < bestScore) {
118
+ bestScore = score;
119
+ best = c;
120
+ }
121
+ }
122
+ return best;
123
+ }
@@ -0,0 +1,23 @@
1
+ import type { StrategyEntry } from './types.js';
2
+ import { GoalRootStrategy } from './goals/goal-root-strategy.js';
3
+ import { HideTop } from './goals/hide-top.js';
4
+
5
+ /** 解析参数:kill/fight → 死角自保开;nokill/flee/pacifist → 关;缺省(null)= 按角色默认。 */
6
+ export function parseHideKillArg(args?: string[]): boolean | null {
7
+ for (const raw of args ?? []) {
8
+ const a = raw.trim().toLowerCase();
9
+ if (a === 'kill' || a === 'fight' || a === 'kill=on') return true;
10
+ if (a === 'nokill' || a === 'flee' || a === 'pacifist' || a === 'kill=off') return false;
11
+ }
12
+ return null;
13
+ }
14
+
15
+ export const strategy: StrategyEntry = {
16
+ id: 'hide',
17
+ description:
18
+ '躲藏策略:在离线从地图算好的若干「非走廊、≥2 出口可逃、远离任务点/出生点」的藏身角落里,选测地最近且不挨威胁的一个潜伏不动;视野里一出现非队友,就像 shrimp-memory 那样保持距离甩开(KeepAway),并边逃边重算排除该威胁的新藏点,逃完落到新点继续潜伏。蟹不躲队友。可选参数 kill / nokill 控制「被单个对手逼进出刀距离且无路可逃时是否出刀自保」——缺省按角色:蟹/章鱼默认开,武士虾/枪虾(及无刀角色)默认关。',
19
+ create(args?: string[]) {
20
+ const killWhenCornered = parseHideKillArg(args);
21
+ return new GoalRootStrategy('hide', () => new HideTop(killWhenCornered), { resetOnMeetingResume: false });
22
+ },
23
+ };
@@ -379,6 +379,7 @@ export async function runStrategyLoop(strategyId: string, args?: string[]): Prom
379
379
 
380
380
  const ctx: StrategyContext = {
381
381
  taskData: [],
382
+ taskLocations: [],
382
383
  emergency: null,
383
384
  taskLocalBlockedUntil: 0,
384
385
  reportCorpseTarget: null,
@@ -669,6 +670,9 @@ export async function runStrategyLoop(strategyId: string, args?: string[]): Prom
669
670
  is_fake_shrimp: t.is_fake_shrimp ?? false,
670
671
  faction: taskFactionByName.get(t.name),
671
672
  }));
673
+ ctx.taskLocations = (mapData?.all_task_locations ?? [])
674
+ .filter((t: any) => t && typeof t.name === 'string' && typeof t.x === 'number' && typeof t.y === 'number')
675
+ .map((t: any) => ({ name: t.name, room: t.room, x: t.x, y: t.y, faction: t.faction }));
672
676
  if (Array.isArray(mapData?.rooms)) {
673
677
  if (ctx.rooms.length === 0) {
674
678
  const byRoom = taskLocationsByRoom(mapData?.all_task_locations);
@@ -1,4 +1,4 @@
1
- import type { GameState, TaskInfo, EmergencyInfo, CorpseInfo } from '../sdk/types.js';
1
+ import type { GameState, TaskInfo, TaskLocation, EmergencyInfo, CorpseInfo } from '../sdk/types.js';
2
2
  import type { Action } from '../sdk/action.js';
3
3
  import type { KnowledgeView } from '../lib/knowledge-store.js';
4
4
 
@@ -57,6 +57,12 @@ export interface RoomTarget {
57
57
 
58
58
  export interface StrategyContext {
59
59
  taskData: TaskInfo[];
60
+ /**
61
+ * 全局任务站点(map 的 all_task_locations),strategy-loop 加载地图后写入。与 taskData(仅自己被
62
+ * 分配、带 status)不同:这是地图上所有任务点的位置,开局固定。用于「同伴停在任务点附近 → 在真实
63
+ * 任务点假装做任务」这类定点行为。首次地图加载前可能为空/未设置。
64
+ */
65
+ taskLocations?: TaskLocation[];
60
66
  emergency: EmergencyInfo | null;
61
67
  taskLocalBlockedUntil: number;
62
68
  reportCorpseTarget: CorpseTarget | null;