Online co-op browser remake of Arknights' Stronghold Protocol: Alliance (client-simulated combat, official waves/bonds/items/strategies, 3D board, facing wheel, operator loadout, official emotes, deployment scripts).
20 KiB
BALANCE.md — difficulty model and measurement (official numbers, no custom tuning)
Owner: match / balance. Tools: tools/balance.mjs (competent-board model — now a measuring tool only),
tools/matchrun.mjs (bot matches). Tests: test/match/balance.test.js, test/match/waves-official.test.js,
test/match/waves.test.js, test/sim/pathing.test.js.
Every enemy number is the official one; the custom balance layer was removed (user request: rules, numbers, enemy kinds / counts and routes must follow the official game; research 08 §6–§7, DESIGN §14 corrections):
- waves = the client's
RandomEnemyGenerater(research 08 §2): a 15-slot type schedule per match (3 types × 3 slots + 6 SPECIAL, shuffled), one weighted special entry per round, each placeholder ACTION replaced byclamp(roundHalfEven(n·P(t)/P(new)), 1, 5)units over the same window, the other movement class not spawned — nokcopies, no kept fly placeholders; verified against all 429 official entry × round compositions and the official count distribution (同盟 险境 R3 ≤ 10 enemies, R13 ≈ 37 — the old generator averaged 28 / 81); - stats = the PRTS per-round
enemyScaletable only (data/config.json, 终极 ×1.15 speed from R3); - leader pool =
bloodPoint[difficulty](co-op; one pool for the split leaders, no alive-player factor — solo keeps ×0.25, flagged [ASSUMED] in data); - bounties: no extra ×0.7 in solo (the 70 % base is already in
enemyScale), spawned with the template's first normal action (research 08 §5); - routes = the official 4-direction flow field with crates at cost 1000 (research 08 §3), act1 m02 without crates.
Removed: every enemyHpMul / enemyAtkMul / enemySpeedMul / bossHpMul of data/tuning.json (it now holds only
the titles.comment_3 rule) and the flyPlaceholders knob. gamedata.js ignores those keys if an old file carries them.
Measured with the official numbers (node tools/balance.mjs --mode all --difficulty ALL --tuning off, 2026-09-28):
average capped leaks per board and round R1–R13 (solo 标准 R1–R8) of the competent board of §1.1 —
| mode | old generator, research numbers | old generator, tuned (shipped before) | official (now) | LP/rd after 联防 |
|---|---|---|---|---|
| 独立 标准 | 1.3 | 0.6 | 0.17 | (= leaks) |
| 独立 险境 | 1.4 | 0.9 | 0.19 | (= leaks) |
| 独立 绝境 | 1.8 | 1.8 | 0.22 | (= leaks) |
| 独立 终极 | 4.1 | 3.1 | 0.65 | (= leaks) |
| 同盟 标准 | 1.4 | 0.5 | 0.31 | 0.13 |
| 同盟 险境 | 2.3 | 1.3 | 0.49 | 0.01 |
| 同盟 绝境 | 5.0 | 2.7 | 0.80 | 0.10 |
| 同盟 终极 | 6.3 | 4.3 | 1.96 | 0.93 |
The old generator spawned 2–4× the official enemy count (every template enemy × k, plus the other class's placeholders); the tuning of the previous pass only compensated for that. With the official counts the difficulty comes out below the old targets — which is the official game's difficulty as far as this model can tell (the model is a competent, layer-rich board; see §1.2 for what it does not capture). Nothing is tuned from these numbers.
1. Method — tools/balance.mjs
For every mode (独立 solo / 同盟 multi) × difficulty × round the tool samples whole matches (stage, the 3 factions, bans,
boss, band and lineups all vary with the seed), builds a competent board for that round, generates the round's
real wave (waves.js: template, the official per-action replacement of the round's pick, enemy scaling, bounties off)
and runs the real
Battle with full content (kits, talents, bonds with layers, IN_BATTLE 特质, equipment, bands, summons). Co-op rounds
field 4 boards against the same wave and then run 联防 exactly like the match (unite.js: ≤ 2 perfect helpers,
only the survivors cost their source LP). Boss rounds build the Final Assault fields exactly like Match.startFinalAssault
(pairs / _s for a lone player, shared pool GameData.bossPoolHp = bloodPoint (solo ×0.25), merged team LP of 15 per player, leaks' lpr,
−1 LP/s after 150 s) and record the pool damage by 150 s, the kill time and the win rate.
Metrics: capped leaks = min(leaks, 10) per board and round (what that board alone would lose; the targets below use it — raw means are dominated by swarm rounds that cap at 10 anyway), LP/rd after 联防 (co-op), perfect-round share, the enemies that leak most, clear time; bosses: share of the pool dealt by 150 s, win rate, kill time.
1.1 The competent board of round r
| R | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| shop level | 1 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 4 | 5 | 5 | 6 | 6 | 6 | 6 |
| units | 2 | 4 | 5 | 7 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 |
| elites (精锐) | 0 | 0 | 0 | 0.5 | 1 | 1.5 | 2 | 3 | 3.5 | 4 | 4.5 | 5 | 6 | 6.5 | 7 |
| equipment | 0 | 0.5 | 1 | 1.5 | 2 | 3 | 3.5 | 4 | 5 | 5.5 | 6 | 7 | 8 | 9 | 9 |
| core bond members | – | – | 3 | 3 | 3 | 3 | 3 | 3 | 4 | 5 | 6 | 6 | 6 | 6 | 6 |
| core bond layers | 0 | 2 | 6 | 12 | 20 | 28 | 38 | 50 | 65 | 80 | 95 | 110 | 130 | 150 | 170 |
| add-on layers (each) | 0 | 1 | 4 | 8 | 12 | 17 | 23 | 30 | 38 | 46 | 55 | 65 | 75 | 90 | 100 |
(fractional values: that many on average.) Sources: income 4, 5, 6 … min(3 + r, 12) and upgrade prices 5 / 8 / 11 /
12 / 13 with −1 per round (research 00-INDEX §3) → L2 at R3, L3 at R5, L4 at R7, L5 at R10, L6 at R12; the board is
full from R5; elites ≈ 0 at R3 → 3 at R8 → 6+ at R13; 1–2 add-on bonds; the layer pace reaches Σ active layers ≈ 330
at R14 — the solo Hidden Core threshold is > 350 (research 02 §2.3), i.e. these are boards of players who regularly
reach the Hidden Core. Units are drawn from the match's pool (bans respected) with a tier mix per shop level, around a
core bond with enough members, plus the roles a player reads from the wave preview: 2 blockers, anti-air by the share of
flyers, arts dealers by the armour of the enemies (arts anti-air when the flyers are armoured, e.g. 寒霜 DEF 600), a
medic from R5. Equipment: passive combat items of tier ≤ shop level (golden from R7) on the damage dealers. Layers: the
core curve on the core bond, the add-on curve on every other bond the lineup activates. Placement: the bot's layout
planner with rehearsal of its 5 variants (a player adapts the layout to the previewed wave). Not modeled: bounties, 机变
cards, prep-only effects (their value is folded into the curves). --profile weak|strong scales elites, items and
layers ×0.6 / ×1.4.
1.2 Reference levels (informative — nothing is tuned against them any more)
The previous pass tuned the waves toward: 标准 ≤ 1 capped leak per board and round, 险境 ≤ 2, 绝境 ≈ 2–3.5, 终极 ≈ 3–5. They are kept only to read the measurement. What the model does NOT capture (so real matches are harder than the table): imperfect economy and placement, bans hitting the planned bonds, 机变 / bounty choices (bounties add enemies), the preview being read by a human in 90 s, and losing LP early (fewer rounds to build layers).
2. What the numbers were checked against (research first)
| Suspect | Research | Verdict |
|---|---|---|
Enemy multipliers (config.modes[m].enemyScale, k exponents) |
A3 table (PRTS 下半, user-sourced) — 01-core-rules.md A3 / _criticAddendum.enemyStatMultipliers |
match exactly (every mode, every round incl. ABYSS HP k and ×1.08); kept as the base |
| Wave templates / counts | level data (maxPlayTime, spawns, routes) — DATA |
match the official levels; kept |
| Faction replacement | was: each template enemy × k [ASSUMED] |
replaced by the official per-action rule (research 08 §2.3, client code) |
| Fly placeholders without a FLY faction | was: kept (寒霜 / 暴鸰 / 妖怪, the #1 leak source) [ASSUMED] | official: the other movement class is not spawned (research 08 §2.3 + the PRTS screenshots) |
| Faction mixing | was: every round a ground entry + a FLY entry | official: one entry per round from a 15-slot type schedule |
MOVE_SCALE 0.5 |
every level's moveMultiplier 0.5 (05 §2.2) |
kept |
| Combat time limit unit | maxPlayTime read as game seconds [ASSUMED] |
wrong — real seconds; fixed (§2.1) |
| Boss pool | bloodPoint DATA; co-op "敌方领袖的总生命值不变" | co-op = bloodPoint, no alive scaling; solo ×0.25 kept [ASSUMED]; the solo 标准 ×0.6 removed |
| Solo bounties ×0.7 | 上半 11/18 note = the global solo base | removed (already in enemyScale) |
| Bounty spawn timing | was: the bounty units appended after the host action's own units | official: inserted among them at floor((i+1)·len/(n+1)), then the list spread over the window (client _InsertSpActionToNormal + _CalculateActionPredelay, decoded; waves.js withBounties) |
| 联防 spawn timing | was: owner k at +min(0.5·k, 5) s, each owner's units over the whole window [ASSUMED] | official (decoded _CalculateActionPredelayConsiderUid): owner k at +0.5·k s, unit step min(max(W/M, 0.05·W), 5 s), M = largest owner group — a lone leaker's units now come 5 s apart instead of up to 40 s |
| Leader / leader-part multipliers | was: leaders and their parts never scaled | official: the round multipliers are the ENEMY effects 攻坚装备 / II / III / 补给线 / 急行军 (enemy_attribute_mul; enemy_exclude = 炎佑 — plus the TIMES tokens for 补给线 — never a leader) ⇒ parts take HP/ATK/speed, the leader ATK/speed; only the leader's HP (the server pool) is exempt ("领袖单位于服务器的生命值加成不受上述加成影响"). Co-op 终极 R14: leader ATK ×2.14; solo 标准: ×0.7 |
| Ground routes | was: 8-dir A*, m02 crates active | official 4-dir SPFA + smoothing, crates cost 1000, m02 without crates (research 08 §3) |
| Income / upgrade curve | R1–R3 VERIFIED, +1/round cap 12 ASSUMED | kept (the model's level curve follows it) |
| Operator stats per tier | chess status T1 E1 L55 … T6 E2 L1, elites E2 + skill 7 (03) — DATA | faithful: power comes from layers, elites and skills, which is why the model carries them |
| Player power in the bots | bots fielded T1-heavy boards with 0–3 elites and 20–70 layers per bond at R13 | bot fixes (§5) |
2.1 Combat time limits are real seconds (fidelity fix)
maxPlayTime counts real seconds of the forced 2× battle. Read as game seconds (the old assumption) the rounds' own
spawn schedules do not fit: R2 spawns its last flyer at 43 s of a 45 s limit, R3 its last enemies at 60–62 s of a 55 s
limit (they could never be killed, or never even spawned). As real seconds (× 2 in game seconds) every limit is ≈ the
last spawn + one flyer crossing of the board (≈ 44 s serpentine at 0.45 tiles/s): R2 43 + 44 ≈ 90, R3 62 + 44 ≈ 110,
R5 38 + 67 ≈ 110 (暴鸰 at 0.3 tiles/s).
| R | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
maxPlayTime (real s) |
45 | 45 | 55 | 55 | 55 | 70 | 85 | 85 | 95 | 95 | 115 | 115 | 115 |
| last spawn (game s) | 15 | 43 | 62 | 39 | 38 | 42 | 56 | 67 | 75 | 59 | 68 | 60 | 73 |
| slack, old reading | 30 | 2 | −7 | 16 | 17 | 28 | 29 | 18 | 20 | 36 | 47 | 55 | 42 |
| slack, fixed (× 2) | 75 | 47 | 48 | 71 | 72 | 98 | 114 | 103 | 115 | 131 | 162 | 170 | 157 |
Before the fix 90–100 % of the early "leaks" were enemies still alive (often just spawned) at the limit, not enemies that
reached the objective. gd.combatTimeLimit(r) now returns game seconds (config.combatTimeScale, default 2); the
player-facing combat countdown is unchanged (= maxPlayTime real seconds). Research 00-INDEX §8 #10 should be updated
by its owner.
2.2 The fly placeholders (resolved)
The previous pass measured that the kept fly placeholders (寒霜 DEF 600, 暴鸰) were the most frequent leakers and left a
flyPlaceholders knob for experiments. The official client settles it: a placeholder whose class differs from the
round's pick is isValid = false — not spawned, not sent, not previewed (research 08 §2.3). The knob is gone.
3. Measurement per round (official numbers)
Capped leaks per board (16 solo samples / 6 co-op matches × 4 boards per round), --tuning off (identical: the
tuning file holds only titles), seed 1, rehearsal 5. Leader columns: win rate, share of the pool dealt by 150 s, mean
kill time (8 samples). Re-measured after the review fixes (decoded 联防 timing; leaders take the round ATK / speed
multipliers, their parts all of them): normal rounds unchanged, leader rounds and LP/rd as below.
| mode | R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8 | R9 | R10 | R11 | R12 | R13 | avg | LP/rd |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 独立 标准 | 0.13 | 0.06 | 0.00 | 0.13 | 0.63 | 0.00 | 0.25 | 0.19 | – | – | – | – | – | 0.17 | 0.17 |
| 独立 险境 | 0.00 | 0.00 | 0.00 | 0.31 | 0.00 | 0.00 | 0.63 | 0.19 | 0.06 | 0.00 | 0.00 | 0.63 | 0.63 | 0.19 | 0.19 |
| 独立 绝境 | 0.00 | 0.00 | 0.00 | 0.00 | 0.38 | 0.00 | 0.00 | 0.25 | 0.63 | 0.50 | 0.44 | 0.63 | 0.00 | 0.22 | 0.22 |
| 独立 终极 | 0.13 | 0.00 | 0.19 | 0.25 | 1.00 | 0.88 | 0.44 | 0.81 | 1.50 | 0.94 | 1.13 | 0.06 | 1.19 | 0.65 | 0.65 |
| 同盟 标准 | 0.00 | 0.54 | 0.00 | 0.00 | 1.13 | 0.00 | 1.25 | 0.13 | 0.88 | 0.04 | 0.00 | 0.04 | 0.00 | 0.31 | 0.13 |
| 同盟 险境 | 0.00 | 0.00 | 0.83 | 0.17 | 0.00 | 0.00 | 0.17 | 0.25 | 1.58 | 0.04 | 0.46 | 2.21 | 0.71 | 0.49 | 0.01 |
| 同盟 绝境 | 0.00 | 0.00 | 0.38 | 1.00 | 0.71 | 1.17 | 0.50 | 0.92 | 1.88 | 2.50 | 0.63 | 0.71 | 0.00 | 0.80 | 0.10 |
| 同盟 终极 | 0.00 | 0.04 | 0.21 | 0.92 | 0.83 | 2.79 | 4.00 | 6.00 | 3.83 | 1.29 | 3.71 | 0.83 | 0.96 | 1.96 | 0.93 |
| mode | 最终攻势 (R14; solo 标准 R9) | 隐秘核心 (R15) |
|---|---|---|
| 独立 标准 | pool 64k · win 50 % · 80 % by 150 s · 89.8 s | – |
| 独立 险境 | pool 169k · win 88 % · 97 % · 19.5 s | pool 237k · win 100 % · 100 % · 22.2 s |
| 独立 绝境 | pool 456k · win 63 % · 86 % · 57.0 s | pool 869k · win 50 % · 70 % · 71.1 s |
| 独立 终极 | pool 899k · win 38 % · 79 % · 42.0 s | pool 1.63M · win 25 % · 48 % · 44.9 s |
| 同盟 标准 | pool 224k · win 100 % · 100 % · 18.0 s | – |
| 同盟 险境 | pool 550k · win 100 % · 100 % · 22.0 s | pool 919k · win 100 % · 100 % · 37.8 s |
| 同盟 绝境 | pool 1.68M · win 88 % · 99 % · 32.6 s | pool 3.42M · win 88 % · 94 % · 100.9 s |
| 同盟 终极 | pool 3.70M · win 88 % · 95 % · 41.9 s | pool 6.08M · win 75 % · 87 % · 92.6 s |
Reading: the early game is easy everywhere (official R1–R3 bring 3–10 enemies); leaks concentrate on the second-half specials (深池逐火 TIMES embers, stealth 隐形弩手组长 / 重弩突袭者, 疯狂的逐腐兽, 掠海漂移体) and 终极 R6–R11, where co-op boards still leak 3–6 and 联防 halves the LP cost. Solo leaders (bloodPoint × 0.25 [ASSUMED]) are the hard part of the solo modes; the solo pool factor is the one open number here (research 08 §8 #2).
Reproduce: node tools/balance.mjs --mode all --difficulty ALL --tuning off [--bots 5] [--json].
4. The tuning — removed
The previous pass layered per-round enemy HP × s / ATK × √s (all 8 modes, e.g. 同盟 险境 R13 HP × 0.55) and the solo
标准 leader pool × 0.6 on top of the research numbers, to compensate for the old generator's 2–4× enemy counts. Research
08 decoded the official generator, so the compensation is gone together with its cause: data/tuning.json keeps only
titles.comment_3 (坚若磐石 = least LP lost), gamedata.js has no multiplier layer (enemyScale(r) = the config table,
bossHpMul() = 1, bossPoolHp() = the official pool). Difficulty questions are now answered by measuring (§3), not by
tuning. tools/balance.mjs --tuning remains accepted and has no effect on the numbers.
5. Bots (server/match/bot.js)
As found the bots lost every merge reward made while buying (the free pick-one offer queued by a merge expires at prep end; they only looked at offers when the prep opened), levelled late (L2 at R4 … L6 at R13), valued a T6 barely above a T1 and ignored the 特质 that produce layers — at R13 they fielded T1-heavy boards with 0–3 elites and 20–70 layers per bond (model: 6 elites, 130 / 75). Changes: offers are taken after every buy loop, the level curve is the competent one (early levels only with a full board), tier power 10 → 25 (was 19.5 for T6), recurring layer 特质 (every prep / refresh) and 获得时 layer / economy 特质 add value, and boss rounds are planned against the boss field (the leader counts as 10 tough enemies with a 30 s dwell on its first tiles, so stationary leaders such as 阿利斯泰尔 get hit).
| mode | as found (before the fixes*) | research + time fix | tuned | official waves (now, seeds 1–5) |
|---|---|---|---|---|
| 独立 标准 (1 AI) | 6.7 rounds · 3/10 wins · [5 5 9 9 5 7 6 8 4 9] | 8.1 rounds · 5/10 wins · [7 7 9 9 6 8 9 9 8 9] | 8.9 rounds · 9/10 wins · [9 9 9 9 8 9 9 9 9 9] | 8.8 · 4/5 · [9 9 9 9 8] |
| 独立 险境 (1 AI) | 6.5 rounds · 1/10 wins · [5 4 11 4 5 9 3 7 3 14] | 8.4 rounds · 1/10 wins · [7 6 13 6 8 11 6 8 5 14] | 10.1 rounds · 3/10 wins · [9 14 14 7 9 11 9 9 5 14] | 13.2 · 3/5 · [14 14 13 14 11] |
| 独立 绝境 (1 AI) | 6.9 rounds · 1/10 wins · [5 5 11 5 5 7 3 14 3 11] | 8.6 rounds · 1/10 wins · [7 5 12 8 13 8 6 9 4 14] | 8.6 rounds · 1/10 wins · [7 5 12 8 13 8 6 9 4 14] | 13.2 · 1/5 · [13 14 13 13 13] |
| 独立 终极 (1 AI) | 5.0 rounds · 0/10 wins · [4 4 7 3 3 7 3 7 3 9] | 5.9 rounds · 0/10 wins · [5 4 8 5 5 7 4 7 4 10] | 6.1 rounds · 0/10 wins · [5 4 10 5 4 7 4 7 4 11] | 11.4 · 0/5 · [13 8 13 11 12] |
| 同盟 标准 (4 AI) | 9.1 rounds · 2/10 wins · [9 4 14 11 9 11 6 8 5 14] | 11.6 rounds · 3/10 wins · [11 8 14 12 13 14 8 11 11 14] | 14.0 rounds · 10/10 wins · [14 14 14 14 14 14 14 14 14 14] | 14.0 · 5/5 · [14 14 14 14 14] |
| 同盟 险境 (4 AI) | 6.6 rounds · 0/10 wins · [9 4 8 7 8 6 5 6 4 9] | 9.3 rounds · 0/10 wins · [10 6 12 11 10 7 7 9 9 12] | 11.1 rounds · 2/10 wins · [11 8 14 11 12 7 13 10 11 14] | 14.0 · 5/5 · [14 14 14 14 14] |
| 同盟 绝境 (4 AI) | 4.9 rounds · 0/10 wins · [6 4 6 4 6 6 5 4 3 5] | 7.0 rounds · 0/10 wins · [7 5 8 7 7 7 6 8 5 10] | 8.9 rounds · 0/10 wins · [9 6 10 9 9 8 8 11 7 12] | 11.2 · 0/5 · [12 9 13 10 12] |
| 同盟 终极 (4 AI) | 5.2 rounds · 0/10 wins · [6 4 6 5 5 6 5 5 4 6] | 6.3 rounds · 0/10 wins · [6 5 7 6 6 7 6 6 5 9] | 7.2 rounds · 0/10 wins · [6 5 9 6 8 7 7 8 6 10] | 9.8 · 0/5 · [10 7 13 8 11] |
Official-waves column: node tools/balance.mjs --mode all --difficulty ALL --bots 5 (rounds passed avg · wins · per
seed), measured before the review's leader-multiplier / 联防-timing fixes (§2; not re-run); the other columns are the
previous pass (old generator), kept for history. * the bot code of this pass (§5) with the old time reading and no tuning; the originally reported bots (old bot code) survived ≈ 6.5 rounds on 险境 and won 1/20 on 标准.
6. Match follow-ups shipped with this pass
- Multi-round bounties ("之后的每场作战") also spawn in the Final Assault / Hidden Core, on the owner's half (route to its goal), are shown in the boss-round preview, and the boss battle uses up one of their battles (kill coins → pending funds for the Hidden Core prep).
- A 驰援 card is never offered when its bond has no chess left in the match's pool (every member banned).
- docs/META.md §2.6 example guards
ctx.source.kind === 'choice'(its EffectRef reuses the handler's key). - 坚若磐石 = least LP lost (
titles.comment_3 { stat: 'lpLost', rule: 'min' }in tuning.json; results.js supportsrule: 'min'among the players still alive). - 教鞭 / “神秘顾客” stay a random bounty, documented in docs/META.md §2.5.