Files
Dune/tests/allied-marker-guild-rate.test.ts

165 lines
8.9 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { viewGame, type Game } from '../game/engine';
import {
alliedMarkerRatePlayer as player, createAlliedMarkerGuildRate, createAlliedMarkerGuildRateScene,
declareAlliedMarkerGuildRate, passAlliedMarkerRateResponses, stepAlliedMarkerRate,
type AlliedMarkerGuildRateFixture,
} from './fixture-allied-marker-guild-rate';
function conserve(g: Game) {
for (const p of g.players)
assert.equal(p.reserves + p.tanks + Object.values(p.forces).reduce((a, b) => a + b, 0), 20);
const cards = [...g.deck, ...g.discard, ...g.players.flatMap(p => p.hand)];
assert.equal(new Set(cards.map(c => c.id)).size, cards.length);
}
function uncommitted(f: AlliedMarkerGuildRateFixture, g: Game) {
for (const id of [f.owner, f.recipient]) {
const p = player(g, id), before = player(f.beforeAction, id);
assert.equal(p.spice, before.spice);
assert.equal(p.reserves, before.reserves);
assert.deepEqual(p.forces, before.forces);
assert.equal(p.shipped, before.shipped);
assert.deepEqual(p.noField, before.noField);
assert.equal(p.noFieldEvent, before.noFieldEvent);
}
conserve(g);
}
function settled(f: AlliedMarkerGuildRateFixture, before: Game, after: Game, cost: number,
payer = f.payer) {
const owner = player(after, f.owner), recipient = player(after, f.recipient);
assert.equal(owner.spice, player(before, f.owner).spice - (payer === f.owner ? cost : 0));
assert.equal(recipient.spice, player(before, f.recipient).spice - (payer === f.recipient ? cost : 0));
assert.equal(owner.forces[f.priorSource], 3);
assert.equal(owner.reserves, 17);
assert.equal(owner.forces[f.target] ?? 0, 0);
assert.equal(recipient.forces[f.target] ?? 0, f.amount);
assert.equal(recipient.reserves, 20 - f.amount);
assert.equal(recipient.shipped, true);
assert.equal(owner.shipped, false);
assert.equal(owner.noField!.lastShipped, f.token);
assert.equal(owner.noField!.deployed, null);
assert.deepEqual(owner.noField!.tokens, player(before, f.owner).noField!.tokens);
assert.notEqual(owner.noFieldEvent, player(before, f.owner).noFieldEvent);
assert.equal(recipient.noField, player(before, f.recipient).noField);
assert.equal(after.pendingShipment, null);
assert.equal(after.richeseAllyOffer, null);
assert.equal(after.response, null);
assert.equal(after.players.reduce((sum, p) => sum + p.spice, 0),
before.players.reduce((sum, p) => sum + p.spice, 0) - cost,
'The original Guild-recipient contract still pays the bank.');
conserve(after);
}
for (const token of ['zero', 'five'] as const) {
void test(`controlled allied ${token}: native cancellation charges one full-price marker, then materializes only Guild reserves`, () => {
const f = createAlliedMarkerGuildRateScene({ token });
assert.equal(f.accepted.response?.kind, 'richeseNoField');
uncommitted(f, f.game);
assert.deepEqual(player(f.game, f.owner).noField!.deployed, {
tokenId: 'three', controller: f.owner, location: { territory: 'red_chasm', sector: 7 },
});
const pending = f.game.pendingShipment!;
assert.equal(pending.player, f.recipient);
assert.equal(pending.amount, f.amount);
assert.equal(pending.cost, 1);
assert.equal(pending.alliedNoField!.owner, f.owner);
assert.equal(pending.alliedNoField!.recipient, f.recipient);
assert.equal(pending.alliedNoField!.tokenId, token);
assert.equal(pending.alliedNoField!.payer, f.recipient);
const done = stepAlliedMarkerRate(f.game, f.cancel);
settled(f, f.beforeAction, done, 2);
assert.equal(done.discard.filter(c => c.id === f.karama).length, 1);
assert.equal(player(done, f.observer).hand.some(c => c.id === f.karama), false);
const beforeReplay = structuredClone(done);
assert.throws(() => stepAlliedMarkerRate(done, f.cancel));
assert.deepEqual(done, beforeReplay);
});
}
void test('controlled Basic allied loan: allowing the native rate pays one bank spice and preserves actual five-force custody', () => {
const f = createAlliedMarkerGuildRateScene({ advanced: false });
const done = passAlliedMarkerRateResponses(f.game, 'guildRate');
settled(f, f.beforeAction, done, 1);
assert.ok(player(done, f.observer).hand.some(c => c.id === f.karama));
});
void test('controlled recipient poverty: cancellation releases token, wallets and army; ordinary full-price replacement remains legal', () => {
const f = createAlliedMarkerGuildRateScene({ unaffordable: true });
assert.equal(player(f.beforeAction, f.recipient).spice, 1);
const released = stepAlliedMarkerRate(f.game, f.cancel);
uncommitted(f, released);
assert.equal(released.pendingShipment, null);
assert.equal(released.response, null);
assert.equal(viewGame(released, f.recipient).guildRateCanceled, true);
assert.equal(released.discard.filter(c => c.id === f.karama).length, 1);
const beforeInvalid = structuredClone(released);
assert.throws(() => stepAlliedMarkerRate(released, { actor: f.recipient,
action: { type: 'ship', amount: 1, territory: f.territory, sector: f.sector } }));
assert.deepEqual(released, beforeInvalid);
let done = stepAlliedMarkerRate(released, { actor: f.recipient,
action: { type: 'ship', amount: 1, territory: 'arrakeen', sector: 10 } });
if (done.decision?.kind === 'guildShipment') done = stepAlliedMarkerRate(done,
{ actor: f.recipient, action: { type: 'decision', allow: true } });
assert.equal(done.response, null);
assert.equal(done.pendingShipment, null);
assert.equal(player(done, f.recipient).spice, 0);
assert.equal(player(done, f.recipient).forces['arrakeen:10'], 1);
assert.equal(player(done, f.recipient).reserves, 19);
assert.equal(player(done, f.recipient).shipped, true);
assert.deepEqual(player(done, f.owner).noField, player(f.beforeAction, f.owner).noField);
assert.equal(player(done, f.owner).spice, 10);
conserve(done);
});
void test('controlled supplier-funded half-price loan cannot enlarge lender approval; a new full-price offer needs new recipient consent', () => {
const f = createAlliedMarkerGuildRateScene({ payer: 'supplier' });
assert.equal(player(f.game, f.owner).spice, 10, 'Release must not depend on supplier poverty.');
const released = stepAlliedMarkerRate(f.game, f.cancel);
uncommitted(f, released);
assert.equal(released.pendingShipment, null);
assert.equal(viewGame(released, f.recipient).guildRateCanceled, true);
assert.equal(released.discard.filter(c => c.id === f.karama).length, 1);
const newOffer = stepAlliedMarkerRate(released, { actor: f.owner, action: f.action });
assert.notEqual(newOffer.richeseAllyOffer!.event, f.offer.event);
assert.equal(newOffer.richeseAllyOffer!.payer, f.owner);
assert.equal(viewGame(newOffer, f.recipient).richeseNoField!.allyOffer!.cost, 2);
uncommitted(f, newOffer);
const declined = stepAlliedMarkerRate(newOffer, { actor: f.recipient,
action: { type: 'decision', event: newOffer.richeseAllyOffer!.event, decline: true } });
uncommitted(f, declined);
const replacement = declareAlliedMarkerGuildRate(f, declined);
assert.notEqual(replacement.game.response?.kind, 'guildRate');
settled(f, f.beforeAction, replacement.game, 2);
assert.equal(player(replacement.game, f.recipient).spice, 10, 'Never rewrite the original supplier payer.');
});
void test('controlled canceled native rate does not cancel a subsequently played independent Karama loan price', () => {
const f = createAlliedMarkerGuildRateScene({ unaffordable: true, independentCard: true });
const released = stepAlliedMarkerRate(f.game, f.cancel);
uncommitted(f, released);
assert.ok(f.independentKarama);
const purchased = stepAlliedMarkerRate(released, { actor: f.observer,
action: { type: 'card', mode: 'shipment', target: f.recipient, card: f.independentKarama } });
assert.equal(viewGame(purchased, f.recipient).guildRateCanceled, true);
assert.equal(purchased.karamaShipping?.player, f.recipient);
assert.equal(purchased.karamaShipping?.card, f.independentKarama);
assert.equal(purchased.discard.filter(c => c.id === f.independentKarama).length, 1);
const replacement = declareAlliedMarkerGuildRate(f, purchased);
assert.notEqual(replacement.game.response?.kind, 'guildRate');
settled(f, f.beforeAction, replacement.game, 1);
assert.equal(replacement.game.karamaShipping, null);
assert.equal(replacement.game.discard.filter(c => c.id === f.karama).length, 1);
assert.equal(replacement.game.discard.filter(c => c.id === f.independentKarama).length, 1);
});
void test('controlled already-active independent Karama price bypasses the native rate window without changing Guild-recipient bank payee', () => {
const f = createAlliedMarkerGuildRate();
const purchased = stepAlliedMarkerRate(f.beforeAction, { actor: f.observer,
action: { type: 'card', mode: 'shipment', target: f.recipient, card: f.karama } });
const done = declareAlliedMarkerGuildRate(f, purchased).game;
assert.notEqual(done.response?.kind, 'guildRate');
settled(f, f.beforeAction, done, 1);
assert.equal(done.discard.filter(c => c.id === f.karama).length, 1);
});