165 lines
8.9 KiB
TypeScript
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);
|
|
});
|