Honor Caladan auction knowledge in Truthtrance
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
34f533ae8d
commit
4ec4f843a7
13 files changed
+241
-25
No files matched your search
@@ -17,7 +17,7 @@ void test('the editor previews No-Field and auction knowledge without revealing
|
||||
assert.match(noField, /you currently have a concealed No-Field with printed value 3/);
|
||||
const auction = preview('Is the current auction card you inspected a Shield?');
|
||||
assert.match(auction, /Recognized freeform question/);
|
||||
assert.match(auction, /the current auction card you inspected as Atreides is Shield/);
|
||||
assert.match(auction, /the current auction card you inspected is Shield/);
|
||||
assert.doesNotMatch(auction, /Your current game information establishes/);
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ void test('the editor previews a mixed auction and force aggregate', () => {
|
||||
const text = 'Is the current auction card you inspected a Shield and do you have at least 3 forces in your reserves?';
|
||||
const html = preview(text);
|
||||
assert.match(html, /Recognized freeform question/);
|
||||
assert.match(html, /the current auction card you inspected as Atreides is Shield/);
|
||||
assert.match(html, /the current auction card you inspected is Shield/);
|
||||
assert.match(html, /AND/);
|
||||
assert.match(html, /currently have at least 3 physical forces in your reserves/);
|
||||
assert.doesNotMatch(html, /Your current game information establishes/);
|
||||
|
||||
@@ -62,11 +62,11 @@ void test('auction peek selector is available in single and compound current-fac
|
||||
const single = renderToStaticMarkup(createElement(TruthFactFields, { value: fact, game: view, target: 'b', onChange() {} }));
|
||||
assert.match(single, /value="auctionPeek" selected/);
|
||||
assert.match(single, /value="Shield" selected/);
|
||||
assert.match(single, /current ordinary auction card/);
|
||||
assert.match(single, /current ordinary auction or Advanced Black Market card/);
|
||||
assert.match(single, /If no current card is known/);
|
||||
const compound: TruthFact = { kind: 'or', terms: [fact, { kind: 'spice', compare: 'gte', value: 3 }] };
|
||||
const grouped = renderToStaticMarkup(createElement(TruthFactExpressionFields, { value: compound, game: view, target: 'b', disabled: false, onChange() {} }));
|
||||
assert.match(grouped, /Inspected ordinary auction card/);
|
||||
assert.match(grouped, /Inspected auction card/);
|
||||
assert.match(grouped, /value="Shield" selected/);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
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');
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createGame, joinGame, newPlayer, type Game } from '../game/engine';
|
||||
import { knowledgeFactAnswer, truthKnowledgeOf } from '../game/truthtrance-knowledge';
|
||||
import { originalAuctionInspectionLot } from '../game/auction-inspection-source';
|
||||
|
||||
function fixture(): Game {
|
||||
const game = createGame('SHAREDPEEK', newPlayer('at', 'Atreides', 'atreides'), true, ['choam']);
|
||||
joinGame(game, newPlayer('occ', 'Occupier', 'guild'));
|
||||
joinGame(game, newPlayer('other', 'Other', 'emperor'));
|
||||
joinGame(game, newPlayer('seller', 'Richese', 'richese'));
|
||||
game.status = 'playing'; game.turn = 2; game.phase = 3; game.homeworldOccupationPreview = true;
|
||||
game.auction = { cards:[{id:'lot-card', name:'Shield', kind:'shield'}], index:0,
|
||||
occupiedEvent:'normal-event', bid:0, bidder:null, active:'occ', passed:[], opener:0, peekKnown:false };
|
||||
game.homeworldAuctionInspection = {lot:originalAuctionInspectionLot(game)!, audience:['at','occ'],
|
||||
native:'at', occupation:'public-qualification', occupiedKnown:true};
|
||||
return game;
|
||||
}
|
||||
function answer(game: Game, actor: string) {
|
||||
const seat = game.players.find(player => player.id === actor); assert.ok(seat);
|
||||
return knowledgeFactAnswer({kind:'auctionPeek', name:'Shield'}, truthKnowledgeOf(game, seat));
|
||||
}
|
||||
|
||||
void test('Caladan share binds the current ordinary lot and is independent of native denial', () => {
|
||||
const game = fixture();
|
||||
assert.equal(answer(game,'occ'),'yes');
|
||||
assert.equal(answer(game,'at'),'unknown');
|
||||
assert.equal(answer(game,'other'),'unknown');
|
||||
game.auction!.peekKnown = true;
|
||||
assert.equal(answer(game,'at'),'yes');
|
||||
game.homeworldAuctionInspection!.occupiedKnown = false;
|
||||
assert.equal(answer(game,'occ'),'unknown');
|
||||
assert.equal(answer(game,'at'),'yes');
|
||||
});
|
||||
|
||||
void test('stale, foreign and expired ordinary lot receipts never grant knowledge', () => {
|
||||
const game = fixture();
|
||||
game.homeworldAuctionInspection!.audience = ['at','other'];
|
||||
assert.equal(answer(game,'occ'),'unknown');
|
||||
game.homeworldAuctionInspection!.audience = ['at','occ'];
|
||||
game.auction!.occupiedEvent = 'new-event';
|
||||
assert.equal(answer(game,'occ'),'unknown');
|
||||
game.auction!.occupiedEvent = 'normal-event'; game.phase = 4;
|
||||
assert.equal(answer(game,'occ'),'unknown');
|
||||
game.phase = 3; delete game.homeworldOccupationPreview;
|
||||
assert.equal(answer(game,'occ'),'unknown');
|
||||
});
|
||||
|
||||
void test('Black Market shared inspection binds its original event and current physical face', () => {
|
||||
const game = fixture(), card = game.auction!.cards[0];
|
||||
game.auction = null;
|
||||
game.players.find(player => player.id === 'seller')!.hand.push(card);
|
||||
game.richeseAuction = {event:'black-one', cardId:card.id, source:'blackMarket', owner:'seller',
|
||||
method:'silent', eligible:['occ'], order:['occ'], tieOrder:['occ'], active:'occ', bid:0,
|
||||
bidder:null, passed:[], acted:[], sealed:{}, outcome:null};
|
||||
game.richesePeekKnown = false;
|
||||
game.homeworldAuctionInspection!.lot = 'black-one';
|
||||
assert.equal(answer(game,'occ'),'yes');
|
||||
assert.equal(answer(game,'at'),'unknown');
|
||||
assert.equal(answer(game,'other'),'unknown');
|
||||
game.richeseAuction.event = 'black-two';
|
||||
assert.equal(answer(game,'occ'),'unknown', 'the same card in a new lot does not reuse a receipt');
|
||||
game.richeseAuction.event = 'black-one'; game.richeseAuction.outcome = {kind:'unbid'};
|
||||
assert.equal(answer(game,'occ'),'unknown');
|
||||
});
|
||||
Reference in new issue
Block a user