Announce eligible Basic leaderless battles before plans
Verify and publish checkpoint container / container (push) Successful in 10m34s

This commit is contained in:
Quintine committed 2026-10-29 06:59:11 +11:00
1 parent 2bac45b294
commit 685bd8bdfc
8 files changed
+417 -11

No files matched your search

+32 -1
View File
@@ -985,6 +985,9 @@ export type Battle = {
plans: Record<string, Plan>;
/** Basic declarations recorded only when a new leaderless plan is sealed. */
basicLeaderlessAnnouncements?: string[];
/** Fresh clean Basic battles announce a bounded no-revival case before plans. */
basicEarlyLeaderlessVersion?: 1;
basicEarlyLeaderless?: string[];
revealed: boolean;
traitorCalls: Record<string, boolean>;
traitorDecisionPromises?: TraitorDecisionPromise[];
@@ -42679,6 +42682,25 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
charityOrderIntegrity(g);
return g;
}
/** The published inability is safe before a plan only when no leader can be
* revived and this clean Basic roster cannot privately transfer a Cheap Hero.
* Other cases retain the original seal-time announcement. */
function announceEarlyBasicLeaderless(g: Game) {
const b = g.battle;
if (!b || b.basicEarlyLeaderlessVersion !== 1 || g.status !== 'playing' || g.advanced ||
g.phase !== 6 || b.revealed || !b.prepared || b.preparation || b.preLeader && !b.preLeader.closed ||
g.response || g.decision || g.truthtrance || g.phaseOpening || g.pendingKarama ||
g.pendingKull || g.pendingTreacheryDiscard || g.pendingNullentropy) return;
for (const id of [b.attacker, b.defender]) {
if (b.plans[id] || b.basicEarlyLeaderless?.includes(id)) continue;
const p = getPlayer(g, id), leaders = controlledLeaders(g, p);
if (!leaders.length || leaders.some(l => l.dead || l.capturedBy || !l.usedAt || l.usedAt === b.territory) ||
p.hand.some(c => c.kind === 'hero' && !(b.voice?.target === id && !b.voice.must &&
playedVoiceMatch(c, 'leader', b.voice.kind)))) continue;
(b.basicEarlyLeaderless ??= []).push(id);
log(g, `${p.name} has no available leader or Cheap Hero for this battle.`);
}
}
function finishActionContinuations(g: Game) {
if (g.victoryContinuation) return;
if (g.pendingNexusHarkonnenBetrayal) return;
@@ -42751,6 +42773,7 @@ function finishActionContinuations(g: Game) {
!g.decision && !g.response && !g.truthtrance && !g.phaseOpening &&
!g.pendingKarama && !g.pendingTerrorEntry && !g.pendingAmbassador)
finishMoritaniPlacement(g);
announceEarlyBasicLeaderless(g);
}
function settleAutomaticContinuations(g: Game) {
if (arrivalOrderWaiting(g)) return;
@@ -46984,6 +47007,11 @@ function applyActionInner(
...choice,
chooserEvent: battleChooserEvent(g.turn, battleOrderAfterBattle(g), g.battleOrder?.uses.length ?? 0),
event: battleEvent,
...(!g.advanced && g.expansions.length === 0 && !g.ecazTreachery && !g.techTokens &&
!g.strongholdCards && !g.leaderSkills && !g.homeworlds && !g.nexusCards && !g.discoveryEnabled &&
!g.sandtrout && !g.coalitionState && g.players.every(seat =>
['atreides', 'beneGesserit', 'emperor', 'fremen', 'guild', 'harkonnen'].includes(seat.faction))
? { basicEarlyLeaderlessVersion: 1 as const } : {}),
stoneAllocationVersion: 1,
...(g.leaderSkills ? { smugglerCollectionVersion: 1 as const } : {}),
...(g.leaderSkills && mentatQuestionModeSupported(g) ? { mentatQuestionVersion: 1 as const } : {}),
@@ -47151,7 +47179,8 @@ function applyActionInner(
b.plans[id] = validatePlan(g, p, action);
if (b.plans[id].leader === null) {
if (!g.advanced) (b.basicLeaderlessAnnouncements ??= []).push(id);
log(g, `${p.name} announced a Battle Plan without a leader or Cheap Hero; no battle cards can be played.`);
if (!b.basicEarlyLeaderless?.includes(id))
log(g, `${p.name} announced a Battle Plan without a leader or Cheap Hero; no battle cards can be played.`);
}
if (b.fullPlan?.target === id) {
recordFullPlanInspection(g, b.fullPlan.owner);
@@ -49031,6 +49060,8 @@ export function viewGame(state: Game, id: string, now?: number) {
})
: [],
submitted: Object.keys(b.plans),
...(b.basicEarlyLeaderless?.some(playerId => !b.plans[playerId])
? { earlyLeaderless: b.basicEarlyLeaderless.filter(playerId => !b.plans[playerId]) } : {}),
leaderless: g.advanced
? Object.keys(b.plans).filter(playerId => b.plans[playerId].leader === null)
: (b.basicLeaderlessAnnouncements ?? []).filter(playerId => b.plans[playerId]?.leader === null),
+1 -1
View File
@@ -2839,7 +2839,7 @@ export const RULE_TOPICS: RuleTopic[] = [
summary: 'Build a legal combination and decide what to keep after winning.',
steps: [
'Choose one weapon, one defense, both, or neither. A worthless card can occupy a weapon or defense slot.',
'Choose an available leader or Cheap Hero when required. Without either, battle cards cannot be played. In Basic and Advanced, sealing a legal leaderless plan announces that fact to the whole table before reveal; its dial, spice and other choices remain sealed. Refresh retains the same announcement without submitting another plan.',
'Choose an available leader or Cheap Hero when required. Without either, battle cards cannot be played. In fresh classic Basic battles without optional modules, when all your leaders are alive but unavailable elsewhere and you lack a legal Cheap Hero, your inability is announced before plans. Other Basic and Advanced leaderless plans announce on sealing; its dial, spice and other choices remain sealed. Refresh retains the same announcement without submitting another plan.',
'On Arrakis, a surviving leader remains in its battle territory for the rest of the Battle phase. A later Lasgun–Shield explosion there also kills previously surviving leader discs still present, even if a different leader fights that later battle. The Basic application is provisional; a successful Traitor call prevents the explosion.',
'A loser normally discards all played cards. If allied with Moritani and the battle has a winner, the defeated ally may retain one played card that a winner could keep. A winner may discard selected played cards; Cheap Heroes are discarded after use.',
],