Enable Basic Kull with Ecaz and Moritani
This commit is contained in:
1 parent
69a9330810
commit
6d50829c0c
13 files changed
+503
-16
No files matched your search
@@ -0,0 +1,96 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine';
|
||||
import { territory } from '../game/board';
|
||||
import { actKullPurpose, kullPurposeClean, kullPurposePlayer, nextKullPurposeStep,
|
||||
type KullPurposeStep } from './fixture-kull-purpose';
|
||||
import { publicKullInventory } from './fixture-public-choam-kull';
|
||||
|
||||
export const act = actKullPurpose;
|
||||
export const clean = kullPurposeClean;
|
||||
export const player = kullPurposePlayer;
|
||||
export const inventory = publicKullInventory;
|
||||
export type E3NativeRoster = 'ecaz' | 'moritani' | 'both';
|
||||
export type E3NativeTarget = 'ecaz' | 'moritani';
|
||||
|
||||
/** Make only ordinary native decisions, declining optional placements and
|
||||
* effects unless the caller has stopped at the requested original control. */
|
||||
function step(game: Game): KullPurposeStep | null {
|
||||
if (!game.phaseOpening && !game.response && game.decision) {
|
||||
const d = game.decision;
|
||||
if (d.kind === 'moritaniSetup') return { actor: d.player,
|
||||
action: { type: 'decision', territory: 'carthag', sector: territory('carthag').sectors[0] } };
|
||||
if (d.kind === 'ecazPlacement' || d.kind === 'moritaniPlacement')
|
||||
return { actor: d.player, action: { type: 'decision', decline: true } };
|
||||
if (d.kind === 'ecazAmbassador') return { actor: d.player,
|
||||
action: { type: 'decision', event: game.pendingAmbassador!.event, decline: true } };
|
||||
}
|
||||
return nextKullPurposeStep(game);
|
||||
}
|
||||
export function advance(game: Game, until: (state: Game) => boolean, actions: KullPurposeStep[] = []): Game {
|
||||
for (let n = 0; n < 1800; n++) {
|
||||
if (until(game)) return game;
|
||||
assert.notEqual(game.status,'finished','The native E3 Kull programme ended before the requested window');
|
||||
const next = step(game); assert.ok(next,`A native continuation is required at ${game.status}/${game.turn}/${game.phase}/${game.decision?.kind}`);
|
||||
game = act(game,next.actor,next.action,actions);
|
||||
}
|
||||
throw Error('The native E3 Kull programme did not reach its finite boundary');
|
||||
}
|
||||
|
||||
/** Real public Basic start; only original unopened Treachery and Spice
|
||||
* positions are chosen before native deal. No force, wallet, token or phase is
|
||||
* staged. Every subsequent placement and cancellation uses native actions. */
|
||||
export function createKullNativeE3Fixture(roster: E3NativeRoster, target: E3NativeTarget = roster === 'moritani' ? 'moritani' : 'ecaz') {
|
||||
assert.ok(roster === 'both' || roster === target);
|
||||
let game = createGame('KULLE3',newPlayer('choam','CHOAM','choam'),false,['choam','ix','ecaz']);
|
||||
const natives = roster === 'both' ? ['ecaz','moritani'] as const : [roster] as const;
|
||||
for (const faction of natives) joinGame(game,newPlayer(faction,faction,faction));
|
||||
for (const faction of ['emperor','guild'] as const) joinGame(game,newPlayer(faction,faction,faction));
|
||||
const actions: KullPurposeStep[] = [];
|
||||
const send = (actor: string, action: Action) => { game = act(game,actor,action,actions); };
|
||||
const reach = (predicate: (state: Game) => boolean) => { game = advance(game,predicate,actions); };
|
||||
send('choam',{type:'moduleOption',module:'kullPreview',enabled:true});
|
||||
for (const seat of game.players) send(seat.id,{type:'ready'});
|
||||
const lobby = structuredClone(game);
|
||||
send('choam',{type:'start'});
|
||||
assert.equal(game.status,'setup'); assert.equal(game.kullPreview,true);
|
||||
const setup = structuredClone(game), originalCards = game.deck.map(card => card.id).sort();
|
||||
const kinds = roster === 'both' ? ['kull','truth','filler','karama','karama'] : ['kull','truth','karama','karama'];
|
||||
const chosen: Game['deck'] = [];
|
||||
for (const kind of kinds) {
|
||||
const index = game.deck.findIndex(card => kind === 'kull' ? card.id === 'ix-kull-wahad' :
|
||||
kind === 'truth' ? card.effect === 'truthtrance' : kind === 'karama' ? card.effect === 'karama' :
|
||||
card.kind === 'worthless' && card.id !== 'ix-kull-wahad');
|
||||
assert.ok(index >= 0); chosen.push(game.deck.splice(index,1)[0]);
|
||||
}
|
||||
game.deck.unshift(...chosen);
|
||||
const beforeDeal = structuredClone(game);
|
||||
reach(state => state.status === 'playing');
|
||||
const afterSetup = structuredClone(game), kull = 'ix-kull-wahad';
|
||||
const karama = player(game,'emperor').hand.find(card => card.effect === 'karama')?.id;
|
||||
const counter = player(game,'guild').hand.find(card => card.effect === 'karama')?.id;
|
||||
const asker = roster === 'moritani' ? 'moritani' : 'ecaz';
|
||||
const truth = player(game,asker).hand.find(card => card.effect === 'truthtrance')?.id;
|
||||
assert.ok(karama && counter && truth && karama !== counter);
|
||||
assert.ok(player(game,'choam').hand.some(card => card.id === kull));
|
||||
assert.deepEqual(inventory(game),originalCards);
|
||||
if (roster !== 'moritani') assert.ok(game.ecazAmbassadors && game.ecazOccupyPreview);
|
||||
if (roster !== 'ecaz') assert.ok(game.moritaniTerror);
|
||||
const spice = game.spiceDeck.findIndex(card => 'territory' in card && !card.discovery);
|
||||
assert.ok(spice >= 0); game.spiceDeck.unshift(game.spiceDeck.splice(spice,1)[0]);
|
||||
reach(state => state.phase <= 1 && clean(state));
|
||||
const early = structuredClone(game);
|
||||
const phase = target === 'ecaz' ? 4 : 8;
|
||||
const decision = target === 'ecaz' ? 'ecazPlacement' : 'moritaniPlacement';
|
||||
reach(state => state.phase === phase && state.decision?.kind === decision && !state.response && !state.phaseOpening);
|
||||
const beforePlacement = structuredClone(game);
|
||||
const token = target === 'ecaz' ? game.ecazAmbassadors!.tokens.find(row => row.effect === 'ecaz' && row.zone === 'supply')
|
||||
: game.moritaniTerror!.tokens.find(row => row.status === 'available');
|
||||
assert.ok(token);
|
||||
send(target,{type:'decision',token:token.id,territory:'carthag'});
|
||||
assert.equal(game.response?.kind,decision);
|
||||
assert.deepEqual(inventory(game),originalCards);
|
||||
return { game, early, beforePlacement, lobby, setup, beforeDeal, afterSetup, actions, originalCards,
|
||||
actor:'emperor', reactor:'choam', counterOwner:'guild', asker, target, token:token.id,
|
||||
kull, karama, counter, truth };
|
||||
}
|
||||
export type KullNativeE3Fixture = ReturnType<typeof createKullNativeE3Fixture>;
|
||||
@@ -19,10 +19,13 @@ void test('Basic printed Kull supports the shared bribe family in its declared n
|
||||
assert.equal(quote({...basic,kullPreview:false}),true);
|
||||
assert.equal(quote({...basic,advanced:true,kullPreview:true}),false);
|
||||
assert.equal(quote({...basic,expansions:['choam'],kullPreview:true}),false);
|
||||
assert.equal(quote({...basic,expansions:['choam','ix','ecaz'],kullPreview:true}),false);
|
||||
assert.equal(quote({...basic,expansions:['choam','ix','ecaz'],kullPreview:true}),true);
|
||||
assert.equal(quote({...basic,players:[{faction:'emperor'},{faction:'guild'}],kullPreview:true}),false);
|
||||
assert.equal(quote({...basic,players:[{faction:'choam'},{faction:'ixians'}],kullPreview:true,mobileStronghold:{}}),true);
|
||||
assert.equal(quote({...basic,players:[{faction:'choam'},{faction:'tleilaxu'}],kullPreview:true}),true);
|
||||
assert.equal(quote({...basic,players:[{faction:'choam'},{faction:'ecaz'}],expansions:['choam','ix','ecaz'],kullPreview:true}),true);
|
||||
assert.equal(quote({...basic,players:[{faction:'choam'},{faction:'moritani'}],expansions:['choam','ix','ecaz'],kullPreview:true}),true);
|
||||
assert.equal(quote({...basic,players:[{faction:'choam'},{faction:'ecaz'}],kullPreview:true}),false);
|
||||
assert.equal(quote({...basic,players:[{faction:'choam'},{faction:'richese'}],kullPreview:true}),false);
|
||||
assert.equal(quote({...basic,players:[{faction:'choam'},{faction:'emperor'},{faction:'emperor'}],kullPreview:true}),false);
|
||||
for (const option of ['semutaPreview','nexusKullPreview','richeseBetrayalPreview','guildBetrayalPreview',
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame, type Game } from '../game/engine';
|
||||
import { act, advance, clean, createKullBribeFixture, inventory, player } from './fixture-kull-bribe';
|
||||
|
||||
/** Build the question and both distinct Karama costs from the original
|
||||
* unopened public setup. Emperor voluntarily retains either four or eight
|
||||
* spice before answering; no hand, force, promise or phase is installed. */
|
||||
function pendingCounter(retained: 4 | 8) {
|
||||
const f = createKullBribeFixture();
|
||||
let game = act(f.early, 'emperor', { type: 'bribe', target: 'guild', amount: 10 - retained });
|
||||
assert.equal(player(game, 'emperor').spice, retained);
|
||||
assert.ok(player(game, 'emperor').hand.some(card => card.id === f.karama));
|
||||
game = act(game, 'guild', { type: 'card', card: f.truth });
|
||||
while (game.truthtrance?.stage === 'priority') {
|
||||
const seat = game.players.find(row => !game.truthtrance!.passed.includes(row.id));
|
||||
assert.ok(seat);
|
||||
game = act(game, seat.id, { type: 'truthPass' });
|
||||
}
|
||||
assert.equal(game.truthtrance?.stage, 'ask');
|
||||
game = act(game, 'guild', { type: 'truthAsk', question: {
|
||||
kind: 'movement', target: 'emperor', territory: 'carthag', minimum: 8,
|
||||
} });
|
||||
assert.ok(viewGame(game, 'emperor').truthMovementAnswers?.includes('yes'));
|
||||
game = act(game, 'emperor', { type: 'truthAnswer', answer: 'yes' });
|
||||
const accepted = structuredClone(game.movementPromises);
|
||||
game = advance(game, state => state.phase === 2 && state.response?.kind === 'choamCharity');
|
||||
|
||||
// Harkonnen attempts the original cancellation with its own physical
|
||||
// Karama; CHOAM declares its unopened-deal Kull, and Emperor may counter
|
||||
// only with its different physical card.
|
||||
game = act(game, 'harkonnen', { type: 'card', mode: 'cancel', card: f.counter });
|
||||
assert.equal(game.pendingKull?.stage, 'offer');
|
||||
assert.deepEqual(game.movementPromises, accepted);
|
||||
const reaction = viewGame(game, 'choam').kullReaction;
|
||||
assert.ok(reaction);
|
||||
assert.ok(reaction.plays.some(row => row.card.id === f.kull && !row.blocked));
|
||||
game = act(game, 'choam', { type: 'kullDecision', event: reaction.event, source: 'printed', card: f.kull });
|
||||
// If no responder can legally counter, native continuation immediately
|
||||
// settles Kull instead of exposing an empty counter window.
|
||||
if (retained === 8) assert.equal(game.pendingKull?.stage, 'counter');
|
||||
assert.deepEqual(game.movementPromises, accepted);
|
||||
return { f, game, accepted };
|
||||
}
|
||||
|
||||
function finishMovement(game: Game) {
|
||||
game = advance(game, state => state.phase === 5 && state.active === 'emperor' && clean(state));
|
||||
// Preserve the required Karama rate when eight forces cannot be shipped for
|
||||
// the four remaining spice without it; ordinary cash payment works with eight.
|
||||
if (player(game, 'emperor').spice === 4) {
|
||||
const card = player(game, 'emperor').hand.find(row => row.effect === 'karama');
|
||||
assert.ok(card);
|
||||
game = advance(act(game, 'emperor', { type: 'card', mode: 'shipment', card: card.id, target: 'emperor' }), clean);
|
||||
}
|
||||
game = advance(act(game, 'emperor', { type: 'ship', territory: 'arrakeen', sector: 10,
|
||||
amount: 8, elite: 0, allyPayment: 0 }), clean);
|
||||
game = advance(act(game, 'emperor', { type: 'move', from: 'arrakeen:10', territory: 'carthag',
|
||||
sector: 11, amount: 8, elite: 0 }), clean);
|
||||
assert.equal(game.movementPromises?.[0].fulfilled, true);
|
||||
return game;
|
||||
}
|
||||
|
||||
void test('a distinct Kull counter cannot spend the only Karama needed by its own movement Yes', () => {
|
||||
const { f, game, accepted } = pendingCounter(4);
|
||||
assert.equal(game.pendingKull, null,
|
||||
'With no legal counter the Kull response settles automatically');
|
||||
assert.deepEqual(game.movementPromises, accepted);
|
||||
const settled = advance(game, clean);
|
||||
assert.equal(settled.pendingKull, null);
|
||||
assert.equal(settled.movementPromises?.[0].released, undefined);
|
||||
assert.ok(player(settled, 'emperor').hand.some(card => card.id === f.karama));
|
||||
assert.deepEqual(inventory(finishMovement(settled)), f.originalCards);
|
||||
});
|
||||
|
||||
void test('the distinct counter remains legal when cash independently funds the promised movement', () => {
|
||||
const { f, game, accepted } = pendingCounter(8);
|
||||
assert.ok(viewGame(game, 'emperor').responseControls?.cancelCards.includes(f.karama));
|
||||
let settled = act(game, 'emperor', { type: 'card', mode: 'cancel', card: f.karama });
|
||||
settled = advance(settled, clean);
|
||||
assert.equal(settled.pendingKull, null);
|
||||
assert.deepEqual(settled.movementPromises, accepted);
|
||||
assert.equal(settled.discard.filter(card => card.id === f.karama).length, 1);
|
||||
assert.deepEqual(inventory(finishMovement(settled)), f.originalCards);
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame, type Game } from '../game/engine';
|
||||
import { act, advance, clean, createKullBribeFixture, inventory, player } from './fixture-kull-bribe';
|
||||
|
||||
/** Real public Basic CHOAM/Emperor/Guild/Harkonnen game, selected unopened
|
||||
* cards and Spice position, and a genuine ordinary bribe. Harkonnen starts
|
||||
* with ten forces in Carthag, ten reserves, ten Spice and a physical Karama.
|
||||
* Paying seven leaves only the Karama-discounted five-force city shipment
|
||||
* available to complete a fifteen-force move in this phase. */
|
||||
function movementSource() {
|
||||
const f = createKullBribeFixture();
|
||||
let game = act(f.game, 'harkonnen', { type: 'bribe', target: 'guild', amount: 7 });
|
||||
assert.equal(player(game, 'harkonnen').spice, 3);
|
||||
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: 'movement', target: 'harkonnen', territory: 'imperial_basin', minimum: 15,
|
||||
} });
|
||||
assert.deepEqual(viewGame(game, 'harkonnen').truthMovementAnswers, ['yes', 'no']);
|
||||
game = act(game, 'harkonnen', { type: 'truthAnswer', answer: 'yes' });
|
||||
assert.equal(game.movementPromises?.[0].answer, true);
|
||||
game = advance(game, g => g.phase === 5 && g.active === 'harkonnen' && clean(g));
|
||||
assert.ok(player(game, 'harkonnen').hand.some(card => card.id === f.counter));
|
||||
assert.deepEqual(inventory(game), f.originalCards);
|
||||
return { f, game };
|
||||
}
|
||||
|
||||
function offer(f: ReturnType<typeof createKullBribeFixture>, game: Game): Game {
|
||||
game = act(game, 'harkonnen', { type: 'card', mode: 'shipment', card: f.counter, target: 'harkonnen' });
|
||||
assert.equal(game.pendingKull?.stage, 'offer');
|
||||
assert.ok(player(game, 'harkonnen').hand.some(card => card.id === f.counter));
|
||||
assert.equal(game.movementPromises?.[0].released, undefined);
|
||||
return game;
|
||||
}
|
||||
|
||||
function declare(f: ReturnType<typeof createKullBribeFixture>, game: Game): Game {
|
||||
const reaction = viewGame(game, 'choam').kullReaction; assert.ok(reaction);
|
||||
return act(game, 'choam', { type: 'kullDecision', event: reaction.event, source: 'printed', card: f.kull });
|
||||
}
|
||||
|
||||
function finishMove(f: ReturnType<typeof createKullBribeFixture>, game: Game): Game {
|
||||
game = act(game, 'harkonnen', { type: 'ship', territory: 'carthag', sector: 11, amount: 5, elite: 0, allyPayment: 0 });
|
||||
game = advance(game, clean);
|
||||
assert.equal(player(game, 'harkonnen').spice, 0);
|
||||
assert.equal(player(game, 'harkonnen').forces['carthag:11'], 15);
|
||||
game = act(game, 'harkonnen', { type: 'move', from: 'carthag:11', territory: 'imperial_basin', sector: 9, amount: 15, elite: 0 });
|
||||
assert.equal(game.movementPromises?.[0].fulfilled, true);
|
||||
assert.deepEqual(inventory(game), f.originalCards);
|
||||
return game;
|
||||
}
|
||||
|
||||
void test('a Kull preview offers the legal movement Yes using a future Karama and actual decline', () => {
|
||||
const { f, game } = movementSource();
|
||||
const pending = offer(f, game);
|
||||
const reaction = viewGame(pending, 'choam').kullReaction; assert.ok(reaction);
|
||||
const declined = act(pending, 'choam', { type: 'kullDecision', event: reaction.event, decline: true });
|
||||
assert.equal(declined.karamaShipping?.owner, 'harkonnen');
|
||||
assert.equal(declined.movementPromises?.[0].released, undefined);
|
||||
finishMove(f, declined);
|
||||
});
|
||||
|
||||
void test('a distinct Karama counter preserves the original movement route and card custody', () => {
|
||||
const { f, game } = movementSource();
|
||||
const declared = declare(f, offer(f, game));
|
||||
assert.ok(viewGame(declared, 'emperor').responseControls?.cancelCards?.includes(f.karama));
|
||||
const countered = act(declared, 'emperor', { type: 'card', mode: 'cancel', card: f.karama });
|
||||
const resumed = advance(countered, clean);
|
||||
assert.equal(resumed.karamaShipping?.owner, 'harkonnen');
|
||||
assert.equal(resumed.movementPromises?.[0].released, undefined);
|
||||
assert.equal(resumed.discard.filter(card => card.id === f.karama).length, 1);
|
||||
assert.ok(player(resumed, 'choam').hand.some(card => card.id === f.kull));
|
||||
finishMove(f, resumed);
|
||||
});
|
||||
|
||||
void test('successful Kull preserves the original Karama and releases an impossible movement promise', () => {
|
||||
const { f, game } = movementSource();
|
||||
const declared = declare(f, offer(f, game));
|
||||
const blocked = advance(declared, clean);
|
||||
assert.equal(blocked.karamaShipping, null);
|
||||
assert.ok(player(blocked, 'harkonnen').hand.some(card => card.id === f.counter));
|
||||
assert.equal(blocked.discard.filter(card => card.id === f.kull).length, 1);
|
||||
assert.equal(blocked.movementPromises?.[0].released, true);
|
||||
assert.deepEqual(inventory(blocked), f.originalCards);
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { bribePromiseModeSupported } from '../game/bribe-promises';
|
||||
import { act, advance, inventory, player, createKullNativeE3Fixture,
|
||||
type E3NativeRoster, type E3NativeTarget, type KullNativeE3Fixture } from './fixture-kull-native-e3';
|
||||
|
||||
const held = (g: Game, playerId: string, card: string) => player(g,playerId).hand.some(row => row.id === card);
|
||||
const finished = (g: Game) => !g.pendingKull && !g.response && !g.phaseOpening &&
|
||||
!g.pendingEcazPlacement && !g.pendingMoritaniPlacement;
|
||||
function offer(f: KullNativeE3Fixture, state: Game): Game {
|
||||
const cards = viewGame(state,f.actor).responseControls?.cancelCards ?? [];
|
||||
assert.ok(cards.includes(f.karama),'The original Emperor holds a usable physical Karama');
|
||||
const game = act(state,f.actor,{type:'card',mode:'cancel',card:f.karama});
|
||||
assert.equal(game.pendingKull?.stage,'offer');
|
||||
const reaction = viewGame(game,f.reactor).kullReaction; assert.ok(reaction);
|
||||
assert.ok(reaction.plays.some(row => row.card.id === f.kull && !row.blocked));
|
||||
assert.ok(held(game,f.actor,f.karama));
|
||||
return game;
|
||||
}
|
||||
function declaration(f: KullNativeE3Fixture, state: Game): Game {
|
||||
const reaction = viewGame(state,f.reactor).kullReaction; assert.ok(reaction);
|
||||
return act(state,f.reactor,{type:'kullDecision',event:reaction.event,source:'printed',card:f.kull});
|
||||
}
|
||||
function placementResult(f: KullNativeE3Fixture, state: Game, placed: boolean) {
|
||||
if (f.target === 'ecaz') {
|
||||
const token = state.ecazAmbassadors?.tokens.find(row => row.id === f.token); assert.ok(token);
|
||||
assert.equal(token.zone,placed ? 'placed' : 'supply');
|
||||
assert.equal(token.location,placed ? 'carthag' : null);
|
||||
} else {
|
||||
const token = state.moritaniTerror?.tokens.find(row => row.id === f.token); assert.ok(token);
|
||||
assert.equal(token.status,placed ? 'placed' : 'available');
|
||||
assert.equal(token.location,placed ? 'carthag' : null);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [roster,target] of [['ecaz','ecaz'],['moritani','moritani'],
|
||||
['both','ecaz'],['both','moritani']] as const satisfies readonly (readonly [E3NativeRoster,E3NativeTarget])[]) {
|
||||
void test(`Basic ${roster} Kull preserves real ${target} placement through each counter outcome`, () => {
|
||||
const f = createKullNativeE3Fixture(roster,target);
|
||||
assert.ok(f.actions.some(row => row.actor === f.lobby.host && row.action.type === 'start'));
|
||||
assert.equal(bribePromiseModeSupported(f.game),true);
|
||||
const pending = offer(f,f.game), reaction = viewGame(pending,f.reactor).kullReaction!;
|
||||
const decline = advance(act(pending,f.reactor,{type:'kullDecision',event:reaction.event,decline:true}),finished);
|
||||
placementResult(f,decline,false);
|
||||
assert.ok(held(decline,f.reactor,f.kull));
|
||||
assert.equal(decline.discard.filter(row => row.id === f.karama).length,1);
|
||||
assert.deepEqual(inventory(decline),f.originalCards);
|
||||
|
||||
const declared = declaration(f,pending);
|
||||
const success = advance(declared,finished);
|
||||
placementResult(f,success,true);
|
||||
assert.ok(held(success,f.actor,f.karama));
|
||||
assert.equal(success.discard.filter(row => row.id === f.kull).length,1);
|
||||
assert.deepEqual(inventory(success),f.originalCards);
|
||||
|
||||
const cards = viewGame(declared,f.counterOwner).responseControls?.cancelCards ?? [];
|
||||
assert.ok(cards.includes(f.counter),'Guild holds a distinct counter to Kull');
|
||||
const countered = advance(act(declared,f.counterOwner,{type:'card',mode:'cancel',card:f.counter}),finished);
|
||||
placementResult(f,countered,false);
|
||||
assert.ok(held(countered,f.reactor,f.kull));
|
||||
assert.equal(countered.discard.filter(row => row.id === f.counter).length,1);
|
||||
assert.equal(countered.discard.filter(row => row.id === f.karama).length,1);
|
||||
assert.deepEqual(inventory(countered),f.originalCards);
|
||||
});
|
||||
}
|
||||
|
||||
void test('the four minimal CHOAM bot profiles can take a native Ecaz Kull reaction', () => {
|
||||
const f = createKullNativeE3Fixture('ecaz'), pending = offer(f,f.game);
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(pending,f.reactor);
|
||||
view.players.find(row => row.id === f.reactor)!.bot = difficulty;
|
||||
const action = botActions(view)[0]; assert.ok(action,difficulty);
|
||||
assert.deepEqual(action,{type:'kullDecision',event:view.kullReaction!.event,source:'printed',card:f.kull});
|
||||
assert.equal(act(pending,f.reactor,action).pendingKull?.stage,'counter');
|
||||
}
|
||||
});
|
||||
|
||||
void test('a native Ecaz Charity-funded bribe survives Kull decline, block and counter', () => {
|
||||
const f = createKullNativeE3Fixture('ecaz');
|
||||
const purse = player(f.early,f.actor).spice;
|
||||
assert.ok(purse > 0);
|
||||
let game = act(f.early,f.actor,{type:'bribe',target:f.counterOwner,amount:purse});
|
||||
assert.equal(player(game,f.actor).spice,0);
|
||||
game = act(game,f.asker,{type:'card',card:f.truth});
|
||||
while (game.truthtrance?.stage === 'priority') {
|
||||
const passer = game.players.find(row => !game.truthtrance!.passed.includes(row.id)); assert.ok(passer);
|
||||
game = act(game,passer.id,{type:'truthPass'});
|
||||
}
|
||||
game = act(game,f.asker,{type:'truthAsk',question:{kind:'bribe',target:f.actor,
|
||||
turn:game.turn,recipient:f.counterOwner,minimum:1}});
|
||||
assert.ok(viewGame(game,f.actor).truthBribeAnswers?.includes('yes'));
|
||||
game = act(game,f.actor,{type:'truthAnswer',answer:'yes'});
|
||||
const promise = structuredClone(game.bribePromises);
|
||||
game = advance(game,state => state.phase === 2 && state.response?.kind === 'choamCharity');
|
||||
const pending = offer(f,game), reaction = viewGame(pending,f.reactor).kullReaction!;
|
||||
assert.equal(pending.pendingKull?.resume.response?.kind,'choamCharity');
|
||||
assert.equal(viewGame(pending,f.actor).bribePromiseGuide,null);
|
||||
const declined = act(pending,f.reactor,{type:'kullDecision',event:reaction.event,decline:true});
|
||||
const declared = declaration(f,pending);
|
||||
const countered = act(declared,f.counterOwner,{type:'card',mode:'cancel',card:f.counter});
|
||||
for (const branch of [declined,declared,countered]) {
|
||||
const ready = advance(branch,state => !state.pendingKull && !state.response && !state.phaseOpening && !state.decision);
|
||||
assert.deepEqual(ready.bribePromises,promise);
|
||||
const guide = viewGame(ready,f.actor).bribePromiseGuide; assert.ok(guide);
|
||||
assert.equal(guide.action.type,'charity');
|
||||
const charity = act(ready,f.actor,guide.action);
|
||||
assert.equal(player(charity,f.actor).charityTurn,charity.turn);
|
||||
const paid = act(charity,f.actor,{type:'bribe',target:f.counterOwner,amount:1});
|
||||
assert.equal(player(paid,f.counterOwner).bribes,purse+1);
|
||||
assert.ok(paid.bribePromises?.at(-1)?.fulfilled);
|
||||
assert.deepEqual(inventory(paid),f.originalCards);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user