99 lines
5.5 KiB
TypeScript
99 lines
5.5 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, viewGame, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { TRUTH_CARD_NAMES } from '../game/truthtrance-card-names';
|
|
import { knowledgeFactAnswer, truthKnowledgeOf } from '../game/truthtrance-knowledge';
|
|
import { freshPrivateLotGame, passPrivateLotWindow, privateLotPhysicalIds, privateLotPlayer,
|
|
recyclePrivatePurchaseIntoOriginalNormalLot, sellPrivateBlackMarketLot } from './fixture-homeworld-occupied-private-lots';
|
|
|
|
/** Stage the questioner's card by exchanging existing physical cards. The
|
|
* inspected/seller card remains untouched and no hand size is changed. */
|
|
function stageTruth(game: Game, protectedId: string) {
|
|
const original = privateLotPhysicalIds(game), asker = privateLotPlayer(game, 'ally');
|
|
const held = asker.hand.find(card => card.effect === 'truthtrance');
|
|
if (held) return held.id;
|
|
const stocks = [game.deck, game.discard, ...game.players.filter(p => p.id !== asker.id).map(p => p.hand)];
|
|
const source = stocks.find(cards => cards.some(card => card.effect === 'truthtrance' && card.id !== protectedId));
|
|
assert.ok(source, 'A distinct original Truthtrance must be available');
|
|
const index = source.findIndex(card => card.effect === 'truthtrance' && card.id !== protectedId);
|
|
const truth = source[index];
|
|
if (asker.hand.length) [asker.hand[0], source[index]] = [source[index], asker.hand[0]];
|
|
else asker.hand.push(...source.splice(index, 1));
|
|
assert.deepEqual(privateLotPhysicalIds(game), original);
|
|
return truth.id;
|
|
}
|
|
|
|
function ask(game: Game, card: string, name: string) {
|
|
let next = applyAction(game, 'ally', {type:'card',card});
|
|
while (next.truthtrance?.stage === 'priority') {
|
|
const p = next.players.find(player => !next.truthtrance!.passed.includes(player.id)); assert.ok(p);
|
|
next = applyAction(next, p.id, {type:'truthPass'});
|
|
}
|
|
return applyAction(next, 'ally', {type:'truthAsk', question:{kind:'fact',target:'occupier',fact:{kind:'auctionPeek',name}}});
|
|
}
|
|
|
|
function verify(state: Game, truth: string, name: string, expected: 'yes' | 'no', lot: 'ordinary' | 'blackMarket') {
|
|
const before = privateLotPhysicalIds(state), game = ask(structuredClone(state), truth, name);
|
|
assert.equal(viewGame(game, 'occupier').truthAnswer, expected);
|
|
assert.equal(viewGame(game, 'ally').truthAnswer, null);
|
|
assert.equal(viewGame(game, 'rival').truthAnswer, null);
|
|
if (lot === 'ordinary') {
|
|
assert.ok(viewGame(game, 'occupier').auction?.card);
|
|
assert.equal(viewGame(game, 'ally').auction?.card, null);
|
|
} else {
|
|
assert.ok(viewGame(game, 'occupier').richeseAuction?.card);
|
|
assert.equal(viewGame(game, 'ally').richeseAuction?.card, null);
|
|
}
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(game, 'occupier');
|
|
view.players.find(player => player.id === 'occupier')!.bot = difficulty;
|
|
assert.deepEqual(botActions(view), [{type:'truthAnswer',answer:expected}]);
|
|
const done = applyAction(game, 'occupier', {type:'truthAnswer',answer:expected});
|
|
assert.equal(done.truthHistory?.at(-1)?.answer, expected);
|
|
assert.deepEqual(privateLotPhysicalIds(done), before);
|
|
}
|
|
}
|
|
|
|
for (const denied of [false, true]) {
|
|
void test(`Caladan occupier answers actual Advanced Black Market inspection with native Atreides ${denied ? 'canceled' : 'allowed'}`, () => {
|
|
const fixture = freshPrivateLotGame();
|
|
let game = passPrivateLotWindow(fixture.game, 'richeseBlackMarket');
|
|
game = denied ? applyAction(game, 'rival', {type:'card',mode:'cancel',card:fixture.karama.id})
|
|
: passPrivateLotWindow(game, 'atreidesAuction');
|
|
assert.equal(game.homeworldAuctionInspection?.occupiedKnown, true);
|
|
assert.equal(game.richesePeekKnown, !denied);
|
|
const truth = stageTruth(game, fixture.card.id);
|
|
const wrong = TRUTH_CARD_NAMES.find(name => name !== fixture.card.name); assert.ok(wrong);
|
|
verify(game, truth, fixture.card.name, 'yes', 'blackMarket');
|
|
verify(game, truth, wrong, 'no', 'blackMarket');
|
|
});
|
|
}
|
|
|
|
void test('Caladan occupier answers after the original ordinary lot inspection', () => {
|
|
const fixture = freshPrivateLotGame();
|
|
let game = passPrivateLotWindow(fixture.game, 'richeseBlackMarket');
|
|
game = passPrivateLotWindow(game, 'atreidesAuction');
|
|
game = sellPrivateBlackMarketLot(game);
|
|
game = recyclePrivatePurchaseIntoOriginalNormalLot(game, fixture.card.id);
|
|
game = passPrivateLotWindow(game, 'atreidesAuction');
|
|
const truth = stageTruth(game, fixture.card.id);
|
|
const wrong = TRUTH_CARD_NAMES.find(name => name !== fixture.card.name); assert.ok(wrong);
|
|
verify(game, truth, fixture.card.name, 'yes', 'ordinary');
|
|
verify(game, truth, wrong, 'no', 'ordinary');
|
|
});
|
|
|
|
void test('absent or stale occupied receipt does not expose an auction observation', () => {
|
|
const fixture = freshPrivateLotGame();
|
|
let game = passPrivateLotWindow(fixture.game, 'richeseBlackMarket');
|
|
game = passPrivateLotWindow(game, 'atreidesAuction');
|
|
const receipt = structuredClone(game.homeworldAuctionInspection!);
|
|
game = sellPrivateBlackMarketLot(game);
|
|
game = recyclePrivatePurchaseIntoOriginalNormalLot(game, fixture.card.id);
|
|
const fact = {kind:'auctionPeek',name:fixture.card.name} as const;
|
|
const stale = structuredClone(game); stale.homeworldAuctionInspection = receipt;
|
|
assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf(stale, privateLotPlayer(stale, 'occupier'))), 'unknown');
|
|
assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf(game, privateLotPlayer(game, 'occupier'))), 'unknown');
|
|
});
|