Support Truthtrance No-Field shipment promises
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
9ab9b9a613
commit
7fccd62814
16 files changed
+658
-32
No files matched your search
@@ -0,0 +1,64 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
matchesShipment,
|
||||
parseShipmentClaim,
|
||||
parseShipmentExpression,
|
||||
shipmentClaimDestinations,
|
||||
shipmentClaimHasNoField,
|
||||
shipmentClaimText,
|
||||
ShipmentClaimError,
|
||||
} from '../game/shipment-promises';
|
||||
import { TERRITORIES } from '../game/board';
|
||||
import { shipmentExpressionInputError, shipmentExpressionInputSize } from '../game/shipment-expression-input';
|
||||
|
||||
void test('a No-Field shipment statement keeps identity and denomination private', () => {
|
||||
const wildcard = parseShipmentClaim({ claim: { kind: 'noField' } });
|
||||
assert.deepEqual(wildcard, { claim: { kind: 'noField' } });
|
||||
assert.equal(shipmentClaimHasNoField(wildcard), true);
|
||||
assert.deepEqual(shipmentClaimDestinations(wildcard), TERRITORIES.map(t => t.id));
|
||||
assert.equal(shipmentClaimText(wildcard), 'ship your No-Field from your reserves');
|
||||
assert.equal(matchesShipment(wildcard, null), false);
|
||||
assert.equal(matchesShipment(wildcard, { territory: 'carthag', sector: 11, amount: 1 }), false);
|
||||
assert.equal(matchesShipment(wildcard, { territory: 'carthag', sector: 11, amount: 0, noField: true }), true);
|
||||
assert.equal(shipmentExpressionInputError({ kind: 'noField' }), null);
|
||||
assert.deepEqual(shipmentExpressionInputSize({ kind: 'noField' }), { leaves: 1, depth: 0 });
|
||||
const target = parseShipmentClaim({ claim: { kind: 'noField', territory: 'carthag', sector: 11 } });
|
||||
assert.deepEqual(shipmentClaimDestinations(target), ['carthag']);
|
||||
assert.match(shipmentClaimText(target), /Carthag, sector 11/);
|
||||
assert.equal(matchesShipment(target, { territory: 'carthag', sector: 11, amount: 0, noField: true }), true);
|
||||
assert.equal(matchesShipment(target, { territory: 'carthag', sector: 10, amount: 0, noField: true }), false);
|
||||
assert.equal(matchesShipment(target, { territory: 'arrakeen', sector: 10, amount: 0, noField: true }), false);
|
||||
});
|
||||
|
||||
void test('No-Field and physical clauses constrain the same real shipment', () => {
|
||||
const claim = parseShipmentClaim({ claim: { op: 'and', terms: [
|
||||
{ kind: 'noField', territory: 'carthag' },
|
||||
{ territory: 'carthag', minimum: 2 },
|
||||
] } });
|
||||
assert.equal(matchesShipment(claim, { territory: 'carthag', sector: 11, amount: 2, noField: true }), true);
|
||||
assert.equal(matchesShipment(claim, { territory: 'carthag', sector: 11, amount: 1, noField: true }), false);
|
||||
assert.equal(matchesShipment(claim, { territory: 'carthag', sector: 11, amount: 2 }), false);
|
||||
assert.equal(matchesShipment(claim, null), false);
|
||||
const old = parseShipmentClaim({ territory: 'carthag', minimum: 1 });
|
||||
assert.equal(shipmentClaimHasNoField(old), false);
|
||||
assert.equal(shipmentClaimHasNoField(claim), true);
|
||||
assert.equal(matchesShipment(old, { territory: 'carthag', amount: 0, noField: true }), false);
|
||||
assert.equal(matchesShipment(old, { territory: 'carthag', amount: 1 }), true);
|
||||
const either = parseShipmentClaim({ claim: { op: 'or', terms: [
|
||||
{ kind: 'noField' }, { territory: 'arrakeen', minimum: 3 },
|
||||
] } });
|
||||
assert.equal(matchesShipment(either, { territory: 'carthag', amount: 0, noField: true }), true);
|
||||
assert.equal(matchesShipment(either, { territory: 'arrakeen', amount: 3 }), true);
|
||||
assert.equal(matchesShipment(either, null), false);
|
||||
});
|
||||
|
||||
void test('No-Field statements reject secret identifiers, invalid destinations and standalone sectors', () => {
|
||||
for (const bad of [
|
||||
{ kind: 'noField', token: 'secret' }, { kind: 'noField', value: 3 },
|
||||
{ kind: 'noField', sector: 11 }, { kind: 'noField', territory: 'not-a-territory' },
|
||||
{ kind: 'noField', territory: 'carthag', sector: 10 },
|
||||
{ kind: 'noField', territory: 'carthag', minimum: 1 },
|
||||
]) assert.throws(() => parseShipmentExpression(bad), ShipmentClaimError);
|
||||
assert.match(shipmentExpressionInputError({ kind: 'noField', sector: 11 })!, /No-Field shipment/);
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createElement, isValidElement, type ReactElement, type ReactNode } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { ShipmentExpressionFields } from '../components/shipment-expression-fields';
|
||||
import { TruthShipmentQuestion } from '../components/truth-shipment-question';
|
||||
import { TruthNoFieldShipmentFields, type NoFieldShipmentInput } from '../components/truth-no-field-shipment-fields';
|
||||
import { createGame, joinGame, newPlayer, viewGame, type Action } from '../game/engine';
|
||||
import type { ShipmentExpression } from '../game/shipment-promises';
|
||||
|
||||
type ElementProps = { children?: ReactNode; [key: string]: unknown };
|
||||
type Element = ReactElement<ElementProps>;
|
||||
function elements(node: ReactNode): Element[] {
|
||||
if (Array.isArray(node)) return node.flatMap(elements);
|
||||
if (!isValidElement<ElementProps>(node)) return [];
|
||||
return [node, ...elements(node.props.children)];
|
||||
}
|
||||
function byId(tree: ReactNode, id: string): Element {
|
||||
const element = elements(tree).find(item => item.props.id === id);
|
||||
assert.ok(element, `Expected ${id}`);
|
||||
return element;
|
||||
}
|
||||
function choose(element: Element, value: string) {
|
||||
(element.props.onChange as (event: {currentTarget: {value: string}}) => void)({currentTarget: {value}});
|
||||
}
|
||||
|
||||
void test('No-Field condition authors any destination or one printed territory and sector', () => {
|
||||
let value: NoFieldShipmentInput = {kind: 'noField'};
|
||||
const tree = () => TruthNoFieldShipmentFields({id: 'marker', value, disabled: false, onChange(next) { value = next; }});
|
||||
const html = renderToStaticMarkup(createElement(TruthNoFieldShipmentFields, {id: 'marker', value, disabled: false, onChange() {}}));
|
||||
assert.match(html, /Any destination/);
|
||||
assert.doesNotMatch(html, /token ID|token value/i);
|
||||
choose(byId(tree(), 'marker-no-field-territory'), 'arrakeen');
|
||||
assert.deepEqual(value, {kind:'noField', territory:'arrakeen'});
|
||||
choose(byId(tree(), 'marker-no-field-sector'), '10');
|
||||
assert.deepEqual(value, {kind:'noField', territory:'arrakeen', sector:10});
|
||||
choose(byId(tree(), 'marker-no-field-territory'), 'carthag');
|
||||
assert.deepEqual(value, {kind:'noField', territory:'carthag'}, 'changing destination removes the old sector');
|
||||
choose(byId(tree(), 'marker-no-field-sector'), '10');
|
||||
assert.deepEqual(value, {kind:'noField', territory:'carthag'}, 'invalid sector is ignored');
|
||||
choose(byId(tree(), 'marker-no-field-territory'), '');
|
||||
assert.deepEqual(value, {kind:'noField'});
|
||||
});
|
||||
|
||||
void test('disabled marker fields ignore direct callback invocation', () => {
|
||||
let changes = 0;
|
||||
const value: NoFieldShipmentInput = {kind:'noField', territory:'arrakeen', sector:10};
|
||||
const tree = TruthNoFieldShipmentFields({id:'locked', value, disabled:true, onChange() { changes++; }});
|
||||
assert.equal(byId(tree, 'locked-no-field-territory').props.disabled, true);
|
||||
assert.equal(byId(tree, 'locked-no-field-sector').props.disabled, true);
|
||||
choose(byId(tree, 'locked-no-field-territory'), 'carthag');
|
||||
choose(byId(tree, 'locked-no-field-sector'), '');
|
||||
assert.equal(changes, 0);
|
||||
});
|
||||
|
||||
void test('mixed shipment expression can change one condition to No-Field without replacing the physical condition', () => {
|
||||
let value: ShipmentExpression = {op:'or', terms:[{territory:'carthag',minimum:2}, {territory:'arrakeen',minimum:3}]};
|
||||
const tree = () => ShipmentExpressionFields({value, id:'question', disabled:false, allowNoField:true, onChange(next) { value = next; }});
|
||||
choose(byId(tree(), 'question-1.2-condition-type'), 'noField');
|
||||
assert.deepEqual(value, {op:'or', terms:[{territory:'carthag',minimum:2}, {kind:'noField'}]});
|
||||
const marker = elements(tree()).find(element => element.type === TruthNoFieldShipmentFields);
|
||||
assert.ok(marker);
|
||||
(marker.props.onChange as (claim: NoFieldShipmentInput) => void)({kind:'noField',territory:'arrakeen',sector:10});
|
||||
assert.deepEqual(value, {op:'or', terms:[{territory:'carthag',minimum:2}, {kind:'noField',territory:'arrakeen',sector:10}]});
|
||||
choose(byId(tree(), 'question-1.2-condition-type'), 'physical');
|
||||
assert.deepEqual(value, {op:'or', terms:[{territory:'carthag',minimum:2}, {territory:'carthag',minimum:1}]});
|
||||
});
|
||||
|
||||
void test('a non-Richese target cannot add a No-Field condition and can remove a previously selected one', () => {
|
||||
let value: ShipmentExpression = {kind:'noField'};
|
||||
const tree = () => ShipmentExpressionFields({value, id:'other', disabled:false, allowNoField:false, onChange(next) { value = next; }});
|
||||
const html = renderToStaticMarkup(createElement(ShipmentExpressionFields, {value, id:'other', disabled:false, allowNoField:false, onChange() {}}));
|
||||
assert.match(html, /Choose Richese for a No-Field shipment condition/);
|
||||
choose(byId(tree(), 'other-1-condition-type'), 'physical');
|
||||
assert.deepEqual(value, {territory:'carthag',minimum:1});
|
||||
assert.equal(elements(tree()).some(element => element.props.id === 'other-1-condition-type'), false);
|
||||
});
|
||||
|
||||
void test('shipment question asks an exact No-Field claim for Richese and blocks it for another faction', () => {
|
||||
const source = createGame('MARKERQUESTION', newPlayer('asker','Asker','atreides'), false, ['choam']);
|
||||
joinGame(source, newPlayer('richese','Richese','richese'));
|
||||
joinGame(source, newPlayer('emperor','Emperor','emperor'));
|
||||
const game = viewGame(source, 'asker');
|
||||
game.truthtrance = {stage:'ask', queue:[{player:'asker',card:'truth'}], passed:[], question:null};
|
||||
game.truthShipmentTargets = ['richese','emperor'];
|
||||
let target = 'richese';
|
||||
let claim: ShipmentExpression = {territory:'carthag',minimum:1};
|
||||
const actions: Action[] = [];
|
||||
const question = () => ({kind:'shipment' as const, target, claim});
|
||||
const wrapper = () => TruthShipmentQuestion({game, question:question(), busy:false,
|
||||
act(action) { actions.push(action); }, onChange(next) { claim = next; }});
|
||||
const fieldTree = () => {
|
||||
const field = elements(wrapper()).find(element => element.type === ShipmentExpressionFields);
|
||||
assert.ok(field);
|
||||
return ShipmentExpressionFields(field.props as Parameters<typeof ShipmentExpressionFields>[0]);
|
||||
};
|
||||
const button = () => {
|
||||
const found = elements(wrapper()).find(element => element.props['data-testid'] === 'truth-shipment-ask');
|
||||
assert.ok(found);
|
||||
return found;
|
||||
};
|
||||
choose(byId(fieldTree(), 'truth-shipment-1-condition-type'), 'noField');
|
||||
assert.deepEqual(claim, {kind:'noField'});
|
||||
const marker = elements(fieldTree()).find(element => element.type === TruthNoFieldShipmentFields);
|
||||
assert.ok(marker);
|
||||
(marker.props.onChange as (value: NoFieldShipmentInput) => void)({kind:'noField', territory:'arrakeen', sector:10});
|
||||
assert.equal(button().props.disabled, false);
|
||||
(button().props.onClick as () => void)();
|
||||
assert.deepEqual(actions, [{type:'truthAsk', question:{kind:'shipment', target:'richese',
|
||||
claim:{kind:'noField', territory:'arrakeen', sector:10}}}]);
|
||||
|
||||
target = 'emperor';
|
||||
assert.equal(button().props.disabled, true);
|
||||
(button().props.onClick as () => void)();
|
||||
assert.equal(actions.length, 1);
|
||||
choose(byId(fieldTree(), 'truth-shipment-1-condition-type'), 'physical');
|
||||
assert.deepEqual(claim, {territory:'carthag',minimum:1});
|
||||
assert.equal(button().props.disabled, false);
|
||||
});
|
||||
@@ -0,0 +1,241 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { deployRicheseNoField } from '../game/richese-no-field';
|
||||
import type { TruthQuestion } from '../game/truthtrance';
|
||||
import {
|
||||
createTruthtranceRichesePromisesFixture as fixture,
|
||||
actTruthtranceRichesePromises as act,
|
||||
askTruthtranceRichesePromises as ask,
|
||||
advanceTruthtranceRichesePromises as advance,
|
||||
truthtranceRichesePromisesClean as clean,
|
||||
truthtranceRichesePromisesPlayer as player,
|
||||
truthtranceRichesePromisesInventory as inventory,
|
||||
} from './fixture-truthtrance-richese-promises';
|
||||
|
||||
const markerQuestion = {
|
||||
kind: 'shipment', target: 'richese',
|
||||
claim: {kind: 'noField', territory: 'carthag', sector: 11},
|
||||
} as unknown as TruthQuestion;
|
||||
const reload = (game: Game): Game => JSON.parse(JSON.stringify(game));
|
||||
const seat = (game: Game, id: string) => {
|
||||
const found = game.players.find(row => row.id === id); assert.ok(found); return found;
|
||||
};
|
||||
function reject(game: Game, actor: string, action: Action) {
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => applyAction(game, actor, action));
|
||||
assert.deepEqual(game, before);
|
||||
}
|
||||
function start(answer: 'yes' | 'no') {
|
||||
const f = fixture();
|
||||
const question = ask(structuredClone(f.game), f, markerQuestion);
|
||||
assert.ok(viewGame(question, f.asker).truthShipmentTargets.includes(f.target));
|
||||
assert.deepEqual(viewGame(question, f.target).truthShipmentAnswers, ['yes', 'no']);
|
||||
let game = act(question, f.target, {type:'truthAnswer', answer});
|
||||
game = advance(game, clean);
|
||||
assert.equal(game.shipmentPromises?.length, 1);
|
||||
assert.equal(game.shipmentPromises![0].answer, answer === 'yes');
|
||||
assert.equal(game.shipmentPromises![0].fulfilled, undefined);
|
||||
assert.equal(game.shipmentPromises![0].released, undefined);
|
||||
assert.equal(game.discard.filter(card => card.id === f.truthtrance).length, 1);
|
||||
game = advance(game, state => state.phase === 5 && state.active === f.target && clean(state));
|
||||
return {f, game};
|
||||
}
|
||||
function markerAction(game: Game, value = 0, territory = 'carthag', sector = 11): Action {
|
||||
const richese = player(game);
|
||||
const token = richese.noField!.tokens.find(item => item.value === value); assert.ok(token);
|
||||
assert.ok(richese.noFieldEvent);
|
||||
return {type:'ship', noField:token.id, event:richese.noFieldEvent, territory, sector, allyPayment:0};
|
||||
}
|
||||
function finishResponse(game: Game): Game {
|
||||
return advance(game, clean);
|
||||
}
|
||||
|
||||
void test('Basic early Yes requires the requested owned No-Field, including its zero token', () => {
|
||||
const {f, game: before} = start('yes');
|
||||
const choice = markerAction(before), target = player(before), marker = String(choice.noField);
|
||||
const ownView = viewGame(before, f.target), otherView = viewGame(before, f.asker);
|
||||
assert.ok(ownView.shipmentCompletion?.actions.some(action => action.type === 'ship' && action.noField));
|
||||
assert.equal(otherView.shipmentCompletion, null);
|
||||
assert.equal(otherView.richeseNoField?.private, null);
|
||||
assert.equal(JSON.stringify(otherView).includes(marker), false);
|
||||
reject(before, f.target, {type:'endMovement'});
|
||||
reject(before, f.target, {type:'ship', territory:'carthag', sector:11, amount:1, elite:0, allyPayment:0});
|
||||
reject(before, f.target, markerAction(before, 0, 'arrakeen', 10));
|
||||
const declared = act(reload(before), f.target, choice);
|
||||
const done = finishResponse(declared);
|
||||
assert.equal(player(done).noField?.deployed?.tokenId, marker);
|
||||
assert.deepEqual(player(done).noField?.deployed?.location, {territory:'carthag', sector:11});
|
||||
assert.equal(player(done).reserves, target.reserves);
|
||||
assert.equal(player(done).spice, target.spice - 1);
|
||||
assert.equal(player(done).shipped, true);
|
||||
assert.equal(done.shipmentPromises![0].fulfilled, true);
|
||||
assert.equal(done.shipmentPromises![0].released, undefined);
|
||||
assert.deepEqual(inventory(done), f.originalInventory);
|
||||
assert.equal(viewGame(done, f.asker).richeseNoField?.private, null);
|
||||
reject(done, f.target, choice);
|
||||
});
|
||||
|
||||
void test('Basic No forbids the selected marker shipment but permits a physical shipment', () => {
|
||||
const {f, game: before} = start('no');
|
||||
reject(before, f.target, markerAction(before));
|
||||
const done = finishResponse(act(before, f.target, {
|
||||
type:'ship', territory:'carthag', sector:11, amount:1, elite:0, allyPayment:0,
|
||||
}));
|
||||
assert.equal(player(done).forces['carthag:11'], 1);
|
||||
assert.equal(player(done).noField?.deployed, null);
|
||||
assert.equal(done.shipmentPromises![0].fulfilled, true);
|
||||
assert.deepEqual(inventory(done), f.originalInventory);
|
||||
});
|
||||
|
||||
void test('Basic No permits a marker in a different destination', () => {
|
||||
const {f, game: before} = start('no');
|
||||
const action = markerAction(before, 3, 'arrakeen', 10);
|
||||
const done = finishResponse(act(before, f.target, action));
|
||||
assert.deepEqual(player(done).noField?.deployed?.location, {territory:'arrakeen', sector:10});
|
||||
assert.equal(done.shipmentPromises![0].fulfilled, true);
|
||||
assert.deepEqual(inventory(done), f.originalInventory);
|
||||
});
|
||||
|
||||
void test('an opposing Karama prevents the marker and releases an impossible Yes without spending shipment', () => {
|
||||
const {f, game: original} = start('yes');
|
||||
const game = structuredClone(original), opponent = seat(game, f.observer);
|
||||
const from = game.deck.findIndex(card => card.effect === 'karama');
|
||||
assert.ok(from >= 0 && opponent.hand.length);
|
||||
// Move an existing physical Karama into the observer's hand via a conserved
|
||||
// card swap; the declaration and the resulting counter are actual actions.
|
||||
[opponent.hand[0], game.deck[from]] = [game.deck[from], opponent.hand[0]];
|
||||
const karama = opponent.hand[0].id, before = structuredClone(game);
|
||||
const declared = act(game, f.target, markerAction(game));
|
||||
assert.equal(declared.response?.kind, 'richeseNoField');
|
||||
assert.equal(declared.shipmentPromises![0].fulfilled, undefined);
|
||||
let stopped = act(declared, f.observer, {type:'card', mode:'cancel', card:karama});
|
||||
stopped = finishResponse(stopped);
|
||||
assert.equal(player(stopped).noField?.deployed, null);
|
||||
assert.equal(player(stopped).noFieldBlockedTurn, stopped.turn);
|
||||
assert.equal(player(stopped).shipped, false);
|
||||
assert.equal(player(stopped).spice, player(before).spice);
|
||||
assert.equal(stopped.shipmentPromises![0].released, true);
|
||||
assert.equal(stopped.shipmentPromises![0].fulfilled, undefined);
|
||||
reject(stopped, f.target, markerAction(stopped));
|
||||
const normal = finishResponse(act(stopped, f.target, {
|
||||
type:'ship', territory:'carthag', sector:11, amount:1, elite:0, allyPayment:0,
|
||||
}));
|
||||
assert.equal(player(normal).forces['carthag:11'], 1);
|
||||
assert.deepEqual(inventory(normal), f.originalInventory);
|
||||
});
|
||||
|
||||
void test('all existing bot profiles use the same private feasible answers and legal marker completion', () => {
|
||||
const f = fixture(), asked = ask(structuredClone(f.game), f, markerQuestion);
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const answering = viewGame(asked, f.target);
|
||||
answering.players.find(row => row.id === f.target)!.bot = difficulty;
|
||||
const answer = botActions(answering).find(action => action.type === 'truthAnswer');
|
||||
assert.ok(answer && answering.truthShipmentAnswers?.includes(answer.answer as 'yes' | 'no' | 'unknown'));
|
||||
assert.doesNotThrow(() => act(reload(asked), f.target, answer));
|
||||
const {game} = start('yes');
|
||||
const turn = viewGame(game, f.target);
|
||||
turn.players.find(row => row.id === f.target)!.bot = difficulty;
|
||||
const action = botActions(turn)[0]; assert.ok(action, difficulty);
|
||||
assert.doesNotThrow(() => act(reload(game), f.target, action));
|
||||
}
|
||||
});
|
||||
|
||||
void test('a marker-only declaration does not satisfy an existing physical-force promise', () => {
|
||||
const f = fixture();
|
||||
let game = ask(structuredClone(f.game), f);
|
||||
game = act(game, f.target, {type:'truthAnswer', answer:'yes'});
|
||||
game = advance(game, state => state.phase === 5 && state.active === f.target && clean(state));
|
||||
reject(game, f.target, markerAction(game));
|
||||
const done = finishResponse(act(game, f.target, f.shipmentAction));
|
||||
assert.equal(player(done).forces['carthag:11'], 5);
|
||||
assert.equal(done.shipmentPromises![0].fulfilled, true);
|
||||
});
|
||||
|
||||
void test('Basic cannot promise a single shipment is both a marker and physical-only forces', () => {
|
||||
const f = fixture();
|
||||
const question = {
|
||||
kind:'shipment', target:f.target, claim:{op:'and', terms:[
|
||||
{kind:'noField', territory:'carthag'}, {territory:'carthag', minimum:1},
|
||||
]},
|
||||
} as unknown as TruthQuestion;
|
||||
const asked = ask(structuredClone(f.game), f, question);
|
||||
assert.deepEqual(viewGame(asked, f.target).truthShipmentAnswers, ['no']);
|
||||
reject(asked, f.target, {type:'truthAnswer', answer:'yes'});
|
||||
const done = act(asked, f.target, {type:'truthAnswer', answer:'no'});
|
||||
assert.equal(done.shipmentPromises![0].answer, false);
|
||||
});
|
||||
|
||||
void test('the marker question cannot target a faction that does not own a No-Field', () => {
|
||||
const f = fixture();
|
||||
let game = act(structuredClone(f.game), f.asker, {type:'card', card:f.truthtrance});
|
||||
game = advance(game, state => state.truthtrance?.stage === 'ask');
|
||||
const question = {kind:'shipment', target:f.observer, claim:{kind:'noField', territory:'carthag'}};
|
||||
reject(game, f.asker, {type:'truthAsk', question});
|
||||
});
|
||||
|
||||
void test('a deployed marker can be revealed before the promised different No-Field is shipped', () => {
|
||||
const f = fixture(), staged = structuredClone(f.game), richese = player(staged);
|
||||
const previous = richese.noField!.tokens.find(item => item.value === 3); assert.ok(previous);
|
||||
// Model an earlier turn's original marker with the same physical token and
|
||||
// history; staging does not create forces or change reserves.
|
||||
richese.noField = deployRicheseNoField(richese.noField!, {
|
||||
tokenId: previous.id, controller: richese.id,
|
||||
location: {territory:'polar_sink', sector:0},
|
||||
});
|
||||
let game = ask(staged, f, markerQuestion);
|
||||
assert.ok(viewGame(game, f.target).truthShipmentAnswers?.includes('yes'));
|
||||
game = act(game, f.target, {type:'truthAnswer', answer:'yes'});
|
||||
game = advance(game, state => state.phase === 5 && state.active === f.target && clean(state));
|
||||
const first = viewGame(game, f.target).shipmentCompletion?.actions[0];
|
||||
assert.equal(first?.type, 'revealNoField');
|
||||
assert.equal(first?.token, previous.id);
|
||||
game = act(game, f.target, first!);
|
||||
assert.equal(player(game).noField?.deployed, null);
|
||||
assert.equal(player(game).noField?.lastShipped, previous.id);
|
||||
assert.equal(player(game).forces['polar_sink:0'], 3);
|
||||
const next = viewGame(game, f.target).shipmentCompletion?.actions[0];
|
||||
assert.equal(next?.type, 'ship');
|
||||
assert.ok(next?.noField && next.noField !== previous.id);
|
||||
game = finishResponse(act(game, f.target, next!));
|
||||
assert.equal(game.shipmentPromises![0].fulfilled, true);
|
||||
assert.equal(player(game).noField?.deployed?.tokenId, next!.noField);
|
||||
assert.deepEqual(player(game).noField?.deployed?.location, {territory:'carthag', sector:11});
|
||||
});
|
||||
|
||||
function givePhysicalKarama(game: Game, id: string): string {
|
||||
const target = seat(game, id), held = target.hand.find(card => card.effect === 'karama');
|
||||
if (held) return held.id;
|
||||
assert.ok(target.hand.length);
|
||||
const inDeck = game.deck.findIndex(card => card.effect === 'karama');
|
||||
if (inDeck >= 0) {
|
||||
[target.hand[0], game.deck[inDeck]] = [game.deck[inDeck], target.hand[0]];
|
||||
return target.hand[0].id;
|
||||
}
|
||||
for (const other of game.players) {
|
||||
if (other.id === id) continue;
|
||||
const index = other.hand.findIndex(card => card.effect === 'karama');
|
||||
if (index >= 0) {
|
||||
[target.hand[0], other.hand[index]] = [other.hand[index], target.hand[0]];
|
||||
return target.hand[0].id;
|
||||
}
|
||||
}
|
||||
throw Error('No original physical Karama was available for the fixture');
|
||||
}
|
||||
|
||||
void test('Richese cannot cancel its own promised No-Field while another responder holds Karama', () => {
|
||||
const game = start('yes').game, karama = givePhysicalKarama(game, 'richese');
|
||||
const opposing = givePhysicalKarama(game, 'emperor');
|
||||
assert.notEqual(karama, opposing);
|
||||
assert.ok(player(game).hand.some(card => card.id === karama));
|
||||
const declared = act(game, 'richese', markerAction(game));
|
||||
assert.equal(declared.response?.kind, 'richeseNoField');
|
||||
reject(declared, 'richese', {type:'card', mode:'cancel', card:karama});
|
||||
assert.equal(declared.pendingKull ?? null, null);
|
||||
assert.equal(declared.pendingTreacheryDiscard ?? null, null);
|
||||
assert.ok(player(declared).hand.some(card => card.id === karama));
|
||||
assert.equal(declared.shipmentPromises![0].released, undefined);
|
||||
assert.equal(declared.shipmentPromises![0].fulfilled, undefined);
|
||||
});
|
||||
Reference in new issue
Block a user