Enable Reinforcements in clean Ecaz-Moritani battles
This commit is contained in:
1 parent
1083c6ccbf
commit
6ac5dabdee
11 files changed
+180
-28
No files matched your search
@@ -153,6 +153,7 @@ export function harassCustody(g: Game) {
|
||||
);
|
||||
assert.deepEqual(
|
||||
[
|
||||
...(g.ecazLoyalty?.card ? [g.ecazLoyalty.card] : []),
|
||||
...(g.traitorReserve ?? []),
|
||||
...g.players.flatMap((p) => [
|
||||
...p.traitors,
|
||||
|
||||
@@ -33,9 +33,9 @@ function battle(advanced = false): Game {
|
||||
return game;
|
||||
}
|
||||
|
||||
function selections(view: GameView) {
|
||||
function selections(view: GameView, leader = 'emperor-0') {
|
||||
return (['weapon', 'defense'] as const).map(slot =>
|
||||
battlePlanCardOptions(view, slot, 'emperor-0', '', '').some(card => card.id === cardId));
|
||||
battlePlanCardOptions(view, slot, leader, '', '').some(card => card.id === cardId));
|
||||
}
|
||||
|
||||
function selector(html: string, slot: 'weapon' | 'defense') {
|
||||
@@ -61,6 +61,28 @@ void test('a held Reinforcements card with an owner offer is selectable in eithe
|
||||
assert.doesNotMatch(opponentHtml, /value="ecaz-reinforcements"/);
|
||||
});
|
||||
|
||||
void test('paired Ecaz/Moritani holders have private two-slot controls and legal AI plans', () => {
|
||||
for (const advanced of [false, true]) for (const owner of ['ecaz', 'moritani'] as const) {
|
||||
const game = harassWithdrawGame({ advanced, factions: [owner, owner === 'ecaz' ? 'moritani' : 'ecaz', 'atreides'] });
|
||||
takeHarassCard(game, 'a', cardId);
|
||||
const view = viewGame(game, 'a');
|
||||
assert.deepEqual(selections(view, `${owner}-0`), [true, true]);
|
||||
const html = renderToStaticMarkup(createElement(GameTable, { game: view, send: async () => {}, onExit() {}, busy: false }));
|
||||
assert.match(selector(html, 'weapon'), /value="ecaz-reinforcements"/);
|
||||
assert.match(selector(html, 'defense'), /value="ecaz-reinforcements"/);
|
||||
const opponent = viewGame(game, 'd');
|
||||
assert.equal(opponent.battle?.reinforcements, null);
|
||||
assert.ok(!opponent.players.find(player => player.id === 'a')?.hand?.some(card => card.id === cardId));
|
||||
for (const profile of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) {
|
||||
view.players.find(player => player.id === 'a')!.bot = profile;
|
||||
const choice = botActions(view).find(action =>
|
||||
action.type === 'battlePlan' && (action.weapon === cardId || action.defense === cardId));
|
||||
assert.ok(choice, `${owner} ${profile} ${advanced ? 'Advanced' : 'Basic'}`);
|
||||
assert.ok(applyAction(game, 'a', choice).battle?.plans.a);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
void test('a no-category inspection permits a held Reinforcements card but a second slot cannot reuse it', () => {
|
||||
const view = viewGame(battle(), 'a');
|
||||
view.battle!.ownCommitments = [{ source: 'native', beneficiary: 'd', target: 'a', field: 'weapon', value: null }];
|
||||
|
||||
@@ -69,6 +69,44 @@ void test('Reinforcements consumes exactly three own normal reserves in either c
|
||||
}
|
||||
});
|
||||
|
||||
void test('paired Ecaz/Moritani battles spend one physical Reinforcements cost without changing the card slot or private plan', () => {
|
||||
for (const advanced of [false, true]) for (const owner of ['ecaz', 'moritani'] as const) {
|
||||
let g = harassWithdrawGame({ advanced, factions: [owner, owner === 'ecaz' ? 'moritani' : 'ecaz', 'atreides'] });
|
||||
takeHarassCard(g, 'a', cardId);
|
||||
harassCustody(g);
|
||||
assert.deepEqual(viewGame(g, 'a').battle?.reinforcements, { blocked: null, normal: 3, elite: 0 });
|
||||
assert.equal(viewGame(g, 'd').battle?.reinforcements, null);
|
||||
g = applyAction(JSON.parse(JSON.stringify(g)) as Game, 'a', {
|
||||
type: 'battlePlan', dial: 1, support: advanced ? 1 : 0,
|
||||
leader: `${owner}-0`, defense: cardId,
|
||||
});
|
||||
assert.deepEqual(viewGame(g, 'd').battle?.plans, {});
|
||||
g = applyAction(g, 'd', {
|
||||
type: 'battlePlan', dial: 1, support: advanced ? 1 : 0,
|
||||
leader: `${owner === 'ecaz' ? 'moritani' : 'ecaz'}-0`,
|
||||
});
|
||||
const done = resolve(g);
|
||||
assert.equal(done.battle, null);
|
||||
assert.equal(done.players[0].reserves, 12);
|
||||
assert.equal(done.discard.filter(card => card.id === cardId).length, 1);
|
||||
assert.equal(done.log.filter(event => event.text.includes('used Reinforcements: three reserve forces')).length, 1);
|
||||
}
|
||||
});
|
||||
|
||||
void test('co-present Ecaz allied armies do not open the unintegrated Reinforcements battle path', () => {
|
||||
const g = harassWithdrawGame({ advanced: true, factions: ['moritani', 'atreides', 'ecaz'] });
|
||||
g.players[0].ally = 't';
|
||||
g.players[2].ally = 'a';
|
||||
g.players[2].forces = { 'arrakeen:10': 1 };
|
||||
g.players[2].reserves = 19;
|
||||
takeHarassCard(g, 'a', cardId);
|
||||
harassCustody(g);
|
||||
assert.match(viewGame(g, 'a').battle?.reinforcements?.blocked ?? '', /co-present Ecaz allies/);
|
||||
reject(g, 'a', {
|
||||
type: 'battlePlan', dial: 1, support: 1, leader: 'moritani-0', weapon: cardId,
|
||||
}, /co-present Ecaz allies/);
|
||||
});
|
||||
|
||||
void test('opposing physical Harass plan resolves with Reinforcements instead of trapping both revealed plans', () => {
|
||||
for (const advanced of [false, true]) {
|
||||
const g = fixture(advanced);
|
||||
@@ -129,3 +167,14 @@ void test('insufficient reserves, unsupported module, and a paired special rejec
|
||||
g.sandtrout = {} as Game['sandtrout'];
|
||||
reject(g, 'a', { type: 'battlePlan', dial: 1, leader: 'emperor-0', weapon: cardId }, /standalone Ecaz card variant/);
|
||||
});
|
||||
|
||||
void test('other expansion factions remain ineligible for the paired Ecaz card prototype', () => {
|
||||
for (const owner of ['ixians', 'choam'] as const) {
|
||||
const g = harassWithdrawGame({ advanced: true, factions: [owner, 'atreides', 'harkonnen'] });
|
||||
takeHarassCard(g, 'a', cardId);
|
||||
assert.match(viewGame(g, 'a').battle?.reinforcements?.blocked ?? '', /classic or Ecaz\/Moritani/);
|
||||
reject(g, 'a', {
|
||||
type: 'battlePlan', dial: 0, support: 0, leader: `${owner}-0`, weapon: cardId,
|
||||
}, /classic or Ecaz\/Moritani/);
|
||||
}
|
||||
});
|
||||
@@ -13,17 +13,22 @@ const rows = (sqlite: DatabaseSync) => ({
|
||||
});
|
||||
|
||||
void test('a privately sealed Reinforcements plan and its one-time physical cost survive D1-style restart and competing final writes', async t => {
|
||||
for (const advanced of [false, true]) {
|
||||
for (const { advanced, factions } of [
|
||||
{ advanced: false, factions: ['emperor', 'atreides', 'harkonnen'] as const },
|
||||
{ advanced: true, factions: ['emperor', 'atreides', 'harkonnen'] as const },
|
||||
{ advanced: true, factions: ['moritani', 'ecaz', 'atreides'] as const },
|
||||
]) {
|
||||
const store = unitStore();
|
||||
t.after(() => store.sqlite.close());
|
||||
const made = await store.rooms.createRoom('Reinforcements recovery', 'emperor', advanced, []);
|
||||
const made = await store.rooms.createRoom('Reinforcements recovery', factions[0], advanced,
|
||||
factions[0] === 'moritani' ? ['ecaz'] : []);
|
||||
const code = made.view.code;
|
||||
const tokens = [made.token,
|
||||
(await store.rooms.joinRoom(code, 'Atreides', 'atreides')).token!,
|
||||
(await store.rooms.joinRoom(code, 'Harkonnen', 'harkonnen')).token!];
|
||||
const tokens = [made.token];
|
||||
for (const faction of factions.slice(1))
|
||||
tokens.push((await store.rooms.joinRoom(code, faction, faction)).token!);
|
||||
const auths = await Promise.all(tokens.map(token => store.rooms.authenticate(code, token)));
|
||||
const ids = auths.map(auth => auth.playerId) as [string, string, string];
|
||||
let game = harassWithdrawGame({ advanced, ids, factions: ['emperor', 'atreides', 'harkonnen'] });
|
||||
let game = harassWithdrawGame({ advanced, ids, factions });
|
||||
game.code = code;
|
||||
game.version = (await store.rooms.readRoom(code)).version;
|
||||
takeHarassCard(game, ids[0], 'ecaz-reinforcements');
|
||||
@@ -38,7 +43,7 @@ void test('a privately sealed Reinforcements plan and its one-time physical cost
|
||||
return rooms.readRoom(code);
|
||||
};
|
||||
game = await act(0, { type: 'battlePlan', dial: 1, support: advanced ? 1 : 0,
|
||||
leader: 'emperor-0', defense: 'ecaz-reinforcements' });
|
||||
leader: `${factions[0]}-0`, defense: 'ecaz-reinforcements' });
|
||||
const preReveal = rows(store.sqlite);
|
||||
for (const [index, token] of tokens.entries()) {
|
||||
const rooms = store.restart();
|
||||
@@ -53,7 +58,7 @@ void test('a privately sealed Reinforcements plan and its one-time physical cost
|
||||
}
|
||||
assert.deepEqual(rows(store.sqlite), preReveal);
|
||||
game = await act(1, { type: 'battlePlan', dial: 1, support: advanced ? 1 : 0,
|
||||
leader: 'atreides-0' });
|
||||
leader: `${factions[1]}-0` });
|
||||
game = await act(0, { type: 'traitorCall', call: false });
|
||||
const before = rows(store.sqlite);
|
||||
const version = game.version;
|
||||
|
||||
Reference in new issue
Block a user