Support Harkonnen special Karama truth commitments
Verify and publish checkpoint container / container (push) Successful in 10m17s

This commit is contained in:
Quintine committed 2026-10-29 18:08:04 +11:00
1 parent 15a4f3038a
commit b448144c52
15 files changed
+449 -35

No files matched your search

+25 -3
View File
@@ -1,10 +1,11 @@
import { harvesterAvailable } from './cards';
export const WINDOW_CARD_EFFECTS = ['weather', 'atomics', 'harvester', 'karamaAuction', 'karamaShipment', 'karamaBattleCancel', 'thumper', 'amal'] as const;
export const WINDOW_CARD_EFFECTS = ['weather', 'atomics', 'harvester', 'karamaAuction', 'karamaShipment', 'karamaBattleCancel', 'karamaHarkonnenSpecial', 'thumper', 'amal'] as const;
export type WindowCardEffect = typeof WINDOW_CARD_EFFECTS[number];
export function windowCardNativeEffect(effect: WindowCardEffect): 'weather' | 'atomics' | 'harvester' | 'karama' | 'thumper' | 'amal' {
return effect === 'karamaAuction' || effect === 'karamaShipment' || effect === 'karamaBattleCancel' ? 'karama' : effect;
return effect === 'karamaAuction' || effect === 'karamaShipment' || effect === 'karamaBattleCancel' || effect === 'karamaHarkonnenSpecial' ? 'karama' : effect;
}
export type KaramaHarkonnenSpecialSource = { turn: number; owner: string };
export type KaramaBattleCancelSource = {
turn: number;
battle: string;
@@ -52,7 +53,7 @@ export type WindowCardGame = {
ixAuction?: unknown;
active?: string | null;
movementRemaining?: readonly string[] | null;
players?: readonly { id: string; shipped?: boolean }[];
players?: readonly { id: string; shipped?: boolean; faction?: string; specialKaramaUsed?: boolean }[];
karamaShipping?: { player: string } | null;
nexusGuildCunningHistory?: readonly { receipt: { turn: number; owner: string; event: string }; stage: string }[];
/** Public projection contains no physical cards or private funding. */
@@ -66,6 +67,16 @@ const identifier = (value: unknown): value is string =>
const whole = (value: unknown): value is number =>
typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
/** Decode the public owner and turn even after the special power is spent. */
export function parseKaramaHarkonnenSpecialSource(source: unknown): KaramaHarkonnenSpecialSource | null {
if (!identifier(source)) return null;
let tuple: unknown;
try { tuple = JSON.parse(source); } catch { return null; }
if (!Array.isArray(tuple) || tuple.length !== 3 || tuple[0] !== 'karamaHarkonnenSpecial' ||
!whole(tuple[1]) || tuple[1] < 1 || !identifier(tuple[2]) || JSON.stringify(tuple) !== source) return null;
return { turn: tuple[1], owner: tuple[2] };
}
/** Source decoding remains available after the actual grant closes its window. */
export function parseKaramaShipmentSource(source: unknown): KaramaShipmentSource | null {
if (!identifier(source)) return null;
@@ -158,6 +169,17 @@ export function karamaShipmentOpportunities(game: WindowCardGame): KaramaShipmen
* force conditions and chosen effect parameters remain native engine rules. */
function globalWindowCardOpportunity(game: WindowCardGame, effect: WindowCardEffect): WindowCardOpportunity | null {
if (game.status !== 'playing' || !whole(game.turn) || game.turn < 1) return null;
if (effect === 'karamaHarkonnenSpecial') {
const players = game.players;
if (game.advanced !== true || game.phase !== 3 || !Array.isArray(players) || !players.length ||
!players.every(player => object(player) && identifier(player.id) &&
typeof player.faction === 'string' && (player.specialKaramaUsed === undefined ||
typeof player.specialKaramaUsed === 'boolean')) ||
new Set(players.map(player => player.id)).size !== players.length) return null;
const owners = players.filter(player => player.faction === 'harkonnen');
if (owners.length !== 1 || owners[0].specialKaramaUsed === true) return null;
return { effect, turn: game.turn, source: JSON.stringify([effect, game.turn, owners[0].id]) };
}
if (effect === 'karamaBattleCancel') {
const battle = game.battle, response = game.response, players = game.players;
if (game.advanced !== false || game.phase !== 6 || !battle || !identifier(battle.event) ||