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

+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;