Add final Nexus alliances to combined turn commitments
This commit is contained in:
1 parent
921b4fe1e3
commit
1a79fd3131
15 files changed
+1143
-78
No files matched your search
@@ -25,6 +25,7 @@ const bid = (minimum = 3, lot = 2): Expression => ({ kind: 'action', action: { k
|
||||
const forceRevival = (counter: 'total' | 'normal' | 'elite' = 'total', minimum = 1): Expression =>
|
||||
({ kind: 'action', action: { kind: 'forces', counter, minimum } });
|
||||
const leaderRevival = (leader: string): Expression => ({ kind: 'action', action: { kind: 'leader', leader } });
|
||||
const finalAlliance = (partner = 'r'): Expression => ({ kind: 'action', action: { kind: 'alliance', partner } });
|
||||
const claim = (): Expression => ({ kind: 'or', terms: [payment(), { kind: 'and', terms: [fact(), physical('ship', 3), physical('move', 4, 'arrakeen')] }] });
|
||||
|
||||
function elements(node: ReactNode): Element[] {
|
||||
@@ -75,6 +76,14 @@ function auction(next: Action = { type: 'passBid' }): GameView {
|
||||
if (next.type === 'decision') { game.auction.active = 'a'; game.decision = { kind: 'auctionPayment', player: 'p' }; }
|
||||
return game;
|
||||
}
|
||||
function nexus(next: Action = { type: 'alliance', target: 'r' }): GameView {
|
||||
const game = projected(1, next);
|
||||
game.nexus = true; game.spiceWindow = null; game.active = 'a';
|
||||
game.alliancePromiseNexus = { turn: 2 }; game.truthMixedAllianceTargets = ['p', 'a', 'r'];
|
||||
game.allianceOffers = { r: 'p' };
|
||||
game.turnActionPromises[0].mixed = { kind: 'or', terms: [finalAlliance(), payment()] };
|
||||
return game;
|
||||
}
|
||||
function revival(next: Action = { type: 'revive', amount: 3, elite: 1 }): GameView {
|
||||
const game = projected(4, next);
|
||||
game.revivalOrder = createRevivalOrder({ turn: 2, order: ['p', 'a', 'r'], ready: [] });
|
||||
@@ -251,6 +260,120 @@ void test('turn-action preparation history and guides use original public leaves
|
||||
assert.match(answerMarkup, /ordinary force returns add together/); assert.match(answerMarkup, /Bids close with their lot/);
|
||||
});
|
||||
|
||||
void test('turn-action alliance fields offer every public other partner including the current ally only in the open native Nexus', () => {
|
||||
const game = nexus(); game.players.find(player => player.id === 'p')!.ally = 'r'; game.players.find(player => player.id === 'r')!.ally = 'p';
|
||||
for (const player of game.players) for (const key of ['hand', 'traitors', 'faceDancers', 'spice', 'forces', 'reserves', 'leaders', 'tanks'])
|
||||
Object.defineProperty(player, key, { get() { throw Error(`No private ${key} inspection`); }, configurable: true });
|
||||
Object.defineProperty(game.turnActionPromises[0], 'alliance', { get() { throw Error('No private alliance binding'); } });
|
||||
const e = editor(physical(), false, game);
|
||||
select(e.tree(), '1', 'action', 'alliance'); assert.deepEqual(e.value, finalAlliance('')); assert.ok(e.error());
|
||||
const options = elements(find(e.tree(), 'id', 'truth-turn-action-1-partner')).filter(element => element.type === 'option');
|
||||
assert.deepEqual(options.map(option => option.props.value), ['', 'a', 'r']);
|
||||
select(e.tree(), '1', 'partner', 'r'); assert.equal(e.error(), null); assert.deepEqual(e.value, finalAlliance());
|
||||
select(e.tree(), '1', 'partner', 'p'); select(e.tree(), '1', 'partner', 'missing'); assert.deepEqual(e.value, finalAlliance());
|
||||
for (const partner of ['', 'p', 'missing', ' r ']) assert.ok(turnActionInputError(finalAlliance(partner), game, 'p'));
|
||||
select(e.tree(), '1', 'partner', ''); assert.ok(e.error()); select(e.tree(), '1', 'partner', 'a'); assert.equal(e.error(), null);
|
||||
assert.match(e.html(), /actual final membership when this current Nexus closes/);
|
||||
assert.match(e.html(), /No can allow temporary membership followed by a break/);
|
||||
const frozen = editor(finalAlliance(), true, game); select(frozen.tree(), '1', 'partner', 'a'); select(frozen.tree(), '1', 'action', 'payment');
|
||||
assert.equal(frozen.changes, 0); assert.deepEqual(frozen.value, finalAlliance());
|
||||
for (const change of [
|
||||
(current: GameView) => { current.status = 'setup'; }, (current: GameView) => { current.phase = 2; },
|
||||
(current: GameView) => { current.nexus = false; },
|
||||
(current: GameView) => { current.spiceWindow = { territory: 'hagga_basin', sector: 12, amount: 6, harvested: false }; },
|
||||
(current: GameView) => { current.alliancePromiseNexus = null; }, (current: GameView) => { current.alliancePromiseNexus!.turn = 1; },
|
||||
(current: GameView) => { current.truthMixedAllianceTargets = []; },
|
||||
(current: GameView) => { current.response = { kind: 'guildIncome', owner: 'a', amount: 1, passed: [] }; },
|
||||
(current: GameView) => { current.decision = { kind: 'auctionPayment', player: 'p' }; },
|
||||
(current: GameView) => { current.phaseOpening = { passed: [] }; },
|
||||
(current: GameView) => { current.automaticContinuationPending = true; },
|
||||
]) {
|
||||
const current = nexus(); change(current); const invalid = editor(finalAlliance(), false, current);
|
||||
assert.ok(invalid.error()); assert.match(invalid.html(), /Alliance conditions require the current open Nexus/);
|
||||
}
|
||||
});
|
||||
|
||||
void test('turn-action alliance leaves retain the complete sixteen-clause and four-level optional-fact expression', () => {
|
||||
const game = nexus(), e = editor({ kind: 'or', terms: [finalAlliance(), fact()] }, false, game);
|
||||
for (let count = 3; count <= 16; count++) {
|
||||
click(control(e.tree(), 'add-action-1')); select(e.tree(), `1.${count}`, 'action', 'alliance');
|
||||
select(e.tree(), `1.${count}`, 'partner', count % 2 ? 'a' : 'r');
|
||||
}
|
||||
assert.equal(e.error(), null); assert.match(e.html(), /16 \/ 16 conditions/);
|
||||
const full = structuredClone(e.value), changes = e.changes;
|
||||
for (const key of ['add-action-1', 'add-fact-1', 'nested-and-1', 'nested-or-1', 'and-1.1', 'or-1']) {
|
||||
assert.equal(control(e.tree(), key).props.disabled, true); click(control(e.tree(), key));
|
||||
}
|
||||
assert.deepEqual(e.value, full); assert.equal(e.changes, changes);
|
||||
const deep = editor({ kind: 'and', terms: [fact(), finalAlliance()] }, false, game);
|
||||
click(control(deep.tree(), 'or-1.2')); click(control(deep.tree(), 'and-1.2.1')); click(control(deep.tree(), 'or-1.2.1.1'));
|
||||
assert.match(deep.html(), /4 \/ 4 levels of grouping/); assert.equal(deep.error(), null);
|
||||
assert.equal(control(deep.tree(), 'and-1.2.1.1.1').props.disabled, true);
|
||||
});
|
||||
|
||||
void test('turn-action Nexus guide forwards only exact breaks, withdrawals, reciprocal acceptances and Ready without private alliance reads', () => {
|
||||
for (const advanced of [false, true]) for (const kind of ['accept', 'break', 'withdraw', 'ready'] as const) {
|
||||
const game = nexus(kind === 'ready' ? { type: 'ready' } : { type: 'alliance', target: kind === 'accept' ? 'r' : null });
|
||||
game.advanced = advanced;
|
||||
if (kind === 'break') { game.players.find(player => player.id === 'p')!.ally = 'r'; game.players.find(player => player.id === 'r')!.ally = 'p'; }
|
||||
if (kind === 'withdraw') game.allianceOffers = { p: 'r' };
|
||||
if (kind === 'ready') {
|
||||
game.players.find(player => player.id === 'p')!.ally = 'r'; game.players.find(player => player.id === 'r')!.ally = 'p';
|
||||
game.turnActionPromises[0].mixed = { kind: 'and', terms: [finalAlliance(), payment()] }; game.turnActionPromises[0].answer = false;
|
||||
}
|
||||
for (const key of ['alliance', 'compiled', 'preparation', 'progress', 'payments'])
|
||||
Object.defineProperty(game.turnActionPromises[0], key, { get() { throw Error(`No private ${key} inspection`); }, configurable: true });
|
||||
for (const player of game.players) for (const key of ['hand', 'traitors', 'faceDancers', 'spice', 'forces', 'reserves'])
|
||||
Object.defineProperty(player, key, { get() { throw Error(`No private ${key} inspection`); }, configurable: true });
|
||||
const sent: Action[] = [], button = guide({ game, busy: false, act(value) { sent.push(value); } });
|
||||
assert.equal(button.props.disabled, false); press(button); assert.equal(sent[0], game.turnActionGuide!.action);
|
||||
assert.equal(game.turnActionPromises[0].fulfilled, false, 'A guide dispatch does not announce final Nexus fulfillment.');
|
||||
const html = markup(game);
|
||||
assert.match(html, kind === 'accept' ? /Accept Recipient’s alliance invitation/ : kind === 'break' ? /Break your current alliance/
|
||||
: kind === 'withdraw' ? /Withdraw your alliance offer/ : /Confirm Ready for this Nexus/);
|
||||
assert.match(html, /forming an alliance does not fulfill it early/);
|
||||
assert.doesNotMatch(html, /Fulfilled: this whole commitment is complete/);
|
||||
}
|
||||
const invalid = [nexus(), nexus(), nexus(), nexus()];
|
||||
invalid[0].allianceOffers = {};
|
||||
invalid[1].turnActionGuide!.action.target = 'p';
|
||||
invalid[2].turnActionGuide!.action.target = 'missing';
|
||||
invalid[3].players.find(player => player.id === 'r')!.ally = 'a';
|
||||
for (const game of invalid) {
|
||||
const sent: Action[] = [], button = guide({ game, busy: false, act(value) { sent.push(value); } });
|
||||
assert.equal(button.props.disabled, true); press(button); assert.deepEqual(sent, []);
|
||||
}
|
||||
});
|
||||
|
||||
void test('turn-action Nexus callbacks expire with public consent, membership, readiness, source and quiet-window changes', () => {
|
||||
const changes: ((game: GameView) => void)[] = [
|
||||
game => { game.nexus = false; }, game => { game.phase = 2; }, game => { game.turn = 3; }, game => { game.me = 'a'; },
|
||||
game => { game.spiceWindow = { territory: 'hagga_basin', sector: 12, amount: 6, harvested: false }; },
|
||||
game => { game.alliancePromiseNexus = null; }, game => { game.alliancePromiseNexus = { turn: 2 }; },
|
||||
game => { game.alliancePromiseNexus!.turn = 1; }, game => { game.allianceOffers = {}; }, game => { game.allianceOffers.a = 'p'; },
|
||||
game => { game.ready.push('p'); }, game => { game.ready.push('a'); },
|
||||
game => { game.players.find(player => player.id === 'p')!.ally = 'a'; }, game => { game.players.find(player => player.id === 'a')!.ally = 'r'; },
|
||||
game => { game.players = game.players.filter(player => player.id !== 'r'); },
|
||||
game => { game.turnActionGuide = { ...game.turnActionGuide! }; }, game => { game.turnActionGuide!.action = { ...game.turnActionGuide!.action }; },
|
||||
game => { game.turnActionGuide!.action.turnActionPromisePhase = 2; }, game => { game.turnActionGuide!.action.turnActionPromiseEvent = 'stale'; },
|
||||
game => { game.turnActionPromises[0].event = 'stale'; }, game => { game.turnActionPromises[0].fulfilled = true; },
|
||||
game => { game.truthtrance = answering(1).truthtrance; }, game => { game.phaseOpening = { passed: [] }; },
|
||||
game => { game.response = { kind: 'guildIncome', owner: 'a', amount: 1, passed: [] }; }, game => { game.decision = { kind: 'auctionPayment', player: 'p' }; },
|
||||
game => { game.automaticContinuationPending = true; }, game => { game.roomControl = { paused: true, joinLocked: false, revision: 1, updatedAt: null }; },
|
||||
];
|
||||
for (const action of [{ type: 'alliance', target: 'r' }, { type: 'ready' }] satisfies Action[]) for (const change of changes) {
|
||||
const game = nexus(action), sent: Action[] = [], button = guide({ game, busy: false, act(value) { sent.push(value); } });
|
||||
assert.equal(button.props.disabled, false); change(game); press(button); assert.deepEqual(sent, []);
|
||||
}
|
||||
const busy = { game: nexus(), busy: false, act() { assert.fail('Busy Nexus guide cannot send'); } };
|
||||
const button = guide(busy); busy.busy = true; press(button);
|
||||
const replaced = { game: nexus(), busy: false, act() { assert.fail('Replaced Nexus view cannot send'); } };
|
||||
const old = guide(replaced); replaced.game = { ...replaced.game }; press(old);
|
||||
const source = nexus({ type: 'ready' }); source.nexus = false;
|
||||
const retained = guide({ game: source, busy: false, act() { assert.fail('A closed rendered Nexus cannot become a fresh opportunity'); } });
|
||||
source.nexus = true; press(retained);
|
||||
});
|
||||
|
||||
void test('turn-action history retains original own whole questions and terminal status without private compiled branches', () => {
|
||||
const game = projected(); game.turnActionGuide = null;
|
||||
game.turnActionPromises.push(
|
||||
@@ -259,7 +382,7 @@ void test('turn-action history retains original own whole questions and terminal
|
||||
{ ...game.turnActionPromises[0], event: 'controlled-release', released: true },
|
||||
{ ...game.turnActionPromises[0], event: 'controlled-foreign', player: 'a', mixed: { kind: 'and', terms: [fact({ kind: 'hand', name: 'Poison Blade' }), physical('ship', 19, 'sietch_tabr')] } },
|
||||
);
|
||||
for (const promise of game.turnActionPromises) for (const key of ['compiled', 'binding', 'factAnswers', 'residual', 'route', 'payments', 'progress', 'preparation', 'prep', 'bids', 'returns', 'leaders'])
|
||||
for (const promise of game.turnActionPromises) for (const key of ['compiled', 'binding', 'factAnswers', 'residual', 'route', 'payments', 'progress', 'preparation', 'prep', 'bids', 'returns', 'leaders', 'alliance'])
|
||||
Object.defineProperty(promise, key, { get() { throw Error(`No private ${key} inspection`); }, configurable: true });
|
||||
const html = markup(game);
|
||||
for (const text of [/Turn 1/, /You answered Yes/, /You answered No/, /you hold Shield/, / OR /, / AND /,
|
||||
@@ -314,7 +437,7 @@ void test('turn-action ordinary-payment guide retains native out-of-turn timing
|
||||
assert.match(markup(game), /Ready to end this turn while honoring your whole answer/);
|
||||
game.ready.push('p'); press(button); assert.equal(sent.length, 1);
|
||||
for (const phase of [1, 7]) {
|
||||
const current = projected(phase, { type: 'ready' }); current.nexus = true; current.spiceWindow = null;
|
||||
const current = phase === 1 ? nexus({ type: 'ready' }) : projected(phase, { type: 'ready' });
|
||||
const dispatched: Action[] = [], ready = guide({ game: current, busy: false, act(value) { dispatched.push(value); } });
|
||||
assert.equal(ready.props.disabled, false); press(ready); assert.equal(dispatched[0], current.turnActionGuide!.action);
|
||||
if (phase === 1) { current.nexus = false; press(ready); assert.equal(dispatched.length, 1); }
|
||||
|
||||
Reference in new issue
Block a user