feedback5 WK: 自选编队 on the server — room.diy { picks } (lenient: illegal picks dropped, only malformed input is BAD_MSG; stored per session / seat; a match takes the picks its seat had at the start, a change during a match is for the next one; welcome lists diyKitted), PlayerState.diy with the player's data view (a slotted DIY slot is its operator for every rule: bonds from its factions, no 特质, the pick's skill and module, the slot's tier / price / merge), a per-player stock per slotted piece (8 at tier 5, 5 at tier 6 [ASSUMED]; never in the shared pool; none when every bond of it is banned), DIY pieces in that player's shop rolls and reward offers once the 调度中心 reaches the slot's level, battleInput diy, m.private.diy / diyBanned, prep scouting and results name the operator, 信标 never sends a 自选 piece to a teammate [ASSUMED]
The owner's decisions of 2026-10-05 (picks, slots, shop per player, bans); research 0.2.0 §2. Golden unchanged (no scenario slots a DIY pick). Tests: test/match/diy-shop.test.js (9), test/lobby-diy.test.js (4).
This commit is contained in:
1 parent
742f478526
commit
4bee542e61
26 files changed
+1026
-105
No files matched your search
@@ -2,7 +2,9 @@
|
||||
// (test/match/harness.js checkInvariants asserts the list is empty) and by tools/matchrun.mjs --check sweeps.
|
||||
//
|
||||
// collectViolations(m) → string[] (empty when every invariant holds):
|
||||
// pool 0 ≤ left ≤ cap and left + Σ copies held by pieces == cap per base chess; non-pool chess hold 0 copies
|
||||
// pool 0 ≤ left ≤ cap and left + Σ copies held by pieces == cap per base chess; non-pool chess hold 0 copies;
|
||||
// a player's 自选 stock (0.2.0, player/diy.js) the same against its own pieces of each slotted slot — a DIY
|
||||
// piece is always a slotted slot of its owner's, and a DIY shop / reward card one of its stocked slots
|
||||
// economy funds / pendingFunds non-negative integers, LP finite, shop level in range, prices ≥ 0
|
||||
// pieces unique uids; hand 10 / temp 5 slots; temp holds pieces only while the hand is full (a free hand slot pulls
|
||||
// a temp piece in, PlayerState._fillHandFromTemp); chess carry ≤ equipPerChess known items; a normal piece
|
||||
@@ -47,6 +49,9 @@ export function collectViolations(m, { limit = 25 } = {}) {
|
||||
|
||||
for (const ps of m.players.values()) {
|
||||
const id = ps.playerId;
|
||||
// the player's view of the data (its slotted 自选 slots are its operators) and the copies its 自选 pieces hold
|
||||
const pgd = ps.gd || gd;
|
||||
const diyHeld = new Map();
|
||||
if (!Number.isInteger(ps.funds) || ps.funds < 0) fail(`${id}: funds ${ps.funds}`);
|
||||
if (!Number.isInteger(ps.pendingFunds) || ps.pendingFunds < 0) fail(`${id}: pendingFunds ${ps.pendingFunds}`);
|
||||
if (!Number.isFinite(ps.lp)) fail(`${id}: lp ${ps.lp}`);
|
||||
@@ -81,8 +86,9 @@ export function collectViolations(m, { limit = 25 } = {}) {
|
||||
for (const p of all) {
|
||||
note(ps, p);
|
||||
if (p.kind === 'chess') {
|
||||
const rec = gd.chess(p.id);
|
||||
const rec = pgd.chess(p.id);
|
||||
if (!rec) { fail(`${id}: unknown chess ${p.id}`); continue; }
|
||||
if (rec.isDiy && !rec.diyFor) fail(`${id}: owns ${p.id}, a 自选 slot it has not filled`);
|
||||
if (!Array.isArray(p.items) || p.items.length > gd.equipPerChess) fail(`${id}: ${p.id} carries ${p.items && p.items.length} items`);
|
||||
for (const it of p.items || []) {
|
||||
note(ps, it);
|
||||
@@ -93,12 +99,13 @@ export function collectViolations(m, { limit = 25 } = {}) {
|
||||
const maxCopies = rec.isGolden ? gd.goldenCopies : 1;
|
||||
if (!Number.isInteger(p.poolCopies) || p.poolCopies < 0 || p.poolCopies > maxCopies) fail(`${id}: ${p.id} holds ${p.poolCopies} copies`);
|
||||
const base = gd.baseIdOf(p.id);
|
||||
held.set(base, (held.get(base) || 0) + (p.poolCopies || 0));
|
||||
const tally = rec.isDiy ? diyHeld : held;
|
||||
tally.set(base, (tally.get(base) || 0) + (p.poolCopies || 0));
|
||||
} else if (p.kind === 'item') {
|
||||
if (!gd.item(p.id)) fail(`${id}: unknown item ${p.id}`);
|
||||
countItem(p);
|
||||
} else if (p.kind === 'token') {
|
||||
if (!gd.token(p.id)) fail(`${id}: unknown token ${p.id}`);
|
||||
if (!pgd.token(p.id)) fail(`${id}: unknown token ${p.id}`);
|
||||
if (!(p.count >= 1)) fail(`${id}: token stack count ${p.count}`);
|
||||
// summons exist only while their owner is deployed (withdrawing / selling / merging it removes them)
|
||||
if (!boardChessUids.has(p.ownerUid)) fail(`${id}: token ${p.uid} without a deployed owner (${p.ownerUid})`);
|
||||
@@ -115,7 +122,7 @@ export function collectViolations(m, { limit = 25 } = {}) {
|
||||
const [r, c] = parseKey(k);
|
||||
if (!(r >= FIELD.r0 && r <= FIELD.r1 && c >= FIELD.c0 && c <= FIELD.c1)) fail(`${id}: piece outside the board at ${k}`);
|
||||
if (p.kind === 'item') { fail(`${id}: item ${p.id} stands on the board`); continue; }
|
||||
const rec = p.kind === 'token' ? gd.token(p.id) : gd.chess(p.id);
|
||||
const rec = p.kind === 'token' ? pgd.token(p.id) : pgd.chess(p.id);
|
||||
const cls = p.kind === 'chess' ? placeClass(ps, rec) : positionClass(rec);
|
||||
if (rec && !canPlace(dmap, cls, r, c)) fail(`${id}: ${p.id} on an illegal tile ${k}`);
|
||||
// a "只能部署在召唤者攻击范围内" summon inside its owner's attack range (PlayerState.summonRange: a pure read)
|
||||
@@ -126,7 +133,7 @@ export function collectViolations(m, { limit = 25 } = {}) {
|
||||
if (deployed > ps.deployCap) fail(`${id}: ${deployed} chess deployed > cap ${ps.deployCap}`);
|
||||
// merges are immediate
|
||||
const copies = new Map();
|
||||
for (const p of all) if (p.kind === 'chess' && gd.chess(p.id) && !gd.isGolden(p.id)) { const b = gd.baseIdOf(p.id); copies.set(b, (copies.get(b) || 0) + 1); }
|
||||
for (const p of all) if (p.kind === 'chess' && pgd.chess(p.id) && !gd.isGolden(p.id)) { const b = gd.baseIdOf(p.id); copies.set(b, (copies.get(b) || 0) + 1); }
|
||||
for (const [b, n] of copies) {
|
||||
const need = gd.mergeCount(b);
|
||||
if (need > 1 && gd.goldenIdOf(b) && n >= need) fail(`${id}: owns ${n} normal copies of ${b} (merges at ${need})`);
|
||||
@@ -140,7 +147,7 @@ export function collectViolations(m, { limit = 25 } = {}) {
|
||||
}
|
||||
// bonds are up to date
|
||||
try {
|
||||
const fresh = computeBonds(gd, ps);
|
||||
const fresh = computeBonds(pgd, ps);
|
||||
if (JSON.stringify(fresh) !== JSON.stringify(ps.bonds)) fail(`${id}: stale bonds (missing recompute)`);
|
||||
} catch (e) {
|
||||
fail(`${id}: computeBonds threw ${e && e.message}`);
|
||||
@@ -154,15 +161,24 @@ export function collectViolations(m, { limit = 25 } = {}) {
|
||||
if (s.kind === 'chess' ? !gd.chess(s.id) : !gd.item(s.id)) fail(`${id}: shop slot ${i} unknown ${s.kind} ${s.id}`);
|
||||
if (!Number.isInteger(s.basePrice) || s.basePrice < 0) fail(`${id}: shop slot ${i} basePrice ${s.basePrice}`);
|
||||
if (s.kind === 'chess' && banned.has(gd.baseIdOf(s.id))) fail(`${id}: banned chess ${s.id} in the shop`);
|
||||
if (s.kind === 'chess' && gd.chess(s.id)?.isDiy && !(ps.diyStock && ps.diyStock.has(gd.baseIdOf(s.id)))) fail(`${id}: 自选 slot ${s.id} in the shop without its stock`);
|
||||
});
|
||||
for (const o of ps.offers) {
|
||||
for (const s of o.slots || []) {
|
||||
if (s.kind === 'item' ? !gd.item(s.id) : !gd.chess(s.id)) fail(`${id}: bad reward slot ${s.kind} ${s.id}`);
|
||||
if (s.kind !== 'item' && banned.has(gd.baseIdOf(s.id)) && o.source === 'merge') fail(`${id}: banned chess ${s.id} offered as a merge reward`);
|
||||
if (s.kind !== 'item' && gd.chess(s.id)?.isDiy && !(ps.diyStock && ps.diyStock.has(gd.baseIdOf(s.id)))) fail(`${id}: 自选 slot ${s.id} offered without its stock`);
|
||||
}
|
||||
if (!o.slots || !o.slots.length || o.slots.length > 6) fail(`${id}: reward offer with ${o.slots && o.slots.length} slots`);
|
||||
}
|
||||
}
|
||||
// 自选 stock accounting (player/diy.js): left + held == cap per slotted slot; a slot without stock holds nothing
|
||||
for (const [base, e] of ps.diyStock ? ps.diyStock.entries : []) {
|
||||
if (!(e.left >= 0 && e.left <= e.cap)) fail(`${id}: 自选 stock ${base}: left ${e.left} cap ${e.cap}`);
|
||||
const h = diyHeld.get(base) || 0;
|
||||
if (e.left + h !== e.cap) fail(`${id}: 自选 stock ${base}: left ${e.left} + held ${h} != cap ${e.cap}`);
|
||||
}
|
||||
for (const [base, n] of diyHeld) if (!(ps.diyStock && ps.diyStock.has(base)) && n !== 0) fail(`${id}: 自选 slot ${base} without stock holds ${n} copies`);
|
||||
}
|
||||
|
||||
// shared pool accounting
|
||||
|
||||
Reference in new issue
Block a user