Allow Richese reveal during Emperor revival
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
cfccfe5acb
commit
a10e019b01
9 files changed
+260
-16
No files matched your search
@@ -17,6 +17,16 @@ open mode gates.
|
||||
|
||||
## Current checkpoint and work
|
||||
|
||||
**Basic No-Field revelation during Emperor extra revival — 29 October.
|
||||
Prototyped / Partial.** Richese may reveal during a clean pending Emperor
|
||||
allied-revival response, including when Richese is the recipient. Revelation
|
||||
uses the reserves available at that moment and preserves the original payment,
|
||||
return and Karama cancellation. Eight related files pass 39 cases, including
|
||||
both recipients and cancellation; nested interactions, commitments, optional
|
||||
modules and deployed verification remain open. The same batch confirms native
|
||||
Atreides inspection knowledge in Advanced Once Around Black Market bidding.
|
||||
[Scope](RICHESE_NO_FIELD_RULES.md#basic-additional-response-windows--29-october-2026).
|
||||
|
||||
**Tleilaxu special Karama Truthtrance — 29 October. Prototyped / Partial.**
|
||||
In clean Advanced Revival, a whole-turn question can bind the actual physical
|
||||
Tleilaxu Karama use and its native target-specific prevention. The guide uses
|
||||
|
||||
@@ -25,6 +25,13 @@ This interrupt order is a provisional online interpretation.
|
||||
|
||||
## Basic additional response windows — 29 October 2026
|
||||
|
||||
**Prototyped / Partial.** A clean Emperor extra-revival response also admits
|
||||
the Richese owner's voluntary reveal, whether the Emperor's ally is Richese or
|
||||
another faction. The original response, amount, recipient and Karama priority
|
||||
remain in place. The reveal uses current reserves before the paid return; it
|
||||
does not borrow forces from that later return. This interruption order is a
|
||||
provisional online interpretation of the printed before-Battle timing.
|
||||
|
||||
The voluntary before-Battle reveal also works during clean ordinary Emperor
|
||||
allied spice gifts, Tleilaxu revival-income responses, and Atreides' Movement
|
||||
phase Spice forecast when Bene Gesserit is absent. It can additionally occur
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Rule decision index
|
||||
|
||||
## Basic No-Field reveal during Emperor extra revival — 29 October 2026
|
||||
|
||||
Richese may reveal during a clean pending Emperor extra-revival response,
|
||||
using reserves available before the return while retaining the response and
|
||||
its original Karama priority. This online ordering is provisional.
|
||||
[Scope](RICHESE_NO_FIELD_RULES.md#basic-additional-response-windows--29-october-2026).
|
||||
|
||||
## Basic bribes during movement responses — 29 October 2026
|
||||
|
||||
Ordinary nonallied bribes can interrupt supported clean Fremen and Ixian
|
||||
|
||||
@@ -191,12 +191,13 @@ explicitly staged; broader combinations and deployed acceptance remain open.
|
||||
|
||||
**Prototyped / Partial.** The existing `auctionPeek` fact now also reports the
|
||||
native Atreides seat's successful inspection of an active Advanced Richese
|
||||
Black Market lot. It uses the offered card bound to that lot, or the reserved
|
||||
physical seller card in older frames. Missing, canceled, expired, completed or
|
||||
other-faction observations return unknown. This does not disclose the card to
|
||||
Black Market lot, regardless of its normal, Once Around or Silent method. It
|
||||
uses the offered card bound to that lot, or the reserved physical seller card
|
||||
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; a conserved Truthtrance is transferred
|
||||
between hands by the fixture. Semuta, occupied Homeworld inspection and broader
|
||||
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.
|
||||
|
||||
## Atreides ordinary auction knowledge — 27 October 2026
|
||||
@@ -205,8 +206,9 @@ auction interactions remain open.
|
||||
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. Apart from the Black Market extension above, active special auctions and non-Atreides
|
||||
occupied Homeworld inspection are separate and remain unfinished. The card identity is
|
||||
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
|
||||
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.
|
||||
|
||||
+3
-3
@@ -1131,7 +1131,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'In Basic play, your own single concealed No-Field may provisionally ship into an ally’s territory if it can legally enter; move it and any paid accompanying forces out before your turn ends. Paid companions require the High Richese Homeworld power. If the marker remains, reveal it and send its newly materialized forces to Tanks under the provisional no-departure policy. A zero marker reveals but contributes no casualties. Existing physical forces and the ally are not part of that new-visitor loss. Paired markers keep their separate guards.',
|
||||
'A normal No-Field shipment pays the one-force price. The concealed token moves like a force. Before Battle, Richese may reveal it voluntarily; a token involved in battle is revealed with the Battle Plan. Revelation brings the indicated forces from reserves, limited by what remains there.',
|
||||
'In supported Basic native games, voluntary revelation is also available during an Ix phase opening or selected auction and market decisions without replacing that decision. Other interruption windows remain unfinished.',
|
||||
'In supported Basic games, your voluntary reveal can also interrupt a clean Emperor allied spice gift, Tleilaxu revival-income response, Atreides Movement-phase Spice forecast without Bene Gesserit, or Guild’s own ordinary reserve-shipment rate response when the shipment enters a different territory from the marker. The original response and payment continue once; other interruptions remain guarded.',
|
||||
'In supported Basic games, your voluntary reveal can also interrupt a clean Emperor allied spice gift or extra-revival response, Tleilaxu revival-income response, Atreides Movement-phase Spice forecast without Bene Gesserit, or Guild’s own ordinary reserve-shipment rate response when the shipment enters a different territory from the marker. If Richese receives the extra revival, reveal from current reserves before that return. The original response and payment continue once; other interruptions remain guarded.',
|
||||
'During supported Basic games you can also reveal while another faction’s ordinary movement waits for a Fremen or Ixian response or a CHOAM decision. The original move and its response remain pending; a zero No-Field disappears without creating physical forces.',
|
||||
'These supported clean Basic interruptions also work with the Ecaz/Moritani deck and native factions. A token already present does not enter again when it is revealed; pending Ambassador and Terror reactions keep their own controls.',
|
||||
'Provisional Guild transport treats your owned concealed marker as one billable force alongside any same-origin physical counters. Native Guild allies may cross-ship it through the normal permission and rate responses. The actual absent-Guild Secret Ally may cross-ship or return it face down, spending its Nexus once and paying the Bank. Cross keeps concealment; return creates no denomination-sized reserve deposit and retains the same-token next-shipment ban. Ordinary Guild allies cannot return. Select the marker separately from physical counters; ordinary movement remains available.',
|
||||
@@ -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. If the inspection was canceled or the lot is no longer available, the answer is I don’t know. Other special-auction inspections and occupied Homeworld inspections are not yet included.',
|
||||
'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.',
|
||||
'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 Advanced Black Market, revealed Prescience, the Ixian recorded auction group and the current public Richese cache face use shared three-valued fact evaluation; other special auctions and occupied inspection remain separate.', evidence: ['game/truthtrance-knowledge.ts', 'game/truthtrance.ts', 'game/engine.ts'] },
|
||||
{ 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: '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'] },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Game } from './engine';
|
||||
import { validateFremenRevivalGrant } from './fremen-revival-grant';
|
||||
import { richeseRevealForeignMovementPending } from './richese-reveal-foreign-movement';
|
||||
import { eliteRevivalRemaining, paidForceRevivalCost, revivalPrevented } from './revival';
|
||||
|
||||
const factions = new Set([
|
||||
'atreides', 'beneGesserit', 'emperor', 'fremen', 'guild', 'harkonnen',
|
||||
@@ -78,8 +79,8 @@ export function richeseRevealInterruptSupported(g: Game): boolean {
|
||||
if (foreignMovement) return true;
|
||||
if (g.response) {
|
||||
// Only independent, un-nested native responses with a stable original
|
||||
// source are admitted. In particular, shipment and revival transactions
|
||||
// still need their own continuation proofs.
|
||||
// source are admitted. Other shipment and revival transactions still
|
||||
// need their own continuation proofs.
|
||||
const response = g.response;
|
||||
if (g.decision || g.phaseOpening || g.choamMarket ||
|
||||
!Array.isArray(response.passed) || new Set(response.passed).size !== response.passed.length ||
|
||||
@@ -100,6 +101,20 @@ export function richeseRevealInterruptSupported(g: Game): boolean {
|
||||
!!recipient && recipient.id !== owner.id && owner.ally === recipient.id && recipient.ally === owner.id &&
|
||||
Number.isSafeInteger(response.amount) && response.amount! > 0 && response.amount! <= owner.spice && response.elite === 0;
|
||||
}
|
||||
if (response.kind === 'emperorRevival') {
|
||||
const recipient = g.players.find(player => player.id === response.recipient);
|
||||
const amount = response.amount, elite = response.elite;
|
||||
return keys === 'amount,elite,kind,owner,passed,recipient' && g.phase === 4 && owner.faction === 'emperor' &&
|
||||
!!recipient && recipient.id !== owner.id &&
|
||||
owner.ally === recipient.id && recipient.ally === owner.id &&
|
||||
Number.isSafeInteger(amount) && amount! > 0 && amount! <= recipient.tanks &&
|
||||
Number.isSafeInteger(elite) && elite! >= 0 && elite! <= amount! &&
|
||||
elite! <= (recipient.elites?.tanks ?? 0) && amount! - elite! <= recipient.tanks - (recipient.elites?.tanks ?? 0) &&
|
||||
elite! <= eliteRevivalRemaining(recipient, g.advanced) &&
|
||||
Number.isSafeInteger(g.emperorExtra[recipient.id] ?? 0) && (g.emperorExtra[recipient.id] ?? 0) >= 0 &&
|
||||
(g.emperorExtra[recipient.id] ?? 0) + amount! <= 3 &&
|
||||
!revivalPrevented(g, recipient.id) && owner.spice >= paidForceRevivalCost(recipient, amount!, elite!);
|
||||
}
|
||||
if (response.kind === 'revivalIncome')
|
||||
return keys === 'amount,kind,owner,passed,recipient' && owner.faction === 'tleilaxu' &&
|
||||
g.players.some(player => player.id === response.recipient) &&
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { actTruthMovementNativeEarly as act, nextTruthMovementNativeEarlyStep } from './fixture-truth-movement-native-early';
|
||||
import { truthMovementInventory as inventory } from './fixture-truth-movement';
|
||||
|
||||
const seat = (game: Game, id: string) => {
|
||||
const player = game.players.find(candidate => candidate.id === id);
|
||||
assert.ok(player, `Missing ${id}`);
|
||||
return player;
|
||||
};
|
||||
const quiet = (game: Game) => !game.response && !game.decision && !game.phaseOpening &&
|
||||
!game.truthtrance && !game.pendingRevival && !game.pendingTreacheryDiscard;
|
||||
function next(game: Game) {
|
||||
if (game.phase === 5 && game.active === 'emperor' && quiet(game))
|
||||
return {actor:'emperor', action:{type:'endMovement'} satisfies Action};
|
||||
return nextTruthMovementNativeEarlyStep(game, {position:'first',method:'onceAround',direction:'clockwise'});
|
||||
}
|
||||
function reach(game: Game, predicate: (state: Game) => boolean): Game {
|
||||
for (let i = 0; i < 2000; i++) {
|
||||
if (predicate(game)) return game;
|
||||
assert.notEqual(game.status, 'finished');
|
||||
const step = next(game);
|
||||
assert.ok(step, `No native step at ${game.turn}/${game.phase}/${game.decision?.kind}`);
|
||||
game = act(game, step.actor, step.action);
|
||||
}
|
||||
throw Error('The native game did not reach Revival');
|
||||
}
|
||||
|
||||
/** Native Basic setup and first-turn shipment; stage reciprocal alliance and
|
||||
* conserved casualties at the following Revival boundary. A card swap supplies
|
||||
* a real Karama response without creating or destroying a physical card. */
|
||||
function source(value: 0 | 3, recipient: 'atreides' | 'richese') {
|
||||
let game = createGame(`REVEALREVIVAL${value}${recipient}`, newPlayer('richese','Richese','richese'), false, ['choam']);
|
||||
joinGame(game, newPlayer('atreides','Atreides','atreides'));
|
||||
joinGame(game, newPlayer('emperor','Emperor','emperor'));
|
||||
for (const player of game.players) game = act(game, player.id, {type:'ready'});
|
||||
game = act(game, game.host, {type:'start'});
|
||||
game = reach(game, state => state.status === 'playing');
|
||||
for (const position of [0,1]) {
|
||||
const from = game.spiceDeck.findIndex((card,index) => index >= position && 'territory' in card && !card.discovery);
|
||||
assert.ok(from >= position);
|
||||
[game.spiceDeck[position],game.spiceDeck[from]] = [game.spiceDeck[from],game.spiceDeck[position]];
|
||||
}
|
||||
game = reach(game, state => state.turn === 1 && state.phase === 5 && state.active === 'richese' && quiet(state));
|
||||
const owner = seat(game,'richese'), token = owner.noField!.tokens.find(item => item.value === value);
|
||||
assert.ok(token); assert.ok(owner.noFieldEvent);
|
||||
game = act(game,'richese',{type:'ship',noField:token.id,event:owner.noFieldEvent,territory:'polar_sink',sector:0,allyPayment:0});
|
||||
game = reach(game, quiet);
|
||||
game = act(game,'richese',{type:'endMovement'});
|
||||
game = reach(game, state => state.turn === 2 && state.phase === 4 && quiet(state));
|
||||
const allied = seat(game,recipient), emperor = seat(game,'emperor');
|
||||
allied.ally = 'emperor'; emperor.ally = recipient;
|
||||
assert.ok(allied.reserves >= 2); allied.reserves -= 2; allied.tanks += 2;
|
||||
if (recipient === 'richese') {
|
||||
// The future paid return must not increase the earlier reveal's pool.
|
||||
allied.tanks += allied.reserves - 1; allied.reserves = 1;
|
||||
}
|
||||
const responder = seat(game,recipient === 'richese' ? 'atreides' : 'richese');
|
||||
if (!responder.hand.some(card => card.effect === 'karama')) {
|
||||
const zones = [game.deck, game.discard, ...game.players.filter(player => player.id !== responder.id).map(player => player.hand)];
|
||||
const zone = zones.find(cards => cards.some(card => card.effect === 'karama')); assert.ok(zone);
|
||||
assert.ok(responder.hand.length);
|
||||
const index = zone.findIndex(card => card.effect === 'karama');
|
||||
[responder.hand[0],zone[index]] = [zone[index],responder.hand[0]];
|
||||
}
|
||||
const event = seat(game,'richese').noFieldEvent; assert.ok(event);
|
||||
game = act(game,'emperor',{type:'emperorRevival',amount:2});
|
||||
assert.equal(game.response?.kind,'emperorRevival');
|
||||
return {game, token:token.id, event, recipient};
|
||||
}
|
||||
|
||||
function passResponse(game: Game): Game {
|
||||
for (let i = 0; game.response?.kind === 'emperorRevival' && i < 8; i++) {
|
||||
const player = game.players.find(candidate => !game.response!.passed.includes(candidate.id)); assert.ok(player);
|
||||
game = act(game,player.id,{type:'passResponse'});
|
||||
}
|
||||
assert.notEqual(game.response?.kind,'emperorRevival');
|
||||
return game;
|
||||
}
|
||||
|
||||
for (const value of [0,3] as const) {
|
||||
void test(`Richese ${value} reveal preserves an Emperor extra-revival response and its one-time payment`, () => {
|
||||
const {game,token,event,recipient} = source(value,'atreides');
|
||||
const response = structuredClone(game.response), cards = inventory(game);
|
||||
const reserves = seat(game,'richese').reserves, forces = seat(game,'richese').forces['polar_sink:0'] ?? 0;
|
||||
const allyReserves = seat(game,recipient).reserves, allyTanks = seat(game,recipient).tanks;
|
||||
const emperorSpice = seat(game,'emperor').spice;
|
||||
assert.equal(viewGame(game,'richese').richeseNoField?.canReveal,true);
|
||||
let after = act(game,'richese',{type:'revealNoField',token,event});
|
||||
assert.deepEqual(after.response,response);
|
||||
assert.equal(seat(after,'richese').forces['polar_sink:0'] ?? 0,forces + value);
|
||||
assert.equal(seat(after,'richese').reserves,reserves - value);
|
||||
assert.deepEqual(inventory(after),cards);
|
||||
after = passResponse(after);
|
||||
assert.equal(seat(after,'emperor').spice,emperorSpice - 4);
|
||||
assert.equal(seat(after,recipient).reserves,allyReserves + 2);
|
||||
assert.equal(seat(after,recipient).tanks,allyTanks - 2);
|
||||
assert.equal(after.emperorExtra[recipient],2);
|
||||
assert.equal(seat(after,'richese').forces['polar_sink:0'] ?? 0,forces + value);
|
||||
});
|
||||
}
|
||||
|
||||
void test('Emperor extra revival to Richese does not replenish the reserves used by the earlier reveal', () => {
|
||||
const {game,token,event} = source(3,'richese');
|
||||
const response = structuredClone(game.response), reserves = seat(game,'richese').reserves;
|
||||
const tanks = seat(game,'richese').tanks, emperorSpice = seat(game,'emperor').spice;
|
||||
let after = act(game,'richese',{type:'revealNoField',token,event});
|
||||
assert.deepEqual(after.response,response);
|
||||
assert.equal(reserves,1);
|
||||
assert.equal(seat(after,'richese').reserves,0);
|
||||
assert.equal(seat(after,'richese').tanks,tanks);
|
||||
after = passResponse(after);
|
||||
assert.equal(seat(after,'richese').forces['polar_sink:0'],1);
|
||||
assert.equal(seat(after,'richese').reserves,2);
|
||||
assert.equal(seat(after,'richese').tanks,tanks - 2);
|
||||
assert.equal(seat(after,'emperor').spice,emperorSpice - 4);
|
||||
});
|
||||
|
||||
void test('printed Karama cancels the Emperor revival after reveal without undoing it', () => {
|
||||
const {game,token,event,recipient} = source(3,'atreides');
|
||||
const karama = seat(game,'richese').hand.find(card => card.effect === 'karama'); assert.ok(karama);
|
||||
const emperorSpice = seat(game,'emperor').spice, reserves = seat(game,recipient).reserves,
|
||||
tanks = seat(game,recipient).tanks;
|
||||
let after = act(game,'richese',{type:'revealNoField',token,event});
|
||||
after = act(after,'richese',{type:'card',mode:'cancel',card:karama.id});
|
||||
if (after.pendingKull?.stage === 'offer')
|
||||
after = act(after,after.pendingKull.player,{type:'kullDecision',event:after.pendingKull.event,decline:true});
|
||||
while (after.pendingTreacheryDiscard?.reaction?.stage === 'offer') {
|
||||
const reaction = after.pendingTreacheryDiscard.reaction, batch = after.pendingTreacheryDiscard.batch.event;
|
||||
const responder = after.players.find(player => !reaction.passed.includes(player.id)); assert.ok(responder);
|
||||
after = act(after,responder.id,{type:'semutaPass',event:batch});
|
||||
}
|
||||
assert.notEqual(after.response?.kind,'emperorRevival');
|
||||
assert.equal(seat(after,'emperor').spice,emperorSpice);
|
||||
assert.equal(seat(after,recipient).reserves,reserves);
|
||||
assert.equal(seat(after,recipient).tanks,tanks);
|
||||
assert.equal(after.emperorExtra[recipient] ?? 0,0);
|
||||
assert.equal(seat(after,'richese').noField?.deployed,null);
|
||||
assert.equal(seat(after,'richese').forces['polar_sink:0'],3);
|
||||
assert.equal(after.discard.filter(card => card.id === karama.id).length,1);
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createGame, newPlayer, type Game } from '../game/engine';
|
||||
import { richeseRevealInterruptSupported as supported } from '../game/richese-reveal-interruption';
|
||||
|
||||
/** Quote-only staged response; native action and physical continuation are
|
||||
* exercised separately by the integration owner. */
|
||||
function source(target: 'richese' | 'atreides' = 'atreides'): Game {
|
||||
const game = createGame('REVEALEXTRAREVIVAL', newPlayer('richese', 'Richese', 'richese'), false, ['choam']);
|
||||
game.players.push(newPlayer('emperor', 'Emperor', 'emperor'), newPlayer('atreides', 'Atreides', 'atreides'));
|
||||
game.status = 'playing'; game.phase = 4; game.turn = 2;
|
||||
const emperor = game.players.find(player => player.id === 'emperor')!;
|
||||
const recipient = game.players.find(player => player.id === target)!;
|
||||
emperor.ally = target; recipient.ally = emperor.id; recipient.tanks = 2; emperor.spice = 10;
|
||||
game.response = { kind: 'emperorRevival', owner: emperor.id, recipient: target, amount: 2, elite: 0, passed: [] };
|
||||
return game;
|
||||
}
|
||||
|
||||
void test('a clean Emperor extra revival preserves the accepted response for either ally', () => {
|
||||
for (const recipient of ['richese', 'atreides'] as const) {
|
||||
const game = source(recipient), before = structuredClone(game);
|
||||
assert.equal(supported(game), true, recipient);
|
||||
assert.deepEqual(game, before);
|
||||
}
|
||||
});
|
||||
|
||||
void test('extra revival admission checks its original funding, physical forces and mutual ally', () => {
|
||||
for (const mutate of [
|
||||
(g: Game) => { g.phase = 3; },
|
||||
(g: Game) => { g.response!.owner = 'atreides'; },
|
||||
(g: Game) => { g.response!.recipient = 'emperor'; },
|
||||
(g: Game) => { g.players.find(p => p.id === 'atreides')!.ally = null; },
|
||||
(g: Game) => { g.players.find(p => p.id === 'atreides')!.tanks = 1; },
|
||||
(g: Game) => { g.players.find(p => p.id === 'emperor')!.spice = 3; },
|
||||
(g: Game) => { g.response!.amount = 0; },
|
||||
(g: Game) => { g.response!.elite = 1; },
|
||||
(g: Game) => { g.emperorExtra.atreides = 2; },
|
||||
(g: Game) => { g.revivalPrevention = {player:'atreides',turn:g.turn}; },
|
||||
(g: Game) => { g.response!.passed = ['atreides','atreides']; },
|
||||
(g: Game) => { g.response!.intent = 'other'; },
|
||||
(g: Game) => { g.decision = {kind:'auctionPayment',player:'emperor'}; },
|
||||
(g: Game) => { g.pendingRevival = {} as NonNullable<Game['pendingRevival']>; },
|
||||
]) {
|
||||
const game = source(); mutate(game);
|
||||
const before = structuredClone(game);
|
||||
assert.equal(supported(game), false);
|
||||
assert.deepEqual(game, before);
|
||||
}
|
||||
});
|
||||
|
||||
void test('Ixian extra revivals retain the special-force price and typed tanks', () => {
|
||||
const game = source(); game.expansions.push('ix');
|
||||
const recipient = game.players.find(player => player.id === 'atreides')!;
|
||||
recipient.faction = 'ixians'; recipient.elites = {reserves:0,tanks:1,forces:{},revived:0};
|
||||
game.response!.elite = 1;
|
||||
const emperor = game.players.find(player => player.id === 'emperor')!;
|
||||
emperor.spice = 5; assert.equal(supported(game), true);
|
||||
emperor.spice = 4; assert.equal(supported(game), false);
|
||||
emperor.spice = 5; game.response!.elite = 2; assert.equal(supported(game), false);
|
||||
});
|
||||
@@ -15,7 +15,7 @@ const player = (g: Game, id: string) => {
|
||||
/** Genuine Advanced Richese/Atreides/Emperor setup, Black Market offer and
|
||||
* successful inspection. Only the existing Truthtrance is moved between hands
|
||||
* so a different faction can ask the native Atreides seat. */
|
||||
function inspectedLot() {
|
||||
function inspectedLot(method: 'silent' | 'onceAround' = 'silent') {
|
||||
const fixture = createTruthtranceRichesePromisesFixture({ advanced: true });
|
||||
let game = structuredClone(fixture.game);
|
||||
const atreides = player(game, 'atreides'), emperor = player(game, 'emperor');
|
||||
@@ -25,7 +25,7 @@ function inspectedLot() {
|
||||
game = advance(game, state => state.decision?.kind === 'richeseBlackMarket');
|
||||
const card = player(game, 'richese').hand.find(candidate => candidate.name === 'Shield'); assert.ok(card);
|
||||
game = act(game, 'richese', { type:'decision', event:game.richeseBidding!.event,
|
||||
card:card.id, method:'silent' } as Action);
|
||||
card:card.id, method, ...(method === 'onceAround' ? { direction:'clockwise' } : {}) } as Action);
|
||||
for (let count = 0; game.response && count < 20; count++) {
|
||||
const respondent = game.players.find(candidate => !game.response!.passed.includes(candidate.id));
|
||||
assert.ok(respondent); game = act(game, respondent.id, {type:'passResponse'});
|
||||
@@ -48,8 +48,9 @@ function ask(game: Game, truth: string, name: string): Game {
|
||||
fact:{kind:'auctionPeek',name}}});
|
||||
}
|
||||
|
||||
void test('native Advanced Black Market inspection gives Atreides a private Yes or No and all bots answer legally', () => {
|
||||
const {game, card, truth} = inspectedLot();
|
||||
for (const method of ['silent', 'onceAround'] as const) void test(`native Advanced ${method} Black Market inspection gives Atreides a private Yes or No and all bots answer legally`, () => {
|
||||
const {game, card, truth} = inspectedLot(method);
|
||||
assert.equal(game.richeseAuction?.method, method);
|
||||
for (const [name,answer] of [[card.name,'yes'],['Snooper','no']] as const) {
|
||||
const question = ask(structuredClone(game), truth.id, name);
|
||||
assert.equal(viewGame(question, 'atreides').truthAnswer, answer);
|
||||
|
||||
Reference in new issue
Block a user