Files
Dune/tests/fremen-revival-grant-engine.test.ts
T
Quintine 0e3d6a250e Make Fremen allied revival grants cancelable
Bind the original Fremen/ally/turn grant before Karama allowance or prevention.
A prevented grant leaves intrinsic free rates and prior payments intact. Permit
late grants under the explicit provisional Recruits current-rate policy, with
shared controls, minimal bot support and native public chronology evidence.
Complete rules and browser/deployed acceptance remain open.
2026-10-24 18:16:03 +11:00

99 lines
5.3 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { viewGame } from '../game/engine';
import {
createFremenRevivalGrantFixture as fixture,
actFremenRevivalGrant as act,
passFremenRevivalGrantResponses as pass,
advanceFremenRevivalGrant as advance,
fremenRevivalGrantClean as clean,
fremenRevivalGrantPlayer as player,
fremenRevivalGrantInventory as inventory,
} from './fixture-fremen-revival-grant';
for (const advanced of [false, true]) {
void test(`${advanced ? 'Advanced' : 'Basic'} original allied grant waits for its native Karama response then permits three free`, () => {
const f = fixture({ advanced }), before = structuredClone(f.game);
let g = act(f.game, f.fremen, f.grant);
assert.equal(g.response?.kind, 'fremenRevivalGrant');
assert.deepEqual(g.response?.fremenGrant, { turn: 2, owner: f.fremen, recipient: f.ally });
assert.equal(g.freeRevival.includes(f.ally), false);
assert.ok(viewGame(g, f.observer).responseControls?.cancelCards.includes(f.karama));
assert.throws(() => act(g, f.ally, { type: 'revive', amount: 3, elite: 0 }), /response|power|interaction/i);
g = pass(g);
assert.equal(g.freeRevival.filter(id => id === f.ally).length, 1);
const spice = player(g, f.ally).spice;
g = advance(act(g, f.ally, { type: 'revive', amount: 3, elite: 0 }), clean);
assert.equal(player(g, f.ally).spice, spice);
assert.equal(player(g, f.ally).revived, 3);
assert.equal(player(g, f.ally).freeForcesRevived, 3);
assert.deepEqual(inventory(g), f.originalInventory);
assert.deepEqual(f.game, before);
assert.throws(() => act(g, f.fremen, f.grant), /already been granted/);
});
void test(`${advanced ? 'Advanced' : 'Basic'} real Karama denies only the original allied grant and cannot be bypassed by redeclaring`, () => {
const f = fixture({ advanced });
let g = act(f.game, f.fremen, f.grant);
g = act(g, f.observer, { type: 'card', card: f.karama, mode: 'cancel' });
assert.ok(!g.response);
assert.equal(g.freeRevival.includes(f.ally), false);
assert.deepEqual(g.revivalRules?.fremenGrantBlocked, [f.ally]);
assert.equal(g.revivalRules?.freeBlocked?.includes(f.ally) ?? false, false);
assert.equal(viewGame(g, f.ally).revival.freeRemaining, 2);
assert.equal(g.discard.filter(c => c.id === f.karama).length, 1);
const before = structuredClone(g);
assert.throws(() => act(g, f.fremen, f.grant), /Karama prevented/);
assert.deepEqual(g, before);
g = advance(act(g, f.ally, { type: 'revive', amount: 2, elite: 0 }), clean);
assert.equal(player(g, f.ally).freeForcesRevived, 2);
assert.deepEqual(inventory(g), f.originalInventory);
});
void test(`${advanced ? 'Advanced' : 'Basic'} grant prevention expires through the next actual Revival opening`, () => {
const f = fixture({ advanced });
let g = act(f.game, f.fremen, f.grant);
g = act(g, f.observer, { type: 'card', card: f.karama, mode: 'cancel' });
g = advance(g, state => state.turn === 3 && state.phase === 4 && clean(state));
assert.equal(player(g, f.fremen).ally, f.ally);
assert.equal(g.revivalRules?.fremenGrantBlocked?.includes(f.ally) ?? false, false);
assert.equal(viewGame(g, f.fremen).fremenRevivalGrant?.blocked, null);
g = pass(act(g, f.fremen, f.grant));
assert.equal(g.freeRevival.filter(id => id === f.ally).length, 1);
assert.deepEqual(inventory(g), f.originalInventory);
});
void test(`${advanced ? 'Advanced' : 'Basic'} later Recruits grant preserves earned paid/free history and increases the current allowance`, () => {
const f = fixture({ advanced, recruits: true, priorRevival: true });
assert.ok(f.recruitsPlay);
let g = act(f.game, f.fremen, f.recruitsPlay);
const spice = player(g, f.ally).spice;
assert.equal(viewGame(g, f.ally).revival.freeRemaining, 2);
assert.equal(viewGame(g, f.fremen).fremenRevivalGrant?.blocked, null);
g = pass(act(g, f.fremen, f.grant));
assert.equal(viewGame(g, f.ally).revival.freeRemaining, 4);
assert.equal(player(g, f.ally).spice, spice);
assert.equal(player(g, f.ally).revived, 3);
assert.equal(player(g, f.ally).freeForcesRevived, 2);
g = advance(act(g, f.ally, { type: 'revive', amount: 3, elite: 0 }), clean);
assert.equal(player(g, f.ally).spice, spice);
assert.equal(player(g, f.ally).revived, 6);
assert.equal(player(g, f.ally).freeForcesRevived, 5);
assert.equal(player(g, f.ally).tanks, 0);
assert.equal(g.discard.filter(c => c.id === f.recruits).length, 1);
assert.deepEqual(inventory(g), f.originalInventory);
});
void test(`${advanced ? 'Advanced' : 'Basic'} prevention after Recruits leaves intrinsic doubled free returns usable`, () => {
const f = fixture({ advanced, recruits: true });
assert.ok(f.recruitsPlay);
let g = act(f.game, f.fremen, f.recruitsPlay);
g = act(g, f.fremen, f.grant);
g = act(g, f.observer, { type: 'card', card: f.karama, mode: 'cancel' });
assert.ok(!g.response);
assert.equal(viewGame(g, f.ally).revival.freeRemaining, 4);
const spice = player(g, f.ally).spice;
g = advance(act(g, f.ally, { type: 'revive', amount: 4, elite: 0 }), clean);
assert.equal(player(g, f.ally).spice, spice);
assert.equal(player(g, f.ally).freeForcesRevived, 4);
assert.equal(g.freeRevival.includes(f.ally), false);
assert.deepEqual(inventory(g), f.originalInventory);
});
}