feedback7 dober: 教鞭 is the official personal choice of three 战术特训 cards again, not a random one (PR #353 by @Sukvii, the owner's decision of 2026-10-08); golden: coop2-NORMAL-8-serverrun and coop2-ABYSS-10-boosted move
Seen: the Art 教鞭 (杜宾's strategy) drew three 战术特训 cards and added a random one — a documented simplification (docs/META.md §2.5: a personal choice would need its own phase, protocol message, timer and AI takeover). Official: act2autochess choice_event hunter_band_1 is a PERSONAL_CHOOSE; PRTS 下半 记录 §法术 教鞭 "使用后销毁,于3个战术特训的悬赏任务中选择一项". The owner decided on 2026-10-08 to follow it. Taken from PR #353 (code and tests by @Sukvii), ported to the split 0.2.2 modules. Now: ctx.offerBountyChoice -> Match.offerBountyChoice keeps up to three different mode-eligible cards (15 of 20 in 标准 / 险境, 20 in 绝境 / 终极; one meta-rng shuffle) in PlayerState.personalChoice, m.private only (same cards after a reconnect); g.choice { idx, choiceId } confirms it (Match.pickPersonalChoice: owner, id, round, index; a request without a choiceId is still the public draft, a stale id never falls through to it). No new phase or timer: Ready is refused while it is open, the prep's deadline picks at random before the temp pieces and Ready, a bot or a 托管 seat picks with the bounty scorer (botPickCard, the bots' seeded rng; at the prep's start, after the cast, before Ready; the scheduler's fallback picks at random if a step fails). The client reuses ChoiceOverlay (personal: no order, no turns, the prep's countdown) over a locked prep, the Ready reasons, the mock harness; six UI strings in en / ja / ko / zh-TW. An eliminated player's offer goes with it; invariants.js flags an offer outside its owner's own open prep. Not taken from the PR: its §25.24 in the released docs/history/0.2.0.md (the 0.2.2 section is the integrator's), its edits of the Ready toggle beyond the personal-choice case, its tail placement of the i18n keys. Golden: coop2-NORMAL-8-serverrun (ai_0 杜宾 casts 教鞭 from R2 on and now scores the three cards: teamLp 39 -> 34, virtualMs 121400 -> 118400, rng shop 511 -> 483 / bots 18063 -> 18070 / meta 112 -> 103) and coop2-ABYSS-10-boosted (ai_1 杜宾: virtualMs 121650 -> 124650, shop 590 -> 589 / bots 22075 -> 22096 / meta 141 -> 154); both first differ in round 2, every other scenario and family is unchanged; node tools/golden.mjs --twice is identical. Tests: items.test.js (three distinct cards, the tapped one applied once, nothing before the confirm, a refused cast keeps the Art / count / rng / uid, an eliminated player's offer), economy.test.js (Ready refused, the deadline picks before endPrep, solo prep untimed), connection.test.js (two players' offers, reconnect, foreign / duplicate / old ids, an offline player's pick at the deadline), bot.test.js (托管 takeover incl. the failing-step fallback), bot-smarter.test.js (the scorer's pick, deterministic per seed, no rng draw when nothing is pending), followups.test.js (R14 / R15), draft.test.js, unitStats.test.js (protocol), hardening.test.js (invariant), gameLogic / playtest3 / playtest4 (view model, Ready reason, ChoiceView, actions), mock.e2e.test.js and playtest2.real.e2e.test.js (browser, SP_E2E=1).
This commit is contained in:
1 parent
05eb2b807f
commit
56a7b918a9
48 files changed
+901
-159
No files matched your search
@@ -1139,7 +1139,23 @@ test('教鞭 (Art): a 战术特训 bounty (PRTS "于3个战术特训的悬赏任
|
||||
const { m, ps } = setup({ seed: 60 + s });
|
||||
for (let k = 0; k < 2; k++) {
|
||||
const art = giveItem(m, ps, 'chess_item_6_03_m');
|
||||
const deadline = m.deadline;
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.art', itemUid: art.uid, row: 10, col: 5 }), OK);
|
||||
const pending = ps.personalChoice;
|
||||
assert.equal(ps.bounties.length, k, 'offering cards does not add a bounty');
|
||||
assert.equal(pending.cards.length, 3);
|
||||
assert.equal(new Set(pending.cards.map((c) => c.effectId)).size, 3);
|
||||
assert.equal(pending.round, m.round);
|
||||
assert.equal(pending.sourceItemId, art.id);
|
||||
assert.equal(m.deadline, deadline, 'uses the existing PREP clock');
|
||||
assert.equal(ps.find(art.uid), null);
|
||||
assert.equal(ps.round.arts, k + 1, 'consumed when offered');
|
||||
for (const c of pending.cards) assert.ok(training.has(c.effectId) && !m.gd.inactiveEnemies.has(c.enemyKey));
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.choice', idx: 1, choiceId: pending.id }), OK);
|
||||
assert.equal(ps.personalChoice, null);
|
||||
assert.equal(ps.bounties[k].card.effectId, pending.cards[1].effectId, 'the submitted card is applied');
|
||||
assert.equal(ps.round.arts, k + 1, 'picking does not consume another Art');
|
||||
assert.equal(m.handle('p_0', { t: 'g.choice', idx: 1, choiceId: pending.id }).error, 'BAD_TARGET');
|
||||
}
|
||||
assert.equal(ps.bounties.length, 2);
|
||||
for (const b of ps.bounties) {
|
||||
@@ -1150,7 +1166,7 @@ test('教鞭 (Art): a 战术特训 bounty (PRTS "于3个战术特训的悬赏任
|
||||
seen.add(b.card.effectId);
|
||||
}
|
||||
}
|
||||
assert.ok(seen.size >= 5, 'random pick');
|
||||
assert.ok(seen.size >= 5, 'different offered cards across seeds');
|
||||
const { h, m } = setup({ mode: 'coop', humans: 2, seed: 7 });
|
||||
const p0 = h.ps('p_0'), p1 = h.ps('p_1');
|
||||
const art = giveItem(m, p0, 'chess_item_6_01_m');
|
||||
@@ -1161,9 +1177,60 @@ test('教鞭 (Art): a 战术特训 bounty (PRTS "于3个战术特训的悬赏任
|
||||
const passed = p1.hand.find((p) => p && p.id === 'chess_item_6_01_m');
|
||||
assert.deepEqual(m.handle('p_1', { t: 'g.art', itemUid: passed.uid, row: 10, col: 5 }), OK);
|
||||
assert.match(p1.bounties[0].card.effectId, /^enemyeffect_b_/);
|
||||
assert.equal(p1.personalChoice, null, '神秘顾客 still applies its random bounty immediately');
|
||||
cover('chess_item_6_03_m', 'chess_item_6_01_m');
|
||||
});
|
||||
|
||||
test('教鞭: pending and empty-pool failures keep the Art, Arts count and RNG; short offers use actual cards', () => {
|
||||
const { m, ps } = setup();
|
||||
const first = giveItem(m, ps, 'chess_item_6_03_m');
|
||||
const second = giveItem(m, ps, 'chess_item_6_03_m', 'temp');
|
||||
assert.deepEqual(ps.useArt(first.uid, 10, 5), OK);
|
||||
const pending = ps.personalChoice;
|
||||
const rng = m.rngMeta.state(), uid = m.uidSeq;
|
||||
assert.deepEqual(ps.useArt(second.uid, 10, 5), { error: 'BAD_TARGET', detail: '请先完成当前教鞭选择' });
|
||||
assert.equal(ps.personalChoice, pending);
|
||||
assert.ok(ps.find(second.uid));
|
||||
assert.equal(ps.round.arts, 1);
|
||||
assert.equal(m.rngMeta.state(), rng);
|
||||
assert.equal(m.uidSeq, uid);
|
||||
assert.equal(m.pickPersonalChoice(ps, -1, pending.id).error, 'BAD_TARGET');
|
||||
assert.equal(m.pickPersonalChoice(ps, 0.5, pending.id).error, 'BAD_TARGET');
|
||||
assert.equal(m.pickPersonalChoice(ps, 3, pending.id).error, 'BAD_TARGET');
|
||||
assert.equal(ps.personalChoice, pending);
|
||||
assert.deepEqual(m.pickPersonalChoice(ps, 0, pending.id), OK);
|
||||
const training = DATA.choices.cards.bounty.filter((c) => c.payout === 'perfect');
|
||||
m.gd.inactiveEnemies = new Set(training.map((c) => c.enemyKey));
|
||||
assert.deepEqual(ps.useArt(second.uid, 10, 5), { error: 'BAD_TARGET', detail: '当前没有可用的战术特训' });
|
||||
assert.ok(ps.find(second.uid));
|
||||
assert.equal(ps.round.arts, 1);
|
||||
assert.equal(ps.personalChoice, null);
|
||||
assert.equal(m.rngMeta.state(), rng);
|
||||
const allowed = training.find((c) => c.multiRound && m.gd.enemy(c.enemyKey));
|
||||
m.gd.inactiveEnemies.delete(allowed.enemyKey);
|
||||
assert.deepEqual(ps.useArt(second.uid, 10, 5), OK);
|
||||
assert.ok(ps.personalChoice.cards.length > 0 && ps.personalChoice.cards.length < 3);
|
||||
assert.equal(new Set(ps.personalChoice.cards.map((c) => c.effectId)).size, ps.personalChoice.cards.length);
|
||||
assert.equal(ps.find(second.uid), null, 'Arts in temp are consumed too');
|
||||
const view = ps.privateView().personalChoice;
|
||||
assert.ok(view.cards.every((c) => c.kind === 'bounty' && c.rounds === 2));
|
||||
assert.ok(view.cards.every((c) => !/每场/.test(c.descRaw || c.desc)), 'multi-round candidates use the same two-battle text as applied bounties');
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('教鞭: an open choice goes with a player who is eliminated, and nothing of it is left in its views', () => {
|
||||
const { h, m } = setup({ mode: 'coop', humans: 2, seed: 8 });
|
||||
const a = h.ps('p_0');
|
||||
const art = giveItem(m, a, 'chess_item_6_03_m');
|
||||
assert.deepEqual(a.useArt(art.uid, 10, 5), { ok: true });
|
||||
assert.equal(a.privateView().personalChoice.cards.length, 3);
|
||||
a.eliminate(m.round);
|
||||
assert.equal(a.personalChoice, null);
|
||||
assert.equal(a.privateView().personalChoice, null);
|
||||
assert.equal(a.privateView().canReady, false);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('merging: two identical normal items become the golden one (upgradeNum 2); upgradeNum-100 items never merge', () => {
|
||||
const never = Object.values(DATA.items).filter((r) => r.itemType === 'EQUIP' && !r.isGolden && r.upgradeNum === 100).map((r) => r.name).sort();
|
||||
assert.deepEqual(never, ['人事部文档', '信标', '变形同构体', '寻呼模块', '拟态物质', '突变细胞'].sort());
|
||||
|
||||
+65
-65
@@ -1209,8 +1209,8 @@
|
||||
"combat": "server",
|
||||
"boost": null,
|
||||
"setup": {"stageId":"act1autochess_m01","bossId":"boss_1","hiddenBossId":"boss_9","factions":["FLY","DOT","REFLECTION"]},
|
||||
"end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":39,"bossPoolLeft":0,"virtualMs":121400},
|
||||
"rng": {"setup":25,"shop":511,"waves":0,"draft":34,"bots":18063,"meta":112},
|
||||
"end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":34,"bossPoolLeft":0,"virtualMs":118400},
|
||||
"rng": {"setup":25,"shop":483,"waves":0,"draft":34,"bots":18070,"meta":103},
|
||||
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
||||
"rounds": [
|
||||
{
|
||||
@@ -1220,63 +1220,63 @@
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"ai_0": [1,29,5,0,0,1,4,"76d92108",2,"arcaneShip 0/2, preciShip 1/2, soloShip 1/0",10,11,1,9],
|
||||
"ai_1": [1,28,5,0,0,1,4,"fca300a0",2,"arcaneShip 0/2, kjeragShip 0/2, preciShip 1/2, soloShip 1/0, steadShip 0/2",10,10,0,9]
|
||||
"ai_0": [1,30,5,0,2,1,4,"4678f135",2,"arcaneShip 0/2, preciShip 1/2, soloShip 1/0",11,11,0,9],
|
||||
"ai_1": [1,28,5,0,0,1,4,"a2f52619",2,"arcaneShip 0/2, kjeragShip 0/2, preciShip 1/2, soloShip 1/0, steadShip 0/2",10,10,0,9]
|
||||
},
|
||||
{
|
||||
"round": 3,
|
||||
"ai_0": [1,29,6,0,0,1,7,"b7a24935",5,"arcaneShip 0/2, preciShip 1/2, siracusaShip 0/3, soloShip 1/3",9,9,0,15],
|
||||
"ai_1": [1,28,6,0,0,1,7,"bf48e672",6,"arcaneShip 0/2, kjeragShip 0/4, preciShip 1/2, soloShip 1/0, steadShip 1/4",9,9,0,15]
|
||||
"ai_0": [1,30,8,0,0,1,7,"388a275b",5,"arcaneShip 0/2, preciShip 1/2, siracusaShip 0/3, soloShip 1/3",9,9,0,16],
|
||||
"ai_1": [1,28,6,0,0,1,7,"f979585a",6,"arcaneShip 0/2, kjeragShip 0/4, preciShip 1/2, soloShip 1/0, steadShip 1/4",9,9,0,15]
|
||||
},
|
||||
{
|
||||
"round": 4,
|
||||
"ai_0": [1,29,7,0,1,1,8,"e64a4d06",13,"arcaneShip 0/2, kjeragShip 0/2, preciShip 1/2, siracusaShip 0/6, soloShip 1/9, steadShip 1/2",13,13,0,22],
|
||||
"ai_1": [1,28,7,0,0,2,8,"ef6d85b4",6,"arcaneShip 0/2, kjeragShip 0/4, preciShip 1/2, soloShip 1/0, steadShip 1/4",12,12,0,22]
|
||||
"ai_0": [1,30,7,0,2,1,8,"3158c50f",13,"arcaneShip 0/2, kjeragShip 0/2, preciShip 1/2, siracusaShip 0/6, soloShip 1/9, steadShip 1/2",13,13,0,23],
|
||||
"ai_1": [1,28,7,0,0,2,8,"1db5c96f",6,"arcaneShip 0/2, kjeragShip 0/4, preciShip 1/2, soloShip 1/0, steadShip 1/4",12,12,0,22]
|
||||
},
|
||||
{
|
||||
"round": 5,
|
||||
"ai_0": [1,29,9,0,0,3,8,"c1f02bc4",13,"arcaneShip 0/2, kjeragShip 0/2, preciShip 1/2, siracusaShip 0/6, soloShip 1/9, steadShip 1/2",15,15,0,31],
|
||||
"ai_0": [1,30,10,0,0,3,8,"e8ba0f59",13,"arcaneShip 0/2, kjeragShip 0/2, preciShip 1/2, siracusaShip 0/6, soloShip 1/9, steadShip 1/2",15,15,0,33],
|
||||
"ai_1": [1,28,8,0,0,3,8,"a3c40192",6,"arcaneShip 0/2, kjeragShip 0/4, preciShip 1/2, soloShip 1/0, steadShip 1/4",15,15,0,29]
|
||||
},
|
||||
{
|
||||
"round": 6,
|
||||
"ai_0": [1,29,9,0,3,3,8,"41bbfcb2",32,"arcaneShip 0/2, deputShip 1/2, kjeragShip 0/2, preciShip 1/4, siracusaShip 1/12, soloShip 1/14, steadShip 0/2",25,25,0,39],
|
||||
"ai_1": [1,28,9,0,1,3,8,"ee24046e",11,"arcaneShip 0/2, kjeragShip 0/9, preciShip 2/7, siracusaShip 1/0, soloShip 1/0, steadShip 1/4",20,22,2,37]
|
||||
"ai_0": [1,30,9,0,3,3,8,"a4c58969",34,"arcaneShip 0/2, deputShip 1/2, kjeragShip 0/2, preciShip 1/4, siracusaShip 1/14, soloShip 1/14, steadShip 0/2",25,25,0,41],
|
||||
"ai_1": [1,28,9,0,1,3,8,"14c226e1",11,"arcaneShip 0/2, kjeragShip 0/9, preciShip 2/7, siracusaShip 1/0, soloShip 1/0, steadShip 1/4",22,22,0,37]
|
||||
},
|
||||
{
|
||||
"round": 7,
|
||||
"ai_0": [1,29,13,0,2,4,8,"94f5b782",57,"arcaneShip 1/8, deputShip 2/6, kjeragShip 0/2, preciShip 0/4, siracusaShip 1/25, soloShip 1/18, steadShip 0/2",27,27,0,53],
|
||||
"ai_1": [1,28,11,0,3,4,8,"c6add328",15,"arcaneShip 0/2, kjeragShip 0/13, preciShip 2/7, siracusaShip 1/0, soloShip 1/0, steadShip 1/8",27,27,0,49]
|
||||
"ai_0": [1,30,13,0,2,4,8,"e989f361",57,"arcaneShip 1/8, deputShip 2/6, kjeragShip 0/2, preciShip 0/4, siracusaShip 1/25, soloShip 1/18, steadShip 0/2",27,27,0,55],
|
||||
"ai_1": [1,28,11,0,3,4,8,"f88853b8",15,"arcaneShip 0/2, kjeragShip 0/13, preciShip 2/7, siracusaShip 1/0, soloShip 1/0, steadShip 1/8",26,27,1,49]
|
||||
},
|
||||
{
|
||||
"round": 8,
|
||||
"ai_0": [1,29,13,0,1,4,8,"d070c283",88,"arcaneShip 2/18, deputShip 2/10, kjeragShip 0/2, preciShip 0/4, siracusaShip 1/38, soloShip 1/22, steadShip 0/2",24,24,0,66],
|
||||
"ai_1": [1,28,14,0,0,4,8,"3a8e9585",24,"arcaneShip 0/2, emptyShip 1/0, kjeragShip 1/13, preciShip 1/7, siracusaShip 1/4, soloShip 1/0, steadShip 0/8",13,23,10,63]
|
||||
"ai_0": [1,30,13,0,1,4,8,"3b2db317",88,"arcaneShip 2/18, deputShip 2/10, kjeragShip 0/2, preciShip 0/4, siracusaShip 1/38, soloShip 1/22, steadShip 0/2",24,24,0,68],
|
||||
"ai_1": [1,28,14,0,0,4,8,"5589bffd",26,"arcaneShip 0/4, emptyShip 1/0, kjeragShip 1/13, preciShip 1/9, siracusaShip 1/4, soloShip 1/0, steadShip 0/8",17,23,6,63]
|
||||
},
|
||||
{
|
||||
"round": 9,
|
||||
"ai_0": [1,29,13,0,0,4,8,"7ca04eb4",151,"arcaneShip 2/38, deputShip 1/12, indomShip 0/8, kjeragShip 0/2, miraShip 1/2, preciShip 0/4, siracusaShip 1/62, soloShip 1/24, steadShip 0/2, suntShip 1/2, victoriaShip 1/11, visiShip 0/8",15,15,0,79],
|
||||
"ai_1": [1,28,12,0,0,4,8,"1575876e",76,"arcaneShip 0/2, indomShip 0/8, kjeragShip 1/28, preciShip 2/40, siracusaShip 1/8, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",15,15,0,75]
|
||||
"ai_0": [1,30,13,0,0,4,8,"e6a9c830",151,"arcaneShip 2/38, deputShip 1/12, indomShip 0/8, kjeragShip 0/2, miraShip 1/2, preciShip 0/4, siracusaShip 1/62, soloShip 1/24, steadShip 0/2, suntShip 1/2, victoriaShip 1/11, visiShip 0/8",15,15,0,81],
|
||||
"ai_1": [1,28,12,0,0,4,8,"5097fa00",67,"arcaneShip 0/4, indomShip 0/8, kjeragShip 1/28, preciShip 2/31, siracusaShip 1/8, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",15,15,0,75]
|
||||
},
|
||||
{
|
||||
"round": 10,
|
||||
"ai_0": [1,29,12,0,2,5,8,"42c3188f",176,"arcaneShip 2/42, deputShip 1/14, indomShip 0/8, kjeragShip 0/2, miraShip 1/4, preciShip 0/4, siracusaShip 1/72, soloShip 1/26, steadShip 0/2, suntShip 1/4, victoriaShip 1/14, visiShip 0/8",24,24,0,92],
|
||||
"ai_1": [1,28,12,0,0,5,8,"d4d2685b",107,"arcaneShip 0/2, indomShip 0/8, kjeragShip 1/38, preciShip 2/61, siracusaShip 1/8, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",23,23,0,86]
|
||||
"ai_0": [1,30,12,0,2,5,8,"a4f0dde0",176,"arcaneShip 2/42, deputShip 1/14, indomShip 0/8, kjeragShip 0/2, miraShip 1/4, preciShip 0/4, siracusaShip 1/72, soloShip 1/26, steadShip 0/2, suntShip 1/4, victoriaShip 1/14, visiShip 0/8",24,24,0,94],
|
||||
"ai_1": [1,28,12,0,0,5,8,"46894aa5",75,"arcaneShip 0/4, indomShip 0/8, kjeragShip 1/28, preciShip 2/39, siracusaShip 1/8, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",23,23,0,86]
|
||||
},
|
||||
{
|
||||
"round": 11,
|
||||
"ai_0": [1,29,14,0,0,5,8,"bbafb340",217,"arcaneShip 2/50, deputShip 1/16, indomShip 0/8, kjeragShip 0/2, miraShip 1/6, preciShip 0/4, siracusaShip 1/94, soloShip 1/28, steadShip 0/2, suntShip 1/6, victoriaShip 1/17, visiShip 0/8",23,23,0,107],
|
||||
"ai_1": [1,28,12,0,0,5,8,"164a05d7",130,"arcaneShip 0/2, indomShip 0/8, kjeragShip 1/38, preciShip 2/74, siracusaShip 1/18, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",23,23,0,97]
|
||||
"ai_0": [1,30,14,0,0,5,8,"95327720",211,"arcaneShip 2/50, deputShip 1/16, indomShip 0/8, kjeragShip 0/2, miraShip 0/4, preciShip 0/4, siracusaShip 1/94, soloShip 1/28, steadShip 0/2, suntShip 1/6, victoriaShip 1/17, visiShip 0/8",23,23,0,109],
|
||||
"ai_1": [1,28,12,0,0,5,8,"d993478a",94,"arcaneShip 0/4, indomShip 0/8, kjeragShip 1/28, preciShip 2/40, siracusaShip 1/26, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",23,23,0,98]
|
||||
},
|
||||
{
|
||||
"round": 12,
|
||||
"ai_0": [1,29,12,0,0,6,8,"68ce3885",234,"arcaneShip 2/52, deputShip 1/18, indomShip 0/8, kjeragShip 0/2, miraShip 1/8, preciShip 0/4, siracusaShip 1/98, soloShip 1/30, steadShip 0/2, suntShip 1/8, victoriaShip 1/20, visiShip 0/8",34,47,13,119],
|
||||
"ai_1": [1,28,12,0,0,6,8,"94e71f96",174,"arcaneShip 0/2, indomShip 0/8, kjeragShip 1/38, preciShip 2/98, siracusaShip 1/38, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",46,46,0,108]
|
||||
"ai_0": [1,20,12,0,0,6,8,"90727574",226,"arcaneShip 2/52, deputShip 1/18, indomShip 0/8, kjeragShip 0/2, miraShip 0/4, preciShip 0/4, siracusaShip 1/98, soloShip 1/30, steadShip 0/2, suntShip 1/8, victoriaShip 1/20, visiShip 0/8",23,47,24,120],
|
||||
"ai_1": [1,27,12,0,0,6,8,"1f6740ae",140,"arcaneShip 0/4, indomShip 0/8, kjeragShip 1/28, preciShip 2/64, siracusaShip 1/48, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",45,46,1,109]
|
||||
},
|
||||
{
|
||||
"round": 13,
|
||||
"ai_0": [1,19,12,0,0,6,8,"e4c62459",238,"arcaneShip 2/60, deputShip 0/18, indomShip 0/8, kjeragShip 0/2, maniShip 1/0, miraShip 1/8, preciShip 0/4, siracusaShip 1/112, soloShip 1/30, steadShip 0/2, suntShip 1/8, victoriaShip 1/20, visiShip 0/8",22,39,17,129],
|
||||
"ai_1": [1,20,12,0,0,6,8,"804960b4",215,"arcaneShip 0/4, indomShip 0/8, kjeragShip 1/38, preciShip 2/121, siracusaShip 1/56, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",31,39,8,120]
|
||||
"ai_0": [1,13,12,0,0,6,8,"61fd749a",264,"arcaneShip 2/58, deputShip 1/20, indomShip 0/8, kjeragShip 0/2, miraShip 1/10, preciShip 0/4, siracusaShip 1/102, soloShip 1/32, steadShip 0/2, suntShip 1/10, victoriaShip 1/32, visiShip 0/8",32,39,7,130],
|
||||
"ai_1": [1,21,12,0,0,6,8,"9eb5c4f8",178,"arcaneShip 0/6, indomShip 0/8, kjeragShip 1/28, preciShip 2/80, siracusaShip 1/70, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",33,39,6,122]
|
||||
}
|
||||
],
|
||||
"players": {
|
||||
@@ -1285,28 +1285,28 @@
|
||||
"victory": true,
|
||||
"roundsPassed": 14,
|
||||
"eliminatedRound": null,
|
||||
"lp": 19,
|
||||
"lp": 13,
|
||||
"bandId": "band_doberm",
|
||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||
"title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"},
|
||||
"trophies": 4,
|
||||
"reward": 400,
|
||||
"stats": {"activatedLayers":283,"bossDamage":779391,"dmgDealt":3811993,"gold":142,"itemsEquipped":20,"kills":276,"leaks":31,"lpLost":11,"merges":4,"perfectRounds":10,"refreshes":23},
|
||||
"lineup": ["5_20_a@11,3[4_07_e_a+1_01_e_a]","5_12_a@11,6[6_09_e_a+3_08_e_b]","6_11_a@11,7","2_10_a@10,3[3_05_e_a+3_11_e_b]","4_10_a@10,7[4_09_e_a+1_05_e_a]","1_08_b@10,8[1_02_e_a+5_03_e_a]","4_11_b@9,8[4_08_e_a]","6_07_a@9,9[4_10_e_a+4_05_e_a]"],
|
||||
"bonds": ["arcaneShip 2/74","deputShip 0/18","indomShip 0/8","kjeragShip 0/2","maniShip 1/0","miraShip 1/12","preciShip 0/4","siracusaShip 1/130","soloShip 1/30","steadShip 0/2","suntShip 1/8","victoriaShip 1/29","visiShip 0/8"]
|
||||
"stats": {"activatedLayers":328,"bossDamage":362244,"dmgDealt":3468602,"gold":141,"itemsEquipped":23,"kills":264,"leaks":31,"lpLost":17,"merges":3,"perfectRounds":11,"refreshes":22},
|
||||
"lineup": ["2_10_a@12,3[5_03_e_a+3_11_e_a]","5_20_a@11,3[4_07_e_a+1_01_e_b]","1_06_a@11,6[3_05_e_a+2_04_e_a]","5_12_a@11,7[3_10_e_a]","1_13_b@10,3","4_10_a@10,7[1_05_e_a+4_10_e_a]","1_08_b@10,8[3_09_e_a+4_05_e_b]","6_07_a@9,9[3_06_e_a+5_07_e_a]"],
|
||||
"bonds": ["arcaneShip 2/73","deputShip 1/22","indomShip 0/8","kjeragShip 0/2","miraShip 1/16","preciShip 0/4","siracusaShip 1/127","soloShip 1/34","steadShip 0/2","suntShip 1/12","victoriaShip 1/44","visiShip 0/8"]
|
||||
},
|
||||
"ai_1": {
|
||||
"alive": true,
|
||||
"victory": true,
|
||||
"roundsPassed": 14,
|
||||
"eliminatedRound": null,
|
||||
"lp": 20,
|
||||
"lp": 21,
|
||||
"bandId": "band_malkie",
|
||||
"title": {"id":"comment_3","name":"坚若磐石","picId":"comment_icon_3","text":"目标生命值损失最少"},
|
||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||
"trophies": 4,
|
||||
"reward": 400,
|
||||
"stats": {"activatedLayers":215,"bossDamage":570609,"dmgDealt":4056057,"gold":132,"itemsEquipped":21,"kills":261,"leaks":20,"lpLost":8,"merges":3,"perfectRounds":10,"refreshes":22},
|
||||
"lineup": ["3_20_a@12,3[4_03_e_a+5_01_e_a]","5_10_a@11,3[1_01_e_a+3_04_e_b]","5_02_a@11,6","1_17_a@11,7[5_07_e_a+1_02_e_b]","3_11_b@10,3[3_03_e_a+5_02_e_b]","1_08_b@10,8[3_11_e_a+3_06_e_a]","1_02_b@9,4[4_10_e_a+4_08_e_b]","6_18_a@9,9[5_02_e_b+4_11_e_a]"],
|
||||
"bonds": ["arcaneShip 0/4","indomShip 0/8","kjeragShip 1/38","preciShip 2/121","siracusaShip 1/56","soloShip 1/0","steadShip 0/8","suntShip 1/0","victoriaShip 0/8","visiShip 0/8"]
|
||||
"stats": {"activatedLayers":194,"bossDamage":987756,"dmgDealt":4195035,"gold":136,"itemsEquipped":29,"kills":254,"leaks":14,"lpLost":7,"merges":4,"perfectRounds":9,"refreshes":19},
|
||||
"lineup": ["3_20_b@11,3[4_03_e_a+3_04_e_b]","5_02_a@11,6[5_07_e_a]","3_11_b@11,7[3_03_e_a+4_08_e_a]","6_18_a@10,3[5_03_e_a+5_02_e_b]","6_05_a@10,7[5_02_e_a+5_02_e_b]","1_08_b@10,8[4_08_e_b+5_02_e_b]","5_10_a@9,8[1_01_e_a+1_02_e_b]","6_19_a@9,9[4_11_e_a+4_10_e_a]"],
|
||||
"bonds": ["arcaneShip 0/6","indomShip 0/8","kazimierzShip 0/8","kjeragShip 1/36","preciShip 2/80","siracusaShip 1/70","soloShip 1/0","steadShip 0/8","suntShip 1/0","swiftShip 1/8","victoriaShip 0/8","visiShip 0/8"]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1404,8 +1404,8 @@
|
||||
"combat": "client",
|
||||
"boost": {"lp":400,"layers":300},
|
||||
"setup": {"stageId":"act2autochess_m04","bossId":"boss_5","hiddenBossId":"boss_9","factions":["FLY","ELEMENT","INVISIBLE"]},
|
||||
"end": {"victory":true,"roundsPassed":15,"hiddenReached":true,"hiddenCleared":true,"reason":"victory","teamLp":800,"bossPoolLeft":0,"virtualMs":121650},
|
||||
"rng": {"setup":25,"shop":590,"waves":0,"draft":61,"bots":22075,"meta":141},
|
||||
"end": {"victory":true,"roundsPassed":15,"hiddenReached":true,"hiddenCleared":true,"reason":"victory","teamLp":800,"bossPoolLeft":0,"virtualMs":124650},
|
||||
"rng": {"setup":25,"shop":589,"waves":0,"draft":61,"bots":22096,"meta":154},
|
||||
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
||||
"rounds": [
|
||||
{
|
||||
@@ -1416,62 +1416,62 @@
|
||||
{
|
||||
"round": 2,
|
||||
"ai_0": [1,400,5,0,0,1,4,"f5818d14",302,"arcaneShip 0/300, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/300, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 0/300, steadShip 1/302, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",9,9,0,9],
|
||||
"ai_1": [1,400,5,0,1,1,4,"e14f7c28",0,"arcaneShip 0/300, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/300, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 0/300, steadShip 0/300, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",10,10,0,9]
|
||||
"ai_1": [1,400,5,0,1,1,4,"1bb10c7b",0,"arcaneShip 0/300, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/300, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 0/300, steadShip 0/300, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",10,10,0,9]
|
||||
},
|
||||
{
|
||||
"round": 3,
|
||||
"ai_0": [1,400,6,0,1,1,7,"6ff64423",603,"arcaneShip 0/300, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/300, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/300, steadShip 1/303, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",11,11,0,15],
|
||||
"ai_1": [1,400,7,0,2,1,7,"97624e1b",602,"arcaneShip 0/302, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/302, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/300, steadShip 0/300, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",11,11,0,16]
|
||||
"ai_0": [1,400,6,0,1,1,7,"0d06d571",603,"arcaneShip 0/300, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/300, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/300, steadShip 1/303, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",11,11,0,15],
|
||||
"ai_1": [1,400,7,0,2,1,7,"f4115666",602,"arcaneShip 0/302, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/302, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/300, steadShip 0/300, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",11,11,0,16]
|
||||
},
|
||||
{
|
||||
"round": 4,
|
||||
"ai_0": [1,400,8,0,1,2,8,"6153ae71",906,"arcaneShip 0/302, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/302, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/300, steadShip 1/304, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",21,21,0,23],
|
||||
"ai_1": [1,400,9,0,5,2,8,"c56b1a00",909,"arcaneShip 0/302, deputShip 1/302, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/305, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/302, steadShip 0/300, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",22,22,0,25]
|
||||
"ai_1": [1,400,9,0,4,2,8,"d70518d7",917,"arcaneShip 0/302, deputShip 2/304, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 0/300, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",22,22,0,25]
|
||||
},
|
||||
{
|
||||
"round": 5,
|
||||
"ai_0": [1,400,9,0,0,3,8,"74d81bd7",1210,"arcaneShip 0/302, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/302, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/303, soloShip 1/300, steadShip 1/305, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",24,24,0,32],
|
||||
"ai_1": [1,400,13,0,0,3,8,"e04712f6",1523,"arcaneShip 1/304, deputShip 1/304, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/308, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 0/300, suntShip 0/300, swiftShip 0/300, victoriaShip 1/303, visiShip 0/300, yanShip 0/300",24,24,0,38]
|
||||
"ai_0": [1,400,9,0,0,3,8,"c3dfd8c8",1210,"arcaneShip 0/302, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/302, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/303, soloShip 1/300, steadShip 1/305, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",24,24,0,32],
|
||||
"ai_1": [1,400,12,0,0,3,8,"6db164c9",1534,"arcaneShip 1/304, deputShip 1/306, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/314, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/306, steadShip 1/304, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",24,24,0,37]
|
||||
},
|
||||
{
|
||||
"round": 6,
|
||||
"ai_0": [1,400,9,0,0,3,8,"30588739",1220,"arcaneShip 0/302, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/307, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 2/308, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/306, soloShip 1/300, steadShip 1/306, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",27,27,0,42],
|
||||
"ai_1": [1,400,9,0,0,3,8,"50b45347",1535,"arcaneShip 1/306, deputShip 1/306, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/311, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/306, steadShip 0/300, suntShip 0/300, swiftShip 0/300, victoriaShip 1/306, visiShip 0/300, yanShip 0/300",27,27,0,45]
|
||||
"ai_0": [1,400,9,0,0,3,8,"2455cf23",1227,"arcaneShip 0/302, deputShip 1/302, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/304, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/308, soloShip 0/300, steadShip 1/313, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",27,27,0,40],
|
||||
"ai_1": [1,400,9,0,1,3,8,"a34aad2c",1858,"arcaneShip 1/306, deputShip 1/308, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 1/309, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/321, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/308, steadShip 1/306, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",30,30,0,47]
|
||||
},
|
||||
{
|
||||
"round": 7,
|
||||
"ai_0": [1,400,10,0,0,4,8,"8f4587cd",1228,"arcaneShip 0/302, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/307, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 2/311, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/309, soloShip 1/300, steadShip 1/308, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",25,25,0,54],
|
||||
"ai_1": [1,400,10,0,0,4,8,"2daf0b74",1547,"arcaneShip 1/308, deputShip 1/308, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/314, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/308, steadShip 0/300, suntShip 0/300, swiftShip 0/300, victoriaShip 1/309, visiShip 0/300, yanShip 0/300",25,25,0,54]
|
||||
"ai_0": [1,400,10,0,0,4,8,"6d830af8",1244,"arcaneShip 0/302, deputShip 1/304, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/313, soloShip 0/300, steadShip 1/318, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",25,25,0,50],
|
||||
"ai_1": [1,400,11,0,0,4,8,"ed7d0a62",1870,"arcaneShip 1/308, deputShip 1/310, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 1/311, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/323, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/310, steadShip 1/308, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",25,25,0,58]
|
||||
},
|
||||
{
|
||||
"round": 8,
|
||||
"ai_0": [1,400,11,0,0,4,8,"e8308b42",1534,"arcaneShip 1/306, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 1/307, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 2/311, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/309, soloShip 1/300, steadShip 1/310, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",18,18,0,64],
|
||||
"ai_1": [1,400,11,0,3,4,8,"cbdb5901",1561,"arcaneShip 1/310, deputShip 1/310, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/325, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 0/308, steadShip 1/304, suntShip 0/300, swiftShip 0/300, victoriaShip 1/312, visiShip 0/300, yanShip 0/300",19,19,0,63]
|
||||
"ai_0": [1,400,11,0,2,4,8,"f4ce7cb6",1248,"arcaneShip 0/302, deputShip 1/306, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/307, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 2/316, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/313, soloShip 1/302, steadShip 1/324, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",18,18,0,61],
|
||||
"ai_1": [1,400,11,0,2,4,8,"042290ea",2194,"arcaneShip 1/316, deputShip 1/312, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 1/313, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/335, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/312, steadShip 0/308, suntShip 1/302, swiftShip 0/300, victoriaShip 1/304, visiShip 0/300, yanShip 0/300",19,19,0,69]
|
||||
},
|
||||
{
|
||||
"round": 9,
|
||||
"ai_0": [1,400,12,0,2,4,8,"c3934d4d",1864,"arcaneShip 1/314, deputShip 1/302, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 1/309, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/313, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/311, soloShip 0/300, steadShip 1/315, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",24,24,0,75],
|
||||
"ai_1": [1,400,15,0,4,4,8,"eb426307",1574,"arcaneShip 1/310, deputShip 0/310, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/336, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/308, steadShip 1/307, suntShip 0/300, swiftShip 0/300, victoriaShip 1/313, visiShip 0/300, yanShip 0/300",24,24,0,79]
|
||||
"ai_0": [1,400,14,0,0,4,8,"0659edb4",1539,"arcaneShip 1/312, deputShip 0/306, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 1/307, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 2/318, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/313, soloShip 1/302, steadShip 0/324, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",18,24,6,76],
|
||||
"ai_1": [1,400,14,0,1,4,8,"99b9df21",2214,"arcaneShip 1/318, deputShip 1/314, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 1/315, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/341, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/314, steadShip 0/308, suntShip 1/304, swiftShip 0/300, victoriaShip 1/308, visiShip 0/300, yanShip 0/300",24,24,0,84]
|
||||
},
|
||||
{
|
||||
"round": 10,
|
||||
"ai_0": [1,400,14,0,0,5,8,"3aef9421",2166,"arcaneShip 0/314, deputShip 1/304, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 1/311, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/315, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/313, soloShip 1/302, steadShip 1/319, suntShip 1/302, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",12,12,0,89],
|
||||
"ai_1": [1,400,16,0,3,5,8,"0d6049ac",1574,"arcaneShip 1/310, deputShip 0/310, egirShip 0/300, emptyShip 0/300, indomShip 1/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/342, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/308, steadShip 0/307, suntShip 0/300, swiftShip 0/300, victoriaShip 1/314, visiShip 0/300, yanShip 0/300",13,13,0,94]
|
||||
"ai_0": [1,400,12,0,0,5,8,"37cd34cd",1563,"arcaneShip 1/314, deputShip 0/306, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 1/310, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/323, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/316, soloShip 0/302, steadShip 0/324, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",12,12,0,88],
|
||||
"ai_1": [1,400,13,0,3,5,8,"49b505a4",2226,"arcaneShip 1/320, deputShip 1/316, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 1/317, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/347, raidShip 0/300, sargonShip 0/300, siracusaShip 0/306, skillfulShip 0/300, soloShip 0/314, steadShip 0/308, suntShip 1/306, swiftShip 1/308, victoriaShip 1/312, visiShip 0/300, yanShip 0/300",13,13,0,96]
|
||||
},
|
||||
{
|
||||
"round": 11,
|
||||
"ai_0": [1,400,12,0,1,5,8,"0665e01c",2194,"arcaneShip 0/314, deputShip 1/306, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 1/319, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/317, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/315, soloShip 1/304, steadShip 1/329, suntShip 1/304, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",34,34,0,101],
|
||||
"ai_1": [1,400,15,0,3,5,8,"976cdb21",1906,"arcaneShip 1/310, deputShip 0/310, egirShip 0/300, emptyShip 0/300, indomShip 1/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/359, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/308, steadShip 1/314, suntShip 0/300, swiftShip 0/300, victoriaShip 1/315, visiShip 0/300, yanShip 0/300",34,34,0,108]
|
||||
"ai_0": [1,400,12,0,1,5,8,"08857c7b",1899,"arcaneShip 1/328, deputShip 0/306, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 1/315, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/323, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/331, soloShip 1/302, steadShip 0/324, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",34,34,0,101],
|
||||
"ai_1": [1,400,15,0,1,5,8,"3d501b69",2268,"arcaneShip 1/322, deputShip 1/318, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 1/324, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 2/365, raidShip 0/300, sargonShip 0/300, siracusaShip 0/306, skillfulShip 0/300, soloShip 0/314, steadShip 0/308, suntShip 1/308, swiftShip 1/315, victoriaShip 1/316, visiShip 0/300, yanShip 0/300",34,34,0,112]
|
||||
},
|
||||
{
|
||||
"round": 12,
|
||||
"ai_0": [1,400,13,0,0,5,8,"6d98f0bb",2193,"arcaneShip 0/316, deputShip 1/308, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 1/326, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/326, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/317, soloShip 1/306, steadShip 0/329, suntShip 1/306, swiftShip 1/304, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",14,14,0,113],
|
||||
"ai_1": [1,400,15,0,2,6,8,"9c1759d4",2235,"arcaneShip 1/312, deputShip 1/312, egirShip 0/300, emptyShip 0/300, indomShip 1/302, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/374, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/302, soloShip 1/310, steadShip 1/323, suntShip 0/300, swiftShip 0/300, victoriaShip 0/315, visiShip 0/300, yanShip 0/300",16,16,0,122]
|
||||
"ai_0": [1,400,13,0,0,6,8,"d89e40fc",2247,"arcaneShip 1/338, deputShip 0/306, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 1/315, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/323, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/341, soloShip 1/302, steadShip 1/328, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",12,14,2,115],
|
||||
"ai_1": [1,400,13,0,3,6,8,"97f8bb50",2300,"arcaneShip 1/324, deputShip 1/320, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 1/326, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 2/378, raidShip 0/300, sargonShip 0/300, siracusaShip 0/306, skillfulShip 0/300, soloShip 0/314, steadShip 0/308, suntShip 1/310, swiftShip 1/322, victoriaShip 1/320, visiShip 0/300, yanShip 0/300",15,15,0,126]
|
||||
},
|
||||
{
|
||||
"round": 13,
|
||||
"ai_0": [1,400,12,0,0,6,8,"1467bee8",2207,"arcaneShip 0/316, deputShip 1/310, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 1/328, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/328, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/319, soloShip 1/308, steadShip 0/329, suntShip 1/308, swiftShip 1/306, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",41,41,0,125],
|
||||
"ai_1": [1,400,14,0,4,6,8,"aa87d884",1963,"arcaneShip 1/319, deputShip 1/314, egirShip 0/300, emptyShip 0/300, indomShip 1/304, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/396, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/304, soloShip 0/310, steadShip 0/323, suntShip 0/300, swiftShip 0/300, victoriaShip 1/326, visiShip 0/300, yanShip 0/305",43,43,0,139]
|
||||
"ai_0": [1,400,12,0,0,6,8,"2208b5c6",2293,"arcaneShip 1/350, deputShip 0/306, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 1/325, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/328, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/356, soloShip 1/302, steadShip 1/332, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",41,41,0,129],
|
||||
"ai_1": [1,400,15,0,0,6,8,"65aff572",2352,"arcaneShip 1/326, deputShip 1/322, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 1/338, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 2/401, raidShip 0/300, sargonShip 0/300, siracusaShip 0/306, skillfulShip 0/300, soloShip 0/314, steadShip 0/308, suntShip 1/312, swiftShip 1/329, victoriaShip 1/324, visiShip 0/300, yanShip 0/300",41,41,0,141]
|
||||
}
|
||||
],
|
||||
"players": {
|
||||
@@ -1485,9 +1485,9 @@
|
||||
"title": {"id":"comment_3","name":"坚若磐石","picId":"comment_icon_3","text":"目标生命值损失最少"},
|
||||
"trophies": 8,
|
||||
"reward": 425,
|
||||
"stats": {"activatedLayers":2267,"bossDamage":6609998,"dmgDealt":23342655,"gold":149,"itemsEquipped":23,"kills":290,"leaks":0,"lpLost":0,"merges":6,"perfectRounds":13,"refreshes":16},
|
||||
"lineup": ["3_16_b@12,8[3_01_e_a+1_01_e_b]","4_09_b@11,5","3_11_b@11,8[3_02_e_a+6_06_e_a]","1_02_b@10,4[1_02_e_a+4_02_e_a]","5_14_a@10,8[3_03_e_a+1_05_e_a]","6_20_a@9,6","3_20_a@9,8[3_07_e_a+3_08_e_a]","6_05_a@9,9[2_04_e_a+5_02_e_b]"],
|
||||
"bonds": ["arcaneShip 0/316","deputShip 1/312","egirShip 0/300","emptyShip 0/300","indomShip 0/300","investShip 0/300","kazimierzShip 0/302","kjeragShip 1/343","lateranoShip 0/300","maniShip 0/300","miraShip 0/300","preciShip 1/343","raidShip 0/300","sargonShip 0/300","siracusaShip 0/300","skillfulShip 0/319","soloShip 1/310","steadShip 1/341","suntShip 1/310","swiftShip 1/308","victoriaShip 0/300","visiShip 0/300","yanShip 0/300"]
|
||||
"stats": {"activatedLayers":2333,"bossDamage":3297740,"dmgDealt":11942829,"gold":151,"itemsEquipped":16,"kills":277,"leaks":8,"lpLost":0,"merges":3,"perfectRounds":11,"refreshes":21},
|
||||
"lineup": ["2_17_a@12,8[4_12_e_a+4_08_e_b]","1_14_b@11,5","1_09_b@11,8","6_16_a@10,3[4_03_e_b+5_02_e_a]","1_02_a@10,4[3_02_e_a+1_01_e_b]","4_13_a@9,7[1_01_e_a+3_08_e_a]","3_11_a@9,8[3_03_e_b+3_07_e_a]","5_12_a@9,9[1_05_e_a+3_05_e_b]"],
|
||||
"bonds": ["arcaneShip 1/364","deputShip 0/306","egirShip 0/300","emptyShip 0/300","indomShip 0/300","investShip 0/300","kazimierzShip 0/302","kjeragShip 1/325","lateranoShip 0/300","maniShip 0/300","miraShip 0/300","preciShip 1/328","raidShip 0/300","sargonShip 0/300","siracusaShip 0/300","skillfulShip 1/374","soloShip 1/302","steadShip 1/340","suntShip 1/300","swiftShip 0/302","victoriaShip 0/300","visiShip 0/300","yanShip 0/300"]
|
||||
},
|
||||
"ai_1": {
|
||||
"alive": true,
|
||||
@@ -1499,9 +1499,9 @@
|
||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||
"trophies": 8,
|
||||
"reward": 425,
|
||||
"stats": {"activatedLayers":2301,"bossDamage":7282442,"dmgDealt":25059114,"gold":165,"itemsEquipped":31,"kills":276,"leaks":0,"lpLost":0,"merges":5,"perfectRounds":13,"refreshes":38},
|
||||
"lineup": ["2_10_a@11,5[2_03_e_b+3_09_e_a]","5_03_a@11,8[1_02_e_a+5_02_e_a]","1_06_a@10,3[1_05_e_a+3_02_e_a]","3_16_a@10,4[3_03_e_b+4_04_e_b]","5_06_a@10,8[3_09_e_b+3_09_e_b]","4_21_b@9,6[4_06_e_a+3_10_e_b]","5_02_a@9,8[4_09_e_a+3_06_e_b]","1_17_b@9,9[3_08_e_a+1_01_e_a]"],
|
||||
"bonds": ["arcaneShip 1/327","deputShip 1/318","egirShip 0/300","emptyShip 0/300","indomShip 1/308","investShip 0/300","kazimierzShip 0/300","kjeragShip 0/300","lateranoShip 0/300","maniShip 0/300","miraShip 0/300","preciShip 1/404","raidShip 0/300","sargonShip 0/300","siracusaShip 0/300","skillfulShip 1/308","soloShip 0/310","steadShip 0/323","suntShip 1/304","swiftShip 0/300","victoriaShip 1/332","visiShip 0/300","yanShip 0/305"]
|
||||
"stats": {"activatedLayers":2416,"bossDamage":10592658,"dmgDealt":34563753,"gold":166,"itemsEquipped":33,"kills":284,"leaks":0,"lpLost":0,"merges":5,"perfectRounds":13,"refreshes":34},
|
||||
"lineup": ["5_08_a@11,5[4_06_e_a+3_10_e_b]","3_11_b@11,8[5_01_e_a+4_03_e_a]","2_10_b@10,3[1_01_e_a+3_10_e_a]","5_14_a@10,8[3_09_e_b+1_05_e_a]","3_20_b@9,6[4_09_e_a+3_06_e_b]","4_14_a@9,7[2_03_e_b+5_02_e_a]","1_06_b@9,8[2_03_e_b+3_03_e_a]","1_17_b@9,9[1_05_e_b+4_09_e_b]"],
|
||||
"bonds": ["arcaneShip 1/330","deputShip 1/326","egirShip 0/300","emptyShip 0/300","indomShip 0/300","investShip 0/300","kazimierzShip 0/300","kjeragShip 1/342","lateranoShip 0/300","maniShip 0/300","miraShip 0/300","preciShip 2/427","raidShip 0/300","sargonShip 0/300","siracusaShip 0/306","skillfulShip 0/300","soloShip 0/314","steadShip 0/308","suntShip 2/316","swiftShip 1/343","victoriaShip 1/332","visiShip 0/300","yanShip 0/300"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -38,9 +38,45 @@ test('bounty pick: the card the own board can beat, never one it cannot (even wh
|
||||
assert.equal(botPickCard(m, ps, [hard, easy, huge].map((c, idx) => ({ ...c, idx })), [0, 1, 2]), 1);
|
||||
ps.lp = 2;
|
||||
assert.equal(botPickCard(m, ps, [hard, huge, easy].map((c, idx) => ({ ...c, idx })), [0, 1, 2]), 2, 'low LP: still the beatable one');
|
||||
const raw = [hard, easy].map((c) => DATA.choices.cards.bounty.find((x) => x.effectId === c.id));
|
||||
assert.deepEqual(m.offerBountyChoice(ps, raw, 'chess_item_6_03_m'), { ok: true });
|
||||
assert.deepEqual(m.autoPickPersonalChoice(ps, 'bot'), { ok: true });
|
||||
assert.equal(ps.personalChoice, null);
|
||||
assert.equal(ps.bounties.at(-1).card.enemyKey, easy.enemyKey, 'personal cards use bounty scoring too');
|
||||
const metaState = m.rngMeta.state(), botState = m.rngBots.state();
|
||||
m.autoPickPersonalChoice(ps, 'bot');
|
||||
m.autoPickPersonalChoice(ps, 'random');
|
||||
assert.equal(m.rngMeta.state(), metaState);
|
||||
assert.equal(m.rngBots.state(), botState, 'nothing pending: no RNG draws');
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('a bot\'s 教鞭 pick is deterministic: one seed offers the same three cards and picks the same one, drawing only on the bots\' rng', () => {
|
||||
const runs = [];
|
||||
for (let k = 0; k < 2; k++) {
|
||||
const h = soloBot({ seed: 5, difficulty: 'HARD' }).start();
|
||||
const m = h.m;
|
||||
const ps = m.order[0];
|
||||
h.run(() => m.phase === PHASE.PREP && m.round === 1);
|
||||
ps.lp = 999;
|
||||
h.run(() => m.phase === PHASE.PREP && m.round === 5);
|
||||
const art = giveItem(m, ps, 'chess_item_6_03_m');
|
||||
assert.deepEqual(ps.useArt(art.uid, 10, 5), { ok: true });
|
||||
const offered = ps.personalChoice.cards.map((c) => c.effectId);
|
||||
assert.equal(offered.length, 3);
|
||||
assert.equal(new Set(offered).size, 3, 'three different cards');
|
||||
const meta = m.rngMeta.state(), bots = m.rngBots.state();
|
||||
assert.deepEqual(m.autoPickPersonalChoice(ps, 'bot'), { ok: true });
|
||||
assert.equal(m.rngMeta.state(), meta, 'the pick leaves the meta rng alone');
|
||||
assert.notEqual(m.rngBots.state(), bots, 'one scoring draw per card from the bots\' rng');
|
||||
assert.equal(ps.personalChoice, null);
|
||||
runs.push({ offered, picked: ps.bounties.at(-1).card.effectId, bots: m.rngBots.state() });
|
||||
assert.ok(offered.includes(runs[k].picked));
|
||||
m.dispose();
|
||||
}
|
||||
assert.deepEqual(runs[0], runs[1]);
|
||||
});
|
||||
|
||||
test('bounty pick in real drafts (绝境 R3 悬赏决策): a card the board likely beats (p ≥ 0.5) whenever a near-sure one (p ≥ 0.9) is offered', () => {
|
||||
let drafts = 0;
|
||||
for (const seed of [1, 2, 3, 4]) {
|
||||
@@ -138,9 +174,12 @@ test('bounty Arts (教鞭): kept after a battle with leaks, used after a perfect
|
||||
assert.ok(owns(ps, WHIP), 'kept after a battle with leaks (not destroyed)');
|
||||
h.run(() => m.phase === PHASE.PREP && m.round === 4);
|
||||
m.lastResults.set(ps.playerId, clean);
|
||||
giveItem(m, ps, WHIP);
|
||||
const bounties = ps.bounties.length;
|
||||
h.run(() => m.phase === PHASE.COMBAT && m.round === 4);
|
||||
assert.ok(!owns(ps, WHIP) && ps.bounties.length > bounties, 'used after a perfect battle');
|
||||
assert.ok(!owns(ps, WHIP) && ps.bounties.length === bounties + 2, 'both Arts used and picked after a perfect battle');
|
||||
assert.equal(ps.personalChoice, null);
|
||||
assert.equal(ps.round.arts, 2);
|
||||
assert.equal(m.errorCount, 0);
|
||||
m.dispose();
|
||||
}
|
||||
|
||||
+27
-1
@@ -4,7 +4,7 @@ import assert from 'node:assert/strict';
|
||||
import { PHASE } from '../../shared/constants.js';
|
||||
import { fieldModel, planLayout, rehearse, rangeTiles, REHEARSAL_VARIANTS, LAYOUT_PARAMS, botPickCard, botPickBand } from '../../server/match/bot.js';
|
||||
import { FIELD, canPlace, placeClass, parseKey } from '../../server/match/board.js';
|
||||
import { makeMatch, checkInvariants, give, DATA } from './harness.js';
|
||||
import { makeMatch, checkInvariants, give, giveItem, DATA } from './harness.js';
|
||||
|
||||
const soloBot = (o = {}) => makeMatch({ mode: 'solo', difficulty: 'NORMAL', seats: [{ seat: 0, playerId: 'ai_0', name: 'AI', isBot: true, connected: true }], ...o });
|
||||
|
||||
@@ -222,6 +222,32 @@ test('a sliced rehearsal is dropped when the prep ends first; the default layout
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('AI takeover resolves a personal choice in untimed PREP, including generator-error Ready cleanup', () => {
|
||||
for (const broken of [false, true]) {
|
||||
const h = makeMatch({ mode: 'coop', humans: 1, seed: 6, fake: true, botSliceMs: 0 }).start();
|
||||
h.toPrep(1);
|
||||
const m = h.m, ps = h.ps('p_0');
|
||||
assert.equal(m.deadline, 0, 'single-human co-op is untimed');
|
||||
const art = giveItem(m, ps, 'chess_item_6_03_m');
|
||||
assert.deepEqual(ps.useArt(art.uid, 10, 5), { ok: true });
|
||||
const pick = m.autoPickPersonalChoice.bind(m), modes = [];
|
||||
m.autoPickPersonalChoice = (p, mode) => {
|
||||
if (p.personalChoice) modes.push(mode);
|
||||
if (broken && mode === 'bot') throw new Error('scripted choice scoring failure');
|
||||
return pick(p, mode);
|
||||
};
|
||||
assert.deepEqual(m.handle(ps.playerId, { t: 'g.autoplay', on: true }), { ok: true });
|
||||
assert.ok(h.run(() => m.phase !== PHASE.PREP));
|
||||
assert.equal(ps.personalChoice, null);
|
||||
assert.equal(ps.bounties.length, 1);
|
||||
assert.equal(ps.round.arts, 1);
|
||||
assert.ok(ps.ready);
|
||||
assert.ok(modes.includes(broken ? 'random' : 'bot'));
|
||||
assert.equal(m.errorCount > 0, broken);
|
||||
m.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('economy: the bot fills the board first (8 units by round 4), levels on its curve and keeps a hand slot free', () => {
|
||||
for (const seed of [5, 8]) {
|
||||
const h = soloBot({ seed }).start();
|
||||
|
||||
@@ -4,7 +4,7 @@ import assert from 'node:assert/strict';
|
||||
import { PHASE, ERR } from '../../shared/constants.js';
|
||||
import { Match } from '../../server/match/Match.js';
|
||||
import { StubMatch } from '../../server/match/StubMatch.js';
|
||||
import { DATA, makeMatch, checkInvariants, give, chessOfTier } from './harness.js';
|
||||
import { DATA, makeMatch, checkInvariants, give, giveItem, chessOfTier } from './harness.js';
|
||||
import { bossPoolHp } from '../../server/match/finalAssault.js';
|
||||
|
||||
const STATUSES = ['acting', 'ready', 'deciding', 'combat', 'done', 'helping', 'left', 'dead'];
|
||||
@@ -51,6 +51,7 @@ test('m.private shape in PREP matches DESIGN §8.3 (pieces, slots, bonds, effect
|
||||
assert.ok(s.kind === 'chess' || s.kind === 'item');
|
||||
}
|
||||
assert.equal(priv.shop.rewardOffer, null);
|
||||
assert.equal(priv.personalChoice, null);
|
||||
assert.equal(priv.deployCap, 8);
|
||||
assert.ok(priv.effects.some((e) => e.iconKind === 'band' && e.name));
|
||||
assert.ok(priv.nextEnemies.length > 0);
|
||||
@@ -109,6 +110,45 @@ test('disconnect: the seat keeps playing; draft turns / prep auto-resolve at dea
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('教鞭: simultaneous private choices survive reconnect; foreign, duplicate and old IDs cannot add bounties', () => {
|
||||
const h = makeMatch({ mode: 'coop', humans: 2, spectators: ['watcher'], seed: 63, fake: true }).start();
|
||||
h.toPrep(1);
|
||||
const m = h.m, a = h.ps('p_0'), b = h.ps('p_1');
|
||||
for (const ps of [a, b]) {
|
||||
const art = giveItem(m, ps, 'chess_item_6_03_m');
|
||||
assert.deepEqual(ps.useArt(art.uid, 10, 5), { ok: true });
|
||||
}
|
||||
const aChoice = a.privateView().personalChoice, bChoice = b.privateView().personalChoice;
|
||||
assert.notEqual(aChoice.id, bChoice.id);
|
||||
assert.equal(m.handle('p_0', { t: 'g.choice', idx: 0, choiceId: bChoice.id }).error, ERR.BAD_TARGET);
|
||||
assert.equal(a.bounties.length + b.bounties.length, 0);
|
||||
m.onDisconnect('p_0');
|
||||
m.onReconnect('p_0');
|
||||
assert.deepEqual(h.lastTo('p_0', 'm.private').personalChoice, aChoice);
|
||||
m.flush(true);
|
||||
assert.deepEqual(h.lastTo('p_1', 'm.private').personalChoice, bChoice);
|
||||
assert.ok(!h.allTo('watcher', 'm.private').length);
|
||||
assert.ok(!JSON.stringify(m.publicView()).includes(aChoice.id));
|
||||
assert.ok(!JSON.stringify(m.prepFieldMeta(a)).includes(aChoice.id));
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.choice', idx: 1, choiceId: aChoice.id }), { ok: true });
|
||||
m.flush(true);
|
||||
assert.equal(h.lastTo('p_0', 'm.private').personalChoice, null);
|
||||
assert.equal(m.handle('p_0', { t: 'g.choice', idx: 1, choiceId: aChoice.id }).error, ERR.BAD_TARGET);
|
||||
const next = giveItem(m, a, 'chess_item_6_03_m');
|
||||
assert.deepEqual(a.useArt(next.uid, 10, 5), { ok: true });
|
||||
const nextId = a.personalChoice.id;
|
||||
assert.notEqual(nextId, aChoice.id);
|
||||
assert.equal(m.handle('p_0', { t: 'g.choice', idx: 0, choiceId: aChoice.id }).error, ERR.BAD_TARGET);
|
||||
assert.equal(a.personalChoice.id, nextId);
|
||||
assert.equal(a.bounties.length, 1);
|
||||
m.onDisconnect('p_1');
|
||||
h.sched.advance(m.deadline - h.sched.now() + 1);
|
||||
m.onReconnect('p_1');
|
||||
assert.equal(h.lastTo('p_1', 'm.private').personalChoice, null);
|
||||
assert.equal(b.bounties.length, 1, 'offline choice picked at the existing PREP deadline');
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('g.autoplay lets the bot play a human seat (buys, places, readies); turning it off returns control', () => {
|
||||
const h = makeMatch({ mode: 'solo', seed: 64, fake: true }).start();
|
||||
const m = h.m;
|
||||
|
||||
@@ -150,6 +150,8 @@ test('机变 (co-op): 6 shared cards, random order, 30 s first / 16 s others, ti
|
||||
assert.deepEqual(sp.order.slice().sort(), ['p_0', 'p_1', 'p_2']);
|
||||
assert.equal(Math.round((m2.deadline - h2.sched.now()) / 1000), 30, 'first picker 30 s');
|
||||
const [a, b, c] = sp.order;
|
||||
assert.equal(m2.handle(a, { t: 'g.choice', idx: 2, choiceId: 'old.choice.1' }).error, ERR.WRONG_PHASE);
|
||||
assert.deepEqual(m2.sp.picks, {}, 'personal IDs never fall through to the global draft');
|
||||
assert.equal(m2.handle(b, { t: 'g.choice', idx: 0 }).error, ERR.NOT_YOUR_TURN);
|
||||
assert.deepEqual(m2.handle(a, { t: 'g.choice', idx: 2 }), { ok: true });
|
||||
assert.equal(m2.handle(a, { t: 'g.choice', idx: 3 }).error, ERR.ALREADY);
|
||||
|
||||
@@ -214,6 +214,15 @@ test('ready gating: temp must be empty (a freed hand slot pulls the temp piece i
|
||||
assert.equal(a.hand[3], t);
|
||||
assert.ok(a.tempEmpty);
|
||||
assert.equal(a.privateView().canReady, true);
|
||||
const art = giveItem(m, a, 'chess_item_6_03_m', 'temp');
|
||||
assert.deepEqual(a.useArt(art.uid, 10, 5), { ok: true });
|
||||
assert.ok(a.tempEmpty);
|
||||
assert.equal(a.privateView().canReady, false);
|
||||
assert.deepEqual(a.setReady(true), { error: ERR.BAD_TARGET, detail: '请先完成教鞭选择' });
|
||||
assert.deepEqual(a.setReady(false), { ok: true });
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.choice', idx: 0, choiceId: a.personalChoice.id }), { ok: true });
|
||||
assert.equal(a.privateView().canReady, true);
|
||||
assert.equal(a.round.arts, 1);
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.ready', ready: true }), { ok: true });
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.refresh' }), { error: ERR.WRONG_PHASE, detail: 'ready' });
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.ready', ready: false }), { ok: true });
|
||||
@@ -235,13 +244,26 @@ test('prep deadline (co-op): unready players are auto-readied and their temp is
|
||||
const a = h.ps('p_0');
|
||||
const id = chessOfTier(1).find((x) => m.pool.has(x));
|
||||
const left = m.pool.left(id);
|
||||
const art = giveItem(m, a, 'chess_item_6_03_m');
|
||||
assert.deepEqual(a.useArt(art.uid, 10, 5), { ok: true });
|
||||
fillHand(a);
|
||||
give(m, a, id, 'temp');
|
||||
assert.ok(!a.tempEmpty, 'a full hand: it waits in temp');
|
||||
const endPrep = m.endPrep.bind(m);
|
||||
let checked = false;
|
||||
m.endPrep = () => {
|
||||
assert.equal(a.personalChoice, null, 'deadline picks before endPrep builds the battle');
|
||||
assert.equal(a.bounties.length, 1);
|
||||
assert.equal(a.round.arts, 1);
|
||||
assert.ok(a.ready && a.tempEmpty);
|
||||
checked = true;
|
||||
endPrep();
|
||||
};
|
||||
assert.ok(m.deadline > h.sched.now(), 'co-op prep is timed');
|
||||
assert.equal(Math.round((m.deadline - h.sched.now()) / 1000), m.gd.prepTime(1));
|
||||
h.sched.advance(m.deadline - h.sched.now() + 1);
|
||||
assert.notEqual(m.phase, PHASE.PREP);
|
||||
assert.ok(checked);
|
||||
assert.ok(a.tempEmpty);
|
||||
assert.equal(m.pool.left(id), left, 'temp chess sold back to the pool');
|
||||
checkInvariants(m);
|
||||
@@ -252,7 +274,12 @@ test('solo prep and 机变 are untimed', () => {
|
||||
const h = makeMatch({ mode: 'solo', difficulty: 'NORMAL', seed: 13 }).start();
|
||||
h.toPrep(1);
|
||||
assert.equal(h.m.deadline, 0);
|
||||
const ps = h.ps('p_0');
|
||||
const art = giveItem(h.m, ps, 'chess_item_6_03_m');
|
||||
assert.deepEqual(ps.useArt(art.uid, 10, 5), { ok: true });
|
||||
const id = ps.personalChoice.id;
|
||||
h.sched.advance(10 * 60 * 1000);
|
||||
assert.equal(h.m.phase, PHASE.PREP);
|
||||
assert.equal(ps.personalChoice.id, id, 'no separate personal-choice timer');
|
||||
h.m.dispose();
|
||||
});
|
||||
@@ -11,7 +11,7 @@ import { fieldModel } from '../../server/match/bot.js';
|
||||
import { FIELD, parseKey } from '../../server/match/board.js';
|
||||
import { createRng } from '../../server/sim/rng.js';
|
||||
import { FakeBattle } from './fakeBattle.js';
|
||||
import { DATA, makeMatch, checkInvariants } from './harness.js';
|
||||
import { DATA, makeMatch, checkInvariants, giveItem } from './harness.js';
|
||||
|
||||
const bossFields = () => FakeBattle.instances.filter((b) => b.kind === 'boss' || b.kind === 'hidden');
|
||||
|
||||
@@ -90,6 +90,40 @@ test('perfect-payout bounties in the boss rounds: a 战术特训 card\'s coins j
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('教鞭 in the boss rounds (R14, R15): the card the owner chose spawns in the boss battle and pays at the next income, the Hidden Core takes another choice', () => {
|
||||
const h = makeMatch({ mode: 'coop', difficulty: 'NORMAL', humans: 2, seed: 61, fake: true,
|
||||
script: (b) => (b.kind === 'boss' || b.kind === 'hidden' ? { bossDps: 1e9, coins: { p_0: 3 } } : {}) }).start();
|
||||
const m = h.m;
|
||||
h.toPrep(14);
|
||||
const ps = h.ps('p_0');
|
||||
const art = giveItem(m, ps, 'chess_item_6_03_m');
|
||||
assert.deepEqual(ps.useArt(art.uid, 10, 5), { ok: true });
|
||||
assert.equal(ps.bounties.length, 0, 'nothing is applied before the owner confirms');
|
||||
assert.equal(ps.setReady(true).error, 'BAD_TARGET', 'Ready waits for the choice');
|
||||
const card = ps.personalChoice.cards[2];
|
||||
assert.deepEqual(m.handle(ps.playerId, { t: 'g.choice', idx: 2, choiceId: ps.personalChoice.id }), { ok: true });
|
||||
assert.equal(ps.bounties[0].card.effectId, card.effectId, 'the card of the tapped slot');
|
||||
const pending = ps.pendingFunds;
|
||||
h.drive(() => m.phase === PHASE.FINAL_ASSAULT);
|
||||
m.hiddenLayerSum = 1e6;
|
||||
assert.ok(bossFields()[0].opts.spawns.some((s) => s.tag === 'bounty' && s.enemyKey === card.enemyKey && s.ownerPlayerId === ps.playerId), 'the chosen card\'s enemies come in the boss battle');
|
||||
h.run(() => m.runner === null);
|
||||
assert.equal(ps.pendingFunds, pending + 3 + card.coin, 'kill coins + the chosen card\'s coins');
|
||||
h.toPrep(15);
|
||||
assert.equal(ps.funds, m.gd.income(15) + pending + 3 + card.coin, 'paid at the R15 income, apart from the base income');
|
||||
const hiddenArt = giveItem(m, ps, 'chess_item_6_03_m');
|
||||
assert.deepEqual(ps.useArt(hiddenArt.uid, 10, 5), { ok: true });
|
||||
const hiddenCard = ps.personalChoice.cards[0];
|
||||
assert.deepEqual(m.handle(ps.playerId, { t: 'g.choice', idx: 0, choiceId: ps.personalChoice.id }), { ok: true });
|
||||
const owed = ps.bounties.filter((b) => b.card.payout === 'perfect').reduce((n, b) => n + b.card.coin, 0);
|
||||
h.drive(() => m.phase === PHASE.HIDDEN_CORE);
|
||||
assert.ok(bossFields().at(-1).opts.spawns.some((s) => s.tag === 'bounty' && s.enemyKey === hiddenCard.enemyKey));
|
||||
h.runToEnd();
|
||||
assert.equal(ps.pendingFunds, 3 + owed, 'the Hidden Core pays by the same rule');
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('boss victory without a perfect own result keeps the kill coins but gives no perfect-card funds', () => {
|
||||
const h = makeMatch({ mode: 'coop', humans: 2, seed: 61, fake: true,
|
||||
script: (b) => (b.kind === 'boss' ? { bossDps: 1e9, leaks: { p_0: 1 }, coins: { p_0: 3 } } : {}) }).start();
|
||||
|
||||
@@ -397,6 +397,22 @@ test('collectViolations reports corrupted state (funds, pool accounting, duplica
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('collectViolations: a 教鞭 choice belongs to its owner\'s open prep only', () => {
|
||||
const { m, ps } = prepSolo({ seed: 922 });
|
||||
const art = giveItem(m, ps, 'chess_item_6_03_m');
|
||||
assert.deepEqual(ps.useArt(art.uid, 10, 5), { ok: true });
|
||||
assert.deepEqual(collectViolations(m), [], 'an open choice in its own prep is fine');
|
||||
const pc = ps.personalChoice;
|
||||
pc.round = m.round - 1;
|
||||
assert.match(collectViolations(m).join('\n'), /personal choice outside its own prep/, 'a choice left from an earlier round');
|
||||
pc.round = m.round;
|
||||
pc.cards = [pc.cards[0], pc.cards[0]];
|
||||
assert.match(collectViolations(m).join('\n'), /personal choice of 2 cards/, 'the same card twice');
|
||||
ps.personalChoice = null;
|
||||
assert.deepEqual(collectViolations(m), []);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('the rule auditor flags an LP loss that does not match the leaks (engine regression guard)', () => {
|
||||
const h = makeMatch({ mode: 'coop', humans: 2, seed: 921, fake: true, script: (b) => (b.kind === 'normal' ? { leaks: Object.fromEntries(b.players.map((p) => [p, 2])) } : {}) }).start();
|
||||
const m = h.m;
|
||||
|
||||
@@ -122,6 +122,7 @@ export function makeMatch(o = {}) {
|
||||
if (idx != null) m.handle(ps.playerId, { t: 'g.choice', idx });
|
||||
}
|
||||
if (ready && m.phase === 'PREP' && ps.alive && !ps.ready) {
|
||||
if (ps.personalChoice) m.handle(ps.playerId, { t: 'g.choice', idx: 0, choiceId: ps.personalChoice.id });
|
||||
if (!ps.tempEmpty) ps.resolveTemp();
|
||||
m.handle(ps.playerId, { t: 'g.ready', ready: true });
|
||||
}
|
||||
|
||||
@@ -42,6 +42,11 @@ test('protocol: g.unitStats { seq? } and g.bandFocus { bandId? } validate; m.uni
|
||||
assert.equal(validateC2S({ t: 'g.bandFocus', bandId: 'band_bldsk' }), null);
|
||||
assert.notEqual(validateC2S({ t: 'g.bandFocus', bandId: 'a b' }), null);
|
||||
assert.ok(S2C.includes('m.unitStats'));
|
||||
assert.equal(validateC2S({ t: 'g.choice', idx: 0 }), null);
|
||||
assert.equal(validateC2S({ t: 'g.choice', idx: 2, choiceId: 'seed-2.choice.12' }), null);
|
||||
for (const choiceId of [null, '', 'a b', 12, 'a'.repeat(65)]) {
|
||||
assert.notEqual(validateC2S({ t: 'g.choice', idx: 0, choiceId }), null);
|
||||
}
|
||||
});
|
||||
|
||||
test('unitStatsEntry: effective stats next to the base, rounded for display; the interval from bat / aspd when absent', () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
phaseMode, phaseBanner, isCombatPhase, isBossPhase, countdownState, phaseTotalSeconds, sortBonds, bondTier, nextThreshold,
|
||||
battleOverSfx, ownRoundLoss, uniteResultBox, battleResultBox, roundResultBox, RESULT_BOX_MS,
|
||||
bondMembers, memberHeadCount, bannedPerBond, priceTone, mergeProgress, shopBlockReason, deploySets, indexPieces, placementContext, canPlace,
|
||||
boardTargets, dropIntent, normalizeDraft, normalizeSp, groupEnemies, factionTypes, snapHud, bossFrac, attackInterval, fmtNum,
|
||||
boardTargets, dropIntent, normalizeDraft, normalizeSp, normalizePersonalChoice, groupEnemies, factionTypes, snapHud, bossFrac, attackInterval, fmtNum,
|
||||
rangeGridBox, shortcutFor, sanitizeSettings, DEFAULT_SETTINGS, normalizeResult, cycleField, fieldLabel, homeFieldId,
|
||||
activeBubbles, sortedPlayers, tileKey, prepCapsuleLabel, prepCamera, dropFailureReason, terrainInfo,
|
||||
} from '../../public/js/ui/gameLogic.js';
|
||||
@@ -532,6 +532,32 @@ describe('drafts', () => {
|
||||
assert.equal(normalizeSp(null), null);
|
||||
assert.equal(normalizeSp({ cards: new Array(9).fill({}) }).cards.length, 6, 'at most 6 cards');
|
||||
});
|
||||
test('personal choice: only the living recipient in the current PREP, independent of the public draft', () => {
|
||||
const pub = { phase: PHASE.PREP, round: 14, sp: { family: 'supply', cards: ['global'] } };
|
||||
const priv = { playerId: 'a', alive: true, canReady: false, temp: [], personalChoice: {
|
||||
id: 'seed.choice.3', round: 14, sourceItemId: 'chess_item_6_03_m',
|
||||
cards: [{ id: 'e1', kind: 'bounty', name: '战术特训', coin: 2 }, { id: 'e2', kind: 'bounty' }],
|
||||
} };
|
||||
const before = JSON.stringify({ pub, priv });
|
||||
const sp = normalizePersonalChoice(pub, priv, 'a');
|
||||
assert.equal(sp.id, priv.personalChoice.id);
|
||||
assert.equal(sp.family, 'bounty');
|
||||
assert.equal(sp.name, '教鞭 · 战术特训');
|
||||
assert.equal(sp.turnPid, 'a');
|
||||
assert.equal(sp.pickOf.size, 0);
|
||||
assert.deepEqual(sp.cards.map((c) => c.idx), [0, 1]);
|
||||
assert.ok(sp.cards.every((c) => c.takenBy === null));
|
||||
assert.equal(JSON.stringify({ pub, priv }), before);
|
||||
assert.equal(shopBlockReason('ready', { priv, editable: true }), '请先完成教鞭选择');
|
||||
assert.equal(shopBlockReason('ready', { priv: { ...priv, temp: [{}] }, editable: true }), '请先完成教鞭选择');
|
||||
assert.match(shopBlockReason('ready', { priv: { ...priv, personalChoice: null }, editable: true }), /临时整备区/);
|
||||
for (const phase of [PHASE.SP_DRAFT, PHASE.COMBAT, PHASE.ROUND_START]) assert.equal(normalizePersonalChoice({ ...pub, phase }, priv, 'a'), null);
|
||||
assert.equal(normalizePersonalChoice(pub, null, 'a'), null, 'spectator has no private state');
|
||||
assert.equal(normalizePersonalChoice(pub, priv, 'b'), null);
|
||||
assert.equal(normalizePersonalChoice(pub, { ...priv, alive: false }, 'a'), null);
|
||||
assert.equal(normalizePersonalChoice({ ...pub, round: 15 }, priv, 'a'), null);
|
||||
assert.equal(normalizePersonalChoice(pub, { ...priv, personalChoice: null }, 'a'), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fields, players, emotes', () => {
|
||||
|
||||
+140
-1
@@ -15,7 +15,7 @@
|
||||
|
||||
import { test, describe, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync, mkdirSync } from 'node:fs';
|
||||
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
@@ -71,6 +71,16 @@ describe('in-match UI (mock harness, headless Chrome)', { skip: !ENABLED && 'set
|
||||
}
|
||||
const mockState = (page) => page.evaluate(() => JSON.parse(JSON.stringify(globalThis.__MOCK__.S().priv)));
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const useWhip = (page) => page.evaluate(async () => {
|
||||
const { net } = await import('/js/net.js');
|
||||
const uid = 900000 + globalThis.__MOCK__.S().requests.length;
|
||||
globalThis.__MOCK__.mutate((S) => {
|
||||
const idx = S.priv.hand.findIndex((p) => !p);
|
||||
S.priv.hand[idx] = { uid, kind: 'item', id: 'chess_item_6_03_m', tier: 6 };
|
||||
});
|
||||
await net.request('g.art', { itemUid: uid, row: 10, col: 5 });
|
||||
return globalThis.__MOCK__.S().priv.personalChoice;
|
||||
});
|
||||
/** Swipe the open direction wheel from its centre towards `dir` and release (research 09 §1.2). */
|
||||
const swipeWheel = async (page, dir = 'RIGHT') => {
|
||||
await page.waitForSelector('.fwheel__dia', { timeout: 4000 });
|
||||
@@ -345,6 +355,135 @@ describe('in-match UI (mock harness, headless Chrome)', { skip: !ENABLED && 'set
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('教鞭: PREP cards confirm twice, use its timer, reset on a new ID; old replies cannot clear a new busy pick', async () => {
|
||||
const { page, problems } = await open('phase=PREP', { render: 'fallback' });
|
||||
await page.click('.gm__gear');
|
||||
await page.waitForSelector('.modal .set-keys');
|
||||
await page.click('.set-key[data-action="ready"]');
|
||||
await page.waitForSelector('.set-key.is-waiting[data-action="ready"]');
|
||||
await page.keyboard.press('KeyW');
|
||||
await page.waitForFunction(() => document.querySelector('.set-key[data-action="ready"]')?.textContent.trim() === 'W');
|
||||
await page.click('.modal__actions .btn--primary');
|
||||
await page.waitForSelector('.modal', { hidden: true });
|
||||
const deadline = await page.evaluate(() => globalThis.__MOCK__.S().pub.deadline);
|
||||
const first = await useWhip(page);
|
||||
await page.waitForSelector('.spov[aria-label="教鞭选择"]');
|
||||
assert.equal(await page.evaluate(() => globalThis.__MOCK__.S().pub.deadline), deadline);
|
||||
assert.equal(await page.$('.spov__order'), null);
|
||||
assert.ok(await page.$('.spov .countdown'));
|
||||
assert.doesNotMatch(await page.$eval('.spov', (el) => el.textContent), /机变阶段|当前轮到|正在决策/);
|
||||
assert.equal(await page.$eval('.readybtn', (el) => el.disabled), true);
|
||||
assert.match(await page.$eval('[data-testid="ready-why"]', (el) => el.textContent), /请先完成教鞭选择/);
|
||||
await page.evaluate(async () => { await (await import('/js/ui/lang.js')).loadLangIndex(); });
|
||||
for (const lang of ['en', 'ja', 'ko', 'zh-TW']) {
|
||||
const messages = JSON.parse(readFileSync(path.join(ROOT, `public/i18n/${lang}.json`), 'utf8'));
|
||||
await page.evaluate(async (code) => {
|
||||
await (await import('/js/ui/lang.js')).switchLang(code);
|
||||
globalThis.__MOCK__.pushPublic(); // this harness has no App/useLang root; render again with the active language
|
||||
}, lang);
|
||||
await page.waitForFunction((label) => document.querySelector('.spov')?.getAttribute('aria-label') === label, {}, messages['教鞭选择']);
|
||||
assert.equal(await page.$eval('.spov__title', (el) => el.textContent), `${messages['教鞭 · 战术特训']}|${messages['请选择一项战术特训']}`);
|
||||
assert.match(await page.$eval('.spov__sub', (el) => el.textContent), new RegExp(messages['休整期结束时未选择将自动选定']));
|
||||
assert.equal(await page.$eval('[data-testid="ready-why"]', (el) => el.textContent), messages['请先完成教鞭选择']);
|
||||
assert.equal((await mockState(page)).personalChoice.id, first.id, 'language switch keeps the same pending offer');
|
||||
}
|
||||
await page.evaluate(async () => { await (await import('/js/ui/lang.js')).switchLang('zh'); globalThis.__MOCK__.pushPublic(); });
|
||||
await page.waitForSelector('.spov[aria-label="教鞭选择"]');
|
||||
const requests = await page.evaluate(() => globalThis.__MOCK__.S().requests.length);
|
||||
for (const key of ['KeyW', 'KeyR', 'KeyF', 'KeyD', 'KeyQ', 'KeyX']) await page.keyboard.press(key);
|
||||
await sleep(150);
|
||||
assert.equal(await page.evaluate(() => globalThis.__MOCK__.S().requests.length), requests, 'background shortcuts send no intents');
|
||||
assert.match(await page.$$eval('.toast', (els) => els.map((el) => el.textContent).join('\n')), /请先完成教鞭选择/);
|
||||
assert.equal(await page.$eval('.readybtn__key', (el) => el.textContent), 'W');
|
||||
await page.click('.spcard.is-pickable');
|
||||
await page.waitForSelector('.spcard.is-armed');
|
||||
await sleep(200); // the overlay attaches its Escape listener in an effect, a frame after the highlight shows
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForFunction(() => !document.querySelector('.spcard.is-armed'));
|
||||
assert.equal((await mockState(page)).personalChoice.id, first.id, 'Escape only removes the highlight');
|
||||
await page.screenshot({ path: path.join(OUT, 'ui-personal-choice-1920.png') });
|
||||
await page.evaluate(async (id) => {
|
||||
const { net } = await import('/js/net.js');
|
||||
const request = net.request;
|
||||
globalThis.__choiceReplies = {};
|
||||
net.request = async (t, fields) => {
|
||||
if (t !== 'g.choice' || fields.choiceId === undefined) return request(t, fields);
|
||||
if (fields.choiceId === id) {
|
||||
const result = await request(t, fields);
|
||||
await new Promise((resolve) => { globalThis.__choiceReplies[fields.choiceId] = resolve; });
|
||||
return result;
|
||||
}
|
||||
await new Promise((resolve) => { globalThis.__choiceReplies[fields.choiceId] = resolve; });
|
||||
return request(t, fields);
|
||||
};
|
||||
}, first.id);
|
||||
await page.click('.spcard.is-pickable');
|
||||
assert.equal((await mockState(page)).personalChoice.id, first.id, 'one tap does not apply it');
|
||||
await page.click('.spcard.is-armed');
|
||||
await page.waitForFunction(() => !document.querySelector('.spov'));
|
||||
const firstRequest = await page.evaluate(() => globalThis.__MOCK__.S().requests.filter(([t]) => t === 'g.choice').at(-1));
|
||||
assert.deepEqual(firstRequest, ['g.choice', { idx: 0, choiceId: first.id }]);
|
||||
const second = await useWhip(page);
|
||||
await page.waitForSelector('.spov');
|
||||
await page.click('.spcard.is-pickable');
|
||||
await page.waitForSelector('.spcard.is-armed');
|
||||
const thirdId = second.id + '.next';
|
||||
await page.evaluate((id) => globalThis.__MOCK__.mutate((S) => { S.pub.deadline = 0; S.priv.personalChoice.id = id; }), thirdId);
|
||||
await page.waitForFunction(() => !document.querySelector('.spcard.is-armed'));
|
||||
assert.equal(await page.$('.spov .countdown'), null, 'co-op PREP without a deadline is untimed');
|
||||
await page.click('.spcard.is-pickable');
|
||||
await page.click('.spov__confirm');
|
||||
await page.waitForSelector('.spcard.is-busy');
|
||||
await page.waitForFunction((id) => !!globalThis.__choiceReplies[id], {}, first.id);
|
||||
await page.evaluate((id) => globalThis.__choiceReplies[id](), first.id);
|
||||
await sleep(150);
|
||||
assert.ok(await page.$('.spcard.is-busy'), 'old reply leaves the newer choice busy');
|
||||
await page.evaluate((id) => globalThis.__choiceReplies[id](), thirdId);
|
||||
await page.waitForFunction(() => !document.querySelector('.spov'));
|
||||
assert.equal((await mockState(page)).personalChoice, null);
|
||||
assert.deepEqual(problems, []);
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('教鞭 appearing cancels the uncommitted direction wheel and active drag, releasing its held piece', async () => {
|
||||
const { page, problems } = await open('phase=PREP', { render: 'fallback' });
|
||||
const s = await mockState(page);
|
||||
const hand = s.hand.find((p) => p && p.kind === 'chess');
|
||||
const target = s.board.find((p) => p.kind === 'chess' && p.row === 9);
|
||||
const selector = `.ff-piece[data-uid="${hand.uid}"]`;
|
||||
const center = (sel) => page.$eval(sel, (el) => { const r = el.getBoundingClientRect(); return { x: r.left + r.width / 2, y: r.top + r.height / 2 }; });
|
||||
const homeStyle = await page.$eval(selector, (el) => el.style.transform);
|
||||
const from = await center(selector), to = await center(`.ff-piece[data-uid="${target.uid}"]`);
|
||||
await page.mouse.move(from.x, from.y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(to.x, to.y, { steps: 12 });
|
||||
await sleep(80);
|
||||
await page.mouse.up();
|
||||
await page.waitForSelector('.fwheel');
|
||||
await useWhip(page);
|
||||
await page.waitForSelector('.spov');
|
||||
await page.waitForFunction(() => !document.querySelector('.fwheel'));
|
||||
await page.waitForFunction((sel, transform) => document.querySelector(sel)?.style.transform === transform, {}, selector, homeStyle);
|
||||
assert.equal(await page.$eval(selector, (el) => el.style.transform), homeStyle, 'cancelFacing released the held bench piece');
|
||||
assert.equal(await page.$('.ff-piece.is-draggable'), null);
|
||||
await page.click('.spcard.is-pickable');
|
||||
await page.click('.spov__confirm');
|
||||
await page.waitForFunction(() => !document.querySelector('.spov'));
|
||||
const current = await center(selector);
|
||||
await page.mouse.move(current.x, current.y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(current.x + 90, current.y - 40, { steps: 8 });
|
||||
await page.waitForSelector('.gm.is-dragging');
|
||||
await useWhip(page);
|
||||
await page.waitForSelector('.spov');
|
||||
await page.waitForFunction(() => !document.querySelector('.gm.is-dragging, .ff-piece.is-lifted'));
|
||||
await page.mouse.up();
|
||||
assert.deepEqual((await mockState(page)).board, s.board);
|
||||
assert.ok(!(await page.evaluate(() => globalThis.__MOCK__.S().requests.some(([t]) => t === 'g.move'))));
|
||||
assert.deepEqual(problems, []);
|
||||
await page.close();
|
||||
});
|
||||
|
||||
// ---- round-1 UI fixes ----------------------------------------------------------------------------------------
|
||||
const rectOf = (page, sel) => page.$eval(sel, (el) => { const r = el.getBoundingClientRect(); return { left: r.left, top: r.top, right: r.right, bottom: r.bottom }; });
|
||||
const overlaps = (a, b) => !!a && !!b && a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
|
||||
|
||||
@@ -345,7 +345,7 @@ describe('user playtest #2 item 10 — boss-round prep on the boss field (real s
|
||||
});
|
||||
|
||||
test('co-op 险境 (R14): host on the left half, guest on the mirrored right half; scouting and 返回 keep the pen hidden', { skip: only('coop'), timeout: 10 * 60 * 1000 }, async () => {
|
||||
const srv = await startRealServer({ fast: { timerScale: 0.5, combatSpeed: 8, startRound: 'boss' } });
|
||||
const srv = await startRealServer({ fast: { timerScale: 1, combatSpeed: 8, startRound: 'boss', items: ['chess_item_6_03_m'] } });
|
||||
const P = await pptr();
|
||||
const host = new Client(P, srv.base, 'host', { prefix: 'fix' });
|
||||
const guest = new Client(P, srv.base, 'guest', { prefix: 'fix', w: 1280, h: 720 });
|
||||
@@ -412,6 +412,47 @@ describe('user playtest #2 item 10 — boss-round prep on the boss field (real s
|
||||
assert.deepEqual(back.prepField, { kind: 'bossPrep', side: 'L', mirror: false }, '返回: the own boss half again');
|
||||
}
|
||||
await penRoundTrip(guest, 'guest');
|
||||
// Real clients open their own 教鞭 candidates together; refreshing one tab restores that same private choice.
|
||||
const choiceOf = (c) => c.page.evaluate(() => JSON.parse(JSON.stringify(globalThis.__SP__.store.get().match.private.personalChoice)));
|
||||
const deadline = await host.page.evaluate(() => globalThis.__SP__.store.get().match.public.deadline);
|
||||
await Promise.all([[host, hp], [guest, gp]].map(async ([c, placed]) => {
|
||||
const art = (await c.handPieces('item')).find((p) => p.id === 'chess_item_6_03_m');
|
||||
assert.ok(art, `${c.label}: starter 教鞭`);
|
||||
await c.drag(await c.piecePoint(art.uid), await c.tilePoint(placed.row, placed.col));
|
||||
await c.page.waitForSelector('.spov[aria-label="教鞭选择"]', { timeout: 6000 });
|
||||
}));
|
||||
const hostChoice = await choiceOf(host), guestChoice = await choiceOf(guest);
|
||||
assert.notEqual(hostChoice.id, guestChoice.id);
|
||||
for (const c of [host, guest]) {
|
||||
assert.equal((await choiceOf(c)).cards.length, 3);
|
||||
assert.equal((await c.st()).canReady, false);
|
||||
assert.equal(await c.page.$('.spov__order'), null);
|
||||
assert.equal(await c.page.evaluate(() => globalThis.__SP__.store.get().match.public.deadline), deadline);
|
||||
}
|
||||
await host.page.reload({ waitUntil: 'networkidle0' });
|
||||
await host.page.waitForSelector('.spov[aria-label="教鞭选择"]', { timeout: 20000 });
|
||||
assert.deepEqual(await choiceOf(host), hostChoice, 'refresh reuses the ID and cards');
|
||||
assert.deepEqual(await choiceOf(guest), guestChoice, 'the teammate keeps its own candidates');
|
||||
await host.page.evaluate(async () => { await (await import('/js/ui/lang.js')).switchLang('en'); });
|
||||
await host.page.waitForSelector('.spov[aria-label="Pointing Stick Choice"]');
|
||||
assert.match(await host.page.$eval('.spov__title', (el) => el.textContent), /Pointing Stick · Tactical Training/);
|
||||
assert.equal(await host.page.$eval('[data-testid="ready-why"]', (el) => el.textContent), 'Complete the Pointing Stick choice first');
|
||||
assert.equal(await guest.page.$eval('.spov', (el) => el.getAttribute('aria-label')), '教鞭选择', 'language stays per client');
|
||||
assert.deepEqual(await choiceOf(host), hostChoice, 'language switch does not replace the offer');
|
||||
await host.page.evaluate(async () => { await (await import('/js/ui/lang.js')).switchLang('zh'); });
|
||||
await host.page.waitForSelector('.spov[aria-label="教鞭选择"]');
|
||||
const refused = await host.page.evaluate(async (id) => {
|
||||
try { await globalThis.__SP__.net.request('g.choice', { idx: 0, choiceId: id }); return null; }
|
||||
catch (e) { return e.code; }
|
||||
}, guestChoice.id);
|
||||
assert.equal(refused, 'BAD_TARGET', 'a live client cannot submit the teammate\'s ID');
|
||||
await host.shot('dobermann-private-reconnect');
|
||||
for (const c of [host, guest]) {
|
||||
await c.click('.spcard.is-pickable');
|
||||
assert.ok(await choiceOf(c), 'first tap only highlights');
|
||||
await c.click('.spov__confirm');
|
||||
await c.page.waitForFunction(() => globalThis.__SP__.store.get().match.private.personalChoice === null, { timeout: 6000 });
|
||||
}
|
||||
for (const c of [host, guest]) await c.click('.readybtn');
|
||||
await checkBossBattle(host, hp, 'L', 'host');
|
||||
await checkBossBattle(guest, gp, 'R', 'guest');
|
||||
|
||||
@@ -222,6 +222,18 @@ describe('3: the temp overflow row (临时整备区)', () => {
|
||||
assert.deepEqual(tempInfo(null), { count: 0, items: 0 });
|
||||
});
|
||||
|
||||
test('Ready shows the personal-choice reason and describes it even with no temp pieces', () => {
|
||||
for (const temp of [[], [{ uid: 1, kind: 'item' }]]) {
|
||||
const v = ReadyToggle({ priv: { ...priv(temp), canReady: false, personalChoice: { id: 'choice.1' } }, onToggle() {} });
|
||||
const nodes = [...walk(v)];
|
||||
const btn = nodes.find((n) => n.type === 'button');
|
||||
assert.equal(btn.props.disabled, true);
|
||||
assert.equal(btn.props['aria-describedby'], 'readywrap-why');
|
||||
assert.equal(textOf(nodes.find((n) => hasClass(n, 'readywrap__why'))), '请先完成教鞭选择');
|
||||
assert.equal(nodes.find((n) => n.type?.name === 'Tooltip').props.text, '请先完成教鞭选择');
|
||||
}
|
||||
});
|
||||
|
||||
test('the row frame: the outer corners of both end tiles, the label on the side with room (mirrored Final Assault prep too)', () => {
|
||||
// tileScreen polys: [back-left, back-right, front-right, front-left] (render/app.js tileScreen corner order)
|
||||
const tile = (cx, y, w = 100, d = 60) => ({ x: cx, y, s: w, poly: [[cx - w / 2 + 6, y - d / 2], [cx + w / 2 - 6, y - d / 2], [cx + w / 2, y + d / 2], [cx - w / 2, y + d / 2]] });
|
||||
@@ -258,7 +270,7 @@ describe('3: the temp overflow row (临时整备区)', () => {
|
||||
test('while the ready button shows its reason the effects column moves down a line (on phones the ready count ran into it)', () => {
|
||||
const game = read('public/js/screens/game.js');
|
||||
// the same condition as ReadyToggle's reason: PREP, alive, not ready, pieces in the row
|
||||
assert.match(game, /const readyWhy = phase === PHASE\.PREP && alive && !priv\?\.ready && temp\.count > 0;/);
|
||||
assert.match(game, /const readyWhy = phase === PHASE\.PREP && alive && !priv\?\.ready && \(hasPersonalChoice \|\| temp\.count > 0\);/);
|
||||
assert.match(game, /readyWhy && 'has-readywhy'/);
|
||||
const css = read('public/css/screens/game.css');
|
||||
assert.match(css, /\.gm__effects \{ position: absolute; right: \.3rem; top: 2\.4rem; \}/);
|
||||
@@ -389,6 +401,18 @@ describe('9: busy indicators and data loading', () => {
|
||||
assert.ok(![...walk(landed)].some((n) => hasClass(n, 'spcard__busy')));
|
||||
});
|
||||
|
||||
test('choice requests carry the personal ID while global requests keep their existing shape', async () => {
|
||||
const { net } = await import('../../public/js/net.js');
|
||||
const { actions } = await import('../../public/js/ui/gameActions.js');
|
||||
const request = net.request, sent = [];
|
||||
net.request = async (t, fields) => { sent.push([t, fields]); };
|
||||
try {
|
||||
assert.equal(await actions.choice(0), true);
|
||||
assert.equal(await actions.choice(2, 'seed.choice.3'), true);
|
||||
assert.deepEqual(sent, [['g.choice', { idx: 0 }], ['g.choice', { idx: 2, choiceId: 'seed.choice.3' }]]);
|
||||
} finally { net.request = request; }
|
||||
});
|
||||
|
||||
test('the "作战结束,等待队友完成作战" hourglass stands still', () => {
|
||||
const css = read('public/css/screens/game.css');
|
||||
assert.doesNotMatch(css, /hg-flip/);
|
||||
|
||||
@@ -130,6 +130,28 @@ describe('2: 机变 cards take two taps (select → confirm), like buying in the
|
||||
assert.match(textOf(lone.find((n) => hasClass(n, 'spov__sub'))), /无时间限制/);
|
||||
});
|
||||
|
||||
test('教鞭 ChoiceView: personal heading, no global order or turn; timing follows PREP deadline', () => {
|
||||
const s = sp({ name: '教鞭 · 战术特训', desc: '请选择一项战术特训', cards: sp().cards.slice(0, 2) });
|
||||
for (const [solo, deadline] of [[false, 0], [true, 123456]]) {
|
||||
let confirmed = false;
|
||||
const v = ChoiceView({ pub: { players: [{ playerId: 'me', name: 'Me' }, { playerId: 'p2', name: 'P2' }], deadline }, sp: s,
|
||||
...me, solo, personal: true, armed: 0, total: 74, onConfirm: () => { confirmed = true; } });
|
||||
const nodes = [...walk(v)];
|
||||
assert.equal(v.props['aria-label'], '教鞭选择');
|
||||
assert.ok(!nodes.some((n) => hasClass(n, 'spov__order')));
|
||||
assert.doesNotMatch(textOf(v), /机变阶段|当前轮到|正在决策/);
|
||||
const clock = nodes.find((n) => n.type?.name === 'Countdown');
|
||||
assert.equal(!!clock, deadline > 0);
|
||||
if (clock) { assert.equal(clock.props.deadline, deadline); assert.equal(clock.props.total, 74); }
|
||||
assert.match(textOf(nodes.find((n) => hasClass(n, 'spov__sub'))), deadline ? /休整期结束时未选择将自动选定/ : /无时间限制/);
|
||||
const btn = nodes.find((n) => n.type === Button);
|
||||
btn.props.onClick();
|
||||
assert.ok(confirmed);
|
||||
assert.match(btn.props.title, /确认选择「A」/);
|
||||
assert.match(nodes.find((n) => hasClass(n, 'spcard')).props.title, /^A\na$/);
|
||||
}
|
||||
});
|
||||
|
||||
test('the overlay keeps the selection itself and drops it on Esc / a tap elsewhere (source contract)', () => {
|
||||
const src = read('public/js/ui/choiceOverlay.js');
|
||||
assert.match(src, /export function ChoiceOverlay\(props\) \{[\s\S]*useState\(null\)[\s\S]*armedCard\(sel, sp/);
|
||||
|
||||
Reference in new issue
Block a user