feedback1 cell: bots let their placement step judge the operator a 突变细胞 transformation gained

With the bench rule the gained operator starts the next prep on the bench while its carrier's deployment
is free. The buy loop's pre-emptive bench shed sells the weakest bench single by piece value while the
bench is crowded, before the placement step runs: in 20 solo 绝境 matches on 昆图斯 it sold 44 of 222
gained operators that way.

- bot.js: botPrepEndSteps remembers the chess owned when the prep ends (rememberOwned); the shed's
  keepFresh now also spares a piece gained since then (a transformation's operator, battle-result /
  round-start grants), so the lineup step decides about it. No memory yet (a first prep, a seat just
  put under AI 托管) changes nothing.
- botbench, decisions only: 昆图斯 (seeds 201-220) 绝境 7 -> 11 / 20 wins with the fix (8 on the WA
  build), 险境 16 -> 17 / 20 (15); the bots' own strategies unchanged within noise (solo 绝境 8 / 12 both,
  solo 险境 19 / 24 both, co-op 绝境 4 AI 9 -> 8 / 36).
- Test: a real solo 绝境 昆图斯 bot match (seed 9 sold its R7 operator in the R8 prep before the fix):
  no gained operator shed in its first prep, none waiting on the bench beside a free deployment.
- DESIGN §21.1 / §21.6 / §6.6, META and the CHANGELOG line say so.
This commit is contained in:
sganggs committed 2026-10-03 15:47:51 +08:00
1 parent 2981ff7ae6
commit 3dcbcd572b
5 files changed
+69 -11

No files matched your search

+1 -1
View File
@@ -41,7 +41,7 @@
- 拉普兰德:获得她之后本回合的首次刷新也会叠叙拉古层数,每名拉普兰德各自计算。
- 昆图斯的突变细胞不会用完消失:每场战斗后装备者换成高一阶的随机干员,突变细胞和其他装备退回整备区,可以再次配发。
- 突变细胞换来的新干员和官方一样进入整备区,原来的格子空出来、可放置角色加 1,需要自己重新部署;凑齐同名干员时照常合成精锐(PR #2 指出)。
- 突变细胞换来的新干员和官方一样进入整备区,原来的格子空出来、可放置角色加 1,需要自己重新部署(人机和 AI 托管会照常部署它);凑齐同名干员时照常合成精锐(PR #2 指出)。
- 标准模拟按官方禁用奥术等盟约,卡片和盟约详情标明「本局禁用」。
- 变形同构体配转职装备时,盟约成员列表和干员卡片算上装备者(标「同构」)。
- 带奥术法阵时,场上炎佑造成伤害也会使目标失去特殊能力 5 秒;带源石溶剂时炎佑每秒受到 60 点伤害。
+4 -4
View File
@@ -384,7 +384,7 @@ Per match: pick stage (weighted among the mode's `config.modes[m].stages`, weigh
### 6.6 AI players (bot.js)
Deterministic-ish heuristic player used for (a) AI teammates, (b) auto-play for disconnected humans (only auto-ready + auto-picks, not shopping, unless "AI 托管" is toggled), (c) tests. Strategy (§21.6): pick a band (weighted by starting LP); each prep: take offers, sell dead bench weight, fill the board, complete affordable merges, level on the curve, then buy only merge progress / lineup upgrades worth more than the refreshes their price would buy (shop odds of completing held pairs), freeze for an unaffordable third copy (坎诺特 keeps 5 funds back); build around a committed focus core bond (a teammate's main bond on its strip counts against it; bonds the mode switches off never) + a second bond; value dealers by DPS vs the round's DEF / RES (an attack on every enemy in range counts double — 阵法术师 / 轰击术师), with the range the unit is deployed with; place by the exposure model + rehearsal on the server's deploy map (no 深水区; a tactician's 援军 within `PlayerState.summonRange`; `g.move` judges legality); items by effect (META §1.5: 突变细胞 on the least valuable normal single below 6阶, a cell left in temp gets a hand slot made for it, the operator its transformation gains is deployed by the next prep's lineup step like any owned unit — AI 托管 too, §21.1); 机变 bounties by expected payout − expected LP loss over the offered cards; resolve temp — on a human seat under AI 托管 never by destroying an item while a bench operator can be sold for the slot —; ready. A bot's whole prep — shop decisions and layout planning (`botPrepBeginSteps`, 25–70 ms late in a 4-bot match), the layout rehearsal and `botPrepEndSteps` — runs in wall-clock slices of `botSliceMs` (8 ms) per scheduler callback on a real host (step generators that yield between whole actions, never with a transient board; the same actions in the same order, so decisions are unchanged; at once in virtual time).
Deterministic-ish heuristic player used for (a) AI teammates, (b) auto-play for disconnected humans (only auto-ready + auto-picks, not shopping, unless "AI 托管" is toggled), (c) tests. Strategy (§21.6): pick a band (weighted by starting LP); each prep: take offers, sell dead bench weight, fill the board, complete affordable merges, level on the curve, then buy only merge progress / lineup upgrades worth more than the refreshes their price would buy (shop odds of completing held pairs), freeze for an unaffordable third copy (坎诺特 keeps 5 funds back); build around a committed focus core bond (a teammate's main bond on its strip counts against it; bonds the mode switches off never) + a second bond; value dealers by DPS vs the round's DEF / RES (an attack on every enemy in range counts double — 阵法术师 / 轰击术师), with the range the unit is deployed with; place by the exposure model + rehearsal on the server's deploy map (no 深水区; a tactician's 援军 within `PlayerState.summonRange`; `g.move` judges legality); items by effect (META §1.5: 突变细胞 on the least valuable normal single below 6阶, a cell left in temp gets a hand slot made for it, the operator its transformation gains is deployed by the next prep's lineup step like any owned unit — AI 托管 too; the buy loop's bench shed never sells a piece gained since the bot's last prep, §21.1); 机变 bounties by expected payout − expected LP loss over the offered cards; resolve temp — on a human seat under AI 托管 never by destroying an item while a bench operator can be sold for the slot —; ready. A bot's whole prep — shop decisions and layout planning (`botPrepBeginSteps`, 25–70 ms late in a 4-bot match), the layout rehearsal and `botPrepEndSteps` — runs in wall-clock slices of `botSliceMs` (8 ms) per scheduler callback on a real host (step generators that yield between whole actions, never with a transient board; the same actions in the same order, so decisions are unchanged; at once in virtual time).
---
@@ -1127,9 +1127,9 @@ Eighteen workstreams (WA–WR, each on its own branch from master 7f13473) were
- **#4 seen**: "昆图斯策略给的突变细胞用完就没了". **Cause**: research 04's [ASSUMED] "item consumed"; the built-in and a content override both destroyed the cell.
- **#4 official**: the text "战斗结束后,装备者替换为高一阶的随机干员" never says 销毁 (every consumable's text does; data EQUIP, upgradeNum 100); PRTS 下半/PRTS盟约记录 备注 "生效时,原干员销毁,获得一名高一阶的随机初始干员(最高六阶)"; PRTS 卫戍协议/帮助 "佩戴的装备无法手动卸除,在失去该干员(干员出售、销毁、合并等)或装备合并为进阶品质时自动卸除"; community guides re-inject it every round (bilibili cv47000418, cv48003106; cg.163.com "这个道具可以无限使用").
- **#4 where the new operator goes** (corrected after the WA merge; pointed out in the closed PR #2, whose playtest claim the footage confirms): official gameplay footage — bilibili BV1vzyVBuEN9 (上半, ≈ 8:24) and BV1Qkw1zMEoR (下半, ≈ 7:25) — shows the next prep with the carrier's tile **empty**, 剩余可放置角色 one higher and the 整备区 holding the returned equipment and the new tier+1 operator, which the player then deploys by hand. It reads the PRTS 备注 as written — a destroy followed by a gain ("原干员销毁,获得一名…") — and PRTS 帮助 sends whatever a player recruits or gains to the 手牌区 (= 整备区; "被发送至手牌区的物资优先从右到左填充空位"). WA had kept the new operator on the carrier's tile when legal [ASSUMED] and counted that tile for a merge the transformation completed [ASSUMED]; both are withdrawn (PR #2 itself still let the merge use the carrier's tile — not adopted).
- **#4 now**: after every battle the carrier is destroyed wherever it stands (board, hand or temp — a board tile is freed and the deploy count drops; its summons are removed, its copies return to the pool). Its equipment, the cell included, returns to the hand **first** (overflow temp; auto-merging like any gain). Then a random NORMAL operator one tier higher (max 6; an elite carrier too) is gained like any gained operator (`PlayerState.transformChess` → `acquireChess`, onGain source `'transform'`): the hand (right → left), overflow temp, and with both full the no-room rule of every gain (back to the pool, toast 整备区已满,获得的干员已返还; an item that found no slot takes one the gain freed, else stays on the gained operator up to 2 slots, else it is destroyed with a log warning, as in a merge). Like any operator on the bench it brings no summon card until it is deployed. A gain that completes a merge merges like any gained copy (§20.11): the elite on a consumed **deployed copy's** tile, else the hand — the carrier's freed tile is no copy's (`_mergeChess` lost its `fromKey` / `fromDir` options, `audit.js` its exception for them); the returned cell is stowed before the elite's summon card. Per-piece state stays with the returned items (商业包装方案's sell count, keyed by the item). One implementation (the built-in); the item card states the rule (items.json `note`). Bots inject their least valuable single normal operator below 6阶 (`cellTarget`: never an elite, a merge pair or a carrier of a cell), make a hand slot for a cell left in temp (`makeHandRoom`), and — AI 托管 too — deploy the gained operator in the next prep's lineup step like any owned unit (a real co-op round in the tests: SETTLE → the next prep). The client needs no special case: `render/app.js setPrep` drops the carrier's view and draws the new piece on its bench slot (checked in headless Chrome: the tile empty, the operator and the cell on the bench, no promotion cue, no console error).
- **#4 now**: after every battle the carrier is destroyed wherever it stands (board, hand or temp — a board tile is freed and the deploy count drops; its summons are removed, its copies return to the pool). Its equipment, the cell included, returns to the hand **first** (overflow temp; auto-merging like any gain). Then a random NORMAL operator one tier higher (max 6; an elite carrier too) is gained like any gained operator (`PlayerState.transformChess` → `acquireChess`, onGain source `'transform'`): the hand (right → left), overflow temp, and with both full the no-room rule of every gain (back to the pool, toast 整备区已满,获得的干员已返还; an item that found no slot takes one the gain freed, else stays on the gained operator up to 2 slots, else it is destroyed with a log warning, as in a merge). Like any operator on the bench it brings no summon card until it is deployed. A gain that completes a merge merges like any gained copy (§20.11): the elite on a consumed **deployed copy's** tile, else the hand — the carrier's freed tile is no copy's (`_mergeChess` lost its `fromKey` / `fromDir` options, `audit.js` its exception for them); the returned cell is stowed before the elite's summon card. Per-piece state stays with the returned items (商业包装方案's sell count, keyed by the item). One implementation (the built-in); the item card states the rule (items.json `note`). Bots inject their least valuable single normal operator below 6阶 (`cellTarget`: never an elite, a merge pair or a carrier of a cell), make a hand slot for a cell left in temp (`makeHandRoom`), and — AI 托管 too — deploy the gained operator in the next prep's lineup step like any owned unit (a real co-op round in the tests: SETTLE → the next prep). The buy loop's pre-emptive bench shed, which sells the weakest bench single by piece value while the bench is crowded, no longer sells a piece gained since the bot's last prep ended (`bot.js rememberOwned`): on the new rule it had sold 44 of 222 gained operators in 20 solo 绝境 matches on 昆图斯 before any placement step saw them (now none; a gained operator stays off the board only when the board is full). Measured (`tools/botbench.mjs`, decisions only): strategy 昆图斯 (`--band band_quintus`, seeds 201–220) 绝境 8 / 20 wins on the WA build → 7 with the bench rule alone → 11 with the shed fix (LP left 16.8 → 13.3 → 17.1), 险境 15 → 16 → 17 (16.3 → 16.2 → 17.6); the bots' own strategies, bench rule alone → with the fix: solo 绝境 (seeds 1–12) 8 → 8 / 12, solo 险境 (seeds 13–36) 19 → 19 / 24, co-op 绝境 4 AI (seeds 1–36) 9 → 8 / 36 (LP left within 0.6) — noise; the shed's cost is a set of the owned uids per prep. The client needs no special case: `render/app.js setPrep` drops the carrier's view and draws the new piece on its bench slot (checked in headless Chrome: the tile empty, the operator and the cell on the bench, no promotion cue, no console error).
- [ASSUMED]: an elite merged this round keeps its copies' highest count (no second firing that round); a copy re-bought after a sale fires on its own first refresh ("获得该干员后"; each extra +4 costs 3 + 1 − 1 = 3 funds); a bench carrier (hand or temp) transforms too (the footage shows deployed carriers); a 6阶 carrier becomes another 6阶; with the hand and temp both full the gained operator and an item without a slot follow the rules of every gain (no source shows that case).
- Tests: `test/match/feedback1-meta.test.js` (a deployed and a bench carrier, the hand full → temp, the hand and temp full, a merge with and without a deployed copy, 商业包装方案's count, bots and AI 托管 redeploying), `test/ui/feedback1-cell.test.js`; `items.test.js`, `merge.test.js`, `facing.test.js`, `playtest6-elite-to-board.test.js` updated.
- Tests: `test/match/feedback1-meta.test.js` (a deployed and a bench carrier, the hand full → temp, the hand and temp full, a merge with and without a deployed copy, 商业包装方案's count, bots and AI 托管 redeploying, a real solo 绝境 昆图斯 bot match: no gained operator shed in its first prep, none waiting on the bench beside a free deployment), `test/ui/feedback1-cell.test.js`; `items.test.js`, `merge.test.js`, `facing.test.js`, `playtest6-elite-to-board.test.js` updated.
### 21.2 Bounty drafts by the official round structures; special offers shown as what they hold (#2, #6) — `tools/build-data.mjs BOUNTY_INITIAL_SETS / BOUNTY_BOSS_GROUPS / BOUNTY_HUNTER_GROUPS / SHOP_DRAFT / TACTIC_DRAFT`, `match/choices.js bountyDraftCards / shopDraftCards / tacticDraftCards`, choices.json `bountyDrafts` / `shopDraft` / `tacticDraft` / `draftPool` / schedule `bountyDraft`, `test/fixtures/official-bounty-drafts.json`; `match/PlayerState.js pushRewardOffer / pushItemOffer`, `match/effectsMeta.js offerLabel`, `ui/gameLogic.js offerHeader`, `ui/shopBar.js RewardCards`, `ui/rewardOverlay.js`
@@ -1183,7 +1183,7 @@ Eighteen workstreams (WA–WR, each on its own branch from master 7f13473) were
- **Measured on the 0.1.0 bot** (`tools/botbench.mjs`, same seeds on two builds): 0–2 refreshes a prep and side-grade singles sold again for 1 (≈ 2 merges a match, ≈ 2 elites at R13); a level-up could spend the funds a third copy needed; the bounty score ignored the enemy (co-op 绝境: 155 of ≈ 310 bounty enemies leaked); 信标 (destroys its carrier) went on the best operator; DPS ignored DEF / RES; the focus bond flip-flopped.
- **Now** (decisions only, no rule or balance change): refresh vs buy by the shop odds of completing held pairs (a full board buys only merge progress or a lineup upgrade worth more than the refreshes its price would pay for; an elite's value includes the merge's free pick); ≤ 4 pairs on the bench, merges before level-ups, a freeze for an unaffordable third copy; one committed focus core bond + a second bond, a teammate's main core bond read from its public bond strip (humans too) counting against it; dealers valued by DPS against the round's DEF / RES; 坎诺特 keeps its interest capital (5) back; bounty picks by expected payout − expected leaks × (2 + 20 / LP) with the kill chance from the exposure model — an expected-value pick (co-op leaks are bound by the drafts, §21.2); items by effect (信标 on a bench single, 拟态物质 on a pair, both 博士投影 on the strongest normal operator, 突变细胞 per §21.1, bond items on a focus member, survival items on blockers); 教鞭 / “神秘顾客” used after a perfect battle and kept otherwise — a bot drops a kept one only for a hand slot, a human's seat under AI 托管 never; a tactician's 援军 inside `PlayerState.summonRange`.
- **Result** (seeds 1–40, rehearsal 3, on the WF branch): 独立 绝境 19 → 26 / 40 wins (LP left 14.3 → 16.6), 同盟 绝境 (4 AI) 5 → 12 / 40 (5.1 → 12.7), 标准 unchanged; merges per bot ≈ 2 → 3–4; forced strategies (solo 绝境, seeds 201–220): 坎诺特 9 → 14, 昆图斯 9 → 11, 杜宾 8 → 9. **Decision time unchanged**: heuristics CPU p50 / p95 31 / 74 → 28 / 70 ms solo and 17 / 39 → 17 / 38 ms co-op; the whole prep with its rehearsal 89 / 205 and 83 / 162 ms wall.
- **Reconciled with the merged rules** (§21.19): the server's legality (deploy map without the 深水区, `summonRange`), WA's cell rule, WB's draft structures (it scores whatever is offered), 本局禁用 bonds (never planned), the range a unit is deployed with, and the AoE of §21.18 in its DPS value. On the merged build, seeds 1–40: solo 绝境 27 → 27 wins, co-op 绝境 9 → 11 (within the noise); decision time stays (heuristics CPU 24 / 57 ms solo, 16 / 36 ms co-op; the whole prep 78 / 153 and 73 / 151 ms wall; BALANCE §5). After the QA: WB's 22-match bounty lists (a giant in every R11 bounty list, 14 of 22 R11s a 悬赏决策) take the same bot to 22 / 40 and 9 / 40 — a rule change; the bench-shed fix (never sell a piece of this prep, §21.19) then 24 / 40 and 10 / 40. **The 0.1.1 build against 0.1.0 (19 / 40, 5 / 40): 24 / 40 solo 绝境, 10 / 40 co-op 绝境 (4 AI)** — part of it is the rules: 0.1.0's bot on the 0.1.1 rules wins 23 / 40 and 7 / 40 (LP left 14.6 / 5.2 against 17.4 / 13.1, merges per bot 2.2 / 2.0 against 3.8 / 3.1); same-prep buy → sell in co-op 绝境 (2 AI 托管 + 2 bots, seeds 21–26) 29 → 3; decision time unchanged (one thread, seeds 1–3: the whole prep 81 / 187 → 81 / 184 ms wall solo, 71 / 153 → 71 / 129 ms co-op; heuristics CPU 36 / 88 → 35 / 86 and 16 / 40 → 16 / 40 ms).
- **Reconciled with the merged rules** (§21.19): the server's legality (deploy map without the 深水区, `summonRange`), WA's cell rule (and its destroy-then-gain correction: the gained operator is deployed by the lineup step, the bench shed spares a piece gained since the last prep — §21.1), WB's draft structures (it scores whatever is offered), 本局禁用 bonds (never planned), the range a unit is deployed with, and the AoE of §21.18 in its DPS value. On the merged build, seeds 1–40: solo 绝境 27 → 27 wins, co-op 绝境 9 → 11 (within the noise); decision time stays (heuristics CPU 24 / 57 ms solo, 16 / 36 ms co-op; the whole prep 78 / 153 and 73 / 151 ms wall; BALANCE §5). After the QA: WB's 22-match bounty lists (a giant in every R11 bounty list, 14 of 22 R11s a 悬赏决策) take the same bot to 22 / 40 and 9 / 40 — a rule change; the bench-shed fix (never sell a piece of this prep, §21.19) then 24 / 40 and 10 / 40. **The 0.1.1 build against 0.1.0 (19 / 40, 5 / 40): 24 / 40 solo 绝境, 10 / 40 co-op 绝境 (4 AI)** — part of it is the rules: 0.1.0's bot on the 0.1.1 rules wins 23 / 40 and 7 / 40 (LP left 14.6 / 5.2 against 17.4 / 13.1, merges per bot 2.2 / 2.0 against 3.8 / 3.1); same-prep buy → sell in co-op 绝境 (2 AI 托管 + 2 bots, seeds 21–26) 29 → 3; decision time unchanged (one thread, seeds 1–3: the whole prep 81 / 187 → 81 / 184 ms wall solo, 71 / 153 → 71 / 129 ms co-op; heuristics CPU 36 / 88 → 35 / 86 and 16 / 40 → 16 / 40 ms).
- [ASSUMED] (tuned; the official AI is undocumented): MERGE_HIT 36, MAX_PAIRS 4, BENCH_BUDGET 6, FOCUS_MEMBER 4, ARMOR_WEIGHT 10, BOUNTY_KILL 2, the LP value 2 + 20 / LP, the teammate penalty 6, the 坎诺特 reserve = the band's interest capital, `CROWD` (×2 / ×1.3 / ×1.4).
- Tests: `test/match/bot-smarter.test.js` (bounty picks, item carriers, Arts kept / AI 托管 safe, tactician range, pair via buy or freeze, 坎诺特, the bond strip, the merged rules, a CPU cost guard < 300 ms), `test/match/feedback1-meta.test.js` (AI 托管 never destroys an item to clear temp).
+3 -1
View File
@@ -238,7 +238,9 @@ gets a hand slot made for it, `makeHandRoom` — on a bot's own seat by destroyi
chess can be sold, never a human's item under AI 托管 — on such a seat any item left in temp with a full hand gets a
hand slot the same way, a bench operator sold for it; only a hand of nothing but items still drops it, as the temp
deadline would; the operator a transformation gains waits on the bench and the next prep's lineup step deploys it
like any owned unit — AI 托管 too), 身份牌 / 通讯机 / 寻呼模块 on a focus member; 画卷 copies the most
like any owned unit — AI 托管 too; the buy loop's bench shed, which sells by piece value while the bench is crowded,
never sells a piece gained since the bot's last prep ended, `rememberOwned`), 身份牌 / 通讯机 / 寻呼模块 on a focus
member; 画卷 copies the most
valuable deployed operator; 教鞭 / “神秘顾客” are used after a perfect battle and otherwise kept in the hand — a bot drops
a kept one only when it needs the hand slot (“神秘顾客” then pays its fund), a human's seat under AI 托管 never. 机变: a
bounty card is scored by its expected payout minus its expected leaks × the value of an LP (2 + 20 / LP), the kill
+24 -5
View File
@@ -53,8 +53,9 @@
// 5. items by what they do (itemTarget): equipment on the strongest deployed damage dealers (survival items on
// blockers, bond signature items on a member), 信标 on a bench single, 拟态物质 on a pair, 博士投影 (both
// qualities) on the strongest normal operator, 突变细胞 on the least valuable normal single below 6阶 (cellTarget:
// never an elite or a pair member; the operator its transformation gains joins the bench and steps 3–4 of the next
// prep deploy it like any owned unit), bond items on a focus member; Arts (useArt): 画卷 copies the most valuable
// never an elite or a pair member; the operator its transformation gains joins the bench — the buy loop's bench
// shed leaves a piece gained since the last prep alone, rememberOwned — and steps 3–4 of the next prep deploy it
// like any owned unit), bond items on a focus member; Arts (useArt): 画卷 copies the most valuable
// deployed operator, 教鞭 / “神秘顾客” are used after a perfect battle and kept otherwise (consume-on-equip items /
// Arts only with a handler)
// 6. resolve the temp slots (a 突变细胞 left there — it comes back after every transformation — gets a hand slot made
@@ -1167,7 +1168,10 @@ function refreshValue(m, ps, ctx) {
/**
* Sell the weakest bench chess (temp and hand; the hand only with `handOnly`) that is not part of a merge pair (or
* anything when keepPairs is false) and does not carry a 突变细胞 (its transformation after the battle is the point).
* `keepFresh`: never a piece that came this prep (`boughtRound`: bought for the lineup, an elite just merged, a reward).
* `keepFresh`: never a piece that came this prep (`boughtRound`: bought for the lineup, an elite just merged, a reward)
* nor one gained since the bot's last prep ended (unseen, see rememberOwned: the operator a 突变细胞 transformation
* gains joins the bench while its carrier's deployment is free — the placement step decides about it, not the shed's
* piece value).
*/
function sellWeakestHand(m, ps, { keepPairs = true, below = Infinity, handOnly = false, keepFresh = false } = {}) {
const ctx = context(m, ps);
@@ -1177,7 +1181,7 @@ function sellWeakestHand(m, ps, { keepPairs = true, below = Infinity, handOnly =
if (!p || p.kind !== 'chess' || (p.items || []).some((it) => isMutationCell(m.gd, it.id))) continue;
const base = m.gd.baseIdOf(p.id);
if (keepPairs && !m.gd.isGolden(p.id) && (ctx.copies.get(base) || 0) >= 2) continue;
if (keepFresh && p.boughtRound === m.round) continue;
if (keepFresh && (p.boughtRound === m.round || unseen(ps, p))) continue;
const v = pieceValue(m, ps, p, ctx);
if (v < worstV) { worstV = v; worst = p; }
}
@@ -1185,6 +1189,17 @@ function sellWeakestHand(m, ps, { keepPairs = true, below = Infinity, handOnly =
return tryDo(() => ps.sell(worst.uid));
}
/**
* The chess the bot owned when its last prep ended (`ps._botSeenUids`, written by botPrepEndSteps before Ready). A
* piece outside it was gained after that prep — at SETTLE (a 突变细胞 transformation's operator, battle-result grants),
* at the round start or in 机变 — and has not been through a placement step yet. No memory yet (a bot's first prep, a
* seat just put under AI 托管): nothing counts as unseen.
*/
function rememberOwned(ps) {
ps._botSeenUids = new Set(ps.allChess().map((p) => p.uid));
}
const unseen = (ps, p) => !!ps._botSeenUids && !ps._botSeenUids.has(p.uid);
/** Lineup gain of one copy of chess `id` (full board): best single swap into the current best lineup `cur`. */
function lineupGain(m, ps, id, ctx, cur) {
const probe = { uid: -1, kind: 'chess', id, items: [] };
@@ -1296,6 +1311,8 @@ export function* botPrepEndSteps(m, ps, job = null) {
// 5. temp → hand / sell / destroy; keep one hand slot free for next round's merges
resolveTemp(m, ps);
if (freeSlot(ps.hand) < 0) freeHandSlot(m, ps);
// what the next prep's bench shed may judge by value: what is owned now (later gains wait for a placement step)
rememberOwned(ps);
tryDo(() => ps.setReady(true));
}
@@ -1380,7 +1397,9 @@ function* buyLoopSteps(m, ps, { fillOnly = false, maxRefreshes = 0 } = {}) {
const mergesOnly = fillOnly && boardFull;
// keep room for merges: a crowded bench sheds its weakest single — never one that came this prep (the buy picks by
// lineup gain, the shed by piece value: it used to sell the single just bought for 3–5 back for 1; QA of the 0.1.1
// bots). The shed is pre-emptive (one slot is still free), so with only fresh singles it waits for the next prep.
// bots) nor one gained since the last prep (the operator a 突变细胞 transformation put on the bench, its deployment
// freed: in 20 solo 绝境 昆图斯 matches the shed sold 44 of 222 before the placement step could deploy them). The
// shed is pre-emptive (one slot is still free), so with only fresh singles it waits for the next prep.
const used = ps.hand.filter(Boolean).length;
if (used >= gd.benchSize - 1) sellWeakestHand(m, ps, { keepFresh: true });
let best = -1;
+37
View File
@@ -520,6 +520,43 @@ test('#4 bots and AI 托管 deploy the gained operator in their normal placement
m.dispose();
});
test('#4 a bot on 昆图斯 lets the placement step judge each gained operator: the bench shed never sells one in its first prep, and no deployment stays free while one waits on the bench', () => {
// a real solo 绝境 bot match on strategy 昆图斯 (the only band on offer, as tools/botbench.mjs --band) through R8: the
// buy loop's pre-emptive bench shed sells by piece value while the bench is crowded — before rememberOwned it sold this
// seed's R7 operator in the R8 prep (20 such matches: 44 of 222 gained operators, before any placement step saw them)
const seats = [{ seat: 0, playerId: 'ai_0', name: 'AI', isBot: true, connected: true }];
const h = makeMatch({ mode: 'solo', difficulty: 'HARD', seats, seed: 9, registry: REG });
const m = h.m;
m.gd.bandIds = () => ['band_quintus'];
const ps = h.ps('ai_0');
const gained = new Map(); // uid → round of the SETTLE that gained it
const shed = [];
const waiting = [];
const transform = ps.transformChess.bind(ps);
ps.transformChess = (...a) => { const np = transform(...a); if (np) gained.set(np.uid, m.round); return np; };
const sell = ps.sell.bind(ps);
ps.sell = (uid) => {
if (gained.get(uid) === m.round - 1 && /buyLoopSteps/.test(new Error().stack)) shed.push(`R${m.round} ${ps.find(uid)?.piece.id}`);
return sell(uid);
};
const setReady = ps.setReady.bind(ps);
ps.setReady = (on) => {
for (const [uid, r] of gained) {
const loc = r === m.round - 1 ? ps.find(uid) : null;
if (on && loc && loc.area !== 'board' && ps.deployCount < ps.deployCap) waiting.push(`R${m.round} ${loc.piece.id} ${ps.deployCount}/${ps.deployCap}`);
}
return setReady(on);
};
h.start();
h.run(() => h.ended != null || (m.phase === 'PREP' && m.round === 9));
assert.equal(ps.bandId, 'band_quintus');
assert.ok(gained.size >= 5, `the cell transformed after every battle from R3 (${gained.size})`);
assert.deepEqual(shed, [], 'no operator sold by the bench shed in the prep after its gain');
assert.deepEqual(waiting, [], 'a gained operator off the board only when the board is full');
checkInvariants(m);
m.dispose();
});
test('#10 AI 托管 never destroys a human\'s item to clear temp: a bench operator is sold for its slot instead', () => {
const s = setup({ seed: 34 });
const { m, ps } = s;