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
@@ -89,7 +89,7 @@ export function TruthFactFields({ value: fact, game, target, onChange }: {
|
||||
<option value="stormDial">Known storm dial</option>
|
||||
<option value="stormForecast">Known storm forecast</option>
|
||||
<option value="spicePeek">Known next Spice Blow card</option>
|
||||
<option value="auctionPeek">Inspected ordinary auction card</option>
|
||||
<option value="auctionPeek">Inspected auction card</option>
|
||||
<option value="cacheLot">Public Richese cache auction card</option>
|
||||
<option value="ixPoolPeek">Recorded Ixian inspected auction pool includes a card</option>
|
||||
<option value="presciencePeek">Inspected battle plan field</option>
|
||||
|
||||
@@ -90,7 +90,7 @@ export function KnowledgeFactFields({
|
||||
{TRUTH_CARD_NAMES.map((name) => <option key={name} value={name}>{name}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<p className="fine">This asks about the current ordinary auction card the target has privately inspected. If no current card is known, the answer is I don’t know. Only the answer is published.</p>
|
||||
<p className="fine">This asks about the current ordinary auction or Advanced Black Market card the target privately inspected, including a supported Caladan occupation. If no current card is known, the answer is I don’t know. Only the answer is published.</p>
|
||||
</>
|
||||
) : value.kind === 'spicePeek' ? (
|
||||
<>
|
||||
|
||||
@@ -17,6 +17,16 @@ open mode gates.
|
||||
|
||||
## Current checkpoint and work
|
||||
|
||||
**Caladan occupier's auction knowledge — 29 October. Prototyped / Partial.**
|
||||
An eligible occupier can answer the existing Truthtrance auction-inspection
|
||||
fact using its actual current ordinary or Advanced Black Market inspection,
|
||||
including when native Atreides' inspection is canceled. The fact uses the same
|
||||
original-lot identity and audience as the private auction view. Eight related
|
||||
files pass 36 cases; the runtime fixture stages a conserved Truthtrance card
|
||||
and a controlled occupation. Wider Homeworld interactions and deployed
|
||||
verification remain open.
|
||||
[Scope](TRUTHTRANCE_KNOWLEDGE.md#caladan-occupiers-auction-knowledge--29-october-2026).
|
||||
|
||||
**Freeform Traitor and Face Dancer facts — 29 October. Prototyped / Partial.**
|
||||
New current-fact questions recognize unique public leader names for held
|
||||
Traitors and unrevealed Face Dancers, including supported compound facts.
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Rule decision index
|
||||
|
||||
## Caladan occupier's auction knowledge — 29 October 2026
|
||||
|
||||
The existing Truthtrance auction fact recognizes a successfully inspected
|
||||
current lot for its entitled Caladan occupier, using the same lot and audience
|
||||
as the private auction view. Native Atreides cancellation remains separate.
|
||||
[Scope](TRUTHTRANCE_KNOWLEDGE.md#caladan-occupiers-auction-knowledge--29-october-2026).
|
||||
|
||||
## Freeform Traitor and Face Dancer facts — 29 October 2026
|
||||
|
||||
Bounded current-fact wording resolves exact unique public leader names to the
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# Truthtrance: recorded predictions and storm knowledge
|
||||
|
||||
## Caladan occupier's auction knowledge — 29 October 2026
|
||||
|
||||
**Prototyped / Partial.** The existing `auctionPeek` fact also checks a
|
||||
Caladan occupier's successful private inspection of the original current
|
||||
ordinary or Advanced Black Market lot. It uses the same audience and lot
|
||||
identity as the auction view. The occupied inspection remains separately
|
||||
available when the native Atreides inspection was canceled; a different
|
||||
viewer, unavailable inspection or stale lot yields I don't know. The target
|
||||
alone sees the checked answer before replying, and bots use that same answer.
|
||||
|
||||
The runtime fixture uses a genuine auction and controlled Caladan occupation;
|
||||
it stages a conserved Truthtrance card. Eight related files pass 36 cases,
|
||||
including native cancellation and a recycled physical card in a later lot.
|
||||
This does not certify wider Homeworld interactions or deployed play.
|
||||
|
||||
## Freeform Traitor and Face Dancer facts — 29 October 2026
|
||||
|
||||
**Prototyped / Partial.** New current-fact questions recognize an exact
|
||||
@@ -271,8 +286,9 @@ in older frames. Missing, canceled, expired, completed or other-faction
|
||||
observations return unknown. This does not disclose the card to
|
||||
the questioner. A real Advanced setup exercises the offer, inspection, private
|
||||
Yes/No question and four bot responses for Silent and Once Around; a conserved
|
||||
Truthtrance is transferred between hands by the fixture. Semuta, occupied Homeworld inspection and broader
|
||||
auction interactions remain open.
|
||||
Truthtrance is transferred between hands by the fixture. Semuta and broader
|
||||
auction interactions remain open. The later Caladan section above adds its
|
||||
occupied inspection.
|
||||
|
||||
## Atreides ordinary auction knowledge — 27 October 2026
|
||||
|
||||
@@ -281,8 +297,8 @@ whether Atreides' currently inspected ordinary auction card has the named
|
||||
printed face. The answer uses only the current lot's successful native
|
||||
inspection. Other factions, canceled or expired inspections and missing lots
|
||||
return unknown. The Black Market extension is described above, while public
|
||||
cache lots use the separate `cacheLot` fact. Non-Atreides occupied Homeworld
|
||||
inspection remains unfinished. The card identity is
|
||||
cache lots use the separate `cacheLot` fact. The later Caladan section above
|
||||
adds the occupied inspection. The card identity is
|
||||
not added to another seat's view; the selected answer is public through the
|
||||
normal Truthtrance flow. Single and compound selectors use the public printed
|
||||
name registry, and existing bots answer from their own projected scalar.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Game } from './engine';
|
||||
|
||||
/** Physical card identity is not a lot identity: the same card can later
|
||||
* re-enter an ordinary auction. Return null when no current lot exists. */
|
||||
export function originalAuctionInspectionLot(
|
||||
game: Pick<Game, 'richeseAuction' | 'auction' | 'turn'>,
|
||||
): string | null {
|
||||
if (game.richeseAuction) return typeof game.richeseAuction.event === 'string'
|
||||
? game.richeseAuction.event : null;
|
||||
const auction = game.auction;
|
||||
if (!auction || !Number.isSafeInteger(auction.index) || auction.index < 0 ||
|
||||
!Array.isArray(auction.cards) || !auction.cards[auction.index] ||
|
||||
typeof auction.cards[auction.index].id !== 'string') return null;
|
||||
return JSON.stringify([auction.occupiedEvent ?? game.turn, auction.index, auction.cards[auction.index].id]);
|
||||
}
|
||||
+2
-5
@@ -145,6 +145,7 @@ import { diplomatRetreatChoices, type DiplomatRetreatSelection, type DiplomatRet
|
||||
import { ECAZ_START_FORCES, quoteEcazStartingForces } from './ecaz-setup';
|
||||
import { battleCardSlotEligible, battleCategoryInspectionValue, fixedBattleInspectionMatches, validBattleSlotPair, usesNativeBattleSpecial } from './battle-card-slots';
|
||||
import { HARASS_WITHDRAW_CARD, HarassWithdrawError, isHarassWithdraw, quoteHarassWithdraw, defaultHarassWithdrawAllocation, harassWithdrawNeedsAllocation, type HarassWithdrawSelection, type HarassWithdrawContext, type HarassWithdrawPreview } from './harass-withdraw';
|
||||
import { originalAuctionInspectionLot } from './auction-inspection-source';
|
||||
import { discoveryChoices, discoveryStashSignature, type DiscoveryStash } from './discovery-actions';
|
||||
import { createStormSource, validateStormSource, discoveryStormOffer, createDiscoveryStorm, chooseDiscoveryStorm, validateDiscoveryStorm, finishDiscoveryStorm, discoveryStormDistance, discoveryStormSaphoModes, reorderDiscoveryStorm, type StormMovementSource, type DiscoveryStorm } from './discovery-storm';
|
||||
import { completeDiscoveryFlight, discoveryFlightOffer, quoteDiscoveryFlight, validateDiscoveryFlight, type DiscoveryFlightReceipt } from './discovery-flight';
|
||||
@@ -16146,15 +16147,11 @@ function auctionTechnologyQuote<T>(run: () => T): T {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/** Card identity alone is not a lot: Technology can return a previously sold card. */
|
||||
function originalAuctionInspectionLot(g: Game) {
|
||||
return g.richeseAuction?.event ?? JSON.stringify([g.auction!.occupiedEvent ?? g.turn,
|
||||
g.auction!.index, g.auction!.cards[g.auction!.index].id]);
|
||||
}
|
||||
function openOccupiedAuctionInspection(g: Game) {
|
||||
if (!g.homeworldOccupationPreview) return;
|
||||
const authority = quoteOccupiedBiddingAuthority(g, 'atreidesInspection');
|
||||
const lot = originalAuctionInspectionLot(g);
|
||||
requireRule(lot !== null, 'The original Caladan inspection needs an active auction lot.');
|
||||
g.homeworldAuctionInspection = { lot, audience: authority.blocked ? [] : [...authority.inspectionAudience],
|
||||
native: authority.provider, occupation: JSON.stringify(quoteStableHomeworldOccupation(g, 'caladan')), occupiedKnown: false };
|
||||
}
|
||||
|
||||
+2
-2
@@ -3314,7 +3314,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
steps: [
|
||||
'Choose a recorded prediction faction, compare its turn, or compare the target’s current submitted storm dial or known Fremen forecast. Exactly, at least and at most are available for numeric facts.',
|
||||
'Ask whether Atreides’ currently known next Spice Card is a sandworm, Sandtrout, or a blow at a named printed territory. If the peek was canceled or is no longer available, the answer is I don’t know.',
|
||||
'Ask whether the current ordinary auction card or Advanced Richese Black Market card inspected by Atreides has a named Treachery Card face. The Black Market question supports normal, Once Around and Silent bidding. If the inspection was canceled or the lot is no longer available, the answer is I don’t know. Occupied Homeworld inspections are not yet included.',
|
||||
'Ask whether the current ordinary auction card or Advanced Richese Black Market card inspected by Atreides or an entitled Caladan occupier has a named Treachery Card face. The Black Market question supports normal, Once Around and Silent bidding. If the inspection was canceled or the lot is no longer available, the answer is I don’t know; an occupied inspection remains separate from cancellation of the native Atreides inspection.',
|
||||
'In a Basic or Advanced Richese cache auction, ask any participant whether the currently offered public cache card has a named face. If no supported cache lot is active, the answer is I don’t know. This is separate from the Atreides inspection question.',
|
||||
'In Basic or Advanced Bidding, ask Ixians whether their recorded inspected auction group contains a named card. This refers to the recorded group, including cards later sold, rather than what remains for sale. If the inspection is unavailable or belongs to another player, the answer is I don’t know.',
|
||||
'Ask an entitled participant whether an already answered Prescience reveals a named leader, weapon or defense, no element, or an exact force dial. A pending, unavailable or differently inspected element yields I don’t know.',
|
||||
@@ -3326,7 +3326,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
],
|
||||
related: ['card-truthtrance', 'truthtrance-spice', 'privacy'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Stored prediction, entitled storm knowledge, native Atreides peeks including all Advanced Black Market methods, revealed Prescience, the Ixian recorded auction group and the current public Richese cache face use shared three-valued fact evaluation; occupied inspection remains separate.', evidence: ['game/truthtrance-knowledge.ts', 'game/truthtrance.ts', 'game/engine.ts'] },
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Stored prediction, entitled storm knowledge, native Atreides and eligible Caladan-occupier peeks including Advanced Black Market methods, revealed Prescience, the Ixian recorded auction group and the current public Richese cache face use shared three-valued fact evaluation.', evidence: ['game/truthtrance-knowledge.ts', 'game/truthtrance.ts', 'game/engine.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Fact choices, numeric comparisons, public faction and card names, bounded inputs and compound question previews.', evidence: ['components/truthtrance-knowledge.tsx', 'components/mixed-shipment-fields.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'All four profiles answer the owner-only verified scalar through the existing Truthtrance flow. Strategic question selection and freeform understanding remain incomplete.', evidence: ['game/bots.ts', 'tests/truthtrance-knowledge.test.ts'] },
|
||||
{ area: 'Documentation', status: 'Partial', detail: 'Current-knowledge semantics, unknown cases and original FAQ authority are explicit.', evidence: ['docs/TRUTHTRANCE_KNOWLEDGE.md'] },
|
||||
|
||||
@@ -3,6 +3,7 @@ import { leaders, SPICE_CARDS } from './cards';
|
||||
import { gameTerritories, territory } from './board';
|
||||
import { TRUTH_CARD_NAMES } from './truthtrance-card-names';
|
||||
import { battleCategoryInspectionValue } from './battle-card-slots';
|
||||
import { originalAuctionInspectionLot } from './auction-inspection-source';
|
||||
import type { Game, Player } from './engine';
|
||||
|
||||
type Comparison = 'eq' | 'gte' | 'lte';
|
||||
@@ -98,17 +99,24 @@ export function parseKnowledgeFact(value: unknown): KnowledgeFact {
|
||||
}
|
||||
|
||||
export function truthKnowledgeOf(
|
||||
g: Pick<Game, 'phase' | 'turn' | 'advanced' | 'stormDials' | 'stormDialers' | 'stormCard' | 'stormCardKnown' | 'spicePeekKnown' | 'spiceDeck' | 'auction' | 'richeseAuction' | 'richeseCache' | 'richesePeekKnown' | 'richeseOfferedCard' | 'ixAuction' | 'ixAuctionKnown' | 'homeworldOccupationPreview' | 'battle' | 'players' | 'dukeVidal' | 'mobileStronghold' | 'discoveries'>,
|
||||
g: Pick<Game, 'phase' | 'turn' | 'advanced' | 'stormDials' | 'stormDialers' | 'stormCard' | 'stormCardKnown' | 'spicePeekKnown' | 'spiceDeck' | 'auction' | 'richeseAuction' | 'richeseCache' | 'richesePeekKnown' | 'richeseOfferedCard' | 'ixAuction' | 'ixAuctionKnown' | 'homeworldOccupationPreview' | 'homeworldAuctionInspection' | 'battle' | 'players' | 'dukeVidal' | 'mobileStronghold' | 'discoveries'>,
|
||||
p: Pick<Player, 'id' | 'faction' | 'prediction'>,
|
||||
): TruthKnowledge {
|
||||
const dial = g.stormDials[p.id];
|
||||
const peek = g.phase === 5 && p.faction === 'atreides' && g.spicePeekKnown
|
||||
? g.spiceDeck[0] : undefined;
|
||||
const lot = g.phase === 3 && p.faction === 'atreides' && !g.richeseAuction && !g.ixAuction ? g.auction : null;
|
||||
// The private lot is read only after the same inspection entitlement used by
|
||||
// the ordinary auction view has resolved successfully for this faction.
|
||||
const richeseLot = g.phase === 3 && g.advanced && p.faction === 'atreides' &&
|
||||
g.richeseAuction?.source === 'blackMarket' && !g.richeseAuction.outcome && g.richesePeekKnown
|
||||
// Match the private auction projection's original-lot and audience checks.
|
||||
// The occupied share may succeed even if native Atreides was canceled.
|
||||
const source = g.phase === 3 && g.homeworldOccupationPreview ? originalAuctionInspectionLot(g) : null;
|
||||
const receipt = g.homeworldAuctionInspection;
|
||||
const occupied = !!source && !!receipt && receipt.lot === source && receipt.occupiedKnown === true &&
|
||||
p.id !== receipt.native && Array.isArray(receipt.audience) && receipt.audience.includes(p.id) &&
|
||||
g.players.some(seat => seat.id === p.id);
|
||||
const lot = g.phase === 3 && !g.richeseAuction && !g.ixAuction &&
|
||||
((p.faction === 'atreides' && g.auction?.peekKnown) || occupied) ? g.auction : null;
|
||||
const richeseLot = g.phase === 3 && g.advanced &&
|
||||
g.richeseAuction?.source === 'blackMarket' && !g.richeseAuction.outcome &&
|
||||
((p.faction === 'atreides' && g.richesePeekKnown) || occupied)
|
||||
? g.richeseAuction : null;
|
||||
// The offered copy is only valid for this actual lot. Older frames can
|
||||
// recover the still-reserved physical card from the seller's hand.
|
||||
@@ -116,7 +124,7 @@ export function truthKnowledgeOf(
|
||||
? g.richeseOfferedCard : richeseLot
|
||||
? g.players.find(seat => seat.id === richeseLot.owner)?.hand.find(card => card.id === richeseLot.cardId)
|
||||
: undefined;
|
||||
const lotName = lot?.peekKnown ? lot.cards[lot.index]?.name : offered?.name;
|
||||
const lotName = lot ? lot.cards[lot.index]?.name : offered?.name;
|
||||
// A Richese cache lot is public in Basic and Advanced; it is not the
|
||||
// owner's private cache inventory or an Advanced Black Market card.
|
||||
const cacheLot = g.phase === 3 && g.players.some(seat => seat.id === p.id) &&
|
||||
@@ -216,7 +224,7 @@ export function knowledgeFactText(fact: KnowledgeFact): string {
|
||||
if (fact.kind === 'presciencePeek')
|
||||
return `the ${fact.field} you observed through battle Prescience is ${fact.field === 'dial' ? fact.value : fact.name}`;
|
||||
if (fact.kind === 'auctionPeek')
|
||||
return `the current auction card you inspected as Atreides is ${fact.name}`;
|
||||
return `the current auction card you inspected is ${fact.name}`;
|
||||
if (fact.kind === 'cacheLot')
|
||||
return `the current public Richese cache auction card is ${fact.name}`;
|
||||
if (fact.kind === 'ixPoolPeek')
|
||||
|
||||
@@ -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