@steve02081504/fount-p2p 0.0.31 → 0.0.33

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.
Files changed (70) hide show
  1. package/AGENTS.md +79 -0
  2. package/README.md +1 -1
  3. package/discovery/nostr.mjs +11 -0
  4. package/docs/evfs.md +32 -0
  5. package/docs/infra.md +25 -0
  6. package/docs/mesh.md +67 -0
  7. package/docs/reputation.md +22 -0
  8. package/docs/runtime.md +74 -0
  9. package/docs/signaling.md +33 -0
  10. package/docs/transports.md +92 -0
  11. package/docs/wire.md +21 -0
  12. package/index.mjs +2 -0
  13. package/link/providers/nostr.mjs +2 -12
  14. package/link/providers/webrtc.mjs +1 -1
  15. package/link/rtc/polyfill.mjs +1 -1
  16. package/node/instance.mjs +7 -3
  17. package/node/signaling_config.mjs +52 -29
  18. package/package.json +6 -10
  19. package/sim/AGENTS.md +29 -0
  20. package/sim/admission.mjs +56 -0
  21. package/sim/anomaly.mjs +39 -0
  22. package/sim/apply.mjs +77 -0
  23. package/sim/attack_space.mjs +113 -0
  24. package/sim/attacks.mjs +614 -0
  25. package/sim/behavior.mjs +78 -0
  26. package/sim/cli.mjs +282 -0
  27. package/sim/coevolution.mjs +245 -0
  28. package/sim/cold_start.md +13 -0
  29. package/sim/constants.mjs +44 -0
  30. package/sim/discovery.mjs +259 -0
  31. package/sim/duration.mjs +64 -0
  32. package/sim/graph_adj.mjs +28 -0
  33. package/sim/integrity.mjs +58 -0
  34. package/sim/metrics.mjs +423 -0
  35. package/sim/model.mjs +1176 -0
  36. package/sim/optimizer.mjs +250 -0
  37. package/sim/propagation.mjs +57 -0
  38. package/sim/report.mjs +176 -0
  39. package/sim/reputation_social.tunables.json +6 -0
  40. package/sim/rng.mjs +62 -0
  41. package/sim/scenarios.mjs +345 -0
  42. package/sim/sim_pool.mjs +169 -0
  43. package/sim/sim_worker.mjs +31 -0
  44. package/sim/social_reputation.mjs +30 -0
  45. package/sim/space.mjs +312 -0
  46. package/sim/test/admission.test.mjs +25 -0
  47. package/sim/test/attack_space.test.mjs +39 -0
  48. package/sim/test/behavior_vector.test.mjs +37 -0
  49. package/sim/test/coevolution.test.mjs +32 -0
  50. package/sim/test/cold_start.test.mjs +46 -0
  51. package/sim/test/defense.test.mjs +320 -0
  52. package/sim/test/discovery.test.mjs +68 -0
  53. package/sim/test/duration.test.mjs +20 -0
  54. package/sim/test/fidelity.test.mjs +151 -0
  55. package/sim/test/integrity.test.mjs +56 -0
  56. package/sim/test/optimizer.test.mjs +24 -0
  57. package/sim/test/propagation.test.mjs +29 -0
  58. package/sim/test/sim_model_regressions.test.mjs +110 -0
  59. package/sim/test/slow_drip.test.mjs +28 -0
  60. package/sim/test/smoke.test.mjs +52 -0
  61. package/sim/test/transport.test.mjs +55 -0
  62. package/sim/test/tunables_apply.test.mjs +64 -0
  63. package/sim/test/tunables_param_space.test.mjs +74 -0
  64. package/sim/test/vulnerability.test.mjs +127 -0
  65. package/sim/transport.mjs +135 -0
  66. package/sim/tunables_bundle.mjs +71 -0
  67. package/sim/vulnerability.mjs +532 -0
  68. package/transport/link_registry.mjs +7 -0
  69. package/transport/runtime_bootstrap.mjs +120 -103
  70. package/deno.json +0 -7
@@ -4,25 +4,64 @@ import process from 'node:process'
4
4
 
5
5
  /**
6
6
  * @typedef {{
7
- * relayOverride: string[] | null
8
- * iceLocalHostnamePolicy: IceLocalHostnamePolicy
9
- * trickleIceOff: boolean
7
+ * channels: ChannelsConfig
10
8
  * }} SignalingRuntimeConfig
11
9
  */
12
10
 
13
- const ICE_LOCAL_HOSTNAME_POLICIES = new Set(['none', 'rewrite-loopback', 'drop'])
11
+ /**
12
+ * @typedef {Record<string, object | boolean>} ChannelsConfig
13
+ */
14
14
 
15
15
  /**
16
- * 生产默认:win32 丢弃 `.local` host candidate;其它平台不过滤。
17
- * @returns {SignalingRuntimeConfig} 默认信令运行时配置
16
+ * webrtc 通道默认配置。
17
+ * @returns {{ iceLocalHostnamePolicy: IceLocalHostnamePolicy, trickleIceOff: boolean }} webrtc 通道默认配置
18
18
  */
19
- export function defaultSignalingRuntimeConfig() {
19
+ function defaultWebRtcConfig() {
20
20
  const iceLocalHostnamePolicy = process.platform === 'win32' ? 'drop' : 'none'
21
- return {
22
- relayOverride: null,
23
- iceLocalHostnamePolicy,
24
- trickleIceOff: iceLocalHostnamePolicy !== 'none',
21
+ return { iceLocalHostnamePolicy, trickleIceOff: iceLocalHostnamePolicy !== 'none' }
22
+ }
23
+
24
+ /** 各介质的默认 channel 配置;对象值是启用并覆盖默认。 */
25
+ const DEFAULT_CHANNEL_CONFIG = {
26
+ nostr: true,
27
+ lan: true,
28
+ bt: true,
29
+ webrtc: defaultWebRtcConfig(),
30
+ }
31
+
32
+ /**
33
+ * 归一到完整 channels 记录:false 禁用,其余(undefined / true / object)启用并合并默认配置。
34
+ * 未提及的通道保持默认启用。
35
+ * @param {object | undefined} [raw] 原始 channels 配置
36
+ * @returns {ChannelsConfig} 每个已知通道都有明确值(object | false)的完整记录
37
+ */
38
+ function resolveChannels(raw = {}) {
39
+ /** @type {ChannelsConfig} */
40
+ const out = {}
41
+ for (const name of Object.keys(DEFAULT_CHANNEL_CONFIG)) {
42
+ const value = raw[name]
43
+ out[name] = value !== false && { ...Object(DEFAULT_CHANNEL_CONFIG[name]), ...Object(value) }
25
44
  }
45
+ return out
46
+ }
47
+
48
+ /**
49
+ * 生成禁用全部通道的 channels 配置;`overrides` 可重新启用/覆盖指定通道。
50
+ * @param {Record<string, object | boolean>} [overrides] 需覆盖的通道配置
51
+ * @returns {ChannelsConfig} 除覆盖外全部 false 的 channels 配置
52
+ */
53
+ export function disableAllChannels(overrides = {}) {
54
+ /** @type {ChannelsConfig} */
55
+ const disabledChannels = {}
56
+ for (const name of Object.keys(DEFAULT_CHANNEL_CONFIG)) disabledChannels[name] = false
57
+ return { ...disabledChannels, ...overrides }
58
+ }
59
+
60
+ /**
61
+ * @returns {SignalingRuntimeConfig} 默认信令运行时配置
62
+ */
63
+ export function defaultSignalingRuntimeConfig() {
64
+ return { channels: resolveChannels() }
26
65
  }
27
66
 
28
67
  /**
@@ -30,22 +69,6 @@ export function defaultSignalingRuntimeConfig() {
30
69
  * @returns {SignalingRuntimeConfig} 合并后的信令运行时配置
31
70
  */
32
71
  export function resolveSignalingRuntimeConfig(patch = {}) {
33
- const base = defaultSignalingRuntimeConfig()
34
- if (!patch || typeof patch !== 'object') return base
35
- const policyRaw = patch.iceLocalHostnamePolicy
36
- const iceLocalHostnamePolicy = ICE_LOCAL_HOSTNAME_POLICIES.has(/** @type {string} */ policyRaw)
37
- ? /** @type {IceLocalHostnamePolicy} */ policyRaw
38
- : base.iceLocalHostnamePolicy
39
- let { relayOverride } = base
40
- if (Object.prototype.hasOwnProperty.call(patch, 'relayOverride'))
41
- relayOverride = patch.relayOverride == null
42
- ? null
43
- : [...new Set((Array.isArray(patch.relayOverride) ? patch.relayOverride : [])
44
- .map(url => String(url || ''))
45
- .filter(url => url.startsWith('wss://')))]
46
- return {
47
- relayOverride,
48
- iceLocalHostnamePolicy,
49
- trickleIceOff: patch.trickleIceOff !== undefined ? !!patch.trickleIceOff : iceLocalHostnamePolicy !== 'none',
50
- }
72
+ if (!patch || typeof patch !== 'object') return defaultSignalingRuntimeConfig()
73
+ return { channels: resolveChannels(patch.channels) }
51
74
  }
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@steve02081504/fount-p2p",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
5
5
  "keywords": [
6
6
  "network",
7
7
  "p2p",
8
8
  "fount",
9
- "AI"
9
+ "AI",
10
+ "nostr",
11
+ "LAN",
12
+ "bluetooth"
10
13
  ],
11
14
  "type": "module",
12
15
  "repository": {
@@ -23,13 +26,6 @@
23
26
  "test:live": "node --test --test-concurrency=1 --test-force-exit test/live/*.test.mjs",
24
27
  "test:sim": "node --test --test-concurrency=1 --test-force-exit sim/test/*.test.mjs"
25
28
  },
26
- "files": [
27
- "**/*.mjs",
28
- "**/*.json",
29
- "!test/**",
30
- "!sim/**",
31
- "!scripts/**"
32
- ],
33
29
  "bin": {
34
30
  "fount-p2p": "infra/cli.mjs"
35
31
  },
@@ -82,7 +78,7 @@
82
78
  "optionalDependencies": {
83
79
  "@stoprocent/bleno": "latest",
84
80
  "@stoprocent/noble": "latest",
85
- "node-datachannel": "latest"
81
+ "node-datachannel": "0.33.0"
86
82
  },
87
83
  "allowScripts": {
88
84
  "node-datachannel": true
package/sim/AGENTS.md ADDED
@@ -0,0 +1,29 @@
1
+ ---
2
+ description: P2P tunables simulation / co-evolution harness — fidelity boundary between reused real logic and heuristic proxy environment
3
+ globs: sim/**
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # P2P Sim Harness Guide
8
+
9
+ In-process co-evolution of **tunables** (`*.tunables.json`) against an **attack genome**, scored by `metrics.mjs`. Search proxy — **not** wire-protocol replay.
10
+
11
+ ## Fidelity boundary
12
+
13
+ - **Reused verbatim** (import real decision functions; do not re-model): `reputation/engine.mjs`, `reputation/math.mjs`, `sim/social_reputation.mjs` (`*Pure`), `trust_graph/engine.mjs` (`pickTop`), `trust_graph/resolve.mjs`, `governance/join_pow.mjs`.
14
+ - **Tunables source:** `tunables_bundle.mjs` loads in-package `*.tunables.json` plus `sim/reputation_social.tunables.json`. Write social tunables back via `cli.mjs mine --social-tunables PATH` (`apply.mjs`).
15
+ - **`PARAM_SPACE` ↔ defaults:** every `PARAM_SPACE` key must exist in `loadDefaultTunables()`; clear both sides when deleting a key.
16
+ - **`socialRepHideThreshold`:** hide when `score < threshold`. Default `0` (suppress negatives only). Raise the threshold to raise `falsePositiveRate` — never use a negative threshold for "stricter".
17
+ - **Heuristic proxy** (not the real path): `model.mjs`, `discovery.mjs`, `transport.mjs`, `integrity.mjs` — analytical "params → defense" only.
18
+ - **Mesh-first:** explore slots and cold-start join are mandatory; fanout/trust stay separate from link presence. Policy: [docs/mesh.md](../docs/mesh.md). Wiring: [cold_start.md](cold_start.md).
19
+
20
+ ## Anti-drift
21
+
22
+ - Do not hand-copy runtime constants. RTC budget from `transport/rtc_connection_budget.mjs`. `EXPLORE_MAX_PER_SOURCE` mirrors `peer_pool.mjs` but stays local (importing `peer_pool` pulls fs) — `test/fidelity.test.mjs` asserts equality.
23
+ - Signaling source names (`DEFAULT_SIGNALING_SOURCES`) must be real provider ids (`lan` / `nostr` / `bt`). No `tracker` provider.
24
+ - New sim constants that shadow real ones need a matching assertion in `fidelity.test.mjs`.
25
+
26
+ ## Determinism
27
+
28
+ - Seeded via `rng.mjs` (`createRng`). `runSimulation(...)` must be pure — `fidelity.test.mjs` asserts serial == parallel == batched snapshots. Use `simulationContext.now` (virtual clock, +60s/round); no wall-clock or unseeded RNG.
29
+ - Round state object is `simulationContext` (not `ctx`); `buildWorld` returns `{ simulationContext }`.
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 入群 PoW 成本模型:算力预算限速 Sybil/洗白造号。
3
+ */
4
+ import { expectedJoinPowHashes, powVoluntaryBonus } from '../governance/join_pow.mjs'
5
+
6
+ /** 每回合可消耗的期望哈希预算(相对 2^16 基准) */
7
+ const HASH_BUDGET_PER_ROUND = 65536
8
+
9
+ /**
10
+ * @param {import('./tunables_bundle.mjs').TunablesBundle['admission']} admission admission tunables
11
+ * @returns {number} 准入 floor bit
12
+ */
13
+ export function resolvePowFloorBits(admission) {
14
+ return Math.max(1, Math.floor(Number(admission?.powFloorBits ?? admission?.powDifficultyBits ?? 18)))
15
+ }
16
+
17
+ /**
18
+ * @param {number} achievedBits 解达成 bit
19
+ * @param {import('./tunables_bundle.mjs').TunablesBundle['admission']} admission admission tunables
20
+ * @returns {number} 自愿封顶信誉加成
21
+ */
22
+ export function simPowVoluntaryBonus(achievedBits, admission) {
23
+ return powVoluntaryBonus(achievedBits, resolvePowFloorBits(admission), admission)
24
+ }
25
+
26
+ /**
27
+ * @param {number} difficultyBits PoW floor bit
28
+ * @returns {number} 造一个新身份期望回合成本
29
+ */
30
+ export function roundsPerIdentity(difficultyBits) {
31
+ const expected = expectedJoinPowHashes(difficultyBits)
32
+ return Math.max(1, expected / HASH_BUDGET_PER_ROUND)
33
+ }
34
+
35
+ /**
36
+ * @param {number} requested 请求数量
37
+ * @param {number} difficultyBits PoW 难度
38
+ * @param {number} rounds 仿真总回合
39
+ * @returns {number} 预算内可生成的恶意身份数
40
+ */
41
+ export function capMaliciousByPowBudget(requested, difficultyBits, rounds) {
42
+ const cost = roundsPerIdentity(difficultyBits)
43
+ const budget = Math.max(1, Math.floor(rounds / cost))
44
+ return Math.min(requested, budget)
45
+ }
46
+
47
+ /**
48
+ * @param {number} difficultyBits PoW 难度
49
+ * @param {number} rounds 回合数
50
+ * @returns {number} 诚实新人入群延迟惩罚 0..1(越高=越慢)
51
+ */
52
+ export function honestJoinDelayPenalty(difficultyBits, rounds) {
53
+ const cost = roundsPerIdentity(difficultyBits)
54
+ const delay = cost / Math.max(1, rounds * 0.25)
55
+ return Math.min(1, delay)
56
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * 仿真器中的本地可逆异常隔离(映射生产 reputation_engine 纯函数)。
3
+ */
4
+ import {
5
+ isQuarantinedPure,
6
+ observeBehaviorSamplePure,
7
+ } from '../reputation/engine.mjs'
8
+
9
+ /**
10
+ * @param {import('../node/reputation_store.mjs').ReputationFile} reputation 观察者信誉表
11
+ * @param {string} peerId 对端 nodeHash
12
+ * @param {number} sample 行为样本
13
+ * @param {number} now 当前时间
14
+ * @param {import('../reputation/tunables.json')} tunables reputation tunables
15
+ * @returns {boolean} 是否触发隔离
16
+ */
17
+ export function simObservePeerBehavior(reputation, peerId, sample, now, tunables) {
18
+ return observeBehaviorSamplePure(reputation, peerId, sample, now, tunables).anomaly
19
+ }
20
+
21
+ /**
22
+ * @param {import('../node/reputation_store.mjs').ReputationFile} reputation 观察者信誉表
23
+ * @param {string} peerId 对端 nodeHash
24
+ * @param {number} now 当前时间
25
+ * @returns {boolean} 是否隔离中
26
+ */
27
+ export function simIsPeerQuarantined(reputation, peerId, now) {
28
+ return isQuarantinedPure(reputation, peerId, now)
29
+ }
30
+
31
+ /**
32
+ * @param {number} discoveryReach 发现可达 0..1
33
+ * @param {() => number} rng 随机源
34
+ * @returns {boolean} 攻击是否触达该观察者
35
+ */
36
+ export function attackReachesObserver(discoveryReach, rng) {
37
+ const reach = Math.max(0, Math.min(1, discoveryReach))
38
+ return rng() < 0.15 + reach * 0.85
39
+ }
package/sim/apply.mjs ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * 按模块写回 tunables JSON。
3
+ */
4
+ import { writeFile } from 'node:fs/promises'
5
+ import path from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ import { normalizeBundle, sanitizeBundle } from './space.mjs'
9
+
10
+ /** @typedef {import('./tunables_bundle.mjs').TunablesBundle} TunablesBundle */
11
+
12
+ /** @type {Record<Exclude<keyof TunablesBundle, 'social'>, URL>} */
13
+ const PACKAGE_MODULE_URLS = {
14
+ reputation: new URL('../reputation/tunables.json', import.meta.url),
15
+ trustGraph: new URL('../trust_graph/tunables.json', import.meta.url),
16
+ mailbox: new URL('../mailbox/tunables.json', import.meta.url),
17
+ archive: new URL('../dag/tunables.json', import.meta.url),
18
+ admission: new URL('../governance/tunables.json', import.meta.url),
19
+ }
20
+
21
+ /**
22
+ * @param {TunablesBundle} bundle 完整 tunables
23
+ * @returns {TunablesBundle} 写盘前规整后的 bundle
24
+ */
25
+ export function prepareBundleForApply(bundle) {
26
+ return sanitizeBundle(normalizeBundle(bundle))
27
+ }
28
+
29
+ /**
30
+ * @param {string} socialTunablesPath 外部 social shell 的 reputation_social.tunables.json 绝对或相对路径
31
+ * @returns {string} 规范化绝对路径
32
+ */
33
+ export function resolveSocialTunablesPath(socialTunablesPath) {
34
+ const raw = String(socialTunablesPath ?? '').trim()
35
+ if (!raw) throw new Error('socialTunablesPath required to write social tunables')
36
+ return path.resolve(raw)
37
+ }
38
+
39
+ /**
40
+ * @param {keyof TunablesBundle} module 模块键
41
+ * @param {Record<string, unknown>} data JSON 内容
42
+ * @param {string} [socialTunablesPath] social 模块写入路径(module === 'social' 时必填)
43
+ * @returns {Promise<string>} 写入路径
44
+ */
45
+ export async function writeModuleTunables(module, data, socialTunablesPath) {
46
+ const filePath = module === 'social'
47
+ ? resolveSocialTunablesPath(socialTunablesPath)
48
+ : fileURLToPath(PACKAGE_MODULE_URLS[module])
49
+ await writeFile(filePath, `${JSON.stringify(data, null, '\t')}\n`, 'utf8')
50
+ return filePath
51
+ }
52
+
53
+ /**
54
+ * @param {TunablesBundle} bundle 完整 tunables
55
+ * @param {{ socialTunablesPath: string }} paths 外部模块路径(social 写回 shell 侧 JSON)
56
+ * @returns {Promise<string[]>} 各模块 JSON 写入路径
57
+ */
58
+ export async function applyTunablesBundle(bundle, paths) {
59
+ const socialPath = resolveSocialTunablesPath(paths?.socialTunablesPath)
60
+ const ready = prepareBundleForApply(bundle)
61
+ const written = []
62
+ written.push(await writeModuleTunables('reputation', ready.reputation))
63
+ written.push(await writeModuleTunables('trustGraph', ready.trustGraph))
64
+ written.push(await writeModuleTunables('social', ready.social, socialPath))
65
+ written.push(await writeModuleTunables('mailbox', ready.mailbox))
66
+ written.push(await writeModuleTunables('archive', ready.archive))
67
+ written.push(await writeModuleTunables('admission', ready.admission))
68
+ return written
69
+ }
70
+
71
+ /**
72
+ * @param {string} [dir] sim 目录(默认本模块目录)
73
+ * @returns {string} results 子目录绝对路径
74
+ */
75
+ export function resultsDirFromSim(dir = path.dirname(fileURLToPath(import.meta.url))) {
76
+ return path.join(dir, 'results')
77
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * 攻击者可进化基因(与 space.mjs 采样器共用 latent 语义)。
3
+ */
4
+ import { createRng } from './rng.mjs'
5
+ import { normalizeParam, sampleParam } from './space.mjs'
6
+ import { loadDefaultTunables } from './tunables_bundle.mjs'
7
+
8
+ /** @typedef {import('./attacks.mjs').AttackKind} AttackKind */
9
+
10
+ /**
11
+ * @typedef {{
12
+ * global: { activationRate: number, burstSize: number, targetBiasHighRep: number, eclipseFocus: number },
13
+ * byAttack: Partial<Record<AttackKind, { activationRate?: number, burstSize?: number, collusionAllyRate?: number, sleeperTurnFrac?: number, hintWeightMul?: number }>>,
14
+ * }} AttackGenome
15
+ */
16
+
17
+ /** @type {import('./space.mjs').ParamSpec[]} */
18
+ const GLOBAL_ATTACK_SPECS = [
19
+ { module: 'reputation', key: 'relayRepBump', kind: 'unit' }, // reuse kind; values remapped below
20
+ ]
21
+
22
+ const DEFAULT_GENOME = Object.freeze({
23
+ global: {
24
+ activationRate: 0.55,
25
+ burstSize: 4,
26
+ targetBiasHighRep: 0.35,
27
+ eclipseFocus: 0.65,
28
+ },
29
+ byAttack: {},
30
+ })
31
+
32
+ /**
33
+ * @param {() => number} rng 随机源
34
+ * @returns {AttackGenome} 随机攻击基因组
35
+ */
36
+ export function randomAttackGenome(rng) {
37
+ return {
38
+ global: {
39
+ activationRate: 0.2 + rng() * 0.75,
40
+ burstSize: 1 + Math.floor(rng() * 8),
41
+ targetBiasHighRep: rng() * 0.9,
42
+ eclipseFocus: 0.3 + rng() * 0.65,
43
+ },
44
+ byAttack: {},
45
+ }
46
+ }
47
+
48
+ /**
49
+ * @param {AttackGenome} parent 父代
50
+ * @param {number} seed 种子
51
+ * @returns {AttackGenome} 变异子代
52
+ */
53
+ export function mutateAttackGenome(parent, seed) {
54
+ const rng = createRng(seed)
55
+ const child = structuredClone(parent)
56
+ if (rng() < 0.4) child.global.activationRate = Math.max(0.05, Math.min(0.98, child.global.activationRate + (rng() - 0.5) * 0.2))
57
+ if (rng() < 0.35) child.global.burstSize = Math.max(1, Math.min(12, child.global.burstSize + Math.floor((rng() - 0.5) * 4)))
58
+ if (rng() < 0.3) child.global.targetBiasHighRep = Math.max(0, Math.min(1, child.global.targetBiasHighRep + (rng() - 0.5) * 0.25))
59
+ if (rng() < 0.3) child.global.eclipseFocus = Math.max(0.1, Math.min(1, child.global.eclipseFocus + (rng() - 0.5) * 0.25))
60
+ return child
61
+ }
62
+
63
+ /**
64
+ * @param {AttackKind} attack 攻击类型
65
+ * @param {AttackGenome} genome 基因组
66
+ * @returns {{ activationRate: number, burstSize: number, targetBiasHighRep: number, eclipseFocus: number, collusionAllyRate: number, sleeperTurnFrac: number, hintWeightMul: number }} 有效参数
67
+ */
68
+ export function resolveAttackParams(attack, genome) {
69
+ const g = genome.global
70
+ const row = genome.byAttack[attack] ?? {}
71
+ return {
72
+ activationRate: row.activationRate ?? g.activationRate,
73
+ burstSize: Math.max(1, Math.floor(row.burstSize ?? g.burstSize)),
74
+ targetBiasHighRep: g.targetBiasHighRep,
75
+ eclipseFocus: g.eclipseFocus,
76
+ collusionAllyRate: row.collusionAllyRate ?? 0.25,
77
+ sleeperTurnFrac: row.sleeperTurnFrac ?? 0.35,
78
+ hintWeightMul: row.hintWeightMul ?? 3,
79
+ }
80
+ }
81
+
82
+ /**
83
+ * @param {AttackGenome | undefined} genome 基因组
84
+ * @returns {AttackGenome} 默认补齐
85
+ */
86
+ export function normalizeAttackGenome(genome) {
87
+ if (!genome) return structuredClone(DEFAULT_GENOME)
88
+ return {
89
+ global: { ...DEFAULT_GENOME.global, ...genome.global },
90
+ byAttack: { ...genome.byAttack },
91
+ }
92
+ }
93
+
94
+ /** 名人堂容量 */
95
+ export const ATTACK_HOF_SIZE = 6
96
+
97
+ /**
98
+ * @param {Array<{ genome: AttackGenome, fitness: number }>} hall 名人堂
99
+ * @param {AttackGenome} genome 候选
100
+ * @param {number} fitness 适应度
101
+ * @returns {Array<{ genome: AttackGenome, fitness: number }>} 更新后名人堂
102
+ */
103
+ export function updateAttackHallOfFame(hall, genome, fitness) {
104
+ const next = [...hall, { genome: structuredClone(genome), fitness }]
105
+ next.sort((a, b) => b.fitness - a.fitness)
106
+ return next.slice(0, ATTACK_HOF_SIZE)
107
+ }
108
+
109
+ // silence unused import warning from eslint if any
110
+ void GLOBAL_ATTACK_SPECS
111
+ void loadDefaultTunables
112
+ void normalizeParam
113
+ void sampleParam