Retire occupied Ecaz Duke tenure through captivity
This commit is contained in:
1 parent
f0ebdf9c87
commit
b5fb6aeb64
15 files changed
+767
-45
No files matched your search
@@ -0,0 +1,179 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { newPlayer } from '../game/engine';
|
||||
import {
|
||||
acquireDuke, captureDuke, createDukeVidal, dukeController, executeCapturedDuke,
|
||||
expireDuke, releaseCapturedDuke, retireHomeworldDukeTenure, returnedDukeCapture,
|
||||
} from '../game/duke-vidal';
|
||||
import { createHomeworldCustody } from '../game/homeworld-custody';
|
||||
import { homeworldContext } from '../game/homeworld-game';
|
||||
import { createHomeworldOccupationHistory, observeHomeworldOccupation } from '../game/homeworld-occupation-history';
|
||||
import {
|
||||
createHomeworldEcazDukeReceipt, quoteHomeworldEcazDuke,
|
||||
quoteHomeworldEcazDukeLifecycle as lifecycle, releaseCapturedHomeworldDuke,
|
||||
type HomeworldEcazDukeContext as Context,
|
||||
} from '../game/homeworld-ecaz-duke';
|
||||
|
||||
const player = (g: Context, id: string) => {
|
||||
const p = g.players.find(p => p.id === id); assert.ok(p); return p;
|
||||
};
|
||||
function observe(g: Context, event: string) {
|
||||
g.homeworldOccupationHistory = observeHomeworldOccupation(g.homeworldOccupationHistory!,
|
||||
homeworldContext(g), g.homeworlds!.custody!, g.turn, 'change', event);
|
||||
}
|
||||
/** Controlled helper positions, not native battle/capture chronology. Original
|
||||
* typed pools and canonical observation create the occupied-source receipt;
|
||||
* real Duke capture/release/execution helpers then exercise its lifecycle. */
|
||||
function position(allied = false): Context {
|
||||
const players = (['ecaz', 'guild', 'atreides', 'harkonnen'] as const)
|
||||
.map(faction => newPlayer(faction, faction, faction));
|
||||
const context = homeworldContext({ advanced: true, players });
|
||||
const custody = createHomeworldCustody(context);
|
||||
const g: Context = { status: 'playing', advanced: true, turn: 1, players,
|
||||
homeworlds: { custody, historyVersion: 1 }, homeworldOccupationPreview: true,
|
||||
homeworldOccupationHistory: createHomeworldOccupationHistory(context, custody, 1, 'controlled-native-pools'),
|
||||
dukeVidal: createDukeVidal() };
|
||||
player(g, 'ecaz').tanks += player(g, 'ecaz').reserves;
|
||||
player(g, 'ecaz').reserves = 0;
|
||||
player(g, 'guild').reserves--;
|
||||
custody.visitors['homeworld:ecaz'] = { guild: { normal: 1, elite: 0 } };
|
||||
observe(g, 'controlled-guild-occupation');
|
||||
if (allied) {
|
||||
player(g, 'guild').ally = 'atreides';
|
||||
player(g, 'atreides').ally = 'guild';
|
||||
}
|
||||
const offer = quoteHomeworldEcazDuke(g);
|
||||
assert.ok(offer.event && offer.owner); assert.equal(offer.blocked, null);
|
||||
const recipient = allied ? 'atreides' : 'guild';
|
||||
g.homeworldEcazDukeGrants = [createHomeworldEcazDukeReceipt(g, offer.owner, offer.event, recipient)];
|
||||
g.dukeVidal = captureDuke(acquireDuke(g.dukeVidal!, recipient, 1, 'homeworldEcaz'),
|
||||
'harkonnen', recipient, 1, 'sietch_tabr');
|
||||
return g;
|
||||
}
|
||||
function depart(g: Context) {
|
||||
const group = g.homeworlds!.custody!.visitors['homeworld:ecaz'].guild;
|
||||
player(g, 'guild').reserves += group.normal + group.elite;
|
||||
delete g.homeworlds!.custody!.visitors['homeworld:ecaz'].guild;
|
||||
observe(g, 'controlled-guild-departure');
|
||||
}
|
||||
const noTenure = { controller: null, acquiredTurn: null, source: null };
|
||||
|
||||
void test('early captive return restores a still-qualified occupied Ecaz tenure without a second grant', () => {
|
||||
const g = position(), before = structuredClone(g);
|
||||
assert.equal(lifecycle(g).release, false);
|
||||
assert.match(lifecycle(g).blocked!, /captured, concealed or Ghola/);
|
||||
const released = releaseCapturedHomeworldDuke(g, 'harkonnen');
|
||||
assert.equal(released.controller, 'guild'); assert.equal(released.source, 'homeworldEcaz');
|
||||
assert.equal(released.acquiredTurn, 1); assert.equal(released.leader.capturedBy, undefined);
|
||||
assert.equal(released.captureVisibility, undefined); assert.equal(released.leader.concealed, undefined);
|
||||
assert.deepEqual(g, before);
|
||||
g.dukeVidal = released;
|
||||
assert.equal(quoteHomeworldEcazDuke(g).claimed?.recipient, 'guild');
|
||||
assert.match(quoteHomeworldEcazDuke(g).blocked!, /already chose/);
|
||||
});
|
||||
|
||||
void test('public turn-end retirement preserves the captive disc and later release cannot revive its lease', () => {
|
||||
const g = position(), held = g.dukeVidal!;
|
||||
held.leader.deaths = 2; held.leader.concealed!.deaths = 2;
|
||||
held.leader.usedAt = 'carthag';
|
||||
const before = structuredClone(held), retired = expireDuke(held, 1);
|
||||
assert.deepEqual(held, before);
|
||||
assert.deepEqual(retired.leader, held.leader);
|
||||
assert.equal(dukeController(retired), 'harkonnen');
|
||||
assert.equal(retired.controller, null); assert.equal(retired.source, null);
|
||||
assert.equal(retired.acquiredTurn, null); assert.deepEqual(retired.captureVisibility, noTenure);
|
||||
assert.equal(retired.leader.concealed?.controller, 'guild');
|
||||
g.dukeVidal = retired;
|
||||
const released = releaseCapturedHomeworldDuke(g, 'harkonnen', 2);
|
||||
assert.equal(dukeController(released), null); assert.equal(released.source, null);
|
||||
assert.equal(released.leader.usedAt, 'carthag'); assert.equal(released.leader.deaths, 2);
|
||||
assert.equal(released.leader.concealed, undefined);
|
||||
assert.throws(() => releaseCapturedHomeworldDuke(g, 'atreides', 2));
|
||||
});
|
||||
|
||||
void test('departure, replacement qualification and lost recipient are rechecked during actual captive return', () => {
|
||||
for (const reason of ['occupationEnded', 'occupationChanged', 'recipientChanged'] as const) {
|
||||
const g = position(reason === 'recipientChanged');
|
||||
if (reason === 'recipientChanged') player(g, 'atreides').ally = null;
|
||||
else {
|
||||
depart(g);
|
||||
if (reason === 'occupationChanged') {
|
||||
player(g, 'guild').reserves--;
|
||||
g.homeworlds!.custody!.visitors['homeworld:ecaz'].guild = { normal: 1, elite: 0 };
|
||||
observe(g, 'controlled-new-guild-qualification');
|
||||
}
|
||||
}
|
||||
const before = structuredClone(g);
|
||||
assert.equal(lifecycle(g).reason, reason); assert.equal(lifecycle(g).release, true);
|
||||
assert.equal(lifecycle(g).blocked, null);
|
||||
const released = releaseCapturedHomeworldDuke(g, 'harkonnen');
|
||||
assert.equal(released.controller, null); assert.equal(released.source, null);
|
||||
assert.equal(released.leader.capturedBy, undefined);
|
||||
assert.deepEqual(g, before, 'release recheck is pure even before settled-boundary reconciliation');
|
||||
}
|
||||
});
|
||||
|
||||
void test('secret execution retains the same public lease until public expiry and retires its visibility only then', () => {
|
||||
const living = position(), executed = structuredClone(living);
|
||||
executed.dukeVidal = executeCapturedDuke(executed.dukeVidal!, 'harkonnen', 1);
|
||||
assert.equal(returnedDukeCapture(executed.dukeVidal), true);
|
||||
assert.deepEqual(lifecycle(executed), lifecycle(living), 'actual private death is not a lease-expiry reason');
|
||||
const original = structuredClone(executed.dukeVidal);
|
||||
const atDeadline = expireDuke(executed.dukeVidal, 1);
|
||||
assert.deepEqual(executed.dukeVidal, original);
|
||||
assert.deepEqual(atDeadline.leader, original.leader);
|
||||
assert.deepEqual(atDeadline.captureVisibility, noTenure);
|
||||
assert.equal(returnedDukeCapture(atDeadline), true);
|
||||
depart(living); depart(executed);
|
||||
assert.deepEqual(lifecycle(executed), lifecycle(living));
|
||||
assert.equal(lifecycle(executed).reason, 'occupationEnded');
|
||||
executed.dukeVidal = retireHomeworldDukeTenure(executed.dukeVidal, 1);
|
||||
assert.equal(returnedDukeCapture(executed.dukeVidal), true);
|
||||
assert.equal(executed.dukeVidal.leader.dead, true);
|
||||
assert.equal(executed.dukeVidal.leader.deaths, 2);
|
||||
assert.equal(executed.dukeVidal.leader.concealed?.controller, 'guild');
|
||||
});
|
||||
|
||||
void test('retired custody still permits the original captor execution and Ecaz-only return mechanisms', () => {
|
||||
const g = position();
|
||||
const retired = retireHomeworldDukeTenure(g.dukeVidal!, 1);
|
||||
const executed = executeCapturedDuke(retired, 'harkonnen', 1);
|
||||
assert.equal(returnedDukeCapture(executed), true);
|
||||
assert.deepEqual(executed.captureVisibility, noTenure);
|
||||
assert.equal(executed.leader.deaths, 2); assert.equal(executed.leader.concealed?.controller, 'guild');
|
||||
const revived = acquireDuke(executed, 'ecaz', 1, 'ecazNexus');
|
||||
assert.equal(revived.controller, 'ecaz'); assert.equal(revived.source, 'ecazNexus');
|
||||
assert.equal(revived.leader.deaths, 2); assert.equal(revived.leader.dead, false);
|
||||
assert.equal(revived.captureVisibility, undefined);
|
||||
const returned = releaseCapturedDuke(retired, 'harkonnen', 1);
|
||||
assert.equal(returned.controller, null); assert.equal(returned.leader.dead, false);
|
||||
assert.equal(returned.leader.id, retired.leader.id);
|
||||
});
|
||||
|
||||
void test('non-homeworld captive return and expiry keep their original tenure rules', () => {
|
||||
const g = position();
|
||||
for (const source of ['ecaz', 'moritani', 'ecazNexus', 'ally'] as const) {
|
||||
const captured = captureDuke(acquireDuke(createDukeVidal(), 'guild', 1, source),
|
||||
'harkonnen', 'guild', 1, 'sietch_tabr');
|
||||
g.dukeVidal = captured;
|
||||
assert.deepEqual(expireDuke(captured, 1), captured);
|
||||
assert.deepEqual(retireHomeworldDukeTenure(captured, 1), captured);
|
||||
assert.deepEqual(releaseCapturedHomeworldDuke(g, 'harkonnen'), releaseCapturedDuke(captured, 'harkonnen', 1));
|
||||
}
|
||||
});
|
||||
|
||||
void test('a retired captured or concealed disc still cannot receive a fresh occupied Ecaz grant', () => {
|
||||
const g = position();
|
||||
const held = retireHomeworldDukeTenure(g.dukeVidal!, 1);
|
||||
depart(g);
|
||||
player(g, 'guild').reserves--;
|
||||
g.homeworlds!.custody!.visitors['homeworld:ecaz'].guild = { normal: 1, elite: 0 };
|
||||
observe(g, 'controlled-new-qualification');
|
||||
for (const state of [held, executeCapturedDuke(held, 'harkonnen', 1)]) {
|
||||
g.dukeVidal = state;
|
||||
assert.match(quoteHomeworldEcazDuke(g).blocked!, /captured, concealed or Ghola/);
|
||||
}
|
||||
// Deliberately invalid negative input: no legal foreign Duke revival is asserted.
|
||||
g.dukeVidal = createDukeVidal(); g.dukeVidal.leader.gholaBy = 'atreides';
|
||||
assert.match(quoteHomeworldEcazDuke(g).blocked!, /captured, concealed or Ghola/);
|
||||
});
|
||||
Reference in new issue
Block a user