Expand independent-card Semuta continuations
Verify and publish checkpoint container / container (push) Successful in 11m22s
Verify and publish checkpoint container / container (push) Successful in 11m22s
This commit is contained in:
1 parent
6719245f40
commit
50c6201d33
6 files changed
+136
-4
No files matched your search
@@ -0,0 +1,105 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { distance, location, TERRITORIES } from '../game/board';
|
||||
import { viewGame, type Action, type Game } from '../game/engine';
|
||||
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
||||
import { actIndependentOrdinary as act, advanceIndependentOrdinary,
|
||||
createIndependentOrdinaryFixture, independentOrdinaryClean,
|
||||
independentOrdinaryInventory as inventory, independentOrdinaryPlayer as player,
|
||||
} from './fixture-semuta-independent-ordinary';
|
||||
import { moveSemutaEmperorCard, semutaEmperorCardId } from './fixture-semuta-emperor-share';
|
||||
|
||||
type Kind = 'box' | 'distrans' | 'truth' | 'ornithopter';
|
||||
function close(game: Game, holder: string, claim: boolean): Game {
|
||||
const batch = game.pendingTreacheryDiscard; assert.ok(batch?.reaction);
|
||||
const event = batch.batch.event;
|
||||
if (claim) return act(game, holder, {type:'semutaCommit', event});
|
||||
for (const seat of game.players) game = act(game, seat.id, {type:'semutaPass', event});
|
||||
return game;
|
||||
}
|
||||
|
||||
/** Real public independent-card game and Semuta acquisition. The additional
|
||||
* printed card is moved once between existing physical zones to isolate this
|
||||
* interaction; no force, money, phase or event is staged. */
|
||||
function fixture(kind: Kind) {
|
||||
const source = createIndependentOrdinaryFixture('hajr');
|
||||
let game = structuredClone(source.game);
|
||||
const effect = kind === 'box' ? 'nullentropyBox' : kind === 'truth' ? 'truthtrance' : kind;
|
||||
const card = semutaEmperorCardId(game, item => item.effect === effect);
|
||||
moveSemutaEmperorCard(game, card, source.owner);
|
||||
const initialInventory = inventory(game);
|
||||
let given: string | undefined, selected: string | undefined;
|
||||
if (kind === 'box') {
|
||||
// Use the genuinely bought Hajr to create an actual discard target.
|
||||
game = act(game, source.owner, source.play);
|
||||
game = close(game, source.holder, false);
|
||||
selected = source.card;
|
||||
game = act(game, source.owner, {type:'card', card});
|
||||
// A sole eligible discard is selected automatically by the native Box.
|
||||
if (game.pendingNullentropy)
|
||||
game = act(game, source.owner, {type:'decision', event:game.pendingNullentropy.event, card:selected});
|
||||
} else if (kind === 'distrans') {
|
||||
given = source.card;
|
||||
game = act(game, source.owner, {type:'card', card, target:'emperor', give:given});
|
||||
} else if (kind === 'truth') {
|
||||
game = act(game, source.owner, {type:'card', card});
|
||||
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'});
|
||||
}
|
||||
game = act(game, source.owner, {type:'truthAsk', question:{kind:'freeform', target:'emperor',
|
||||
scope:'fact', text:'Do you intend to protect Arrakeen?'}});
|
||||
game = act(game, 'emperor', {type:'truthAnswer', answer:'yes'});
|
||||
} else {
|
||||
const open = TERRITORIES.flatMap(place => place.type === 'sand' ? place.sectors.map(sector => ({place, sector})) : [])
|
||||
.find(({place, sector}) => sector !== game.storm && game.players.every(seat =>
|
||||
!Object.keys(seat.forces).some(key => key.startsWith(`${place.id}:`))));
|
||||
assert.ok(open);
|
||||
game = act(game, source.owner, {type:'ship', territory:open.place.id, sector:open.sector, amount:1, elite:0, allyPayment:0});
|
||||
game = advanceIndependentOrdinary(game, independentOrdinaryClean);
|
||||
const from = location(open.place.id, open.sector);
|
||||
const target = TERRITORIES.flatMap(place => place.sectors.map(sector => ({place, sector})))
|
||||
.find(({place, sector}) => place.id !== open.place.id && sector !== game.storm &&
|
||||
distance(from, location(place.id, sector)) <= 1 && game.players.every(seat =>
|
||||
!Object.keys(seat.forces).some(key => key.startsWith(`${place.id}:`))));
|
||||
assert.ok(target);
|
||||
const action: Action = {type:'move', from, amount:1, elite:0, territory:target.place.id,
|
||||
sector:target.sector, movementCard:card, ornithopter:'range3'};
|
||||
game = act(game, source.owner, action);
|
||||
}
|
||||
return {game, owner:source.owner, holder:source.holder, card, given, selected, initialInventory};
|
||||
}
|
||||
|
||||
for (const kind of ['box','distrans','truth','ornithopter'] as const) for (const claim of [false,true]) {
|
||||
void test(`independent-card ${kind} Semuta ${claim ? 'claim' : 'pass'} preserves the completed effect`, () => {
|
||||
const f = fixture(kind), pending = f.game.pendingTreacheryDiscard; assert.ok(pending?.reaction);
|
||||
assert.deepEqual(pending.batch.entries.map(entry => entry.card.id), [f.card]);
|
||||
assert.equal(viewGame(f.game, f.holder).semutaReaction?.canCommit, true);
|
||||
if (kind === 'distrans') {
|
||||
assert.ok(player(f.game,'emperor').hand.some(card => card.id === f.given));
|
||||
assert.ok(!pending.batch.entries.some(entry => entry.card.id === f.given));
|
||||
}
|
||||
if (kind === 'box') assert.ok(player(f.game,f.owner).hand.some(card => card.id === f.selected));
|
||||
const before = structuredClone({players:f.game.players.map(row => ({id:row.id,spice:row.spice,forces:row.forces})),
|
||||
truthHistory:f.game.truthHistory, moved:player(f.game,f.owner).moved});
|
||||
const done = close(f.game, f.holder, claim);
|
||||
assert.equal(done.pendingTreacheryDiscard, null);
|
||||
assert.deepEqual({players:done.players.map(row => ({id:row.id,spice:row.spice,forces:row.forces})),
|
||||
truthHistory:done.truthHistory, moved:player(done,f.owner).moved}, before);
|
||||
assert.equal(player(done,f.holder).hand.some(card => card.id === f.card), claim);
|
||||
assert.equal(done.discard.some(card => card.id === f.card), !claim);
|
||||
assert.equal(done.discard.some(card => card.id === SEMUTA_DRUG_ID), claim);
|
||||
assert.deepEqual(inventory(done), f.initialInventory);
|
||||
});
|
||||
}
|
||||
|
||||
void test('all AI profiles can answer the independent-card Distrans discard', () => {
|
||||
const f = fixture('distrans');
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const game = structuredClone(f.game); player(game,f.holder).bot = difficulty;
|
||||
const answer = botActions(viewGame(game,f.holder)).find(action => action.type === 'semutaCommit' || action.type === 'semutaPass');
|
||||
assert.ok(answer); assert.doesNotThrow(() => act(game,f.holder,answer));
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user