feedback5 WE2: AI 托管 buys the player's own 自选 pieces more readily and levels toward them a round earlier (#13)

bot.js: the player's own slotted piece scores DIY_PIECE_BONUS (12) more; levelUp takes the
step that opens a slotted tier with copies left one round earlier (diyOpensAt). Bots field
no 自选 piece. Exports buyScoreOf / levelUp / diyOpensAt for the tests. Docs: DESIGN §6.6,
PLAYING. Golden: coop2-NORMAL-70-diy moved — the 自选 seat under AI 托管 reaches level 6 one
round earlier and fields its tier-6 Sharp at round 12.
This commit is contained in:
sganggs committed 2026-10-06 15:54:07 +08:00
1 parent d31de31e31
commit 2eaef154d5
5 files changed
+98 -16

No files matched your search

+1 -1
View File
@@ -418,7 +418,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; never one built around a bond the mode switches off — `gd.bandBondIds`, §21.26); 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`; a melee chess whose trait reads 「可以放置于远程位」 (钩索师 / 推击手) on a 高台 whose range covers the enemy road, else on the ground, counted as a blocker in the lineup either way — §3; `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).
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; never one built around a bond the mode switches off — `gd.bandBondIds`, §21.26); 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`; a melee chess whose trait reads 「可以放置于远程位」 (钩索师 / 推击手) on a 高台 whose range covers the enemy road, else on the ground, counted as a blocker in the lineup either way — §3; `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). Under AI 托管 a human's own 自选 pieces score `DIY_PIECE_BONUS` (12) more in the shop / reward choice and the 调度中心 is levelled one round earlier for the step that opens a slotted tier (`levelUp`, `diyOpensAt`; 0.2.0 — bots field no 自选 piece).
---
+1 -1
View File
@@ -60,7 +60,7 @@
- **晋升精锐**:3 名同名干员(无论是否部署)自动合成**精锐**(等级、技能、模组大幅提升;风丸只需 2 名),精锐不会再合成。如果被合成的干员里有**已经部署在战场上**的,精锐会直接出现在**那名干员的位置**(保持原来的朝向,不多占部署位;有两三名都在场上时,取最先部署的那名——最左一列优先,同一列从上到下);否则精锐进入整备区(满了进入临时整备区)。原先配发的装备都退回整备区,需要重新配发;被合成干员的召唤物随之移除,场上的精锐会把自己的召唤物送进整备区。点选一张能合成的商店卡片时,精锐将出现的那一格会亮起金色边框,同时打开的详情卡里也会写明去向(「可晋升:精锐干员将出现在作战区原位置 / 将进入整备区」;详情卡会自动停靠在不遮挡这一格的一侧)。在回合开始或结算时因策略、奖励等完成的合成,精锐出现时同样会播放金色的晋升特效。
- **晋级奖励**:每次晋升后获得一次特殊招募:从 3 名 **高一阶**(最高 VI)、**互不相同**的随机干员中**免费**挑 1 名(该阶剩余干员不足 3 名时由低一阶补足)。它不能刷新、不能冻结、本回合结束失效。
- **干员持有(补位)**:大厅、房间和「确认本局信息」里的「干员调配」按钮打开的界面有两个标签页:「干员调配」选择每名干员携带的技能和精锐的模组;「**干员持有**」可以把你没有的干员**下掉**(点一下卡片切换「持有 / 未持有」)。和官方一样只有 55 张「普通」棋子能下掉(界面按阶级列出本赛季商店里的 53 名,每张写着由谁替补、用几技能);74 张「预设」棋子(多是 1–2 阶的常见干员)无论是否持有都由本人上场,不在其中。默认全部持有。下掉的干员和官方一样**显示为替补干员**:商店和晋升奖励的卡片、整备区和临时整备区里的单位、作战区、结算界面的阵容,都是替补干员的头像、名字和模型(例如银灰显示为 Sharp),带一个小小的「替补」标记;详情卡显示替补干员的立绘、名字、技能、天赋和模组,名字下面写着「银灰的替补」。**盟约、特质、阶级、价格、合成仍按原干员**(卡片和详情卡上的盟约、特质就是原干员的),作战时的属性、技能、天赋、模组、攻击范围、近战位 / 远程位都是替补干员的(例如银灰 → Sharp 的 2 技能「亮剑」)。同一名替补干员可能替补好几名干员(Sharp 替补忍冬、银灰、隐德来希等),它们按各自的原干员分别合成:看阶级和盟约区分,点开详情卡能看到替补的是谁。同盟模拟中队友查看你的整备区和作战区、你的盟约成员时,看到的也是替补干员。替补的技能和模组由棋子固定,「干员调配」对它不生效;替补干员没有召唤物(下掉缪尔赛思就没有流形)。这是**局外设置**:下一局开始时生效,对局中修改只保存到下一局;同盟模拟中只影响你自己的棋子,队友的棋子按队友的设置。没有官方的好友借用:想用本人上场,改回「持有」即可。「全部持有」一键恢复,「导出 / 导入」可以把设置带到别的浏览器;大厅的「干员调配」按钮上会显示「替补 N」提醒你有几名干员被下掉。
- **自选编队**:同一个界面的第三个标签页「**自选编队**」有 4 个名额:5 阶两个、6 阶两个。点名额打开选择列表,每个名额可以放一名**你持有的 6★ 干员**(棋子池里没有的那些,标「已持有」:3 个技能任选其一,精锐的模组任选其一或不装备;只在集成战略或生息演算里生效的模组不能选:ISW-A、SO-A / SO-B、RA-A)或一名**原型干员**(标「原型」:Sharp、Mechanist 等 9 名 6★,5 阶另有 6 名 4★ 预备干员;技能和模组与补位时相同,不能更改)。同一名原型干员可以同时放进一个 5 阶和一个 6 阶名额;你持有的干员只能放一个名额;同一阶的两个名额不能放同一人。列表只列出已经做好战斗逻辑的干员(原型干员全部可选,持有的 6★ 随战斗逻辑完成陆续加入)。对局中,自选干员**只出现在你自己的商店里**:调度中心升到 5 级后才会刷出 5 阶名额的干员,6 级后才会刷出 6 阶的;刷新概率和同阶干员一样,每个名额有自己的库存(5 阶 8 张、6 阶 5 张,不与队友共用),价格与同阶干员相同,晋升奖励里也可能出现;效果、奖励或机变卡「获得一名随机干员」时,也可能抽到你自己的自选干员(按那个效果自己的阶级规则,不看调度中心等级)。三张合成精锐,精锐时所选模组生效(5 阶 1 级、6 阶 3 级)。自选干员**没有特质**,盟约按干员的实际阵营分配(推进之王是维多利亚,煌同时是炎和维多利亚,没有对应阵营的是协防干员),和普通棋子一样计入盟约人数;本局禁用了它全部的盟约时,它不会出现在商店里(协防不会被禁用),开局的「确认本局信息」、选策略时的「本局信息」和对局中的「本局信息」都会在「本局禁用干员」下写明是哪一名。商店卡片、整备区、作战区和详情卡上都标着「自选」,详情卡显示所选的技能和模组。这同样是**局外设置**:下一局开始时生效,对局中修改只保存到下一局;AI 队友不使用自选;信标不会把自选干员送给队友。「导出 / 导入」「全部清空」可用,大厅的「干员调配」按钮上会显示「自选 N」。
- **自选编队**:同一个界面的第三个标签页「**自选编队**」有 4 个名额:5 阶两个、6 阶两个。点名额打开选择列表,每个名额可以放一名**你持有的 6★ 干员**(棋子池里没有的那些,标「已持有」:3 个技能任选其一,精锐的模组任选其一或不装备;只在集成战略或生息演算里生效的模组不能选:ISW-A、SO-A / SO-B、RA-A)或一名**原型干员**(标「原型」:Sharp、Mechanist 等 9 名 6★,5 阶另有 6 名 4★ 预备干员;技能和模组与补位时相同,不能更改)。同一名原型干员可以同时放进一个 5 阶和一个 6 阶名额;你持有的干员只能放一个名额;同一阶的两个名额不能放同一人。列表只列出已经做好战斗逻辑的干员(原型干员全部可选,持有的 6★ 随战斗逻辑完成陆续加入)。对局中,自选干员**只出现在你自己的商店里**:调度中心升到 5 级后才会刷出 5 阶名额的干员,6 级后才会刷出 6 阶的;刷新概率和同阶干员一样,每个名额有自己的库存(5 阶 8 张、6 阶 5 张,不与队友共用),价格与同阶干员相同,晋升奖励里也可能出现;效果、奖励或机变卡「获得一名随机干员」时,也可能抽到你自己的自选干员(按那个效果自己的阶级规则,不看调度中心等级)。三张合成精锐,精锐时所选模组生效(5 阶 1 级、6 阶 3 级)。自选干员**没有特质**,盟约按干员的实际阵营分配(推进之王是维多利亚,煌同时是炎和维多利亚,没有对应阵营的是协防干员),和普通棋子一样计入盟约人数;本局禁用了它全部的盟约时,它不会出现在商店里(协防不会被禁用),开局的「确认本局信息」、选策略时的「本局信息」和对局中的「本局信息」都会在「本局禁用干员」下写明是哪一名。商店卡片、整备区、作战区和详情卡上都标着「自选」,详情卡显示所选的技能和模组。这同样是**局外设置**:下一局开始时生效,对局中修改只保存到下一局;AI 队友不使用自选;你开启 AI 托管时,AI 会优先买你的自选干员,并在快能刷出自选名额时提早一回合升级调度中心;信标不会把自选干员送给队友。「导出 / 导入」「全部清空」可用,大厅的「干员调配」按钮上会显示「自选 N」。
- 每回合商店会自动刷新未冻结的格子。
- **「本回合首次主动刷新」按每名干员各自计算**(拉普兰德):她在本回合遇到的第一次主动刷新生效,在整备区里也算——先刷新、再买下拉普兰德,下一次刷新她仍会叠层;同一回合内用已经生效过的拉普兰德合成的精锐,本回合不再生效。
+25 -1
View File
@@ -119,6 +119,12 @@ const MAX_REFRESHES = 16;
const BENCH_BUDGET = 6;
/** Shop level the bot aims for at the start of round r (index = round). */
const LEVEL_TARGET = [1, 1, 1, 2, 2, 3, 3, 4, 4, 4, 5, 5, 6, 6, 6, 6];
/**
* AI 托管 of a human with 自选 picks (0.2.0; bots field none): the buy score of one of the player's own slotted pieces —
* the operator the player chose to field (follow-up #13: under AI 托管 they were rarely bought) — and the 调度中心 is
* levelled one round earlier for the step that opens a slotted tier (levelUp).
*/
export const DIY_PIECE_BONUS = 12;
const TIER_POWER = [0, 10, 12.5, 15, 18, 21.5, 25];
/** Prep-side 特质 that keep adding bond layers every round / every refresh (a player's main layer engine). */
const RECURRING_TRAIT_EVENTS = new Set(['SERVER_PREP_START', 'SERVER_PREP_FIN', 'SERVER_REFRESH_SHOP']);
@@ -563,6 +569,11 @@ function chooseLineup(m, ps, ctx) {
return { set, bench, score };
}
/** buyScore with a fresh context (tests). */
export function buyScoreOf(m, ps, id) {
return buyScore(m, ps, id, context(m, ps));
}
/** Shop / reward score of acquiring one copy of chess `id` (0 = not worth it). */
function buyScore(m, ps, id, ctx) {
const gd = ps?.gd || m.gd;
@@ -589,6 +600,8 @@ function buyScore(m, ps, id, ctx) {
if (isHealer(c)) s += ctx.roles.healers === 0 && m.round >= 3 ? 6 : ctx.roles.healers >= 2 ? -14 : -3;
if (c.attackKind === 'none' && !isHealer(c)) s -= 4;
if (ctx.model && !isHealer(c)) s += ARMOR_WEIGHT * 0.6 * (armorFit(ctx.model, c) - 0.6);
// the player's own 自选 piece (a slotted slot: its composed record carries diyFor) — AI 托管 of a human with picks
if (c.diyFor) s += DIY_PIECE_BONUS;
return s;
}
@@ -1385,7 +1398,16 @@ function maybeFreeze(m, ps) {
if (ps.shop.slots.some((s) => s && !s.sold && s.kind === 'chess' && ps.priceOf(s) > ps.funds && ps.completesChessMerge(s.id))) tryDo(() => ps.freeze());
}
function levelUp(m, ps, { spare = false } = {}) {
/** Whether level `lv` of the 调度中心 opens one of the player's slotted 自选 pieces with copies left (player/diy.js). */
export function diyOpensAt(ps, lv) {
const st = ps.diyStock;
if (!st || !st.entries || !st.entries.size) return false;
for (const e of st.entries.values()) if (e.shopLevel === lv && e.left > 0) return true;
return false;
}
/** The 调度中心 level-ups of a bot's prep (exported for tests). */
export function levelUp(m, ps, { spare = false } = {}) {
const gd = ps?.gd || m.gd;
for (let guard = 0; guard < 6; guard++) {
if (ps.shop.level >= gd.maxShopLevel) return;
@@ -1398,6 +1420,8 @@ function levelUp(m, ps, { spare = false } = {}) {
const boardReady = ps.allChess().length >= (r <= 4 ? ps.deployCap : Math.min(ps.deployCap, 6));
let want = price === 0;
if (!want && ps.shop.level < target && (boardReady || r >= 6)) want = true;
// a human's 自选 slots of the next level (AI 托管): that step one round earlier (DIY_PIECE_BONUS)
if (!want && boardReady && ps.shop.level < nextTarget && diyOpensAt(ps, ps.shop.level + 1)) want = true;
if (!want && ps.shop.level < nextTarget && price <= 2 && boardReady) want = true;
if (!want && spare && ps.funds >= price + 1 + fundsReserve(m, ps) && ps.shop.level < nextTarget + 1 && boardReady) want = true;
if (!want && ps.funds >= price + 14) want = true;
+13 -13
View File
@@ -1623,7 +1623,7 @@
"boost": null,
"setup": {"stageId":"act1autochess_m03","bossId":"boss_7","hiddenBossId":"boss_10","factions":["ELEMENT","INVISIBLE","REFLECTION"]},
"end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":55,"bossPoolLeft":0,"virtualMs":122900},
"rng": {"setup":25,"shop":461,"waves":0,"draft":34,"bots":16433,"meta":20},
"rng": {"setup":25,"shop":503,"waves":0,"draft":34,"bots":16504,"meta":20},
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
"rounds": [
{"round":1,"h_0":[1,27,4,0,0,5,1,"3e4697a0",0,"",5,5,0,3],"ai_0":[1,28,4,0,0,1,2,"a1734bc6",0,"sargonShip 0/2, soloShip 1/0",5,5,0,4]},
@@ -1666,18 +1666,18 @@
},
{
"round": 11,
"h_0": [1,27,14,0,2,5,8,"7c42dc27",357,"arcaneShip 0/2, indomShip 1/16, preciShip 2/109, sargonShip 1/137, skillfulShip 1/8, steadShip 1/51, suntShip 1/0, victoriaShip 0/19, visiShip 1/36",19,19,0,105],
"h_0": [1,27,14,0,2,6,8,"a5cfb056",363,"arcaneShip 0/2, indomShip 1/16, preciShip 2/116, sargonShip 1/139, steadShip 1/51, suntShip 1/0, swiftShip 1/5, victoriaShip 0/19, visiShip 1/36",19,19,0,106],
"ai_0": [1,28,12,0,0,5,8,"fe5ac1a4",53,"sargonShip 0/4, siracusaShip 1/12, skillfulShip 1/12, soloShip 1/0, suntShip 2/0, visiShip 1/0, yanShip 1/29",19,19,0,99]
},
{
"round": 12,
"h_0": [1,27,16,0,2,6,8,"3cdb76eb",388,"arcaneShip 0/2, indomShip 2/16, preciShip 2/141, sargonShip 1/179, skillfulShip 0/8, steadShip 0/51, suntShip 1/0, victoriaShip 0/19, visiShip 1/52",33,33,0,122],
"ai_0": [1,28,12,0,0,6,8,"547fcf92",67,"sargonShip 0/8, siracusaShip 1/16, skillfulShip 1/18, soloShip 1/0, suntShip 2/0, yanShip 1/33",16,33,17,112]
"h_0": [1,27,16,0,2,6,8,"750c100e",381,"arcaneShip 0/2, emptyShip 1/0, indomShip 0/16, preciShip 2/148, sargonShip 1/181, steadShip 0/51, suntShip 1/0, swiftShip 0/5, victoriaShip 0/19, visiShip 1/52",33,33,0,125],
"ai_0": [1,28,12,0,0,6,8,"d3c53bfd",63,"sargonShip 0/8, siracusaShip 1/12, skillfulShip 1/18, soloShip 1/0, suntShip 2/0, visiShip 1/0, yanShip 1/33",18,33,15,112]
},
{
"round": 13,
"h_0": [1,27,16,0,0,6,8,"d0f5bd71",508,"arcaneShip 0/2, indomShip 1/16, preciShip 2/173, sargonShip 1/239, skillfulShip 0/8, steadShip 0/51, suntShip 1/0, swiftShip 1/12, victoriaShip 0/19, visiShip 1/68",50,50,0,138],
"ai_0": [1,28,12,0,0,6,8,"d2ff3200",90,"sargonShip 0/8, siracusaShip 1/24, skillfulShip 1/24, soloShip 1/0, suntShip 2/0, victoriaShip 0/5, yanShip 1/42",32,50,18,125]
"h_0": [1,27,16,4,0,6,8,"d90306b7",486,"arcaneShip 0/2, indomShip 0/16, maniShip 1/0, preciShip 2/166, sargonShip 1/209, steadShip 0/51, suntShip 1/0, swiftShip 0/5, victoriaShip 1/31, visiShip 1/80",50,50,0,140],
"ai_0": [1,28,12,0,0,6,8,"5700d2b4",81,"miraShip 1/0, sargonShip 0/8, siracusaShip 1/20, skillfulShip 1/24, soloShip 1/0, suntShip 2/0, visiShip 1/0, yanShip 1/37",14,50,36,124]
}
],
"players": {
@@ -1691,9 +1691,9 @@
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
"trophies": 4,
"reward": 400,
"stats": {"activatedLayers":534,"bossDamage":572474,"dmgDealt":4818483,"gold":154,"itemsEquipped":23,"kills":349,"leaks":0,"lpLost":0,"merges":4,"perfectRounds":13,"refreshes":23},
"lineup": ["5_21_b@12,8[3_07_e_b+1_05_e_a]","5_22_a@11,7","4_20_b@11,8","5_02_a@10,5[5_01_e_a+6_01_e_a]","6_05_a@10,6[2_01_e_a+4_08_e_a]","6_06_a@10,8[1_01_e_a+3_04_e_a]","4_07_b@9,8[5_03_e_a+4_11_e_b]","4_23_b@9,9[4_04_e_a]"],
"bonds": ["arcaneShip 0/2","indomShip 1/16","preciShip 2/173","sargonShip 1/239","skillfulShip 0/8","steadShip 0/51","suntShip 1/0","swiftShip 1/22","victoriaShip 0/19","visiShip 1/84"]
"stats": {"activatedLayers":502,"bossDamage":596123,"dmgDealt":5126250,"gold":156,"itemsEquipped":23,"kills":364,"leaks":0,"lpLost":0,"merges":4,"perfectRounds":13,"refreshes":29},
"lineup": ["6_05_a@12,8[2_01_e_a+4_08_e_a]","5_02_a@11,6[5_01_e_a+6_01_e_a]","6_08_a@11,7[3_09_e_a]","4_20_b@11,8[6_07_e_a+1_05_e_a]","6_11_a@10,6","5_21_b@10,8[3_04_e_a+6_09_e_a]","4_07_b@9,8[3_07_e_a+5_07_e_a]","4_23_b@9,9[4_04_e_b+4_11_e_a]"],
"bonds": ["arcaneShip 0/2","indomShip 0/16","maniShip 1/0","preciShip 2/166","sargonShip 1/209","steadShip 0/51","suntShip 1/0","swiftShip 0/5","victoriaShip 1/31","visiShip 1/96"]
},
"ai_0": {
"alive": true,
@@ -1705,9 +1705,9 @@
"title": {"id":"comment_3","name":"坚若磐石","picId":"comment_icon_3","text":"目标生命值损失最少"},
"trophies": 4,
"reward": 400,
"stats": {"activatedLayers":113,"bossDamage":215026,"dmgDealt":2640057,"gold":137,"itemsEquipped":20,"kills":247,"leaks":46,"lpLost":0,"merges":6,"perfectRounds":8,"refreshes":13},
"lineup": ["6_05_a@12,8[3_07_e_a+6_09_e_a]","2_04_b@11,7","1_07_b@11,8","6_11_a@10,6","3_03_b@10,8[3_11_e_b+2_04_e_a]","5_02_a@9,7[1_02_e_b]","1_08_b@9,8[4_06_e_a+1_05_e_a]","3_02_b@9,9[4_10_e_a+4_11_e_a]"],
"bonds": ["maniShip 1/0","preciShip 1/0","sargonShip 1/8","siracusaShip 1/24","skillfulShip 1/30","soloShip 1/0","suntShip 2/0","victoriaShip 0/10","yanShip 1/51"]
"stats": {"activatedLayers":105,"bossDamage":191377,"dmgDealt":2449031,"gold":135,"itemsEquipped":20,"kills":231,"leaks":62,"lpLost":0,"merges":6,"perfectRounds":8,"refreshes":12},
"lineup": ["2_04_b@12,8[6_02_e_a+6_09_e_a]","5_02_a@11,7","6_05_a@11,8","5_20_a@10,5[1_05_e_a+1_02_e_b]","6_11_a@10,6","3_03_b@10,8[3_11_e_b+2_04_e_a]","1_08_b@9,8[4_06_e_a+4_11_e_a]","3_02_b@9,9[4_04_e_a]"],
"bonds": ["maniShip 1/0","miraShip 1/0","preciShip 1/0","sargonShip 1/8","siracusaShip 1/28","skillfulShip 1/30","soloShip 1/0","suntShip 1/0","visiShip 1/0","yanShip 1/39"]
}
},
"diy": {
@@ -1730,7 +1730,7 @@
[9,"",""],
[10,"",""],
[11,"",""],
[12,"",""],
[12,"","6_diy1_a→char_609_acguad"],
[13,"",""]
],
"stock": {"chess_char_5_diy1_a":8,"chess_char_5_diy2_a":8,"chess_char_6_diy1_a":5,"chess_char_6_diy2_a":5}
+58
View File
@@ -0,0 +1,58 @@
// test/match/feedback5-we2-bot-diy.test.js — 0.2.0 WE2 #13: AI 托管 of a human with 自选 picks buys the player's own slotted
// pieces more readily (bot.js buyScore +DIY_PIECE_BONUS) and levels the 调度中心 one round earlier for the step that opens a
// slotted tier (levelUp, diyOpensAt). Bots field no 自选 piece: nothing changes for them.
// Run: node --test test/match/feedback5-we2-bot-diy.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { buyScoreOf, levelUp, diyOpensAt, DIY_PIECE_BONUS } from '../../server/match/bot.js';
import { diyGameData } from '../../server/match/player/diy.js';
import { makeMatch, give, chessOfTier, checkInvariants } from './harness.js';
const T5A = 'chess_char_5_diy1_a';
const PICK = { charId: 'char_112_siege', skillIndex: 2, uniEquipId: 'uniequip_002_siege' }; // 推进之王
function prep(seed = 911) {
const seats = [
{ seat: 0, playerId: 'p_0', name: 'P0', isBot: false, connected: true, diy: { [T5A]: PICK } },
{ seat: 1, playerId: 'p_1', name: 'P1', isBot: false, connected: true },
];
const h = makeMatch({ mode: 'coop', difficulty: 'NORMAL', seats, seed }).start();
h.toPrep(1);
return { h, m: h.m, p0: h.ps('p_0'), p1: h.ps('p_1') };
}
test('#13 buyScore: the player\'s own 自选 piece scores DIY_PIECE_BONUS more than the same record without the 自选 mark', () => {
const { m, p0 } = prep();
const rec = p0.gd.chess(T5A);
assert.ok(rec.diyFor, 'a composed 自选 record');
const withMark = buyScoreOf(m, p0, T5A);
// the same player and record, only the 自选 mark taken off (a view of the match's data with that record)
const plain = { ...rec };
delete plain.diyFor;
const saved = p0.gd;
p0.gd = diyGameData(m.gd, new Map([[T5A, plain]]));
let without;
try { without = buyScoreOf(m, p0, T5A); } finally { p0.gd = saved; }
assert.equal(withMark - without, DIY_PIECE_BONUS);
checkInvariants(m);
m.dispose();
});
test('#13 levelUp: one round earlier for the step that opens a slotted tier (round 9: level 4 → 5 at a price above 2); a player without picks waits', () => {
const { m, p0, p1 } = prep(912);
m.round = 9;
for (const ps of [p0, p1]) {
ps.shop.level = 4;
ps.shop.upgradePrice = 5;
ps.funds = 6;
const ids = chessOfTier(1).filter((id) => m.pool.has(id) && m.pool.left(id) > 0).slice(0, 6);
for (const id of ids) give(m, ps, id, 'hand');
}
assert.ok(diyOpensAt(p0, 5) && !diyOpensAt(p1, 5) && !diyOpensAt(p0, 6));
levelUp(m, p0);
levelUp(m, p1);
assert.equal(p0.shop.level, 5, 'p_0 (a tier-5 自选 piece) levels now');
assert.equal(p1.shop.level, 4, 'p_1 waits for round 10 (LEVEL_TARGET)');
m.dispose();
});