Support group Harkonnen traitor calls
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-28 23:46:15 +11:00
1 parent 83ccd71821
commit 478b38f5de
10 files changed
+343 -13

No files matched your search

+2 -2
View File
@@ -4125,9 +4125,9 @@ function policyActions(g: GameView): Action[] {
if (b.revealed) {
const ownsTraitorDecision =
b.traitorVoters.includes(me.id) && !b.traitorSubmitted.includes(me.id);
const beneficiary = [b.attacker, b.defender].includes(me.id)
const beneficiary = b.traitorBeneficiary ?? ([b.attacker, b.defender].includes(me.id)
? me.id
: me.ally;
: me.ally);
const enemy = b.attacker === beneficiary ? b.defender : b.attacker;
const preferredCall =
ownsTraitorDecision &&
+10 -4
View File
@@ -3939,8 +3939,12 @@ function nexusTraitorParentSignature(g: Game, event: string) {
function traitorDeclarationContext(g: Game): TraitorDeclarationContext {
const b = g.battle!;
return { event: b.event!, attacker: b.attacker, defender: b.defender, plans: b.plans,
status: g.status, advanced: g.advanced, coalitionState: g.coalitionState, expansions: g.expansions,
homeworlds: g.homeworlds, nexusCards: g.nexusCards, leaderSkills: g.leaderSkills,
techTokens: g.techTokens, strongholdCards: g.strongholdCards, ecazTreachery: g.ecazTreachery,
discoveryEnabled: g.discoveryEnabled,
...(b.ecazOccupy ? { ecazOccupy: ecazBattleProfile(g)! } : {}),
players: g.players.map(({id,faction,ally,traitors}) => ({id,faction,ally,traitors})),
players: g.players.map(({id,faction,ally,allySinceTurn,traitors}) => ({id,faction,ally,allySinceTurn,traitors})),
heroLeaderIds: physicalTreacheryCards(g).filter(c => c.kind === 'hero').map(c => c.id),
};
}
@@ -32545,9 +32549,10 @@ function traitorVoters(g: Game, b: Battle) {
for (const owner of [b.ecazOccupy.ecaz, b.ecazOccupy.ally])
if (!voters.includes(owner)) voters.push(owner);
const harkonnen = byFaction(g, 'harkonnen');
const harkonnenBeneficiary = harkonnen && coalitionBattleBeneficiary(g, harkonnen.id, b.attacker, b.defender);
if (
harkonnen?.ally &&
voters.includes(harkonnen.ally) &&
harkonnen && harkonnenBeneficiary &&
voters.includes(harkonnenBeneficiary) &&
!voters.includes(harkonnen.id)
)
voters.push(harkonnen.id);
@@ -32555,7 +32560,7 @@ function traitorVoters(g: Game, b: Battle) {
}
function traitorBeneficiary(g: Game, b: Battle, id: string) {
if (b.ecazOccupy?.lead && [b.ecazOccupy.ecaz, b.ecazOccupy.ally].includes(id)) return b.ecazOccupy.lead;
return [b.attacker, b.defender].includes(id) ? id : getPlayer(g, id).ally;
return coalitionBattleBeneficiary(g, id, b.attacker, b.defender);
}
/** One native eligibility quote for declarations and private Truthtrance answers. */
function traitorCallBlock(g: Game, b: Battle, id: string, held = getPlayer(g, id).traitors): string | null {
@@ -47453,6 +47458,7 @@ export function viewGame(state: Game, id: string, now?: number) {
? { [id]: b.plans[id] }
: {},
traitorSubmitted: Object.keys(b.traitorCalls),
traitorBeneficiary: traitorVoters(g, b).includes(id) ? traitorBeneficiary(g, b, id) : null,
traitorCallChoices: (() => { const context = traitorTruthContext(g, id); return context
? turnActionTraitorChoices(g, id,
traitorDecisionRule(() => traitorDecisionChoices(context, b.traitorDecisionPromises ?? []))) : null; })(),
+2 -2
View File
@@ -4500,11 +4500,11 @@ export const RULE_TOPICS: RuleTopic[] = [
example:
'At the final turn with neither Guild nor Fremen, two players each hold two qualifying strongholds and a third holds one. The two leaders of the count share the win.',
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'A host-selectable shorter two-member target shares victory calculation. Unexposed staged Basic groups connect Nexus, selected funding, Emperor and Fremen aid, and native Voice and Prescience; other group benefits remain open.', evidence: ['game/alliance-victory-option.ts', 'game/victory-progress.ts', 'game/victory-quote.ts', 'game/engine.ts', 'game/coalition-battle-preparation.ts', 'game/terminal-cancellation.ts'] },
{ area: 'Implementation', status: 'Partial', detail: 'A host-selectable shorter two-member target shares victory calculation. Unexposed staged Basic groups connect Nexus, selected funding, Emperor and Fremen aid, Voice, Prescience and Harkonnen traitor support; other group benefits remain open.', evidence: ['game/alliance-victory-option.ts', 'game/victory-progress.ts', 'game/victory-quote.ts', 'game/engine.ts', 'game/coalition-battle-preparation.ts', 'game/traitor-declarations.ts', 'game/terminal-cancellation.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'The lobby offers the pair option. A staged group Emperor can choose either ally for spice and extra revival; group games cannot yet be started from the lobby.', evidence: ['components/game-table.tsx', 'components/coalition-emperor-aid.tsx', 'tests/short-alliance-victory-controls.test.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'Legal alliance and victory paths use the shared target; staged group Emperor bots can offer a targeted revival. Strategic tuning remains deferred.', evidence: ['game/bots.ts', 'game/victory-progress.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'The printed option and provisional two-controller precedence are recorded.', evidence: ['docs/OPTIONAL_ALLIANCE_VICTORY.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Focused pair and staged group evidence includes real Nexus actions, Emperor and Fremen aid and battle preparation; larger alliances and full combinations remain open.', evidence: ['tests/alliance-victory-option.test.ts', 'tests/short-alliance-victory-runtime.test.ts', 'tests/coalition-emperor-aid.test.ts', 'tests/coalition-fremen-revival.test.ts', 'tests/coalition-battle-preparation.test.ts', 'tests/coalition-battle-power-question.test.ts'] },
{ area: 'Verification', status: 'Partial', detail: 'Focused pair and staged group evidence includes real Nexus actions, Emperor and Fremen aid, battle preparation and allied traitor votes; larger alliances and full combinations remain open.', evidence: ['tests/alliance-victory-option.test.ts', 'tests/short-alliance-victory-runtime.test.ts', 'tests/coalition-emperor-aid.test.ts', 'tests/coalition-fremen-revival.test.ts', 'tests/coalition-battle-preparation.test.ts', 'tests/coalition-battle-power-question.test.ts', 'tests/coalition-harkonnen-traitor.test.ts', 'tests/coalition-traitor-declarations.test.ts'] },
],
related: [
'mentat',
+6 -2
View File
@@ -1,8 +1,9 @@
import { FACTIONS, type FactionId } from './catalog';
import { CHEAP_HERO_TRAITOR } from './traitors';
import { quoteEcazOccupyBattle, type EcazOccupyBattleProfile } from './ecaz-occupy-battle';
import { coalitionBattleBeneficiary, type CoalitionBattlePreparationContext } from './coalition-battle-preparation';
export type TraitorDeclarationContext = {
export type TraitorDeclarationContext = Omit<Partial<CoalitionBattlePreparationContext>, 'players'> & {
ecazOccupy?: EcazOccupyBattleProfile;
event: string;
attacker: string;
@@ -12,6 +13,7 @@ export type TraitorDeclarationContext = {
id: string;
faction: FactionId;
ally?: string | null;
allySinceTurn?: number;
traitors: readonly string[];
}[];
heroLeaderIds: readonly string[];
@@ -98,7 +100,9 @@ function declaredElements(context: TraitorDeclarationContext, voter: string) {
const combatants = [context.attacker, context.defender];
const occupy = context.ecazOccupy;
const coSide = !!occupy && [occupy.ecaz, occupy.ally].includes(voter);
const beneficiary = coSide ? occupy.lead : combatants.includes(voter) ? voter : owner.ally;
const beneficiary = coSide ? occupy.lead : combatants.includes(voter) ? voter :
owner.faction === 'harkonnen' ? coalitionBattleBeneficiary(
{...context, status: context.status ?? ''}, voter, context.attacker, context.defender) : null;
requireDeclaration(
beneficiary &&
combatants.includes(beneficiary) &&