@myclaw163/clawclaw-cli 0.6.67 → 0.6.68
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/references/COMMANDS.md +4 -4
- package/skills/clawclaw/references/KNOWLEDGE.md +14 -12
- package/src/commands/config.ts +30 -30
- package/src/commands/knowledge.test.ts +4 -10
- package/src/commands/knowledge.ts +10 -39
- 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/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/knowledge-store.test.ts +28 -38
- package/src/lib/knowledge-store.ts +52 -57
- package/src/sdk/index.ts +2 -3
- package/src/sdk/types.ts +2 -0
- package/src/strategies/avoid-players.knowledge.md +7 -8
- package/src/strategies/avoid-players.ts +1 -1
- package/src/strategies/corpse-patrol.ts +1 -1
- package/src/strategies/game-utils.ts +29 -17
- package/src/strategies/goals/avoid-players-top.ts +3 -3
- package/src/strategies/goals/corpse-patrol-top.ts +23 -1
- package/src/strategies/goals/leaf-goal.ts +2 -0
- package/src/strategies/goals/lone-kill-task-top.ts +39 -8
- package/src/strategies/goals/normal-shrimp-top.ts +11 -11
- package/src/strategies/goals/paradise-fish-top.ts +32 -15
- package/src/strategies/goals/warrior-shrimp-top.test.ts +4 -3
- package/src/strategies/goals/warrior-shrimp-top.ts +140 -80
- package/src/strategies/kill-lone.knowledge.md +6 -9
- package/src/strategies/lone-kill-task.ts +1 -1
- package/src/strategies/paradise-fish.knowledge.md +7 -8
- package/src/strategies/paradise-fish.ts +1 -1
- package/src/strategies/shrimp-memory.knowledge.md +7 -8
- package/src/strategies/shrimp-memory.ts +1 -1
- package/src/strategies/warrior-memory.knowledge.md +9 -10
- package/src/strategies/warrior-memory.ts +1 -1
|
@@ -46,8 +46,7 @@ describe('KnowledgeStore', () => {
|
|
|
46
46
|
selector: '5',
|
|
47
47
|
key: 'role',
|
|
48
48
|
value: 'impostor',
|
|
49
|
-
tags: ['
|
|
50
|
-
confidence: 0.8,
|
|
49
|
+
tags: ['kill_if_armed'],
|
|
51
50
|
note: 'vent read',
|
|
52
51
|
});
|
|
53
52
|
|
|
@@ -55,9 +54,7 @@ describe('KnowledgeStore', () => {
|
|
|
55
54
|
|
|
56
55
|
expect(file?.gameId).toBe('game-1');
|
|
57
56
|
expect(file?.subjects['player:5']?.facts?.role.value).toBe('impostor');
|
|
58
|
-
expect(file?.subjects['player:5']?.
|
|
59
|
-
expect(file?.subjects['player:5']?.tags?.suspect.note).toBe('vent read');
|
|
60
|
-
expect(file?.subjects['player:5']?.tags?.kill_if_armed.confidence).toBe(0.8);
|
|
57
|
+
expect(file?.subjects['player:5']?.tags?.kill_if_armed.note).toBe('vent read');
|
|
61
58
|
});
|
|
62
59
|
|
|
63
60
|
it('preserves explicit null fact values', () => {
|
|
@@ -79,16 +76,8 @@ describe('KnowledgeStore', () => {
|
|
|
79
76
|
expect(readKnowledgeFileResult(path).status).toBe('invalid');
|
|
80
77
|
});
|
|
81
78
|
|
|
82
|
-
it('rejects invalid confidence values', () => {
|
|
83
|
-
const s = store();
|
|
84
|
-
|
|
85
|
-
expect(() => s.set({ type: 'player', selector: '5', tags: ['avoid'], confidence: Number.NaN })).toThrow(/confidence/);
|
|
86
|
-
expect(() => s.set({ type: 'player', selector: '5', tags: ['avoid'], confidence: -0.1 })).toThrow(/confidence/);
|
|
87
|
-
expect(() => s.set({ type: 'player', selector: '5', tags: ['avoid'], confidence: 1.1 })).toThrow(/confidence/);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
79
|
it('returns an empty file when loading knowledge from another game', () => {
|
|
91
|
-
store('old-game').set({ type: 'player', selector: '5', tags: ['
|
|
80
|
+
store('old-game').set({ type: 'player', selector: '5', tags: ['hostile'] });
|
|
92
81
|
|
|
93
82
|
expect(store('new-game').list()).toEqual({
|
|
94
83
|
version: 1,
|
|
@@ -97,14 +86,30 @@ describe('KnowledgeStore', () => {
|
|
|
97
86
|
});
|
|
98
87
|
});
|
|
99
88
|
|
|
89
|
+
it('treats unmarked players as suspected by default', () => {
|
|
90
|
+
const s = store();
|
|
91
|
+
s.set({ type: 'player', selector: '5', tags: ['hostile'] });
|
|
92
|
+
|
|
93
|
+
const v = view({ '5': 'Bob', '9': 'Zed' });
|
|
94
|
+
|
|
95
|
+
// 显式标记的两档
|
|
96
|
+
expect(v.isHostile({ seat: 5 })).toBe(true);
|
|
97
|
+
expect(v.isSuspect({ seat: 5 })).toBe(false);
|
|
98
|
+
// 完全没标记的人 = 被怀疑(markOf 返回 undefined)
|
|
99
|
+
expect(v.markOf({ seat: 9 })).toBeUndefined();
|
|
100
|
+
expect(v.isSuspect({ name: 'Zed' })).toBe(true);
|
|
101
|
+
expect(v.isHostile({ seat: 9 })).toBe(false);
|
|
102
|
+
expect(v.isTrusted({ seat: 9 })).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
100
105
|
it('aggregates player marks written by seat and by name', () => {
|
|
101
106
|
const s = store();
|
|
102
|
-
s.set({ type: 'player', selector: '5', tags: ['
|
|
107
|
+
s.set({ type: 'player', selector: '5', tags: ['hostile'] });
|
|
103
108
|
|
|
104
109
|
const v = view({ '5': 'Bob' });
|
|
105
110
|
|
|
106
|
-
expect(v.
|
|
107
|
-
expect(v.markOf({ seat: 5 })).toBe('
|
|
111
|
+
expect(v.isHostile({ name: 'Bob' })).toBe(true);
|
|
112
|
+
expect(v.markOf({ seat: 5 })).toBe('hostile');
|
|
108
113
|
});
|
|
109
114
|
|
|
110
115
|
it('uses the latest mark across seat and name selectors', () => {
|
|
@@ -127,12 +132,12 @@ describe('KnowledgeStore', () => {
|
|
|
127
132
|
it('keeps role facts independent from strategy marks', () => {
|
|
128
133
|
const s = store();
|
|
129
134
|
s.set({ type: 'player', selector: '5', key: 'role', value: 'impostor' });
|
|
130
|
-
s.set({ type: 'player', selector: '5', tags: ['
|
|
135
|
+
s.set({ type: 'player', selector: '5', tags: ['trusted'] });
|
|
131
136
|
|
|
132
137
|
const v = view();
|
|
133
138
|
|
|
134
139
|
expect(v.roleOf({ seat: 5 })).toBe('impostor');
|
|
135
|
-
expect(v.markOf({ seat: 5 })).toBe('
|
|
140
|
+
expect(v.markOf({ seat: 5 })).toBe('trusted');
|
|
136
141
|
expect(v.isHostile({ seat: 5 })).toBe(false);
|
|
137
142
|
});
|
|
138
143
|
|
|
@@ -140,38 +145,23 @@ describe('KnowledgeStore', () => {
|
|
|
140
145
|
const s = store();
|
|
141
146
|
s.set({ type: 'player', selector: '5', tags: ['trusted'] });
|
|
142
147
|
tick();
|
|
143
|
-
s.set({ type: 'player', selector: 'Bob', tags: ['
|
|
148
|
+
s.set({ type: 'player', selector: 'Bob', tags: ['hostile'] });
|
|
144
149
|
|
|
145
150
|
const v = view({ '5': 'Bob' });
|
|
146
151
|
|
|
147
|
-
expect(v.
|
|
152
|
+
expect(v.isHostile({ seat: 5 })).toBe(true);
|
|
148
153
|
expect(v.isTrusted({ seat: 5 })).toBe(false);
|
|
149
154
|
expect(v.selectorsWithTag('player', 'trusted')).toEqual([]);
|
|
150
|
-
expect(v.selectorsWithTag('player', '
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('applies confidence thresholds to facts and tags', () => {
|
|
154
|
-
const s = store();
|
|
155
|
-
s.set({ type: 'player', selector: '5', tags: ['suspect'], confidence: 0.5 });
|
|
156
|
-
s.set({ type: 'player', selector: '6', tags: ['hostile'], confidence: 0.7 });
|
|
157
|
-
|
|
158
|
-
const v = view();
|
|
159
|
-
|
|
160
|
-
expect(v.isSuspect({ seat: 5 }, 0.4)).toBe(true);
|
|
161
|
-
expect(v.isSuspect({ seat: 5 }, 0.6)).toBe(false);
|
|
162
|
-
expect(v.isHostile({ seat: 6 }, 0.6)).toBe(true);
|
|
163
|
-
expect(v.isHostile({ seat: 6 }, 0.8)).toBe(false);
|
|
155
|
+
expect(v.selectorsWithTag('player', 'hostile')).toEqual(['Bob']);
|
|
164
156
|
});
|
|
165
157
|
|
|
166
|
-
it('maps legacy tags onto the
|
|
158
|
+
it('maps legacy tags onto the two canonical marks', () => {
|
|
167
159
|
const s = store();
|
|
168
|
-
s.set({ type: 'player', selector: '5', tags: ['avoid'] });
|
|
169
160
|
s.set({ type: 'player', selector: '6', tags: ['kill_if_armed'] });
|
|
170
161
|
s.set({ type: 'player', selector: '7', tags: ['protected'] });
|
|
171
162
|
|
|
172
163
|
const v = view();
|
|
173
164
|
|
|
174
|
-
expect(v.markOf({ seat: 5 })).toBe('suspect');
|
|
175
165
|
expect(v.markOf({ seat: 6 })).toBe('hostile');
|
|
176
166
|
expect(v.markOf({ seat: 7 })).toBe('trusted');
|
|
177
167
|
expect(v.hasTag({ seat: 6 }, 'hostile')).toBe(true);
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* - 跨进程:Agent(短进程 CLI)写,strategy(长进程 loop)按 mtime 轮询读,免重启。
|
|
6
6
|
* - 快照而非日志:单个 JSON 对象,del/clear 直接重写;写入用临时文件 + rename 原子替换。
|
|
7
7
|
* - gameId 作用域:每份知识标记所属对局(= 最新 session 文件名),换局自动失效。
|
|
8
|
-
* - 价值中立:只存推断 +
|
|
8
|
+
* - 价值中立:只存推断 + source,对 value 不做价值判断;
|
|
9
9
|
* 确定事实(role/队友/尸体…)走 state/ctx.teammates,不入库。
|
|
10
|
+
* - 玩家两档标记:只显式标 hostile(坏人)/ trusted(好人),未标记的一律默认「被怀疑」,不再有 suspect 标记,也不存置信度。
|
|
10
11
|
*/
|
|
11
12
|
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
|
|
12
13
|
import { basename, dirname, join } from 'path';
|
|
@@ -20,14 +21,12 @@ export type KnowledgeScope = 'game' | 'session' | 'persistent';
|
|
|
20
21
|
|
|
21
22
|
export interface FactMeta {
|
|
22
23
|
value: unknown;
|
|
23
|
-
confidence?: number;
|
|
24
24
|
note?: string;
|
|
25
25
|
source?: KnowledgeSource;
|
|
26
26
|
ts: number;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export interface TagMeta {
|
|
30
|
-
confidence?: number;
|
|
31
30
|
note?: string;
|
|
32
31
|
source?: KnowledgeSource;
|
|
33
32
|
ts: number;
|
|
@@ -38,7 +37,7 @@ export interface SubjectEntry {
|
|
|
38
37
|
type: string; // 'player' | 'room' | 'task' | 'global' | <自定义>
|
|
39
38
|
selector: string; // player→座位号或名字;room→房名;global→''
|
|
40
39
|
facts?: Record<string, FactMeta>; // 键值事实,如 role: { value: 'impostor' }
|
|
41
|
-
tags?: Record<string, TagMeta>; // 玩家策略主标记:
|
|
40
|
+
tags?: Record<string, TagMeta>; // 玩家策略主标记:hostile / trusted(未标记默认被怀疑);也支持自定义标签
|
|
42
41
|
scope?: KnowledgeScope;
|
|
43
42
|
}
|
|
44
43
|
|
|
@@ -56,7 +55,8 @@ export type KnowledgeFileReadResult =
|
|
|
56
55
|
export type PlayerRef = { name?: string; seat?: number };
|
|
57
56
|
export type SubjectRef = { type: string; selector: string };
|
|
58
57
|
export type KnowledgeRef = PlayerRef | SubjectRef;
|
|
59
|
-
|
|
58
|
+
/** 玩家显式标记只有两档:hostile(坏人)/ trusted(好人)。未标记 = 被怀疑(markOf 返回 undefined)。 */
|
|
59
|
+
export type PlayerMark = 'hostile' | 'trusted';
|
|
60
60
|
|
|
61
61
|
export interface KnowledgeView {
|
|
62
62
|
/** 当前没有任何知识时为 true。 */
|
|
@@ -65,28 +65,24 @@ export interface KnowledgeView {
|
|
|
65
65
|
subject(type: string, selector: string): SubjectEntry | undefined;
|
|
66
66
|
/** 取某 subject 的键值事实。player ref 会按座位↔名字归一化解析。 */
|
|
67
67
|
getFact(ref: KnowledgeRef, key: string): FactMeta | undefined;
|
|
68
|
-
/** 该 subject
|
|
69
|
-
hasTag(ref: KnowledgeRef, tag: string
|
|
70
|
-
/** 按 type / tag / factKey
|
|
71
|
-
query(q: { type?: string; tag?: string; factKey?: string
|
|
68
|
+
/** 该 subject 是否带某标签。玩家两档 mark 及旧别名按最新结论解析。 */
|
|
69
|
+
hasTag(ref: KnowledgeRef, tag: string): boolean;
|
|
70
|
+
/** 按 type / tag / factKey 筛选 subject。玩家 mark 标签同样遵守最新结论。 */
|
|
71
|
+
query(q: { type?: string; tag?: string; factKey?: string }): SubjectEntry[];
|
|
72
72
|
/** 某 type 下带某标签的全部 selector(喂给 matchesAnyTarget 用)。玩家 mark 标签同样过滤旧结论。 */
|
|
73
|
-
selectorsWithTag(type: string, tag: string
|
|
73
|
+
selectorsWithTag(type: string, tag: string): string[];
|
|
74
74
|
// —— player 便捷语法糖 ——
|
|
75
75
|
/** 最近一次 role 事实。身份事实与策略 mark 完全解耦。 */
|
|
76
76
|
roleOf(p: PlayerRef): string | undefined;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
/** @deprecated Use isTrusted. */
|
|
84
|
-
isCleared(p: PlayerRef): boolean;
|
|
77
|
+
/** 最新显式标记;未标记返回 undefined(= 被怀疑)。 */
|
|
78
|
+
markOf(p: PlayerRef): PlayerMark | undefined;
|
|
79
|
+
/** 被怀疑 = 既非 hostile 也非 trusted(含完全未标记者)。 */
|
|
80
|
+
isSuspect(p: PlayerRef): boolean;
|
|
81
|
+
isHostile(p: PlayerRef): boolean;
|
|
82
|
+
isTrusted(p: PlayerRef): boolean;
|
|
85
83
|
}
|
|
86
84
|
|
|
87
85
|
const PLAYER_MARK_ALIASES: Record<string, PlayerMark> = {
|
|
88
|
-
suspect: 'suspect',
|
|
89
|
-
avoid: 'suspect',
|
|
90
86
|
hostile: 'hostile',
|
|
91
87
|
kill_if_armed: 'hostile',
|
|
92
88
|
trusted: 'trusted',
|
|
@@ -95,6 +91,14 @@ const PLAYER_MARK_ALIASES: Record<string, PlayerMark> = {
|
|
|
95
91
|
do_not_kill: 'trusted',
|
|
96
92
|
};
|
|
97
93
|
|
|
94
|
+
/**
|
|
95
|
+
* 两档化后被废弃的旧玩家标记:一律视同未标记(= 被怀疑),不映射到任何 canonical mark
|
|
96
|
+
*(「被怀疑」就是默认态,没有对应标签)。读取侧与写入侧共用这一份清单,避免漂移:
|
|
97
|
+
* - 读取侧(buildKnowledgeView.tagActive):旧数据里的 tags.suspect / tags.avoid 不再被 hasTag / query / selectorsWithTag 命中。
|
|
98
|
+
* - 写入侧(knowledge mark 的 removeTags):改判玩家时顺手删掉残留的 raw tag,否则 knowledge get / subject() 仍会暴露 tags.suspect/avoid 误导人。
|
|
99
|
+
*/
|
|
100
|
+
export const DROPPED_PLAYER_MARK_TAGS = new Set(['suspect', 'avoid']);
|
|
101
|
+
|
|
98
102
|
function nowTs(): number {
|
|
99
103
|
return Date.now();
|
|
100
104
|
}
|
|
@@ -180,7 +184,6 @@ export interface KnowledgeSetInput {
|
|
|
180
184
|
value?: unknown;
|
|
181
185
|
tags?: string[];
|
|
182
186
|
removeTags?: string[];
|
|
183
|
-
confidence?: number;
|
|
184
187
|
note?: string;
|
|
185
188
|
source?: KnowledgeSource;
|
|
186
189
|
scope?: KnowledgeScope;
|
|
@@ -210,9 +213,6 @@ export class KnowledgeStore {
|
|
|
210
213
|
|
|
211
214
|
set(input: KnowledgeSetInput): SubjectEntry {
|
|
212
215
|
if (!input.type || !input.selector) throw new Error('knowledge set requires type and selector');
|
|
213
|
-
if (input.confidence != null && (!Number.isFinite(input.confidence) || input.confidence < 0 || input.confidence > 1)) {
|
|
214
|
-
throw new Error('confidence must be a finite number between 0 and 1');
|
|
215
|
-
}
|
|
216
216
|
const file = this.load();
|
|
217
217
|
const key = subjectKey(input.type, input.selector);
|
|
218
218
|
const entry: SubjectEntry = file.subjects[key] ?? { type: input.type, selector: input.selector };
|
|
@@ -223,7 +223,6 @@ export class KnowledgeStore {
|
|
|
223
223
|
entry.facts = entry.facts ?? {};
|
|
224
224
|
entry.facts[input.key] = {
|
|
225
225
|
value: input.value === undefined ? true : input.value,
|
|
226
|
-
confidence: input.confidence,
|
|
227
226
|
note: input.note,
|
|
228
227
|
source,
|
|
229
228
|
ts,
|
|
@@ -233,7 +232,7 @@ export class KnowledgeStore {
|
|
|
233
232
|
entry.tags = entry.tags ?? {};
|
|
234
233
|
for (const tag of input.tags) {
|
|
235
234
|
if (!tag) continue;
|
|
236
|
-
entry.tags[tag] = {
|
|
235
|
+
entry.tags[tag] = { note: input.note, source, ts };
|
|
237
236
|
}
|
|
238
237
|
}
|
|
239
238
|
if (input.removeTags && entry.tags) {
|
|
@@ -283,7 +282,6 @@ export function buildKnowledgeView(
|
|
|
283
282
|
): KnowledgeView {
|
|
284
283
|
const subjects = file?.subjects ?? {};
|
|
285
284
|
const namesBySeat = opts.playerNamesBySeat ?? {};
|
|
286
|
-
const confOk = (conf: number | undefined, min?: number): boolean => min == null || (conf ?? 1) >= min;
|
|
287
285
|
|
|
288
286
|
const resolveAll = (ref: KnowledgeRef): SubjectEntry[] => {
|
|
289
287
|
if (isSubjectRef(ref)) {
|
|
@@ -302,8 +300,9 @@ export function buildKnowledgeView(
|
|
|
302
300
|
return best;
|
|
303
301
|
};
|
|
304
302
|
|
|
305
|
-
type MarkVerdict = { kind: PlayerMark; ts: number
|
|
306
|
-
|
|
303
|
+
type MarkVerdict = { kind: PlayerMark; ts: number };
|
|
304
|
+
// 同一时刻打了冲突标记时,trusted 优先级高于 hostile(好人判断更慎重,不轻易被覆盖)。
|
|
305
|
+
const markRank = (kind: PlayerMark): number => kind === 'trusted' ? 2 : 1;
|
|
307
306
|
const latestMark = (ref: KnowledgeRef): MarkVerdict | undefined => {
|
|
308
307
|
let best: MarkVerdict | undefined;
|
|
309
308
|
const take = (v: MarkVerdict) => {
|
|
@@ -316,56 +315,52 @@ export function buildKnowledgeView(
|
|
|
316
315
|
for (const s of resolveAll(ref)) {
|
|
317
316
|
for (const [tag, meta] of Object.entries(s.tags ?? {})) {
|
|
318
317
|
const kind = PLAYER_MARK_ALIASES[tag];
|
|
319
|
-
if (kind) take({ kind, ts: meta.ts
|
|
318
|
+
if (kind) take({ kind, ts: meta.ts });
|
|
320
319
|
}
|
|
321
320
|
}
|
|
322
321
|
return best;
|
|
323
322
|
};
|
|
324
323
|
|
|
325
324
|
const markKindForTag = (tag: string): PlayerMark | undefined => PLAYER_MARK_ALIASES[tag];
|
|
326
|
-
const tagActive = (s: SubjectEntry, tag: string
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
325
|
+
const tagActive = (s: SubjectEntry, tag: string): boolean => {
|
|
326
|
+
if (s.type === 'player') {
|
|
327
|
+
const requestedMark = markKindForTag(tag);
|
|
328
|
+
if (requestedMark) {
|
|
329
|
+
const carriesRequestedMark = Object.keys(s.tags ?? {}).some(candidate => markKindForTag(candidate) === requestedMark);
|
|
330
|
+
if (!carriesRequestedMark) return false;
|
|
331
|
+
const ref: PlayerRef = /^\d+$/.test(s.selector.trim()) ? { seat: Number(s.selector) } : { name: s.selector };
|
|
332
|
+
const mark = latestMark(ref);
|
|
333
|
+
return !!mark && mark.kind === requestedMark;
|
|
334
|
+
}
|
|
335
|
+
// 废弃的旧玩家标记(suspect/avoid)= 未标记 = 被怀疑,绝不命中;其余自定义标签(如 paradise_fish)照常按原值判定。
|
|
336
|
+
if (DROPPED_PLAYER_MARK_TAGS.has(tag)) return false;
|
|
334
337
|
}
|
|
335
|
-
|
|
336
|
-
return !!meta && confOk(meta.confidence, min);
|
|
338
|
+
return !!s.tags?.[tag];
|
|
337
339
|
};
|
|
338
340
|
|
|
339
341
|
return {
|
|
340
342
|
empty: Object.keys(subjects).length === 0,
|
|
341
343
|
subject: (type, selector) => subjects[subjectKey(type, selector)],
|
|
342
344
|
getFact: (ref, key) => latestFact(ref, key),
|
|
343
|
-
hasTag: (ref, tag
|
|
345
|
+
hasTag: (ref, tag) => resolveAll(ref).some((s) => tagActive(s, tag)),
|
|
344
346
|
query: (q) => Object.values(subjects).filter((s) => {
|
|
345
347
|
if (q.type && s.type !== q.type) return false;
|
|
346
|
-
if (q.factKey)
|
|
347
|
-
|
|
348
|
-
if (!f || !confOk(f.confidence, q.minConfidence)) return false;
|
|
349
|
-
}
|
|
350
|
-
if (q.tag && !tagActive(s, q.tag, q.minConfidence)) return false;
|
|
348
|
+
if (q.factKey && !s.facts?.[q.factKey]) return false;
|
|
349
|
+
if (q.tag && !tagActive(s, q.tag)) return false;
|
|
351
350
|
return true;
|
|
352
351
|
}),
|
|
353
|
-
selectorsWithTag: (type, tag
|
|
354
|
-
.filter((s) => s.type === type && tagActive(s, tag
|
|
352
|
+
selectorsWithTag: (type, tag) => Object.values(subjects)
|
|
353
|
+
.filter((s) => s.type === type && tagActive(s, tag))
|
|
355
354
|
.map((s) => s.selector),
|
|
356
355
|
roleOf: (p) => {
|
|
357
356
|
const fact = latestFact(p, 'role');
|
|
358
357
|
return typeof fact?.value === 'string' ? fact.value : undefined;
|
|
359
358
|
},
|
|
360
|
-
markOf: (p
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
isHostile: (p, min) => latestMark(p)?.kind === 'hostile' && confOk(latestMark(p)?.confidence, min),
|
|
366
|
-
isTrusted: (p, min) => latestMark(p)?.kind === 'trusted' && confOk(latestMark(p)?.confidence, min),
|
|
367
|
-
isConfirmedHostile: (p, min) => latestMark(p)?.kind === 'hostile' && confOk(latestMark(p)?.confidence, min),
|
|
368
|
-
isCleared: (p) => latestMark(p)?.kind === 'trusted',
|
|
359
|
+
markOf: (p) => latestMark(p)?.kind,
|
|
360
|
+
// 未标记 hostile/trusted 的玩家一律「被怀疑」——这就是「除好人坏人外剩下全是被怀疑」的落点。
|
|
361
|
+
isSuspect: (p) => !latestMark(p),
|
|
362
|
+
isHostile: (p) => latestMark(p)?.kind === 'hostile',
|
|
363
|
+
isTrusted: (p) => latestMark(p)?.kind === 'trusted',
|
|
369
364
|
};
|
|
370
365
|
}
|
|
371
366
|
|
package/src/sdk/index.ts
CHANGED
|
@@ -21,8 +21,7 @@
|
|
|
21
21
|
* hasKnownCorpse / corpseAtScene / nearestReportableCorpse / safePatrolStep /
|
|
22
22
|
* nonTeammatesVisible / matchesTarget / isTargetAlive / nearestVisibleTarget /
|
|
23
23
|
* pursueVisibleTarget / killCooldownSecs / hasKillUseRemaining / canUseKill / killRangeFor / killCommitRange /
|
|
24
|
-
* isKnowledgeHostile
|
|
25
|
-
* (含 @deprecated 别名 isKnowledgeKillTarget / isKnowledgeAvoid / isKnowledgeProtected) /
|
|
24
|
+
* isKnowledgeHostile(坏人)/ isKnowledgeThreat(非 trusted = 坏人+被怀疑)/ isKnowledgeTrusted(好人)/
|
|
26
25
|
* PatrolState / SafeTaskOptions / SafePatrolOptions + 常量(TASK_SUBMIT_RADIUS /
|
|
27
26
|
* SHRIMP_VISION_RANGE / SHRIMP_VISION_EXIT_BUFFER / SHRIMP_VISION_RELEASE_RANGE /
|
|
28
27
|
* FOLLOW_RANGE / PATROL_REACHED_DISTANCE / PROGRESS_INTERVAL_MS)
|
|
@@ -53,12 +52,12 @@ export type {
|
|
|
53
52
|
// Game utilities (for user strategies)
|
|
54
53
|
export {
|
|
55
54
|
dist, firstAvailableTask, nearestKnownCorpse, nearestKnownCorpseNav, hasKnownCorpse, corpseAtScene, nearestReportableCorpse,
|
|
55
|
+
emergencyRushDecision, EMERGENCY_RUSH_ALIVE_THRESHOLD,
|
|
56
56
|
safePatrolStep,
|
|
57
57
|
nonTeammatesVisible, matchesTarget, isTargetAlive,
|
|
58
58
|
nearestVisibleTarget, pursueVisibleTarget,
|
|
59
59
|
killCooldownSecs, hasKillUseRemaining, canUseKill, killRangeFor, killCommitRange,
|
|
60
60
|
isKnowledgeHostile, isKnowledgeThreat, isKnowledgeTrusted,
|
|
61
|
-
isKnowledgeKillTarget, isKnowledgeAvoid, isKnowledgeProtected,
|
|
62
61
|
PatrolState,
|
|
63
62
|
TASK_SUBMIT_RADIUS, SHRIMP_VISION_RANGE, SHRIMP_VISION_EXIT_BUFFER, SHRIMP_VISION_RELEASE_RANGE,
|
|
64
63
|
FOLLOW_RANGE, PATROL_REACHED_DISTANCE,
|
package/src/sdk/types.ts
CHANGED
|
@@ -130,6 +130,8 @@ export interface GameState {
|
|
|
130
130
|
players: PlayerInfo[];
|
|
131
131
|
corpses: CorpseInfo[];
|
|
132
132
|
emergency?: EmergencyInfo;
|
|
133
|
+
/** 服务端下发的「已知存活人数」:存活者 + 本玩家尚未目睹其死亡者之和。残局抢修紧急任务的门槛。 */
|
|
134
|
+
alive_count?: number;
|
|
133
135
|
task_progress?: { completed: number; goal: number };
|
|
134
136
|
new_events?: Array<Record<string, any>>;
|
|
135
137
|
stale: boolean;
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
# avoid-players 知识契约
|
|
2
2
|
|
|
3
|
-
`avoid-players`
|
|
3
|
+
`avoid-players` 读取两档玩家标记,**未标记的一律默认「被怀疑」**:
|
|
4
4
|
|
|
5
5
|
| 标记 | 行为 |
|
|
6
6
|
|------|------|
|
|
7
|
-
| `
|
|
8
|
-
| `
|
|
9
|
-
|
|
|
7
|
+
| `hostile`(坏人) | 加入回避名单,进入视野便逃离 |
|
|
8
|
+
| `trusted`(好人) | 不因知识标记而回避 |
|
|
9
|
+
| 未标记(被怀疑) | 不自动回避——照常按房间巡逻(需要躲谁就把它标 hostile,或用启动参数点名) |
|
|
10
10
|
|
|
11
|
-
回避名单 = 启动参数(座位号/名字)∪
|
|
11
|
+
回避名单 = 启动参数(座位号/名字)∪ 知识库 hostile 标记。威胁消失后恢复巡逻。
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
ccl knowledge mark 4
|
|
15
|
-
ccl knowledge mark 4 hostile --confidence 0.9 --note "确认危险"
|
|
14
|
+
ccl knowledge mark 4 hostile --note "确认危险"
|
|
16
15
|
ccl knowledge mark 4 trusted --note "已澄清"
|
|
17
|
-
ccl knowledge del player 4
|
|
16
|
+
ccl knowledge del player 4 # 取消标记 → 回到默认「被怀疑」(不自动回避)
|
|
18
17
|
```
|
|
19
18
|
|
|
20
19
|
`role` 是纯身份事实,不直接控制策略。
|
|
@@ -5,7 +5,7 @@ import { parseTargetArgs } from './player-targets.js';
|
|
|
5
5
|
|
|
6
6
|
export const strategy: StrategyEntry = {
|
|
7
7
|
id: 'avoid-players',
|
|
8
|
-
description: '回避玩家:按房间顺序巡逻,视野里出现回避目标就寻路躲避(推演逃点、持续远离),威胁消失后恢复巡逻。回避名单 = 启动参数(座位号或名字,可多人,可省略)∪ 知识库中标记为
|
|
8
|
+
description: '回避玩家:按房间顺序巡逻,视野里出现回避目标就寻路躲避(推演逃点、持续远离),威胁消失后恢复巡逻。回避名单 = 启动参数(座位号或名字,可多人,可省略)∪ 知识库中标记为 hostile(坏人)的玩家;trusted 与未标记的被怀疑者都不自动回避(要躲就标 hostile 或用启动参数点名)。Agent 可用 `ccl knowledge mark` 动态改判,免重启。',
|
|
9
9
|
create(args?: string[]) {
|
|
10
10
|
const targets = args ? parseTargetArgs(args) : [];
|
|
11
11
|
return new GoalRootStrategy('avoid-players', () => new AvoidPlayersTop(targets), {
|
|
@@ -15,7 +15,7 @@ class CorpsePatrolStrategy extends GoalRootStrategy {
|
|
|
15
15
|
|
|
16
16
|
export const strategy: StrategyEntry = {
|
|
17
17
|
id: 'corpse-patrol',
|
|
18
|
-
description: '发现尸体就在附近40至100距离内随机游荡,但不报告——故意在尸体旁出没制造嫌疑感。没有尸体时巡逻各房间。传入打招呼话术时,视野内出现人就随机发送一条,之后120
|
|
18
|
+
description: '发现尸体就在附近40至100距离内随机游荡,但不报告——故意在尸体旁出没制造嫌疑感。没有尸体时巡逻各房间。传入打招呼话术时,视野内出现人就随机发送一条,之后120秒内不再发言。不做任务,不杀人;但残局(已知存活≤6)出现紧急维修任务时例外,会最高优先级抢着去做,阻止蟹靠破坏倒计时取胜。(天堂鱼默认;进阶版见 paradise-fish)参数:可选:1~3 条打招呼话术。',
|
|
19
19
|
create(args?: string[]) {
|
|
20
20
|
return new CorpsePatrolStrategy(parseGreetingArgs(args, 'corpse-patrol'));
|
|
21
21
|
},
|
|
@@ -327,6 +327,26 @@ export function taskMoveDecision(state: GameState, ctx: StrategyContext, task: T
|
|
|
327
327
|
return { action: Action.move({ x: task.x!, y: task.y! }) };
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
/** 已知存活人数 ≤ 此值才触发「残局抢修紧急任务」。 */
|
|
331
|
+
export const EMERGENCY_RUSH_ALIVE_THRESHOLD = 6;
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* 残局抢修紧急维修任务的「靠近 → 到点提交」决策:已知存活人数(state.alive_count,含本玩家尚未目睹其
|
|
335
|
+
* 死亡者)≤ EMERGENCY_RUSH_ALIVE_THRESHOLD 且当前有进行中的紧急任务时返回该单步决策,否则返回 null
|
|
336
|
+
* (行为不触发,调用方按自身原优先级继续)。
|
|
337
|
+
*
|
|
338
|
+
* 背景:紧急任务超时则蟹阵营直接获胜(后端倒计时),而中立的章鱼/天堂鱼要靠活到最后取胜,因此残局应抢着
|
|
339
|
+
* 把维修做掉、掐断蟹的破坏取胜线。服务端已放开中立/蟹阵营提交紧急任务。紧急任务被任意玩家完成 / 开会 /
|
|
340
|
+
* 超时后,服务端会清空 state.emergency(→ ctx.emergency 为 null),本函数随之自动返回 null。
|
|
341
|
+
*/
|
|
342
|
+
export function emergencyRushDecision(state: GameState, ctx: StrategyContext): BehaviorDecision | null {
|
|
343
|
+
if (typeof state.alive_count !== 'number' || state.alive_count > EMERGENCY_RUSH_ALIVE_THRESHOLD) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
const emergency = firstAvailableTask([], () => true, ctx.emergency, ctx.blockedMoveTarget);
|
|
347
|
+
return emergency ? taskMoveDecision(state, ctx, emergency) : null;
|
|
348
|
+
}
|
|
349
|
+
|
|
330
350
|
export function nearestReportableCorpse(state: GameState): GameState['corpses'][number] | null {
|
|
331
351
|
const nearby = state.corpses
|
|
332
352
|
.map(corpse => {
|
|
@@ -666,29 +686,21 @@ function knowledgeOf(ctx: StrategyContext) {
|
|
|
666
686
|
return ctx.knowledge ?? FALLBACK_KNOWLEDGE;
|
|
667
687
|
}
|
|
668
688
|
|
|
669
|
-
/** hostile
|
|
670
|
-
export function isKnowledgeHostile(p: PlayerRef, ctx: StrategyContext
|
|
671
|
-
return knowledgeOf(ctx).isHostile(p
|
|
689
|
+
/** hostile(坏人):明确敌对。带刀策略刀好时可主动攻击;无刀策略硬回避。 */
|
|
690
|
+
export function isKnowledgeHostile(p: PlayerRef, ctx: StrategyContext): boolean {
|
|
691
|
+
return knowledgeOf(ctx).isHostile(p);
|
|
672
692
|
}
|
|
673
693
|
|
|
674
|
-
/**
|
|
675
|
-
export function isKnowledgeThreat(p: PlayerRef, ctx: StrategyContext
|
|
676
|
-
|
|
677
|
-
return mark === 'suspect' || mark === 'hostile';
|
|
694
|
+
/** 威胁 = 非 trusted(坏人 + 被怀疑)。未标记 trusted 的人一律按需警惕——「除好人外都值得提防」。 */
|
|
695
|
+
export function isKnowledgeThreat(p: PlayerRef, ctx: StrategyContext): boolean {
|
|
696
|
+
return !knowledgeOf(ctx).isTrusted(p);
|
|
678
697
|
}
|
|
679
698
|
|
|
680
|
-
/** trusted
|
|
681
|
-
export function isKnowledgeTrusted(p: PlayerRef, ctx: StrategyContext
|
|
682
|
-
return knowledgeOf(ctx).isTrusted(p
|
|
699
|
+
/** trusted(好人):可信同伴,永远优先于回避与击杀。 */
|
|
700
|
+
export function isKnowledgeTrusted(p: PlayerRef, ctx: StrategyContext): boolean {
|
|
701
|
+
return knowledgeOf(ctx).isTrusted(p);
|
|
683
702
|
}
|
|
684
703
|
|
|
685
|
-
/** @deprecated Use isKnowledgeHostile. */
|
|
686
|
-
export const isKnowledgeKillTarget = isKnowledgeHostile;
|
|
687
|
-
/** @deprecated Use isKnowledgeThreat. */
|
|
688
|
-
export const isKnowledgeAvoid = isKnowledgeThreat;
|
|
689
|
-
/** @deprecated Use isKnowledgeTrusted. */
|
|
690
|
-
export const isKnowledgeProtected = isKnowledgeTrusted;
|
|
691
|
-
|
|
692
704
|
export function resolveRoom(name: string, ctx: StrategyContext): RoomTarget | null {
|
|
693
705
|
const folded = name.trim().toLowerCase();
|
|
694
706
|
if (!folded) return null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GameState, PlayerInfo } from '../../sdk/types.js';
|
|
2
|
-
import { hasReachedRoomTarget,
|
|
2
|
+
import { hasReachedRoomTarget, isKnowledgeHostile, isTargetAlive, matchesAnyTarget, PatrolState, PROGRESS_INTERVAL_MS } from '../game-utils.js';
|
|
3
3
|
import type { BehaviorDecision, RoomTarget, StrategyContext } from '../types.js';
|
|
4
4
|
import { Goal } from './goal.js';
|
|
5
5
|
import { KeepAwayGoal, type KeepAwayGoalOptions, type ThreatResolver } from './keep-away-goal.js';
|
|
@@ -62,10 +62,10 @@ export class AvoidPlayersTop extends Goal {
|
|
|
62
62
|
this.moveMode = 'flee';
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
/** args 回避名单(活着的)∪
|
|
65
|
+
/** args 回避名单(活着的)∪ 知识库标记 hostile(坏人)的玩家。被怀疑者(未标记)不自动回避——照常按房间巡逻。 */
|
|
66
66
|
private threats(state: GameState, ctx: StrategyContext): PlayerInfo[] {
|
|
67
67
|
const validTargets = this.targets.filter(target => isTargetAlive(state, target, ctx));
|
|
68
|
-
return state.players.filter(p => matchesAnyTarget(p, validTargets, ctx) ||
|
|
68
|
+
return state.players.filter(p => matchesAnyTarget(p, validTargets, ctx) || isKnowledgeHostile(p, ctx));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
private setMoveGoal(room: RoomTarget, priority: number, mode: 'patrol' | 'flee', stopOnPlayer: boolean): void {
|
|
@@ -2,6 +2,7 @@ import type { GameState } from '../../sdk/types.js';
|
|
|
2
2
|
import { Action } from '../../sdk/action.js';
|
|
3
3
|
import {
|
|
4
4
|
corpseAtScene,
|
|
5
|
+
emergencyRushDecision,
|
|
5
6
|
nearestKnownCorpseNav,
|
|
6
7
|
patrolStep,
|
|
7
8
|
PatrolState,
|
|
@@ -24,6 +25,14 @@ export class CorpsePatrolTop extends Goal {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
|
|
28
|
+
// 残局抢修紧急任务(已知存活≤6):天堂鱼最高优先级,先于贴尸/巡逻/打招呼——
|
|
29
|
+
// 紧急任务超时则蟹直接获胜,天堂鱼靠活到最后取胜,故残局抢着把维修做掉、掐断蟹的破坏取胜线。
|
|
30
|
+
const emergency = emergencyRushDecision(state, ctx);
|
|
31
|
+
if (emergency) {
|
|
32
|
+
this.emitProgress(state, ctx, null, true);
|
|
33
|
+
return [emergency];
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
const decisions: BehaviorDecision[] = [];
|
|
28
37
|
|
|
29
38
|
const greetingDecision = this.tryGreeting(state);
|
|
@@ -63,13 +72,26 @@ export class CorpsePatrolTop extends Goal {
|
|
|
63
72
|
return { action: Action.speech(text) };
|
|
64
73
|
}
|
|
65
74
|
|
|
66
|
-
private emitProgress(
|
|
75
|
+
private emitProgress(
|
|
76
|
+
state: GameState,
|
|
77
|
+
ctx: StrategyContext,
|
|
78
|
+
corpse: CorpseTarget | null,
|
|
79
|
+
emergencyRush = false,
|
|
80
|
+
): void {
|
|
67
81
|
const now = Date.now();
|
|
68
82
|
if (now - ctx.lastProgressNotifyAt < PROGRESS_INTERVAL_MS) return;
|
|
69
83
|
ctx.lastProgressNotifyAt = now;
|
|
70
84
|
|
|
71
85
|
const room = state.you.room ?? '未知';
|
|
72
86
|
|
|
87
|
+
if (emergencyRush) {
|
|
88
|
+
const name = ctx.emergency?.task_name ?? '紧急维修';
|
|
89
|
+
ctx.notifications.push(
|
|
90
|
+
`[进度] 当前在${room},残局仅剩${state.alive_count}人,抢着去做紧急任务「${name}」,阻止蟹靠破坏倒计时取胜。`,
|
|
91
|
+
);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
73
95
|
let msg = `[进度] 当前在${room}`;
|
|
74
96
|
if (corpse) {
|
|
75
97
|
const label = corpse.name ?? '未知';
|
|
@@ -2,6 +2,8 @@ import type { BehaviorDecision } from '../types.js';
|
|
|
2
2
|
import { Goal } from './goal.js';
|
|
3
3
|
|
|
4
4
|
export const URGENT_GOAL_PRIORITY = 1;
|
|
5
|
+
/** 残局抢修紧急任务:高于游走任务、低于落单猎杀/反射(URGENT),故对发言仍可被打断(< URGENT)。 */
|
|
6
|
+
export const EMERGENCY_GOAL_PRIORITY = 0.6;
|
|
5
7
|
export const WANDER_GOAL_PRIORITY = 0.2;
|
|
6
8
|
|
|
7
9
|
export class LeafGoal extends Goal {
|