Prototype Fremen Nexus free Revival for base factions
This commit is contained in:
1 parent
5c1c944a4f
commit
1e690b7062
10 files changed
+350
-7
No files matched your search
@@ -23,7 +23,7 @@ export function nexusFixture(
|
||||
options: {
|
||||
seatIds?: [string, string, string];
|
||||
advanced?: boolean;
|
||||
hostFaction?: 'fremen' | 'moritani' | 'ecaz';
|
||||
hostFaction?: 'fremen' | 'moritani' | 'ecaz' | 'guild' | 'emperor';
|
||||
} = {},
|
||||
): Game {
|
||||
const [f, a, h] = options.seatIds ?? ['f', 'a', 'h'];
|
||||
@@ -140,7 +140,7 @@ export function nexusTurnTwo(
|
||||
options: {
|
||||
seatIds?: [string, string, string];
|
||||
advanced?: boolean;
|
||||
hostFaction?: 'fremen' | 'moritani' | 'ecaz';
|
||||
hostFaction?: 'fremen' | 'moritani' | 'ecaz' | 'guild' | 'emperor';
|
||||
} = {},
|
||||
): Game {
|
||||
const first = enterNexusSpice(nexusFixture(options));
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { createElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { applyAction, viewGame, type Game } from '../game/engine';
|
||||
import { ecazTreacheryCards } from '../game/ecaz-cards';
|
||||
import { botActions } from '../game/bots';
|
||||
import { nexusFremenRevivalAction } from '../game/nexus-fremen-revival-options';
|
||||
import { NexusFremenRevival } from '../components/nexus-fremen-revival';
|
||||
import { nexusInventory, nexusReady, nexusReload, nexusTurnTwo, orderNexusSpice, finishNexusSpice } from './fixture-nexus-cards';
|
||||
|
||||
function fixture(advanced = false, elite = 0) {
|
||||
let g = nexusTurnTwo({ advanced, hostFaction: advanced ? 'emperor' : 'guild', seatIds: ['q', 'p', 'r'] });
|
||||
orderNexusSpice(g, ['worm', 'land', 'land']);
|
||||
g = nexusReady(nexusReady(g));
|
||||
g = applyAction(g, 'p', { type: 'alliance', target: 'r' });
|
||||
g = applyAction(g, 'r', { type: 'alliance', target: 'p' });
|
||||
g = finishNexusSpice(g);
|
||||
const cards = g.nexusCards!.cards!;
|
||||
cards.deck = ['fremen', ...cards.deck.filter(card => card !== 'fremen')];
|
||||
g = applyAction(g, 'q', { type: 'nexusCardChoice', turn: g.turn, card: null, choice: 'draw', ownRedraws: 0 });
|
||||
for (const id of g.nexusCards!.phase!.eligible)
|
||||
if (!g.nexusCards!.phase!.done.includes(id))
|
||||
g = applyAction(g, id, { type: 'nexusCardChoice', turn: g.turn, card: null, choice: 'keep', ownRedraws: 0 });
|
||||
g = nexusReady(g);
|
||||
for (let i = 0; g.phase === 3 && i < 15; i++) {
|
||||
if (g.phaseOpening)
|
||||
g = applyAction(g, g.players.find(p => !g.phaseOpening!.passed.includes(p.id))!.id, { type: 'ready' });
|
||||
else if (g.auction) g = applyAction(g, g.auction.active, { type: 'passBid' });
|
||||
else g = nexusReady(g);
|
||||
}
|
||||
assert.equal(g.phase, 4);
|
||||
assert.equal(g.decision, null);
|
||||
const p = g.players.find(p => p.id === 'q')!;
|
||||
assert.ok(p.reserves >= 4 && (p.elites?.reserves ?? 0) >= elite);
|
||||
p.reserves -= 4;
|
||||
p.tanks += 4;
|
||||
if (p.elites) { p.elites.reserves -= elite; p.elites.tanks += elite; }
|
||||
nexusInventory(g);
|
||||
const view = viewGame(g, 'q');
|
||||
const action = nexusFremenRevivalAction(view, elite)!;
|
||||
assert.ok(action);
|
||||
return { g, action };
|
||||
}
|
||||
|
||||
for (const advanced of [false, true]) {
|
||||
void test(`Fremen Secret Ally returns an exact free ordinary group in ${advanced ? 'Advanced' : 'Basic'} play`, () => {
|
||||
const { g, action } = fixture(advanced);
|
||||
const before = g.players.find(p => p.id === 'q')!;
|
||||
const spice = before.spice, reserves = before.reserves, tanks = before.tanks;
|
||||
const observer = viewGame(g, 'r');
|
||||
assert.equal(observer.nexusFremenRevival, null);
|
||||
assert.equal(observer.nexusCards?.card, null);
|
||||
const result = applyAction(nexusReload(g), 'q', action);
|
||||
const p = result.players.find(p => p.id === 'q')!;
|
||||
assert.equal(p.reserves, reserves + 3);
|
||||
assert.equal(p.tanks, tanks - 3);
|
||||
assert.equal(p.spice, spice);
|
||||
assert.equal(p.revived, 3);
|
||||
assert.equal(p.freeForcesRevived, 3);
|
||||
assert.equal(result.nexusCards!.cards!.hands.q, null);
|
||||
assert.ok(result.nexusCards!.cards!.discard.includes('fremen'));
|
||||
assert.equal(viewGame(result, 'q').nexusFremenRevival, null);
|
||||
assert.equal(viewGame(result, 'r').nexusFremenRevival, null);
|
||||
nexusInventory(result);
|
||||
assert.deepEqual(nexusReload(result).nexusFremenRevivalHistory, result.nexusFremenRevivalHistory);
|
||||
assert.throws(() => applyAction(nexusReload(result), 'q', action));
|
||||
});
|
||||
}
|
||||
|
||||
void test('Advanced elite cap, legal bot participation and accessible control', () => {
|
||||
const { g } = fixture(true, 1);
|
||||
const view = viewGame(g, 'q');
|
||||
assert.deepEqual(view.nexusFremenRevival?.eliteOptions, [0, 1]);
|
||||
for (const level of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) {
|
||||
const botView = structuredClone(view);
|
||||
botView.players.find(p => p.id === 'q')!.bot = level;
|
||||
const action = botActions(botView)[0];
|
||||
assert.equal(action?.type, 'nexusFremenRevive');
|
||||
const result = applyAction(nexusReload(g), 'q', action);
|
||||
const p = result.players.find(p => p.id === 'q')!;
|
||||
assert.equal(p.elites?.revived, level === 'Easy' ? 0 : 1);
|
||||
assert.equal(p.revived, 3);
|
||||
}
|
||||
const markup = renderToStaticMarkup(createElement(NexusFremenRevival, { game: view, act: () => {}, busy: false }));
|
||||
assert.match(markup, /Fremen Secret Ally revival/);
|
||||
assert.match(markup, /ordinary three-force Revival allowance/);
|
||||
assert.match(markup, /Elite forces to revive/);
|
||||
});
|
||||
|
||||
void test('rejected stale, illegal elite and unaffordable groups preserve original input', () => {
|
||||
const { g, action } = fixture(true, 1);
|
||||
for (const change of [
|
||||
(copy: Game) => { copy.players.find(p => p.id === 'q')!.revived = 1; },
|
||||
(copy: Game) => { copy.players.find(p => p.id === 'q')!.tanks = 2; },
|
||||
(copy: Game) => { copy.players.find(p => p.id === 'q')!.elites!.revived = 1; },
|
||||
]) {
|
||||
const copy = nexusReload(g);
|
||||
change(copy);
|
||||
const before = JSON.stringify(copy);
|
||||
assert.throws(() => applyAction(copy, 'q', action));
|
||||
assert.equal(JSON.stringify(copy), before);
|
||||
}
|
||||
for (const invalid of [{ ...action, elite: 2 }, { ...action, event: 'old' }, { ...action, bonus: true }]) {
|
||||
const copy = nexusReload(g);
|
||||
const before = JSON.stringify(copy);
|
||||
assert.throws(() => applyAction(copy, 'q', invalid));
|
||||
assert.equal(JSON.stringify(copy), before);
|
||||
}
|
||||
});
|
||||
|
||||
void test('independent Ecaz Treachery inventory cannot borrow the bounded revival path', () => {
|
||||
const { g, action } = fixture();
|
||||
g.ecazTreachery = true;
|
||||
g.deck.push(...ecazTreacheryCards());
|
||||
const offer = viewGame(g, 'q').nexusFremenRevival;
|
||||
assert.match(offer?.blocked ?? '', /Combined expansion and module/);
|
||||
const before = JSON.stringify(g);
|
||||
assert.throws(() => applyAction(g, 'q', action));
|
||||
assert.equal(JSON.stringify(g), before);
|
||||
});
|
||||
Reference in new issue
Block a user