Retain actual Cunning appearances, paid Karama and typed remote riding with optional Sandmaster collection and source-parent HMS entry. Preserve Betrayal movement sources and apply its declared worm restriction to Great Maker reserve riding without suppressing votes or casualties. Completed Maker history no longer blocks a later ordinary offer. Validation: frozen 14-file86/86, types/lint/build, nine finite original-action/rendered-control programmes, four minimal legal policies. Discovery destination proof remains focused quotes pending actual arrival; full combinations, assurance and deployed acceptance remain required. No saved-game, restart or deployment operation.
151 lines
11 KiB
TypeScript
151 lines
11 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { botActions } from '../game/bots';
|
|
import { NEXUS_FACTIONS } from '../game/nexus-cards';
|
|
import { presenceAt } from '../game/force-presence';
|
|
import { MOBILE_LOCATION, MOBILE_STRONGHOLD } from '../game/board';
|
|
import {
|
|
createFremenCunningModulesFixture, completeFremenCunningModules,
|
|
fremenCunningModulesPlayer as player,
|
|
} from './fixture-fremen-cunning-modules';
|
|
|
|
function physicalNexus(game: Game): void {
|
|
const cards = game.nexusCards!.cards!;
|
|
assert.deepEqual([...cards.deck, ...cards.discard, ...Object.values(cards.hands).filter(card => card !== null)].sort(), [...NEXUS_FACTIONS].sort());
|
|
}
|
|
for (const expansions of [['ix'], ['choam']] as const) void test(`Basic selected ${expansions[0]} independent/Nexus uses its actual drawn Fremen Cunning after an empty appearance and real closing Nexus`, () => {
|
|
const fixture = createFremenCunningModulesFixture({ expansions: [...expansions] });
|
|
assert.ok(fixture.afterSetup.ecazTreachery && fixture.afterSetup.nexusCards);
|
|
assert.deepEqual(fixture.afterSetup.expansions, [...expansions]);
|
|
assert.equal(fixture.counterPurchase.step.actor, 'guild');
|
|
assert.equal(fixture.counterPurchase.step.action.type, 'bid');
|
|
assert.equal(player(fixture.counterPurchase.after, 'guild').spice, player(fixture.counterPurchase.before, 'guild').spice - 1);
|
|
assert.ok(player(fixture.counterPurchase.after, 'guild').hand.some(card => card.id === fixture.counterCard && card.effect === 'karama'));
|
|
assert.equal(player(fixture.shipment.after, 'fremen').reserves, player(fixture.shipment.before, 'fremen').reserves - 3);
|
|
assert.equal(player(fixture.shipment.after, 'fremen').spice, player(fixture.shipment.before, 'fremen').spice);
|
|
assert.equal(player(fixture.draw.after, 'guild').ally, 'emperor');
|
|
assert.equal(player(fixture.draw.after, 'emperor').ally, 'guild');
|
|
assert.equal(player(fixture.draw.after, 'fremen').ally, null);
|
|
assert.equal(fixture.draw.after.nexusCards!.cards!.hands.fremen, 'fremen');
|
|
assert.equal(fixture.occurrence.initiallyEmpty, true);
|
|
assert.equal(fixture.occurrence.origin, 'natural');
|
|
assert.equal(fixture.occurrence.pile, 0);
|
|
assert.equal(fixture.occurrence.turn, 3);
|
|
assert.ok(fixture.game.players.every(player => presenceAt(player, fixture.occurrence.territory) === 0));
|
|
assert.equal(fixture.game.spice['red_chasm:7'], 8, 'The accepted appearance is captured before the original devouring');
|
|
const done = completeFremenCunningModules(fixture), game = done.game;
|
|
assert.equal(done.spentResponse.response?.kind, 'nexusFremenCunning');
|
|
assert.equal(done.spentResponse.nexusCards!.cards!.hands.fremen, null);
|
|
assert.equal(done.spentResponse.nexusCards!.cards!.discard.filter(card => card === 'fremen').length, 1);
|
|
assert.equal(player(done.spentResponse, 'fremen').forces[fixture.sourceKey], 3);
|
|
assert.equal(done.spentResponse.spice['red_chasm:7'], 8);
|
|
assert.equal(done.beforeRide.spice['red_chasm:7'] ?? 0, 0);
|
|
assert.equal(done.beforeRide.spice['hagga_basin:12'], 6);
|
|
assert.equal(done.beforeRide.nexus, true);
|
|
assert.deepEqual([...done.beforeRide.ready].sort(), done.beforeRide.players.map(player => player.id).sort());
|
|
assert.equal(done.beforeRide.decision?.kind, 'nexusFremenCunningRide');
|
|
assert.equal(done.beforeRide.decision?.player, 'fremen');
|
|
assert.equal(done.beforeRide.nexusCards?.phase?.stage, 'spice', 'Original closing-card choices follow the post-negotiation rides');
|
|
assert.equal(game.nexusFremenCunningRides!.find(ride => ride.occurrence.event === fixture.occurrence.event)!.stage, 'complete');
|
|
assert.equal(player(game, 'fremen').forces[fixture.sourceKey] ?? 0, 0);
|
|
assert.equal(player(game, 'fremen').forces['hagga_basin:12'], 3);
|
|
assert.equal(player(game, 'fremen').moved, player(done.beforeRide, 'fremen').moved);
|
|
assert.equal(player(game, 'fremen').reserves, player(done.beforeRide, 'fremen').reserves);
|
|
assert.equal(player(game, 'fremen').spice, player(done.beforeRide, 'fremen').spice);
|
|
assert.equal(game.spice['hagga_basin:12'], 6);
|
|
const canceled = applyAction(done.spentResponse, 'guild', { type: 'card', mode: 'cancel', card: fixture.counterCard });
|
|
assert.equal(canceled.nexusFremenCunningRides!.find(ride => ride.occurrence.event === fixture.occurrence.event)!.stage, 'complete');
|
|
assert.equal(player(canceled, 'fremen').forces[fixture.sourceKey], 3);
|
|
assert.equal(player(canceled, 'fremen').forces['hagga_basin:12'] ?? 0, 0);
|
|
assert.equal(canceled.nexusCards!.cards!.hands.fremen, null);
|
|
assert.equal(canceled.nexusCards!.cards!.discard.filter(card => card === 'fremen').length, 1);
|
|
assert.equal(canceled.discard.filter(card => card.id === fixture.counterCard).length, 1);
|
|
assert.equal(canceled.spice['red_chasm:7'] ?? 0, 0, 'Canceling the extra ride preserves the original worm destruction');
|
|
physicalNexus(game);
|
|
});
|
|
|
|
void test('Advanced registered Ixians/CHOAM/Ecaz with selected independent decks, World/all14/phase retains typed remote riding and genuine Sandmaster collection', () => {
|
|
const fixture = createFremenCunningModulesFixture({ advanced: true, expansions: ['ix', 'choam', 'ecaz'],
|
|
factions: ['fremen', 'guild', 'emperor', 'ixians', 'choam', 'ecaz'],
|
|
skills: true, homeworlds: true, tech: true, strongholds: true });
|
|
assert.ok(fixture.afterSetup.ecazTreachery && fixture.afterSetup.nexusCards && fixture.afterSetup.homeworlds &&
|
|
fixture.afterSetup.leaderSkills && fixture.afterSetup.techTokens && fixture.afterSetup.strongholdCards);
|
|
assert.equal(fixture.afterSetup.players.length, 6);
|
|
assert.ok(fixture.trainer);
|
|
const assignment = fixture.afterSetup.leaderSkills!.assignments.find(assignment => assignment.owner === 'fremen'); assert.ok(assignment);
|
|
assert.equal(assignment.skill, 'sandmaster');
|
|
assert.equal(assignment.leader, fixture.trainer);
|
|
assert.ok(fixture.actions.some(step => step.actor === 'fremen' && step.action.type === 'leaderSkill' && step.action.skill === 'sandmaster'));
|
|
assert.equal(player(fixture.shipment.after, 'fremen').elites!.reserves, player(fixture.shipment.before, 'fremen').elites!.reserves - 2);
|
|
assert.equal(player(fixture.game, 'fremen').elites!.forces[fixture.sourceKey], 2);
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const own = viewGame(fixture.game, 'fremen'); own.players.find(player => player.id === 'fremen')!.bot = difficulty;
|
|
const action = botActions(own)[0]; assert.ok(action);
|
|
const spent = applyAction(fixture.game, 'fremen', action);
|
|
assert.equal(spent.nexusCards!.cards!.hands.fremen, null);
|
|
assert.equal(spent.response?.kind, 'nexusFremenCunning');
|
|
}
|
|
const done = completeFremenCunningModules(fixture, { collect: true });
|
|
assert.equal(done.collection!.leader, fixture.trainer);
|
|
assert.equal(done.collection!.key, 'hagga_basin:12');
|
|
assert.equal(done.collection!.before, 6);
|
|
assert.equal(player(done.afterRide, 'fremen').spice, player(done.beforeRide, 'fremen').spice + 1);
|
|
assert.equal(done.afterRide.spice['hagga_basin:12'], 5);
|
|
assert.equal(player(done.afterRide, 'fremen').forces['hagga_basin:12'], 3);
|
|
assert.equal(player(done.afterRide, 'fremen').elites!.forces['hagga_basin:12'], 2);
|
|
assert.equal(player(done.afterRide, 'fremen').forces[fixture.sourceKey] ?? 0, 0);
|
|
assert.equal(player(done.afterRide, 'fremen').elites!.forces[fixture.sourceKey] ?? 0, 0);
|
|
assert.equal(player(done.afterRide, 'fremen').moved, player(done.beforeRide, 'fremen').moved);
|
|
assert.equal(player(done.afterRide, 'fremen').shipped, player(done.beforeRide, 'fremen').shipped);
|
|
assert.equal(player(done.afterRide, 'fremen').reserves, player(done.beforeRide, 'fremen').reserves);
|
|
for (const [key, amount] of Object.entries(done.beforeRide.spice))
|
|
if (key !== 'hagga_basin:12') assert.equal(done.afterRide.spice[key], amount, 'A remote ride collects from its destination alone');
|
|
const invalidPile: Action = { ...done.ride.action, sandmasterPile: 'wind_pass:14' };
|
|
assert.throws(() => applyAction(done.beforeRide, 'fremen', invalidPile), /pile|spice|Sandmaster/i);
|
|
assert.throws(() => applyAction(done.beforeRide, 'fremen', { ...done.ride.action, sandmasterCollect: 'yes' } as unknown as Action), /Sandmaster|collect/i);
|
|
for (const collect of [false, undefined]) {
|
|
const plain = { ...done.ride.action };
|
|
delete plain.sandmasterCollect; delete plain.sandmasterPile;
|
|
const result = applyAction(done.beforeRide, 'fremen', { ...plain, ...(collect === undefined ? {} : { sandmasterCollect: collect }) });
|
|
assert.equal(player(result, 'fremen').forces['hagga_basin:12'], 3);
|
|
assert.equal(player(result, 'fremen').elites!.forces['hagga_basin:12'], 2);
|
|
assert.equal(player(result, 'fremen').spice, player(done.beforeRide, 'fremen').spice);
|
|
for (const [key, amount] of Object.entries(done.beforeRide.spice))
|
|
assert.equal(result.spice[key], amount, 'Declining collection leaves each existing pile unchanged');
|
|
assert.equal(result.spice['habbanya_erg:16'], 8,
|
|
'The original second Advanced spice pile continues after the first-pile remote ride');
|
|
}
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const own = viewGame(done.beforeRide, 'fremen'); own.players.find(player => player.id === 'fremen')!.bot = difficulty;
|
|
const action = botActions(own)[0]; assert.ok(action);
|
|
const result = applyAction(done.beforeRide, 'fremen', action);
|
|
assert.equal(result.nexusFremenCunningRides!.find(ride => ride.occurrence.event === fixture.occurrence.event)!.stage, 'complete');
|
|
}
|
|
physicalNexus(done.game);
|
|
});
|
|
|
|
void test('Advanced actual Ixian first-Storm HMS placement at the native desert source admits the typed Cunning arrival into its interior', () => {
|
|
const fixture = createFremenCunningModulesFixture({ advanced: true, expansions: ['ix', 'choam', 'ecaz'],
|
|
factions: ['fremen', 'guild', 'emperor', 'ixians', 'choam', 'ecaz'],
|
|
skills: true, homeworlds: true, tech: true, strongholds: true, hmsAtSource: true });
|
|
const placement = fixture.hmsPlacement; assert.ok(placement);
|
|
assert.equal(placement.before.mobileStronghold!.location, null);
|
|
assert.equal(placement.after.mobileStronghold!.location, 'wind_pass:14');
|
|
assert.equal(placement.step.actor, 'ixians');
|
|
assert.equal(fixture.game.mobileStronghold!.location, 'wind_pass:14');
|
|
const done = completeFremenCunningModules(fixture, { destination: { territory: MOBILE_STRONGHOLD, sector: 0 } });
|
|
assert.equal(player(done.afterRide, 'fremen').forces[MOBILE_LOCATION], 3);
|
|
assert.equal(player(done.afterRide, 'fremen').elites!.forces[MOBILE_LOCATION], 2);
|
|
assert.equal(player(done.afterRide, 'fremen').forces[fixture.sourceKey] ?? 0, 0);
|
|
assert.equal(done.afterRide.mobileStronghold!.location, 'wind_pass:14');
|
|
assert.deepEqual(player(done.afterRide, 'ixians').forces, player(done.beforeRide, 'ixians').forces);
|
|
assert.deepEqual(player(done.afterRide, 'ixians').elites, player(done.beforeRide, 'ixians').elites);
|
|
assert.equal(player(done.afterRide, 'fremen').moved, player(done.beforeRide, 'fremen').moved);
|
|
assert.equal(player(done.afterRide, 'fremen').shipped, player(done.beforeRide, 'fremen').shipped);
|
|
assert.equal(player(done.afterRide, 'fremen').reserves, player(done.beforeRide, 'fremen').reserves);
|
|
assert.equal(player(done.afterRide, 'fremen').spice, player(done.beforeRide, 'fremen').spice);
|
|
physicalNexus(done.game);
|
|
});
|