feedback5: merge WE2 — fidelity (data / match / UI): summon hand counts from the tokens' hidden talents (麦哲伦 / 令 3, 白铁 2), 望's 棋子 on any tile, the 一会儿见! marker no hand card, TRY_SEARCH_ALLY_SKILL → SKILL_RANGE + allies, 斩业星熊's normal attack phys, IS / RA-only modules refused (SO-A / SO-B / RA-A), 黑键 S2 at full SP with a free tile, 重构体 / 战术锚点 placement, 塑心's cap per trait, random grants draw 自选 stock, diyBanned shown, AI 托管 buys 自选, 谬因 S2 / 薇薇安娜 S3 area triggers, #148 regression test, tier*.js / diyRecord shims removed; golden: coop2-NORMAL-70-diy, diy-045 / 078 / 114 / 115 / 116 moved (WE2's own), the W4B moves kept
This commit is contained in:
commit
723ee72d38
79 files changed
+1389
-405
No files matched your search
@@ -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();
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
// test/match/feedback5-we2-diy-grants.test.js — 0.2.0 WE2 #9: an effect / reward / 机变 card that grants a player a random
|
||||
// operator draws that player's 自选 stock too (「自选干员放入后模拟中的补给池随机范围也将被相应扩大」; PRTS 新手教程 "调度中心随机资源
|
||||
// 的范围将被扩大"): effectsMeta ctx.rollChess / ctx.rollPool (player/diy.js diyStockEntries), choices.js 驰援 fallback —
|
||||
// the roll's own tier rules, bonds through the player's view; never another player's stock.
|
||||
// Run: node --test test/match/feedback5-we2-diy-grants.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeCtx } from '../../server/match/effectsMeta.js';
|
||||
import { applyCard, tacticCard } from '../../server/match/choices.js';
|
||||
import { makeMatch, checkInvariants } from './harness.js';
|
||||
|
||||
const T5A = 'chess_char_5_diy1_a';
|
||||
const SIEGE = 'char_112_siege'; // 推进之王 (维多利亚)
|
||||
const PICK = { charId: SIEGE, skillIndex: 2, uniEquipId: 'uniequip_002_siege' };
|
||||
|
||||
/** A co-op match: p_0 slots 推进之王 at T5A, p_1 slots nothing; first prep. */
|
||||
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') };
|
||||
}
|
||||
/** Take every shared-pool copy of the chess `pred` admits (the draw then sees only what is left); m.restore() gives them back. */
|
||||
function drain(m, pred) {
|
||||
const back = (m.drained ??= []);
|
||||
for (const [id, e] of m.pool.entries) if (pred(id, e) && e.left > 0) back.push([id, m.pool.take(id, e.left)]);
|
||||
m.restore = () => { for (const [id, n] of back.splice(0)) m.pool.give(id, n); };
|
||||
}
|
||||
|
||||
test('#9 effect rolls (ctx.rollChess, any tier / bond filter) draw the player\'s 自选 stock — its own player only', () => {
|
||||
const { m, p0, p1 } = prep();
|
||||
assert.equal(p0.diyStock.left(T5A), 8);
|
||||
drain(m, (id, e) => e.tier === 5);
|
||||
const c0 = makeCtx(m, p0, { key: 'test' }, 'onTest');
|
||||
const c1 = makeCtx(m, p1, { key: 'test' }, 'onTest');
|
||||
assert.equal(c0.rollChess({ tier: 5 }), T5A, 'the only tier-5 copies left: her stock');
|
||||
assert.equal(c1.rollChess({ tier: 5 }), null, 'another player never draws it');
|
||||
// the 调度中心 level is not the shop's gate here: an effect's tier rule alone (p_0 is at level 1)
|
||||
assert.ok(p0.shop.level < 5);
|
||||
// a bond filter reads the operator: 推进之王 is 维多利亚
|
||||
drain(m, (id) => (m.gd.chess(id)?.bonds || []).includes('victoriaShip'));
|
||||
assert.equal(c0.rollChess({ bond: 'victoriaShip' }), T5A);
|
||||
assert.equal(c0.rollChess({ bond: 'yanShip', tier: 5 }), null, 'not a 炎 operator');
|
||||
// granting it takes one copy of her own stock
|
||||
assert.ok(c0.grantChess(T5A));
|
||||
assert.equal(p0.diyStock.left(T5A), 7);
|
||||
m.restore();
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('#9 reward pools (ctx.rollPool: a shared-pool chess pool) and the 维多利亚驰援 card draw it too', () => {
|
||||
const { m, p0, p1 } = prep(910);
|
||||
drain(m, (id, e) => e.tier === 5);
|
||||
const c0 = makeCtx(m, p0, { key: 'test' }, 'onTest');
|
||||
const c1 = makeCtx(m, p1, { key: 'test' }, 'onTest');
|
||||
assert.deepEqual(c0.rollPool('pool_chess_shop_5_reward'), { kind: 'chess', id: T5A, golden: false });
|
||||
assert.equal(c1.rollPool('pool_chess_shop_5_reward'), null);
|
||||
// 维多利亚驰援 (allybuff_select_7_7, single_special_choice_gain_bond_chess) with every 维多利亚 copy of the pool taken:
|
||||
// the card grants her 推进之王 from her own stock
|
||||
drain(m, (id) => (m.gd.chess(id)?.bonds || []).includes('victoriaShip'));
|
||||
const card = tacticCard(m.gd.choices.cards.tactic.find((c) => c.effectId === 'allybuff_select_7_7'));
|
||||
const owned = (ps) => [...ps.board.values(), ...ps.hand, ...ps.temp].filter((x) => x && x.kind === 'chess' && x.id === T5A).length;
|
||||
const before = p0.diyStock.left(T5A);
|
||||
applyCard(m, p0, card);
|
||||
assert.equal(owned(p0), 1, 'she got her 自选 piece');
|
||||
assert.equal(p0.diyStock.left(T5A), before - 1, 'from her stock');
|
||||
const had1 = [...p1.hand, ...p1.temp].filter(Boolean).length;
|
||||
applyCard(m, p1, card);
|
||||
assert.equal([...p1.hand, ...p1.temp].filter(Boolean).length, had1, 'p_1 has no 自选 piece to draw: nothing');
|
||||
m.restore();
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
@@ -0,0 +1,181 @@
|
||||
// test/match/feedback5-we2-placement.test.js — 0.2.0 WE2: where the 自选 summon pieces may be placed in prep.
|
||||
// #22 望's 棋子 go on any tile class (PRTS 棋子 部署位置 "全部位", 备注 "游戏内召唤物信息与实际不符(显示为仅部署在近战位)").
|
||||
// #26 Mon3tr's 重构体 only inside her attack range on the ground (her talent "可以在攻击范围内的地面使用一个…重构体");
|
||||
// 凯尔希·思衡托's 战术锚点 only on a ranged tile outside her attack range (PRTS 战术锚点 特性 "仅可以部署在凯尔希·思衡托攻击范围外的
|
||||
// 远程位") — server (PlayerState._legal / summonRange / summonExcluded, board.js 'high'), the client mirror (gameLogic
|
||||
// canPlace) and the re-orientation lift.
|
||||
// Run: node --test test/match/feedback5-we2-placement.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { ERR } from '../../shared/constants.js';
|
||||
import { FIELD, canPlace, placeClass, tileKey, parseKey } from '../../server/match/board.js';
|
||||
import { placementContext, canPlace as clientCanPlace, diyGetter } from '../../public/js/ui/gameLogic.js';
|
||||
import { DATA, makeMatch, checkInvariants } from './harness.js';
|
||||
|
||||
const T5A = 'chess_char_5_diy1_a';
|
||||
const WANG = 'char_2027_wang';
|
||||
const STONE = 'token_10064_wang_stone1';
|
||||
const MONSTR = 'char_4179_monstr';
|
||||
const PROSTS = 'token_10050_monstr_prosts';
|
||||
const KALTS2 = 'char_1052_kalts2';
|
||||
const ANCHOR = 'token_10068_kalts2_mtship';
|
||||
|
||||
/** A solo match whose human slots `picks`, in its first prep on `stage` (战场#01: 3 高台 tiles) with an empty board and hand. */
|
||||
function prep(picks, seed = 2101, stage = 'act2autochess_m01') {
|
||||
const seats = [{ seat: 0, playerId: 'p_0', name: 'P0', isBot: false, connected: true, diy: picks }];
|
||||
const h = makeMatch({ mode: 'solo', difficulty: 'NORMAL', seats, seed }).start();
|
||||
h.toPrep(1);
|
||||
h.setStage(stage);
|
||||
const ps = h.ps('p_0');
|
||||
for (const p of [...ps.board.values(), ...ps.hand.filter(Boolean)]) if (p.kind === 'chess') ps.returnCopies(p);
|
||||
ps.board.clear();
|
||||
ps.hand.fill(null);
|
||||
ps.recompute();
|
||||
ps.funds = 100;
|
||||
return { h, m: h.m, ps };
|
||||
}
|
||||
|
||||
/** Free tiles of the player's deploy map of class `cls` ('melee' | 'ranged'), reading order. */
|
||||
function tilesOf(ps, cls) {
|
||||
const map = ps.deployMap();
|
||||
const out = [];
|
||||
for (let r = FIELD.r1; r >= FIELD.r0; r--) for (let c = FIELD.c0; c <= FIELD.c1; c++) {
|
||||
if (!ps.board.has(tileKey(r, c)) && map.get(tileKey(r, c)) === cls) out.push([r, c]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Gain slot `slotId` (its 自选 operator) and place it on the first free tile of its class; returns the piece. */
|
||||
function placeDiy(m, ps, slotId, at = null) {
|
||||
const piece = ps.acquireChess(slotId, { source: 'buy' });
|
||||
assert.ok(piece, `${slotId} gained`);
|
||||
const map = ps.deployMap();
|
||||
const pos = placeClass(ps, ps.gd.chess(slotId));
|
||||
let tile = at;
|
||||
for (let r = FIELD.r1; r >= FIELD.r0 && !tile; r--) for (let c = FIELD.c0; c <= FIELD.c1 && !tile; c++) {
|
||||
if (!ps.board.has(tileKey(r, c)) && canPlace(map, pos, r, c)) tile = [r, c];
|
||||
}
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: piece.uid, to: { area: 'board', row: tile[0], col: tile[1] } }), { ok: true }, `${slotId} placed`);
|
||||
return piece;
|
||||
}
|
||||
|
||||
test('#22 望\'s 棋子: a hand piece goes on a 高台 (ranged tile) as well as on the ground — PRTS 棋子 全部位', () => {
|
||||
const { m, ps } = prep({ [T5A]: { charId: WANG, skillIndex: 0 } });
|
||||
assert.equal(ps.gd.token(STONE).position, 'ALL');
|
||||
const wang = placeDiy(m, ps, T5A);
|
||||
const stack = ps.hand.find((p) => p && p.kind === 'token' && p.id === STONE && p.ownerUid === wang.uid);
|
||||
assert.ok(stack && stack.count === 6, 'her 6 棋子 joined the hand');
|
||||
const high = tilesOf(ps, 'ranged');
|
||||
const ground = tilesOf(ps, 'melee');
|
||||
assert.ok(high.length && ground.length, 'the stage has both tile classes');
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: stack.uid, to: { area: 'board', row: high[0][0], col: high[0][1] } }), { ok: true }, 'onto a 高台');
|
||||
const left = ps.hand.find((p) => p && p.kind === 'token' && p.id === STONE);
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: left.uid, to: { area: 'board', row: ground[0][0], col: ground[0][1] } }), { ok: true }, 'onto the ground');
|
||||
// still refused off the deploy map (a forbidden tile of the field)
|
||||
const map = ps.deployMap();
|
||||
let off = null;
|
||||
for (let r = FIELD.r1; r >= FIELD.r0 && !off; r--) for (let c = FIELD.c0; c <= FIELD.c1 && !off; c++) if (!map.get(tileKey(r, c)) && !ps.board.has(tileKey(r, c))) off = [r, c];
|
||||
if (off) {
|
||||
const again = ps.hand.find((p) => p && p.kind === 'token' && p.id === STONE);
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: again.uid, to: { area: 'board', row: off[0], col: off[1] } }), { error: ERR.BAD_TILE }, 'not a deployable tile');
|
||||
}
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
/** The owner's attack-range keys on the board (the server's own reading). */
|
||||
const rangeOf = (ps, owner) => ps._ownerRangeOf({ kind: 'token', ownerUid: owner.uid, id: '' });
|
||||
/** The summon stack of `owner` in the hand. */
|
||||
const stackOf = (ps, owner, tokenId) => ps.hand.find((p) => p && p.kind === 'token' && p.id === tokenId && p.ownerUid === owner.uid);
|
||||
/** The client's legality of dropping `uid` on (r, c), from the player's private view. */
|
||||
function clientOk(m, ps, uid, r, c) {
|
||||
const priv = ps.privateView();
|
||||
const getChess = diyGetter((id) => DATA.chess[id] ?? null, priv, DATA);
|
||||
const ctx = placementContext({
|
||||
priv, stage: m.stage, editable: true, getChess, backups: DATA.backups,
|
||||
getToken: (id) => DATA.tokens[id] ?? DATA.backups.tokens[id] ?? null, getItem: (id) => DATA.items[id] ?? null,
|
||||
});
|
||||
return clientCanPlace(ctx, uid, { area: 'board', row: r, col: c }).ok;
|
||||
}
|
||||
/** Every free tile of the field: [r, c, serverLegal]. */
|
||||
function freeTiles(ps, piece) {
|
||||
const out = [];
|
||||
for (let r = FIELD.r1; r >= FIELD.r0; r--) for (let c = FIELD.c0; c <= FIELD.c1; c++) if (!ps.board.has(tileKey(r, c))) out.push([r, c, ps._legal(piece, r, c)]);
|
||||
return out;
|
||||
}
|
||||
|
||||
test('#26 Mon3tr\'s 重构体: only on a ground tile of her attack range — data ownerRange from her talent; server and client agree', () => {
|
||||
assert.equal(DATA.backups.tokens[PROSTS].ownerRange, true);
|
||||
const { m, ps } = prep({ [T5A]: { charId: MONSTR, skillIndex: 0 } });
|
||||
const mon = placeDiy(m, ps, T5A, [10, 5]);
|
||||
const stack = stackOf(ps, mon, PROSTS);
|
||||
assert.ok(stack, 'her 重构体 joined the hand');
|
||||
const range = rangeOf(ps, mon);
|
||||
const map = ps.deployMap();
|
||||
const tiles = freeTiles(ps, stack);
|
||||
let inside = 0, outside = 0;
|
||||
for (const [r, c, legal] of tiles) {
|
||||
const k = tileKey(r, c);
|
||||
assert.equal(legal, range.has(k) && map.get(k) === 'melee', `server ${k}`);
|
||||
assert.equal(clientOk(m, ps, stack.uid, r, c), legal, `client ${k}`);
|
||||
if (legal) inside++; else if (map.get(k) === 'melee') outside++;
|
||||
}
|
||||
assert.ok(inside > 0 && outside > 0, `${inside} legal ground tiles in her range, ${outside} outside`);
|
||||
const [r0, c0] = tiles.find((t) => t[2]);
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: stack.uid, to: { area: 'board', row: r0, col: c0 } }), { ok: true });
|
||||
const off = tiles.find(([r, c]) => map.get(tileKey(r, c)) === 'melee' && !range.has(tileKey(r, c)));
|
||||
const again = ps.board.get(tileKey(r0, c0));
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: again.uid, to: { area: 'board', row: off[0], col: off[1] } }), { error: ERR.BAD_TILE }, 'outside her range');
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('#26 凯尔希·思衡托\'s 战术锚点 (S3): only on a ranged tile outside her attack range; re-oriented so that her range covers it, it goes back to the hand', () => {
|
||||
const rec = DATA.backups.tokens[ANCHOR];
|
||||
assert.deepEqual([rec.position, rec.ownerRangeOutside, rec.rangedTilesOnly, rec.ownerRange], ['RANGED', true, true, false]);
|
||||
// act1 m03: 高台 at (10, 3), (10, 4), (11, 7), (11, 8); she stands on the ground at (12, 5), facing RIGHT — her y-6 takes
|
||||
// (11, 7) in and leaves the other three out
|
||||
const { h, m, ps } = prep({ [T5A]: { charId: KALTS2, skillIndex: 2 } }, 2102, 'act1autochess_m03');
|
||||
const map = ps.deployMap();
|
||||
const highs = [...map].filter(([, v]) => v === 'ranged').map(([k]) => parseKey(k));
|
||||
assert.ok(highs.length >= 2, 'the stage has 高台 tiles');
|
||||
const kal = placeDiy(m, ps, T5A, [12, 5]);
|
||||
const stack = stackOf(ps, kal, ANCHOR);
|
||||
assert.ok(stack && (stack.count || 1) === 1, 'one 战术锚点 in the hand (S3)');
|
||||
const range = rangeOf(ps, kal);
|
||||
const tiles = freeTiles(ps, stack);
|
||||
let legalN = 0;
|
||||
for (const [r, c, legal] of tiles) {
|
||||
const k = tileKey(r, c);
|
||||
assert.equal(legal, map.get(k) === 'ranged' && !range.has(k), `server ${k}`);
|
||||
assert.equal(clientOk(m, ps, stack.uid, r, c), legal, `client ${k}`);
|
||||
if (legal) legalN++;
|
||||
}
|
||||
const insideHigh = highs.find(([r, c]) => range.has(tileKey(r, c)) && !ps.board.has(tileKey(r, c)));
|
||||
const ground = tiles.find(([r, c]) => map.get(tileKey(r, c)) === 'melee' && !range.has(tileKey(r, c)));
|
||||
assert.ok(ground, 'a ground tile outside her range');
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: stack.uid, to: { area: 'board', row: ground[0], col: ground[1] } }), { error: ERR.BAD_TILE }, 'not on the ground');
|
||||
if (insideHigh) assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: stack.uid, to: { area: 'board', row: insideHigh[0], col: insideHigh[1] } }), { error: ERR.BAD_TILE }, 'not inside her range');
|
||||
assert.ok(legalN > 0, 'a 高台 outside her range');
|
||||
// placed on a 高台 outside her range; then she turns so that her range takes that tile in: the anchor goes back
|
||||
for (const dir of ['RIGHT', 'UP', 'DOWN', 'LEFT']) {
|
||||
const atDir = { key: tileKey(12, 5), piece: kal, dir };
|
||||
const keys = ps._ownerRangeOf({ kind: 'token', ownerUid: kal.uid, id: '' }, atDir);
|
||||
const spot = tiles.find(([r, c, legal]) => legal && keys.has(tileKey(r, c)));
|
||||
if (!spot) continue;
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: stack.uid, to: { area: 'board', row: spot[0], col: spot[1] } }), { ok: true }, 'onto a free 高台 outside her range');
|
||||
const placed = ps.board.get(tileKey(spot[0], spot[1]));
|
||||
assert.equal(placed.id, ANCHOR);
|
||||
const toasts = h.sent.length;
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: kal.uid, to: { area: 'board', row: 12, col: 5 }, dir }), { ok: true }, `re-oriented ${dir}`);
|
||||
assert.ok(!ps.board.has(tileKey(spot[0], spot[1])), 'the anchor left the board');
|
||||
assert.ok(stackOf(ps, kal, ANCHOR), 'back on its stack');
|
||||
const said = h.sent.slice(toasts).filter(([, msg]) => msg.t === 'm.toast').map(([, msg]) => msg.msgid || msg.text);
|
||||
assert.ok(said.some((x) => String(x).includes('只能部署在召唤者攻击范围外')), `the toast: ${said.join(' / ')}`);
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
return;
|
||||
}
|
||||
assert.fail('no facing of hers covers a legal 高台');
|
||||
});
|
||||
@@ -12,7 +12,7 @@ after(async () => { if (srv) await srv.close(); });
|
||||
test('/sim/ serves the simulation modules read-only; nothing else of server/ is reachable', async () => {
|
||||
srv = await startServer({ port: 0, host: '127.0.0.1', quiet: true });
|
||||
const get = (p, headers = {}) => fetch(srv.url + p, { redirect: 'manual', headers });
|
||||
for (const p of ['/sim/spec.js', '/sim/Battle.js', '/sim/simdata.js', '/sim/constants.js', '/sim/content/index.js', '/sim/content/kits/tier1.js', '/sim/content/support/index.js']) {
|
||||
for (const p of ['/sim/spec.js', '/sim/Battle.js', '/sim/simdata.js', '/sim/constants.js', '/sim/content/index.js', '/sim/content/kits/index.js', '/sim/content/support/index.js']) {
|
||||
const r = await get(p);
|
||||
assert.equal(r.status, 200, p);
|
||||
assert.match(r.headers.get('content-type'), /^text\/javascript/, p);
|
||||
|
||||
Reference in new issue
Block a user