feedback7 dober: a boss field's perfect-payout card is paid only where the client's result agrees with its own b.progress (b.progress.leaksBy splits the field's LP per player); golden: unchanged

Seen (review of this branch): the Final Assault / Hidden Core path trusts a client's b.result — validateClientResult skips
the leak multiset for boss fields and nothing re-simulates — while the team LP a field costs arrives through b.progress.leaks.
Commit 05eb2b80 paid the perfect-payout cards from the result's leaked list, so a modified authority could have the server
charge a leak and then report two empty lists (LP 58 -> 57 charged, both seats +2: FORGE_BOTH_EMPTY) or pin the leak on the
partner's list (the forger +2, the partner 0: FORGE_PAY).
Now: b.progress carries `leaksBy` (shared/protocol.js, optional; spec.js battleProgress for a boss / hidden field): per
player, the LP of the leak entries the result will show (compactResult's list, lpr clamped as on the wire); `leaks` minus its
sum is the leader's own "扣除目标生命" effects (boss "Doom" / 斥退 / 萨米 Doom), which are no leak and must not cost a perfect
seat its card. The browser runner and the test SimClient send it; the server keeps the highest value per player (f.leaksBy).
Match._bossLeaksAgree (called by _finishFinal) pays a seat of a client-reported field only when (a) the LP the authority
reported was split at all (leaksBy seen whenever lpReported > 0 — a report without the split explains nothing) and (b) the
seat's leaked list shows at least the leak LP reported for it. A server-run field (no client, a takeover) is the truth itself.
Not stopped, and said so in the docs: an authority that lies consistently (a split that blames the partner, a leak labelled as
the leader's effect) — the boss path has no second source; what is caught is a result that contradicts its own progress.
Tests (test first; the three forge tests failed on the parent with [2, 2] / [2, 0] paid, the runner test on the missing field):
clientCombat.test.js — charged leak + two empty lists -> 0 / 0; the leak moved to the partner -> 0 / 0 (with and without a
split); honest split -> the clean seat paid, the leaker not, a leader's effect LP costs nobody; two honest SimClients end to
end; the protocol field; runner.test.js — the browser runner's boss progress carries leaksBy; docs-consistency pins the
b.progress signature. Grok's two repro scripts now print +0 / +0.
Golden: unchanged (matches 18 / full 283 scenarios match; every golden boss field is server-run).
This commit is contained in:
sganggs committed 2026-10-08 22:01:26 +08:00
1 parent e63214571d
commit 66a228c64c
12 files changed
+171 -11

No files matched your search

+5 -2
View File
@@ -382,13 +382,16 @@ export const C2S = {
// client-side combat (DESIGN §14): the authoritative client of a field reports its battle; a 联防 field adds
// `left` = { [leakerId]: its enemies still standing (unspawned, alive, or through again) } (server/sim/spec.js
// uniteLeft; user playtest #6 item 7 — the leakers' live counter)
// uniteLeft; user playtest #6 item 7 — the leakers' live counter); a boss / hidden field adds `leaksBy` = { [playerId]:
// the cumulative LP the enemies that reached that player's goal cost } — `leaks` minus its sum is the leader's own
// "扣除目标生命" effects; the server holds the b.result's leaked lists to it (Match._bossLeaksAgree)
'b.progress': {
battleId: isId, gt: (v) => isNum(v, 0, 1e5), killed: (v) => isInt(v, 0, 1e5), total: (v) => isInt(v, 0, 1e5),
leaks: (v) => isNum(v, 0, 1e6), bossDmg: (v) => isNum(v, 0, BIG),
by: (v) => isMap(v, RESULT_LIMITS.players, isId, (x) => isNum(x, 0, BIG)), done: isBool,
left: (v) => isMap(v, RESULT_LIMITS.players, isId, (x) => isInt(x, 0, 1e5)),
$optional: ['leaks', 'bossDmg', 'by', 'done', 'left'],
leaksBy: (v) => isMap(v, RESULT_LIMITS.players, isId, (x) => isNum(x, 0, 1e6)),
$optional: ['leaks', 'bossDmg', 'by', 'done', 'left', 'leaksBy'],
},
'b.result': { battleId: isId, result: isBattleResult },
};