Integrate private Mentat questions with native Basic skill profiles
This commit is contained in:
1 parent
3fc070fcfa
commit
589e1b9df2
16 files changed
+237
-45
No files matched your search
@@ -0,0 +1,45 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {applyAction, type Game} from '../game/engine';
|
||||
import {completedIxSkillsGame} from './ix-skills-fixture';
|
||||
import {completedTleilaxuSkillsGame} from './tleilaxu-skills-fixture';
|
||||
import {completedChoamSkillsGame} from './choam-skills-fixture';
|
||||
import {completedMoritaniSkillsGame} from './moritani-skills-fixture';
|
||||
import {finishToMovement} from './fixture-advanced-source';
|
||||
|
||||
export const NATIVE_MENTAT_FAMILIES = ['ixians','tleilaxu','choam','moritani'] as const;
|
||||
export type NativeMentatFamily = typeof NATIVE_MENTAT_FAMILIES[number];
|
||||
|
||||
/** Real native setup/assignment and Storm; conserved rule-unit battle position. */
|
||||
export function nativeMentatBattle(family: NativeMentatFamily, matching = true, preview = true, begin = true): Game {
|
||||
const options = {requestedSkill:'mentat' as const};
|
||||
let game = family === 'ixians' ? completedIxSkillsGame(options)
|
||||
: family === 'tleilaxu' ? completedTleilaxuSkillsGame(options)
|
||||
: family === 'choam' ? completedChoamSkillsGame(options)
|
||||
: completedMoritaniSkillsGame(options);
|
||||
game = finishToMovement(game);
|
||||
if (preview) game.mentatQuestionPreview = true;
|
||||
const owner = game.players.find(p => p.faction === family)!;
|
||||
const target = game.players.find(p => p.faction === 'emperor')!;
|
||||
for (const p of game.players) {
|
||||
game.deck.push(...p.hand);
|
||||
p.hand = [];
|
||||
p.reserves += Object.values(p.forces).reduce((sum, n) => sum + n, 0);
|
||||
p.forces = {};
|
||||
if (p.elites) {
|
||||
p.elites.reserves += Object.values(p.elites.forces).reduce((sum, n) => sum + n, 0);
|
||||
p.elites.forces = {};
|
||||
}
|
||||
}
|
||||
for (const p of [owner,target]) {
|
||||
assert.ok(p.reserves - (p.elites?.reserves ?? 0) >= 2);
|
||||
p.reserves -= 2;
|
||||
p.forces['arrakeen:10'] = 2;
|
||||
}
|
||||
const weapon = game.expansions.includes('ix') ? 'Weirding Way' : 'Crysknife';
|
||||
const name = matching ? weapon : 'Baliset';
|
||||
const index = game.deck.findIndex(c => c.name === name);
|
||||
assert.ok(index >= 0);
|
||||
target.hand.push(...game.deck.splice(index,1));
|
||||
Object.assign(game,{phase:6,storm:18,active:owner.id,order:[owner.id,...game.players.filter(p => p.id !== owner.id).map(p => p.id)],ready:[],response:null,decision:null,phaseOpening:null});
|
||||
return begin ? applyAction(game,owner.id,{type:'chooseBattle',territory:'arrakeen',target:target.id}) : game;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {applyAction,viewGame,initializeMoritaniAssassinateGameForAudit,createGam
|
||||
import {DIFFICULTIES} from '../game/bot-profiles';
|
||||
import {moritaniAssassinateSignature} from '../game/moritani-assassinate';
|
||||
import {assassinationGame,stageAssassinationBattle,prepareAssassinationBattle,resolveAssassinationBattle,assassinationChoice,assassinationToMentat,assassinationPhysical,assassinationRejects,assassinationActions} from './moritani-assassinate-fixture';
|
||||
import {advanceToNextStorm,finishToMovement} from './fixture-advanced-source';
|
||||
|
||||
void test('a real lost battle reveals one different opposing traitor, kills its native disc once and replaces the physical card at Mentat',()=>{
|
||||
const staged=stageAssassinationBattle(assassinationGame()),inventory=assassinationPhysical(staged);
|
||||
@@ -96,17 +97,15 @@ void test('a normal Moritani traitor reveal forfeits assassination and cannot cr
|
||||
});
|
||||
|
||||
void test('a sourced normal-reveal forfeiture persists across turns and denies a later genuine loss',()=>{
|
||||
const first=assassinationToMentat(resolveAssassinationBattle(stageAssassinationBattle(assassinationGame(['m','g','e']),'guild-0'),{normalCall:true}));
|
||||
const first=finishToMovement(advanceToNextStorm(assassinationToMentat(resolveAssassinationBattle(stageAssassinationBattle(assassinationGame(['m','g','e']),'guild-0'),{normalCall:true}))));
|
||||
assert.equal(first.moritaniAssassinate!.normalTraitorCall,true);assert.equal(first.moritaniAssassinate!.opportunities.length,0);
|
||||
assert.match(viewGame(first,'m').moritaniAssassinate!.blocked!,/rest of this game/);
|
||||
first.turn++;
|
||||
assert.equal(first.turn,2);
|
||||
first.players=[first.players[0],first.players[2],first.players[1]];
|
||||
const second=resolveAssassinationBattle(stageAssassinationBattle(first));
|
||||
assert.equal(second.lastBattleContext!.result,'normal');assert.notEqual(second.lastBattleContext!.winner,'m');
|
||||
assert.equal(second.moritaniAssassinate!.normalTraitorCall,true);
|
||||
assert.equal(second.moritaniAssassinate!.opportunities.length,0);
|
||||
assert.notEqual(second.decision?.kind,'moritaniAssassinate');
|
||||
assert.match(viewGame(second,'m').moritaniAssassinate!.blocked!,/rest of this game/);
|
||||
});
|
||||
|
||||
void test('saved assassination receipts and cleanup obligations fail closed after corruption',()=>{
|
||||
|
||||
@@ -190,14 +190,8 @@ void test('already-dead targets remain revealable for no death or spice, while p
|
||||
void test('the sourced normal-reveal forfeiture and already normally revealed cards stay explicit private guards', () => {
|
||||
const context = fixture();
|
||||
context.state.normalTraitorCall = true;
|
||||
assert.match(
|
||||
moritaniAssassinateChoices(context).blocked!,
|
||||
/forfeited Assassinate Leaders for the rest of this game/,
|
||||
);
|
||||
assert.throws(
|
||||
() => quoteMoritaniAssassinate(context, 'atreides-1'),
|
||||
/forfeited Assassinate Leaders for the rest of this game/,
|
||||
);
|
||||
assert.deepEqual(moritaniAssassinateChoices(context).cards, []);
|
||||
assert.throws(() => quoteMoritaniAssassinate(context, 'atreides-1'));
|
||||
context.state.normalTraitorCall = false;
|
||||
context.normallyRevealed = ['atreides-1'];
|
||||
const choices = moritaniAssassinateChoices(context);
|
||||
@@ -205,16 +199,9 @@ void test('the sourced normal-reveal forfeiture and already normally revealed ca
|
||||
choices.cards.map((c) => c.card),
|
||||
['atreides-2'],
|
||||
);
|
||||
assert.match(choices.unavailable[0].reason, /normally revealed/);
|
||||
assert.throws(
|
||||
() => quoteMoritaniAssassinate(context, 'atreides-1'),
|
||||
/eligible/,
|
||||
);
|
||||
assert.throws(() => quoteMoritaniAssassinate(context, 'atreides-1'));
|
||||
context.normallyRevealed = ['atreides-1', 'atreides-2'];
|
||||
assert.match(
|
||||
moritaniAssassinateChoices(context).blocked!,
|
||||
/normally revealed/,
|
||||
);
|
||||
assert.deepEqual(moritaniAssassinateChoices(context).cards, []);
|
||||
});
|
||||
|
||||
void test('missing, duplicated, altered or exceptional target custody cannot select another physical leader or invent bounty', () => {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {applyAction,viewGame} from '../game/engine';
|
||||
import {botActions} from '../game/bots';
|
||||
import {DIFFICULTIES} from '../game/bot-profiles';
|
||||
import {NATIVE_MENTAT_FAMILIES,nativeMentatBattle} from './fixture-native-mentat';
|
||||
|
||||
for (const family of NATIVE_MENTAT_FAMILIES) {
|
||||
void test(`${family} Mentat requires an explicit preview and resumes native posture after a matching or fallback answer`,()=>{
|
||||
const ordinary = nativeMentatBattle(family,true,false);
|
||||
assert.equal(ordinary.battle!.mentatQuestion,undefined);
|
||||
assert.equal(ordinary.decision!.kind,'leaderSkillVisibility');
|
||||
for (const matching of [true,false]) {
|
||||
let game = nativeMentatBattle(family,matching);
|
||||
assert.equal(game.decision!.kind,'mentatQuestion');
|
||||
const receipt = game.battle!.mentatQuestion!;
|
||||
const weapon = game.expansions.includes('ix') ? 'Weirding Way' : 'Crysknife';
|
||||
const shown = game.players.find(p => p.id === receipt.target)!.hand[0];
|
||||
const owner = viewGame(game,receipt.owner);
|
||||
assert.ok(owner.mentat.pending!.weapons.includes(weapon));
|
||||
assert.ok(!owner.mentat.pending!.weapons.includes('Chemistry'));
|
||||
game = applyAction(game,receipt.owner,{type:'decision',event:receipt.event,weapon});
|
||||
const choice = viewGame(game,receipt.target).mentat.pending!;
|
||||
assert.deepEqual(choice.cards,[shown]);
|
||||
game = applyAction(game,receipt.target,{type:'decision',event:receipt.event,card:shown.id});
|
||||
assert.equal(game.decision!.kind,'leaderSkillVisibility');
|
||||
assert.equal(viewGame(game,receipt.owner).mentat.history[0].card.id,shown.id);
|
||||
assert.ok(game.players.find(p => p.id === receipt.target)!.hand.some(c => c.id === shown.id));
|
||||
assert.equal(game.battle!.preparation,undefined);
|
||||
game = applyAction(game,receipt.owner,{type:'leaderSkillVisibility',event:game.battle!.event,hide:true});
|
||||
assert.equal(game.battle!.leaderSkillHidden![receipt.owner],true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void test('all four policies complete native Ixian Mentat naming and compulsory disclosure before posture',()=>{
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
let game = nativeMentatBattle('ixians');
|
||||
const receipt = game.battle!.mentatQuestion!;
|
||||
for (const id of [receipt.owner,receipt.target]) {
|
||||
const view = viewGame(game,id);
|
||||
view.players.find(p => p.id === id)!.bot = difficulty;
|
||||
const action = botActions(view)[0];
|
||||
assert.equal(action.event,receipt.event);
|
||||
game = applyAction(game,id,action);
|
||||
}
|
||||
assert.equal(game.battle!.mentatQuestion!.stage,'answered');
|
||||
assert.equal(game.decision!.kind,'leaderSkillVisibility');
|
||||
assert.equal(viewGame(game,receipt.owner).mentat.history[0].card.id,game.players.find(p => p.id === receipt.target)!.hand[0].id);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user