Prototype Fremen Nexus free Revival for base factions

This commit is contained in:
Quintine committed 2026-09-27 14:48:31 +10:00
1 parent 5c1c944a4f
commit 1e690b7062
10 files changed
+350 -7

No files matched your search

+2 -2
View File
@@ -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));
+121
View File
@@ -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);
});