Fix shipment promise forecasts and movement openings
This commit is contained in:
1 parent
21f95bdd5b
commit
cd1f726593
9 files changed
+277
-5
No files matched your search
@@ -0,0 +1,68 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame } from '../game/engine';
|
||||
import { act, advance, clean, createKullBribeFixture, inventory, player } from './fixture-kull-bribe';
|
||||
|
||||
void test('a Basic Kull phase ban cannot fund a later shipment with the blocked auction Karama', () => {
|
||||
// The shared fixture selects the original unopened deal and Spice position;
|
||||
// it uses public Basic opt-in and real setup. No wallet or force is staged.
|
||||
const f = createKullBribeFixture();
|
||||
let game = structuredClone(f.charity);
|
||||
game = advance(game, g => g.phase === 3 && clean(g) && g.auction?.active === f.actor);
|
||||
assert.equal(player(game, f.actor).spice, 10);
|
||||
const lot = game.auction!.cards[game.auction!.index].id;
|
||||
|
||||
// The holder actually attempts an ordinary free purchase. CHOAM interrupts
|
||||
// with its original Kull; everyone with a counter opportunity passes.
|
||||
game = act(game, f.actor, { type: 'card', mode: 'purchase', card: f.karama });
|
||||
const reaction = viewGame(game, f.reactor).kullReaction;
|
||||
assert.ok(reaction && reaction.plays.some(play => play.card.id === f.kull && !play.blocked));
|
||||
game = act(game, f.reactor, { type: 'kullDecision', event: reaction.event, source: 'printed', card: f.kull });
|
||||
game = advance(game, clean);
|
||||
assert.equal(game.phase, 3);
|
||||
assert.ok(game.kullRestrictions?.some(row => row.player === f.actor && row.turn === game.turn && row.phase === 3));
|
||||
assert.ok(player(game, f.actor).hand.some(card => card.id === f.karama));
|
||||
assert.ok(viewGame(game, f.actor).karamaBlocked);
|
||||
const blockedPurchase = structuredClone(game);
|
||||
assert.throws(() => act(game, f.actor, { type: 'card', mode: 'purchase', card: f.karama }), /Kull Wahad/);
|
||||
assert.deepEqual(game, blockedPurchase);
|
||||
|
||||
// Seven of ten spice is an actually funded bid; the real invoice leaves
|
||||
// three. Eight to Carthag costs eight normally or four at the Karama rate.
|
||||
// The one retained Karama cannot both evade this Bidding invoice (Kull) and
|
||||
// legitimately fund that shipment after cash payment.
|
||||
assert.equal(game.auction?.active, f.actor);
|
||||
game = act(game, f.actor, { type: 'bid', amount: 7, allyPayment: 0 });
|
||||
assert.equal(game.auction?.bidder, f.actor);
|
||||
assert.equal(game.auction?.bid, 7);
|
||||
assert.equal(player(game, f.actor).spice, 10);
|
||||
|
||||
game = act(game, f.asker, { type: 'card', card: f.truth });
|
||||
while (game.truthtrance?.stage === 'priority') {
|
||||
const passer = game.players.find(p => !game.truthtrance!.passed.includes(p.id));
|
||||
assert.ok(passer);
|
||||
game = act(game, passer.id, { type: 'truthPass' });
|
||||
}
|
||||
assert.equal(game.truthtrance?.stage, 'ask');
|
||||
game = act(game, f.asker, { type: 'truthAsk', question: {
|
||||
kind: 'shipment', target: f.actor, territory: 'carthag', minimum: 8,
|
||||
} });
|
||||
const respondent = viewGame(game, f.actor);
|
||||
assert.deepEqual(respondent.truthShipmentAnswers, ['no']);
|
||||
assert.ok(respondent.shipmentPromisePolicy == null);
|
||||
assert.equal(respondent.shipmentPreparation, null);
|
||||
const beforeRejected = structuredClone(game);
|
||||
assert.throws(() => act(game, f.actor, { type: 'truthAnswer', answer: 'yes' }));
|
||||
assert.deepEqual(game, beforeRejected);
|
||||
// The other seat's private counter remains absent from an observer's view.
|
||||
assert.ok(!JSON.stringify(viewGame(game, f.asker)).includes(f.counter));
|
||||
game = act(game, f.actor, { type: 'truthAnswer', answer: 'no' });
|
||||
|
||||
// Complete the actual auction; the cash invoice settles automatically when
|
||||
// the blocked Karama leaves no legal alternative payment choice.
|
||||
game = advance(game, g => player(g, f.actor).hand.some(card => card.id === lot));
|
||||
assert.equal(player(game, f.actor).spice, 3);
|
||||
assert.ok(player(game, f.actor).hand.some(card => card.id === lot));
|
||||
assert.ok(player(game, f.actor).hand.some(card => card.id === f.karama));
|
||||
assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame, type Game } from '../game/engine';
|
||||
import { act, advance, clean, createKullBribeFixture, player } from './fixture-kull-bribe';
|
||||
|
||||
function shipmentOpening(answer: 'yes' | 'no', amalHolder?: 'emperor' | 'choam') {
|
||||
const f = createKullBribeFixture();
|
||||
let game = structuredClone(f.charity);
|
||||
// Controlled physical-card supplements: return Emperor’s original Karama
|
||||
// to the unopened deck and move an original Amal to the named hand. No
|
||||
// force, spice, phase or promise is staged.
|
||||
let amal: string | undefined;
|
||||
if (amalHolder) {
|
||||
const own = player(game, f.actor);
|
||||
const karamaIndex = own.hand.findIndex(card => card.id === f.karama);
|
||||
assert.ok(karamaIndex >= 0);
|
||||
game.deck.push(own.hand.splice(karamaIndex, 1)[0]);
|
||||
const index = game.deck.findIndex(card => card.effect === 'amal');
|
||||
assert.ok(index >= 0);
|
||||
const card = game.deck.splice(index, 1)[0];
|
||||
player(game, amalHolder).hand.push(card);
|
||||
amal = card.id;
|
||||
}
|
||||
game = act(game, f.asker, { type: 'card', card: f.truth });
|
||||
while (game.truthtrance?.stage === 'priority') {
|
||||
const passer = game.players.find(p => !game.truthtrance!.passed.includes(p.id));
|
||||
assert.ok(passer);
|
||||
game = act(game, passer.id, { type: 'truthPass' });
|
||||
}
|
||||
game = act(game, f.asker, { type: 'truthAsk', question: {
|
||||
kind: 'shipment', target: f.actor, territory: 'carthag', minimum: 8,
|
||||
} });
|
||||
assert.ok(viewGame(game, f.actor).truthShipmentAnswers?.includes(answer));
|
||||
game = act(game, f.actor, { type: 'truthAnswer', answer });
|
||||
game = advance(game, g => g.phase === 5 && g.phaseOpening?.initialize === true);
|
||||
assert.equal(game.active, null);
|
||||
assert.equal(game.movementRemaining, null);
|
||||
return { f, game, amal };
|
||||
}
|
||||
|
||||
function completeOpening(game: Game) {
|
||||
while (game.phaseOpening) {
|
||||
const passer = game.players.find(p => !game.phaseOpening!.passed.includes(p.id));
|
||||
assert.ok(passer);
|
||||
game = act(game, passer.id, { type: 'ready' });
|
||||
}
|
||||
return game;
|
||||
}
|
||||
|
||||
void test('an early shipment No survives each real Ix Movement opening pass', () => {
|
||||
const { f, game: opening } = shipmentOpening('no');
|
||||
const game = completeOpening(opening);
|
||||
assert.equal(player(game, f.actor).shipped, false);
|
||||
assert.equal(game.phase, 5);
|
||||
assert.ok(game.movementRemaining?.includes(f.actor));
|
||||
assert.equal(game.shipmentPromises?.[0].answer, false);
|
||||
assert.equal(game.shipmentPromises?.[0].released, undefined);
|
||||
});
|
||||
|
||||
void test('an owner cannot play Amal to defeat a positive shipment promise at the Movement opening', () => {
|
||||
const { f, game, amal } = shipmentOpening('yes', 'emperor');
|
||||
assert.ok(amal);
|
||||
assert.equal(player(game, f.actor).spice, 10);
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => act(game, f.actor, { type: 'card', card: amal }), /Truthtrance shipment/);
|
||||
assert.deepEqual(game, before);
|
||||
});
|
||||
|
||||
void test('another player’s Amal releases an impossible shipment only after the opening settles', () => {
|
||||
const { f, game: opening, amal } = shipmentOpening('yes', 'choam');
|
||||
assert.ok(amal);
|
||||
let game = act(opening, f.reactor, { type: 'card', card: amal });
|
||||
assert.equal(player(game, f.actor).spice, 5);
|
||||
assert.equal(game.shipmentPromises?.[0].released, undefined);
|
||||
game = completeOpening(game);
|
||||
game = advance(game, clean);
|
||||
assert.equal(game.shipmentPromises?.[0].released, true);
|
||||
});
|
||||
|
||||
void test('a second-turn shipment promise survives the opening after a real previous shipment', () => {
|
||||
const f = createKullBribeFixture();
|
||||
let game = structuredClone(f.game);
|
||||
assert.equal(game.turn, 1);
|
||||
assert.equal(game.phase, 5);
|
||||
assert.equal(game.active, f.actor);
|
||||
game = act(game, f.actor, { type: 'ship', territory: 'arrakeen', sector: 10,
|
||||
amount: 1, elite: 0, allyPayment: 0 });
|
||||
game = advance(game, clean);
|
||||
assert.equal(player(game, f.actor).shipped, true);
|
||||
game = act(game, f.actor, { type: 'endMovement' });
|
||||
game = advance(game, g => g.turn === 2 && g.phase === 2 && clean(g));
|
||||
assert.deepEqual(game.movementRemaining, []);
|
||||
assert.equal(player(game, f.actor).shipped, true);
|
||||
game = act(game, f.asker, { type: 'card', card: f.truth });
|
||||
while (game.truthtrance?.stage === 'priority') {
|
||||
const passer = game.players.find(p => !game.truthtrance!.passed.includes(p.id));
|
||||
assert.ok(passer);
|
||||
game = act(game, passer.id, { type: 'truthPass' });
|
||||
}
|
||||
game = act(game, f.asker, { type: 'truthAsk', question: {
|
||||
kind: 'shipment', target: f.actor, territory: 'carthag', minimum: 8,
|
||||
} });
|
||||
assert.ok(viewGame(game, f.actor).truthShipmentAnswers?.includes('no'));
|
||||
game = act(game, f.actor, { type: 'truthAnswer', answer: 'no' });
|
||||
game = advance(game, g => g.turn === 2 && g.phase === 5 && g.phaseOpening?.initialize === true);
|
||||
assert.deepEqual(game.movementRemaining, []);
|
||||
assert.equal(player(game, f.actor).shipped, true);
|
||||
game = completeOpening(game);
|
||||
assert.equal(player(game, f.actor).shipped, false);
|
||||
assert.ok(game.movementRemaining?.includes(f.actor));
|
||||
assert.ok(game.shipmentPromises?.some(p => p.turn === 2 && p.answer === false && !p.released));
|
||||
});
|
||||
@@ -66,7 +66,7 @@ function reject(g: Game, actor: string, action: Action) {
|
||||
const before = structuredClone(g); assert.throws(() => applyAction(g, actor, action)); assert.deepEqual(g, before);
|
||||
}
|
||||
|
||||
void test('current-turn target timing distinguishes earlier phases from unused movement turns and evaluates the actual Semuta respondent', () => {
|
||||
void test('current-turn target timing distinguishes earlier phases from unused movement turns and admits native Richese', () => {
|
||||
const g = fixture();
|
||||
assert.equal(shipmentPromiseTurnAvailable(g, 'a', true), true);
|
||||
assert.equal(shipmentPromiseTurnAvailable(g, 'p', true), true);
|
||||
@@ -82,7 +82,7 @@ void test('current-turn target timing distinguishes earlier phases from unused m
|
||||
const mode = { ...g, advanced: true, active: 'r', expansions: ['choam'], semutaPreview: true,
|
||||
players: [{ id: 'r', faction: 'richese' }, { id: 'p', faction: 'emperor' }] };
|
||||
assert.equal(shipmentPromiseModeSupported(mode, 'p'), true);
|
||||
assert.equal(shipmentPromiseModeSupported(mode, 'r'), false);
|
||||
assert.equal(shipmentPromiseModeSupported(mode, 'r'), true);
|
||||
});
|
||||
|
||||
void test('a waiting promise supplies no early executable action, survives the intervening turn and fulfills only actual later delivery', () => {
|
||||
|
||||
Reference in new issue
Block a user