Support public leader preparation with Harkonnen
Verify and publish checkpoint container / container (push) Successful in 10m56s

This commit is contained in:
Quintine committed 2026-10-29 08:51:11 +11:00
1 parent f9ca7ed9a9
commit 31c6e79c80
6 files changed
+288 -11

No files matched your search

+27 -8
View File
@@ -993,6 +993,8 @@ export type Battle = {
basicEarlyLeaderless?: string[];
/** Fresh classic battles with a neutral dead-leader preparation opportunity. */
basicDeadLeaderPreparation?: true;
/** Fresh Harkonnen games qualify early notices from public leader snapshots. */
basicPublicLeaderlessVersion?: 1;
revealed: boolean;
traitorCalls: Record<string, boolean>;
traitorDecisionPromises?: TraitorDecisionPromise[];
@@ -43026,6 +43028,17 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
charityOrderIntegrity(g);
return g;
}
/** Public native-disc state, independent of concealed capture identity. */
function publicNativeBattleLeaders(player: Player, territory: string) {
const leaders = player.leaders.map(leader => ({
dead: leader.concealed ? leader.concealed.dead : leader.dead,
usedAt: leader.concealed ? leader.concealed.usedAt : leader.usedAt,
captured: !leader.concealed && !!leader.capturedBy,
}));
return { hasDead: leaders.some(leader => leader.dead),
unavailable: !leaders.some(leader => !leader.dead && !leader.captured &&
(!leader.usedAt || leader.usedAt === territory)) };
}
/** Publish a clean Basic inability once preparation and battle powers finish. */
function announceEarlyBasicLeaderless(g: Game) {
const b = g.battle;
@@ -43036,7 +43049,9 @@ function announceEarlyBasicLeaderless(g: Game) {
for (const id of [b.attacker, b.defender]) {
if (b.plans[id]) continue;
const p = getPlayer(g, id), leaders = controlledLeaders(g, p);
const availableLeader = leaders.some(l => !l.dead && !l.capturedBy && (!l.usedAt || l.usedAt === b.territory));
// controlledLeaders already includes captives that Harkonnen may use.
// Keep this identical to the availability check in validatePlan.
const availableLeader = leaders.some(l => !l.dead && (!l.usedAt || l.usedAt === b.territory));
const availableHero = p.hand.some(c => c.kind === 'hero' && !(b.voice?.target === id && !b.voice.must &&
playedVoiceMatch(c, 'leader', b.voice.kind)));
if (b.basicEarlyLeaderless?.includes(id)) {
@@ -43046,7 +43061,11 @@ function announceEarlyBasicLeaderless(g: Game) {
}
continue;
}
if (!leaders.length || availableLeader || availableHero ||
if (b.basicPublicLeaderlessVersion === 1 &&
(!publicNativeBattleLeaders(p, b.territory).unavailable ||
p.faction === 'harkonnen' && g.players.some(owner => owner.leaders.some(leader =>
leader.concealed?.captor === p.id && leader.capturedBy === p.id)))) continue;
if ((!leaders.length && !b.basicDeadLeaderPreparation) || availableLeader || availableHero ||
!b.basicDeadLeaderPreparation && leaders.some(l => l.dead || l.capturedBy)) continue;
(b.basicEarlyLeaderless ??= []).push(id);
log(g, `${p.name} has no available leader or Cheap Hero for this battle.`);
@@ -47371,19 +47390,19 @@ function applyActionInner(
!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));
// The candidate depends only on public native leaders, never either hand.
// Harkonnen's concealed captures need a separate public-projection rule.
const deadLeaderCandidates = earlyBasic && !byFaction(g, 'harkonnen')
// Use the same pre-capture snapshot visible to uninvolved players. A
// neutral preparation opportunity must not identify a concealed captive.
const deadLeaderCandidates = earlyBasic
? [choice.attacker, choice.defender].filter(playerId => {
const leaders = getPlayer(g, playerId).leaders;
return leaders.some(leader => leader.dead) && !leaders.some(leader =>
!leader.dead && !leader.capturedBy && (!leader.usedAt || leader.usedAt === choice.territory));
const publicState = publicNativeBattleLeaders(getPlayer(g, playerId), choice.territory);
return publicState.hasDead && publicState.unavailable;
}) : [];
g.battle = {
...choice,
chooserEvent: battleChooserEvent(g.turn, battleOrderAfterBattle(g), g.battleOrder?.uses.length ?? 0),
event: battleEvent,
...(earlyBasic ? { basicEarlyLeaderlessVersion: 1 as const } : {}),
...(earlyBasic && byFaction(g, 'harkonnen') ? { basicPublicLeaderlessVersion: 1 as const } : {}),
...(deadLeaderCandidates.length ? { basicDeadLeaderPreparation: true as const,
preLeader: { event: battleEvent,
ready: [choice.attacker, choice.defender].filter(playerId => !deadLeaderCandidates.includes(playerId)),