Adopt the supplied Advanced specific-effect wording, preserving Basic and legacy phase-wide semantics. Bind signed semantic purposes through ordinary/special activations, counters, feasibility and settlement; retain unrelated human controls and minimal bot actions. Phase expiry remains provisional; prior counter, BG and deferred-overbid choices stay. Validation: frozen12-file111/111, types/lint/build and two original-action/rendered-table programmes. Explicit unopened deal/auction/spice order supports real paid Baliset and shipments; Advanced retains Karama for Baliset cancellation while Basic stays blocked. Four minimal policies remain legal. Correct stale exchange/phase-market/Fremen fixture assumptions. Recovery schema expectations maintained without running deferred SQL assurance. No full modes, browser/deployed acceptance, saved-game/reset/restart or deployment operation.
151 lines
9.4 KiB
TypeScript
151 lines
9.4 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { FACTIONS, type FactionId } from '../game/catalog';
|
|
import type { ResponseWindow } from '../game/engine';
|
|
import {
|
|
isKullPurpose, kullCancellationPurpose, kullSpecialPurpose, kullPurposeLabel,
|
|
kullPurposeBlock, type KullPurpose,
|
|
} from '../game/kull-purpose';
|
|
import {
|
|
activeKullRestrictions, kullBlocksKarama, validateKullPhaseRestrictions,
|
|
type KullContext, type KullPhaseRestriction,
|
|
} from '../game/choam-kull';
|
|
|
|
const context: KullContext = { turn: 3, phase: 5, players: [
|
|
{ id: 'choam', faction: 'choam' }, { id: 'actor', faction: 'emperor' }, { id: 'other', faction: 'guild' },
|
|
] };
|
|
const stamp = { player: 'actor', turn: 3, phase: 5 };
|
|
|
|
void test('Advanced Kull restricts the exact public purpose while legacy/Basic restrictions remain all-purpose', () => {
|
|
const history: KullPhaseRestriction[] = [{ ...stamp, purpose: 'shipment-price' }];
|
|
validateKullPhaseRestrictions(context, history);
|
|
assert.equal(kullBlocksKarama(history, 3, 5, 'actor', 'shipment-price'), true);
|
|
assert.equal(kullBlocksKarama(history, 3, 5, 'actor', 'auction-free-purchase'), false);
|
|
assert.equal(kullBlocksKarama(history, 3, 5, 'actor', kullSpecialPurpose('emperor')), false);
|
|
assert.equal(kullBlocksKarama(history, 3, 5, 'actor'), false);
|
|
assert.equal(kullBlocksKarama(history, 3, 5, 'other', 'shipment-price'), false);
|
|
const legacy = [stamp];
|
|
assert.equal(kullBlocksKarama(legacy, 3, 5, 'actor'), true);
|
|
for (const purpose of ['shipment-price', 'auction-free-purchase', 'special:emperor', 'cancel:voice'] as const)
|
|
assert.equal(kullBlocksKarama(legacy, 3, 5, 'actor', purpose), true);
|
|
});
|
|
|
|
void test('active purpose history is copied faithfully and expires by both phase and turn', () => {
|
|
const current = Object.freeze({ ...stamp, purpose: 'cancel:choamWorthless:baliset' as const });
|
|
const history = Object.freeze([current, Object.freeze({ ...stamp, turn: 2, purpose: 'special:emperor' as const })]);
|
|
validateKullPhaseRestrictions(context, history);
|
|
const active = activeKullRestrictions(history, 3, 5);
|
|
assert.deepEqual(active, [current]); assert.notEqual(active[0], current);
|
|
for (const [turn, phase] of [[3, 6], [4, 5], [4, 0]]) {
|
|
assert.deepEqual(activeKullRestrictions(history, turn, phase), []);
|
|
assert.equal(kullBlocksKarama(history, turn, phase, 'actor', current.purpose), false);
|
|
}
|
|
assert.equal(kullBlocksKarama(history, 3, 5, 'actor', 'cancel:choamWorthless:kulon'), false);
|
|
});
|
|
|
|
void test('distinct same-phase purposes coexist, while exact purpose and legacy duplicates reject', () => {
|
|
const history: KullPhaseRestriction[] = [stamp, { ...stamp, purpose: 'shipment-price' },
|
|
{ ...stamp, purpose: 'auction-free-purchase' }, { ...stamp, purpose: 'cancel:voice' }];
|
|
validateKullPhaseRestrictions(context, history);
|
|
assert.equal(activeKullRestrictions(history, 3, 5).length, 4);
|
|
for (const repeated of history)
|
|
assert.throws(() => validateKullPhaseRestrictions(context, [...history, { ...repeated }]), /duplicate/);
|
|
});
|
|
|
|
void test('restriction history rejects noncanonical purpose strings, undefined extra fields, unrelated fields and future stamps', () => {
|
|
const invalid: unknown[] = [
|
|
undefined, null, '', 'all', 'special:Emperor', 'special:emperor:extra', 'cancel:choamWorthless',
|
|
'cancel:choamWorthless:lalala', 'cancel:choamWorthless:laLaLa:extra', 'cancel:eliteStrength',
|
|
'cancel:eliteStrength:unknown', 'cancel:voice:amount', 'cancel:constructor', 5, {},
|
|
'cancel:nexusPrescience', 'cancel:nexusAdvisorFlip',
|
|
];
|
|
for (const purpose of invalid) {
|
|
const restriction = { ...stamp, purpose }, before = structuredClone(restriction);
|
|
assert.throws(() => validateKullPhaseRestrictions(context, [restriction]));
|
|
assert.deepEqual(restriction, before);
|
|
assert.equal(isKullPurpose(purpose), false);
|
|
}
|
|
for (const restriction of [
|
|
{ ...stamp, purpose: 'shipment-price', extra: true },
|
|
{ ...stamp, purpose: 'shipment-price', turn: 4 },
|
|
{ ...stamp, purpose: 'shipment-price', phase: 6 },
|
|
]) assert.throws(() => validateKullPhaseRestrictions(context, [restriction]));
|
|
});
|
|
|
|
void test('each CHOAM ordinary power has its own canonical cancellation purpose without accepting card-name aliases', () => {
|
|
const effects = ['baliset', 'kulon', 'laLaLa', 'gamont', 'jubba', 'kull'];
|
|
const purposes = effects.map(effect => kullCancellationPurpose('choamWorthless', effect));
|
|
assert.equal(new Set(purposes).size, effects.length);
|
|
assert.ok(purposes.every(isKullPurpose));
|
|
for (const detail of [undefined, 'lalala', 'La, La, La!', 'Trip to Gamont', 'baliset:actor', 'constructor'])
|
|
assert.throws(() => kullCancellationPurpose('choamWorthless', detail), /canonical CHOAM effect/);
|
|
const history = [{ ...stamp, purpose: purposes[0] }];
|
|
assert.equal(kullBlocksKarama(history, 3, 5, 'actor', purposes[0]), true);
|
|
for (const purpose of purposes.slice(1)) assert.equal(kullBlocksKarama(history, 3, 5, 'actor', purpose), false);
|
|
});
|
|
|
|
void test('special native variants share the actual faction key and special-force cancellation retains its owning faction', () => {
|
|
for (const faction of FACTIONS) {
|
|
const special = kullSpecialPurpose(faction.id);
|
|
assert.equal(special, `special:${faction.id}`);
|
|
assert.ok(isKullPurpose(special));
|
|
const strength = kullCancellationPurpose('eliteStrength', faction.id);
|
|
assert.equal(strength, `cancel:eliteStrength:${faction.id}`);
|
|
assert.ok(isKullPurpose(strength));
|
|
}
|
|
const special = kullSpecialPurpose('emperor');
|
|
const history = [{ ...stamp, purpose: special }];
|
|
// The engine normalizes its native variants to their actual seated faction.
|
|
assert.equal(kullBlocksKarama(history, 3, 5, 'actor', kullSpecialPurpose('emperor')), true);
|
|
assert.equal(kullBlocksKarama(history, 3, 5, 'actor', kullSpecialPurpose('guild')), false);
|
|
assert.notEqual(kullCancellationPurpose('eliteStrength', 'emperor'), kullCancellationPurpose('eliteStrength', 'fremen'));
|
|
for (const invalid of ['emperorRevival', 'emperorGift', 'Guild', 'unknown', 'constructor'])
|
|
assert.throws(() => kullSpecialPurpose(invalid as FactionId), /actual native faction/);
|
|
assert.throws(() => kullCancellationPurpose('eliteStrength'), /owner faction/);
|
|
});
|
|
|
|
void test('ordinary cancellations use response semantics alone and reject route/card/private-value suffixes', () => {
|
|
for (const kind of ['voice', 'guildIncome', 'guildRate', 'guildTransportPermission', 'nexusFremenCunning',
|
|
'richeseNoField', 'richeseAuction', 'worthlessKarama'] as const) {
|
|
assert.equal(kullCancellationPurpose(kind), `cancel:${kind}`);
|
|
assert.ok(isKullPurpose(kullCancellationPurpose(kind)));
|
|
assert.throws(() => kullCancellationPurpose(kind, 'private-card-or-route'), /without extra details/);
|
|
}
|
|
assert.throws(() => kullCancellationPurpose('unknown' as ResponseWindow['kind']), /canonical response/);
|
|
assert.throws(() => kullPurposeLabel('special:unknown' as KullPurpose), /canonical purpose/);
|
|
});
|
|
|
|
void test('Nexus aliases share their native purpose while distinct transport and worm powers remain separate', () => {
|
|
for (const [alias, native] of [['nexusPrescience', 'prescience'], ['nexusAdvisorFlip', 'advisorFlip']] as const) {
|
|
const purpose = kullCancellationPurpose(native);
|
|
assert.equal(kullCancellationPurpose(alias), purpose);
|
|
assert.equal(isKullPurpose(`cancel:${alias}`), false);
|
|
assert.throws(() => validateKullPhaseRestrictions(context, [{ ...stamp, purpose: `cancel:${alias}` }]));
|
|
assert.equal(kullBlocksKarama([{ ...stamp, purpose }], 3, 5, 'actor', kullCancellationPurpose(alias)), true);
|
|
assert.throws(() => kullCancellationPurpose(alias, 'extra'), /without extra details/);
|
|
}
|
|
for (const [first, second] of [['guildRate', 'guildTransportPermission'], ['wormSurvival', 'wormAllyProtection']] as const) {
|
|
const firstPurpose = kullCancellationPurpose(first), secondPurpose = kullCancellationPurpose(second);
|
|
assert.notEqual(firstPurpose, secondPurpose);
|
|
assert.ok(isKullPurpose(firstPurpose) && isKullPurpose(secondPurpose));
|
|
assert.equal(kullBlocksKarama([{ ...stamp, purpose: firstPurpose }], 3, 5, 'actor', secondPurpose), false);
|
|
}
|
|
});
|
|
|
|
void test('purpose labels and projected blocks are readable and preserve legacy priority without suppressing another purpose', () => {
|
|
assert.equal(kullPurposeLabel('shipment-price'), 'Karama shipment pricing');
|
|
assert.equal(kullPurposeLabel('auction-free-purchase'), 'a free Treachery purchase');
|
|
assert.equal(kullPurposeLabel(kullSpecialPurpose('emperor')), 'the Emperor special Karama power');
|
|
assert.equal(kullPurposeLabel(kullCancellationPurpose('choamWorthless', 'baliset')), 'cancellation of Baliset');
|
|
const view = { karamaPurposeBlocks: [
|
|
{ purpose: 'shipment-price' as const, message: 'Shipment pricing is restricted this phase.' },
|
|
{ purpose: 'cancel:voice' as const, message: 'Canceling Voice is restricted this phase.' },
|
|
] };
|
|
assert.equal(kullPurposeBlock(view, 'shipment-price'), view.karamaPurposeBlocks[0].message);
|
|
assert.equal(kullPurposeBlock(view, 'cancel:voice'), view.karamaPurposeBlocks[1].message);
|
|
assert.equal(kullPurposeBlock(view, 'special:emperor'), null);
|
|
assert.equal(kullPurposeBlock({}, 'shipment-price'), null);
|
|
assert.equal(kullPurposeBlock({ ...view, karamaBlocked: 'All Karama is restricted.' }, 'shipment-price'), 'All Karama is restricted.');
|
|
assert.equal(kullPurposeBlock({ ...view, karamaBlocked: 'All Karama is restricted.' }, 'special:emperor'), 'All Karama is restricted.');
|
|
});
|