Prototype Tleilaxu special Karama Truthtrance
Verify and publish checkpoint container / container (push) Successful in 11m6s
Verify and publish checkpoint container / container (push) Successful in 11m6s
This commit is contained in:
1 parent
5b8d510470
commit
cfccfe5acb
15 files changed
+391
-20
No files matched your search
@@ -0,0 +1,36 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame } from '../game/engine';
|
||||
import { createTleilaxuSpecialKaramaFixture, actTleilaxuSpecialKarama as act, tleilaxuSpecialKaramaPlayer as player } from './fixture-tleilaxu-special-karama';
|
||||
|
||||
void test('Tleilaxu cannot prevent their own revival while committed to reviving forces and using special Karama', () => {
|
||||
const f = createTleilaxuSpecialKaramaFixture(), game = structuredClone(f.game), owner = player(game, f.owner), asker = f.observer;
|
||||
// Controlled casualty; conserve Tleilaxu's physical force counters.
|
||||
assert.ok(owner.reserves > 0); owner.reserves--; owner.tanks++;
|
||||
const hand = player(game, asker).hand;
|
||||
if (!hand.some(card => card.effect === 'truthtrance')) {
|
||||
const zones = [game.deck, game.discard, player(game, f.target).hand];
|
||||
const zone = zones.find(cards => cards.some(card => card.effect === 'truthtrance')); assert.ok(zone);
|
||||
const index = zone.findIndex(card => card.effect === 'truthtrance');
|
||||
[hand[0], zone[index]] = [zone[index], hand[0]];
|
||||
}
|
||||
const truth = hand.find(card => card.effect === 'truthtrance'); assert.ok(truth);
|
||||
let state = act(game, asker, { type: 'card', card: truth.id });
|
||||
for (let count = 0; state.truthtrance?.stage === 'priority' && count < state.players.length; count++) {
|
||||
const actor = state.players.find(seat => !state.truthtrance!.passed.includes(seat.id)); assert.ok(actor);
|
||||
state = act(state, actor.id, { type: 'truthPass' });
|
||||
}
|
||||
state = act(state, asker, { type: 'truthAsk', question: { kind: 'turnActions', target: f.owner, turn: state.turn,
|
||||
mixed: { kind: 'and', terms: [
|
||||
{ kind: 'action', action: { kind: 'forces', counter: 'total', minimum: 1 } },
|
||||
{ kind: 'action', action: { kind: 'windowCardPlay', effect: 'karamaTleilaxuSpecial', source: JSON.stringify(['karamaTleilaxuSpecial', state.turn, f.owner]) } },
|
||||
] } } });
|
||||
assert.ok(viewGame(state, f.owner).truthTurnActionAnswers?.includes('yes'));
|
||||
state = act(state, f.owner, { type: 'truthAnswer', answer: 'yes' });
|
||||
const before = structuredClone(state);
|
||||
assert.throws(() => act(state, f.owner, { ...f.special, target: f.owner }));
|
||||
assert.deepEqual(state, before);
|
||||
state = act(state, f.owner, f.special);
|
||||
assert.deepEqual(state.revivalPrevention, { player: f.target, turn: state.turn });
|
||||
assert.ok(state.turnActionPromises!.some(promise => promise.player === f.owner && !promise.fulfilled && !promise.released));
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame, type Action, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { windowCardOpportunity } from '../game/window-card-play';
|
||||
import {
|
||||
createTleilaxuSpecialKaramaFixture,
|
||||
actTleilaxuSpecialKarama as act,
|
||||
tleilaxuSpecialKaramaPlayer as player,
|
||||
tleilaxuSpecialKaramaInventory as inventory,
|
||||
} from './fixture-tleilaxu-special-karama';
|
||||
|
||||
function fixture() {
|
||||
const f = createTleilaxuSpecialKaramaFixture();
|
||||
const game = structuredClone(f.game), asker = f.observer;
|
||||
const hand = player(game, asker).hand;
|
||||
// Stage one original Truthtrance by exchanging physical cards with an
|
||||
// existing card zone. Neither Karama nor Ghola is displaced or duplicated.
|
||||
let truth = hand.find(card => card.effect === 'truthtrance');
|
||||
if (!truth) {
|
||||
const zones = [game.deck, game.discard, player(game, f.target).hand];
|
||||
const zone = zones.find(cards => cards.some(card => card.effect === 'truthtrance'));
|
||||
assert.ok(zone);
|
||||
const index = zone.findIndex(card => card.effect === 'truthtrance');
|
||||
assert.ok(hand.length > 0);
|
||||
[hand[0], zone[index]] = [zone[index], hand[0]];
|
||||
truth = hand[0];
|
||||
}
|
||||
assert.deepEqual(inventory(game), f.inventory);
|
||||
const source = JSON.stringify(['karamaTleilaxuSpecial', game.turn, f.owner]);
|
||||
assert.equal(windowCardOpportunity(game, 'karamaTleilaxuSpecial')?.source, source);
|
||||
return { ...f, game, asker, truth: truth.id, windowSource: source };
|
||||
}
|
||||
type Fixture = ReturnType<typeof fixture>;
|
||||
function question(f: Fixture, target = f.owner, source = f.windowSource) {
|
||||
return { kind: 'turnActions', target, turn: f.game.turn, mixed: { kind: 'action', action: {
|
||||
kind: 'windowCardPlay', effect: 'karamaTleilaxuSpecial', source,
|
||||
} } };
|
||||
}
|
||||
function declare(f: Fixture) {
|
||||
let game = act(f.game, f.asker, { type: 'card', card: f.truth });
|
||||
for (let count = 0; game.truthtrance?.stage === 'priority' && count < game.players.length; count++) {
|
||||
const actor = game.players.find(seat => !game.truthtrance!.passed.includes(seat.id));
|
||||
assert.ok(actor);
|
||||
game = act(game, actor.id, { type: 'truthPass' });
|
||||
}
|
||||
assert.equal(game.truthtrance?.stage, 'ask');
|
||||
return game;
|
||||
}
|
||||
function ask(f: Fixture) {
|
||||
return act(declare(f), f.asker, { type: 'truthAsk', question: question(f) });
|
||||
}
|
||||
function reject(game: Game, actor: string, action: Action) {
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => act(game, actor, action));
|
||||
assert.deepEqual(game, before);
|
||||
}
|
||||
|
||||
void test('Tleilaxu special Karama Yes spends the physical card and prevents a real revival', () => {
|
||||
const f = fixture(), asked = ask(f);
|
||||
assert.ok(viewGame(asked, f.owner).truthTurnActionAnswers?.includes('yes'));
|
||||
assert.equal(viewGame(asked, f.target).truthTurnActionAnswers, null);
|
||||
let game = act(asked, f.owner, { type: 'truthAnswer', answer: 'yes' });
|
||||
const guide = viewGame(game, f.owner).turnActionGuide; assert.ok(guide);
|
||||
assert.equal(guide.action.type, 'card');
|
||||
assert.equal(guide.action.mode, 'special');
|
||||
assert.equal(guide.action.card, f.karama);
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const bot = viewGame(game, f.owner);
|
||||
bot.players.find(seat => seat.id === f.owner)!.bot = difficulty;
|
||||
assert.deepEqual(botActions(bot)[0], guide.action);
|
||||
}
|
||||
game = act(game, f.owner, f.special);
|
||||
assert.equal(player(game, f.owner).specialKaramaUsed, true);
|
||||
assert.equal(player(game, f.owner).hand.some(card => card.id === f.karama), false);
|
||||
assert.equal(game.discard.filter(card => card.id === f.karama).length, 1);
|
||||
assert.deepEqual(game.revivalPrevention, { player: f.target, turn: game.turn });
|
||||
const promise = game.turnActionPromises!.find(row => row.player === f.owner);
|
||||
assert.equal(promise?.windowCards?.karamaTleilaxuSpecial?.played, true);
|
||||
assert.equal(promise?.fulfilled, true);
|
||||
reject(game, f.target, f.ordinaryRevival);
|
||||
reject(game, f.target, f.gholaAction);
|
||||
assert.equal(viewGame(game, f.asker).players.find(seat => seat.id === f.owner)!.hand, undefined);
|
||||
assert.deepEqual(viewGame(game, f.asker).turnActionPromises, []);
|
||||
assert.deepEqual(inventory(game), f.inventory);
|
||||
});
|
||||
|
||||
void test('Tleilaxu special Karama No prevents its use and preserves the card', () => {
|
||||
const f = fixture(), asked = ask(f);
|
||||
assert.ok(viewGame(asked, f.owner).truthTurnActionAnswers?.includes('no'));
|
||||
const game = act(asked, f.owner, { type: 'truthAnswer', answer: 'no' });
|
||||
reject(game, f.owner, f.special);
|
||||
reject(game, f.owner, { ...f.special, target: f.owner });
|
||||
assert.ok(player(game, f.owner).hand.some(card => card.id === f.karama));
|
||||
assert.notEqual(player(game, f.owner).specialKaramaUsed, true);
|
||||
assert.deepEqual(inventory(game), f.inventory);
|
||||
});
|
||||
|
||||
void test('Tleilaxu may fulfill Yes by preventing its own revival', () => {
|
||||
const f = fixture();
|
||||
let game = act(ask(f), f.owner, { type: 'truthAnswer', answer: 'yes' });
|
||||
game = act(game, f.owner, { ...f.special, target: f.owner });
|
||||
assert.deepEqual(game.revivalPrevention, { player: f.owner, turn: game.turn });
|
||||
assert.equal(game.turnActionPromises!.find(row => row.player === f.owner)?.fulfilled, true);
|
||||
assert.equal(game.discard.filter(card => card.id === f.karama).length, 1);
|
||||
assert.deepEqual(inventory(game), f.inventory);
|
||||
});
|
||||
|
||||
void test('Tleilaxu special source rejects another respondent and forged turns or owners', () => {
|
||||
const f = fixture(), game = declare(f);
|
||||
reject(game, f.asker, { type: 'truthAsk', question: question(f, f.target) });
|
||||
reject(game, f.asker, { type: 'truthAsk', question: question(f, f.owner,
|
||||
JSON.stringify(['karamaTleilaxuSpecial', game.turn + 1, f.owner])) });
|
||||
reject(game, f.asker, { type: 'truthAsk', question: question(f, f.owner,
|
||||
JSON.stringify(['karamaTleilaxuSpecial', game.turn, f.target])) });
|
||||
});
|
||||
@@ -2196,6 +2196,42 @@ void test('Emperor special Karama guide forwards only a current legal force pack
|
||||
owner.tanks = 3; owner.specialKaramaUsed = true; press(button); assert.equal(sent.length, 1);
|
||||
});
|
||||
|
||||
void test('Tleilaxu special Karama authoring uses only the public Revival opportunity', () => {
|
||||
const game = projected(4);
|
||||
game.advanced = true; game.expansions = ['ix'];
|
||||
const owner = game.players.find(player => player.id === 'p')!;
|
||||
owner.faction = 'tleilaxu'; owner.specialKaramaUsed = false;
|
||||
const source = JSON.stringify(['karamaTleilaxuSpecial', game.turn, 'p']);
|
||||
const initial = windowClaim('karamaTleilaxuSpecial', source), e = editor(initial, false, game);
|
||||
Object.defineProperty(owner, 'hand', { get() { throw Error('Question authoring read private hand'); }, configurable: true });
|
||||
assert.equal(e.error(), null);
|
||||
assert.match(e.html(), /Use Tleilaxu’s special Karama during Revival/);
|
||||
assert.match(e.html(), /Turn 2 · Tleilaxu special Karama during Revival/);
|
||||
assert.ok(turnActionInputError(initial, game, 'r'));
|
||||
owner.specialKaramaUsed = true; assert.ok(e.error());
|
||||
owner.specialKaramaUsed = false; game.phase = 5; assert.ok(e.error());
|
||||
});
|
||||
|
||||
void test('Tleilaxu special Karama guide requires the current quoted physical card and target', () => {
|
||||
const source = JSON.stringify(['karamaTleilaxuSpecial', 2, 'p']);
|
||||
const game = projected(4, { type: 'card', mode: 'special', card: 'controlled-printed-karama', target: 'r' });
|
||||
game.advanced = true; game.expansions = ['ix'];
|
||||
const owner = game.players.find(player => player.id === 'p')!;
|
||||
owner.faction = 'tleilaxu'; owner.specialKaramaUsed = false;
|
||||
game.tleilaxuSpecialKarama = { blocked: null, cards: ['controlled-printed-karama'],
|
||||
targets: [{ player: 'r', blocked: null }], pending: false };
|
||||
game.turnActionPromises[0].mixed = windowClaim('karamaTleilaxuSpecial', source);
|
||||
const sent: Action[] = [], props = { game, busy: false, act(action: Action) { sent.push(action); } };
|
||||
const button = guide(props); assert.equal(button.props.disabled, false);
|
||||
assert.match(renderToStaticMarkup(button), /Tleilaxu’s special Karama/);
|
||||
press(button); assert.deepEqual(sent, [game.turnActionGuide!.action]);
|
||||
game.tleilaxuSpecialKarama.targets[0].blocked = 'Unavailable'; press(button); assert.equal(sent.length, 1);
|
||||
game.tleilaxuSpecialKarama.targets[0].blocked = null;
|
||||
game.tleilaxuSpecialKarama.cards.length = 0; press(button); assert.equal(sent.length, 1);
|
||||
game.tleilaxuSpecialKarama.cards.push('controlled-printed-karama');
|
||||
owner.hand = []; press(button); assert.equal(sent.length, 1);
|
||||
});
|
||||
|
||||
void test('Atreides special Karama authoring uses the public battle window and owner', () => {
|
||||
const game = combat(false);
|
||||
game.advanced = true;
|
||||
|
||||
@@ -63,7 +63,8 @@ const power = (claim: BattlePowerClaim = { kind: 'voice', use: true }): Expressi
|
||||
const windowSource = (effect: WindowCardEffect) => effect === 'karamaShipment'
|
||||
? JSON.stringify(['karamaShipment', 1, 'recipient', 'original']) : effect === 'karamaHarkonnenSpecial'
|
||||
? JSON.stringify(['karamaHarkonnenSpecial', 1, 'respondent']) : effect === 'karamaEmperorSpecial'
|
||||
? JSON.stringify(['karamaEmperorSpecial', 1, 'respondent']) : effect === 'karamaAtreidesSpecial'
|
||||
? JSON.stringify(['karamaEmperorSpecial', 1, 'respondent']) : effect === 'karamaTleilaxuSpecial'
|
||||
? JSON.stringify(['karamaTleilaxuSpecial', 1, 'respondent']) : effect === 'karamaAtreidesSpecial'
|
||||
? JSON.stringify(['karamaAtreidesSpecial', 1, 'respondent', 'battle:1', 'carthag']) : effect === 'karamaFremenSpecial'
|
||||
? JSON.stringify(['karamaFremenSpecial', 1, 'respondent']) : effect === 'karamaAuction' ? 'ordinary-auction:1:1' :
|
||||
effect === 'amal' ? 'phase-opening:1:2' : effect === 'thumper' ? 'first-spice-reveal:1' :
|
||||
@@ -1937,6 +1938,41 @@ void test('Emperor special No and source binding retain exact responder, turn an
|
||||
});
|
||||
|
||||
|
||||
void test('Tleilaxu special Karama window records only the original owner and phase after the answer', () => {
|
||||
const source = windowSource('karamaTleilaxuSpecial');
|
||||
const question = windowed(group('and', windowCard('karamaTleilaxuSpecial'), pay()));
|
||||
const signed = { kind: 'windowCardPlay', effect: 'karamaTleilaxuSpecial', source, answer: true };
|
||||
assert.deepEqual(branches(question), [[signed, signedPay()]]);
|
||||
const otherOwner = recordTurnActionWindowCardPlay([question], 'other', 1, 'karamaTleilaxuSpecial', source);
|
||||
assert.equal(otherOwner[0], question);
|
||||
const actual = recordTurnActionWindowCardPlay([question], 'respondent', 1, 'karamaTleilaxuSpecial', source);
|
||||
assert.deepEqual(branches(actual[0]), [[signedPay()]]);
|
||||
assert.equal(recordTurnActionPayment(actual, 'respondent', 1, payment())[0].fulfilled, true);
|
||||
assert.equal(question.windowCards!.karamaTleilaxuSpecial!.played, false);
|
||||
validateTurnActionPromises(actual, seats);
|
||||
const closed = closeTurnActionWindowCardPlay([question], 'respondent', 1, 'karamaTleilaxuSpecial', source);
|
||||
assert.equal(closed[0].windowCards!.karamaTleilaxuSpecial!.closed, true);
|
||||
assert.deepEqual(branches(closed[0]), []);
|
||||
});
|
||||
|
||||
void test('Tleilaxu special No and source binding retain exact responder, turn and old receipts', () => {
|
||||
const source = windowSource('karamaTleilaxuSpecial');
|
||||
const negative = windowed(windowCard('karamaTleilaxuSpecial'), false);
|
||||
assert.throws(() => recordTurnActionWindowCardPlay([negative], 'respondent', 1, 'karamaTleilaxuSpecial', source), TurnActionError);
|
||||
const passed = closeTurnActionWindowCardPlay([negative], 'respondent', 1, 'karamaTleilaxuSpecial', source);
|
||||
assert.equal(passed[0].fulfilled, true);
|
||||
for (const bad of ['', 'storm:1', JSON.stringify(['karamaTleilaxuSpecial', 0, 'respondent']),
|
||||
JSON.stringify(['karamaTleilaxuSpecial', 1, '']), JSON.stringify(['karamaTleilaxuSpecial', 1, 'respondent', 'extra'])])
|
||||
assert.throws(() => parseTurnActionLeaf({ kind: 'windowCardPlay', effect: 'karamaTleilaxuSpecial', source: bad }), TurnActionError);
|
||||
for (const bad of [JSON.stringify(['karamaTleilaxuSpecial', 2, 'respondent']),
|
||||
JSON.stringify(['karamaTleilaxuSpecial', 1, 'other'])])
|
||||
assert.throws(() => turnActionValue(windowed(windowCard('karamaTleilaxuSpecial', bad))), TurnActionError);
|
||||
const old = windowed(windowCard('karamaAuction'));
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(old)), old);
|
||||
assert.doesNotThrow(() => validateTurnActionPromises([old], seats));
|
||||
});
|
||||
|
||||
|
||||
void test('Fremen special Karama window records only the original owner and phase after the answer', () => {
|
||||
const source = windowSource('karamaFremenSpecial');
|
||||
const question = windowed(group('and', windowCard('karamaFremenSpecial'), pay()));
|
||||
|
||||
@@ -23,8 +23,8 @@ function shipments(): WindowCardGame {
|
||||
}
|
||||
|
||||
void test('window roles expose a shared exhaustive tuple and exact native printed-card mapping', () => {
|
||||
assert.deepEqual(WINDOW_CARD_EFFECTS, ['weather', 'atomics', 'harvester', 'karamaAuction', 'karamaShipment', 'karamaBattleCancel', 'karamaHarkonnenSpecial', 'karamaEmperorSpecial', 'karamaAtreidesSpecial', 'karamaFremenSpecial', 'thumper', 'amal']);
|
||||
assert.deepEqual(WINDOW_CARD_EFFECTS.map(windowCardNativeEffect), ['weather', 'atomics', 'harvester', 'karama', 'karama', 'karama', 'karama', 'karama', 'karama', 'karama', 'thumper', 'amal']);
|
||||
assert.deepEqual(WINDOW_CARD_EFFECTS, ['weather', 'atomics', 'harvester', 'karamaAuction', 'karamaShipment', 'karamaBattleCancel', 'karamaHarkonnenSpecial', 'karamaEmperorSpecial', 'karamaAtreidesSpecial', 'karamaFremenSpecial', 'karamaTleilaxuSpecial', 'thumper', 'amal']);
|
||||
assert.deepEqual(WINDOW_CARD_EFFECTS.map(windowCardNativeEffect), ['weather', 'atomics', 'harvester', 'karama', 'karama', 'karama', 'karama', 'karama', 'karama', 'karama', 'karama', 'thumper', 'amal']);
|
||||
});
|
||||
|
||||
void test('Amal names each original phase opening with only its public turn and phase identity', () => {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { parseKaramaTleilaxuSpecialSource as parse, windowCardNativeEffect,
|
||||
windowCardOpportunity, type WindowCardGame } from '../game/window-card-play';
|
||||
|
||||
const source = (): WindowCardGame => ({ status: 'playing', advanced: true, phase: 4, turn: 2,
|
||||
shieldWallDestroyed: false, players: [{ id: 'h', faction: 'tleilaxu', specialKaramaUsed: false },
|
||||
{ id: 'a', faction: 'atreides', specialKaramaUsed: false }] });
|
||||
const event = JSON.stringify(['karamaTleilaxuSpecial', 2, 'h']);
|
||||
|
||||
void test('Tleilaxu special window is public, turn-bound and independent of private custody', () => {
|
||||
const game = source(), before = structuredClone(game);
|
||||
assert.deepEqual(windowCardOpportunity(game, 'karamaTleilaxuSpecial'),
|
||||
{ effect: 'karamaTleilaxuSpecial', source: event, turn: 2 });
|
||||
assert.deepEqual(parse(event), { turn: 2, owner: 'h' });
|
||||
assert.equal(windowCardNativeEffect('karamaTleilaxuSpecial'), 'karama');
|
||||
assert.equal(windowCardOpportunity({ ...game, response: { kind: 'voice' }, decision: { kind: 'other' } },
|
||||
'karamaTleilaxuSpecial')?.source, event);
|
||||
assert.equal(windowCardOpportunity(game, 'karamaTleilaxuSpecial', event)?.source, event);
|
||||
assert.deepEqual(game, before);
|
||||
});
|
||||
|
||||
void test('special opportunity ends with use, turn or phase; malformed sources reject', () => {
|
||||
const game = source();
|
||||
for (const changes of [{ status: 'lobby' }, { advanced: false }, { phase: 3 }, { turn: 0 },
|
||||
{ players: [{ id: 'h', faction: 'tleilaxu', specialKaramaUsed: true }] },
|
||||
{ players: [{ id: 'h', faction: 'atreides' }] },
|
||||
{ players: [{ id: 'h', faction: 'tleilaxu' }, { id: 'h', faction: 'atreides' }] },
|
||||
{ players: [{ id: 'h', faction: 'tleilaxu' }, { id: 'h2', faction: 'tleilaxu' }] }])
|
||||
assert.equal(windowCardOpportunity({ ...game, ...changes } as WindowCardGame, 'karamaTleilaxuSpecial'), null);
|
||||
for (const value of ['', '[]', '["karamaTleilaxuSpecial",2," h"]',
|
||||
'["karamaTleilaxuSpecial", 2,"h"]', '["karamaTleilaxuSpecial",0,"h"]',
|
||||
'["karamaTleilaxuSpecial",2,"h","extra"]']) assert.equal(parse(value), null);
|
||||
assert.equal(windowCardOpportunity(game, 'karamaTleilaxuSpecial', JSON.stringify(['karamaTleilaxuSpecial', 2, 'a'])), null);
|
||||
assert.equal(windowCardOpportunity({ ...game, turn: 3 }, 'karamaTleilaxuSpecial', event), null);
|
||||
assert.ok(windowCardOpportunity({ ...game, players: [{ id: 'h', faction: 'tleilaxu' }] }, 'karamaTleilaxuSpecial'));
|
||||
});
|
||||
Reference in new issue
Block a user