Align Advanced Ix starting cards with Harkonnen pool
This commit is contained in:
1 parent
e530d267a3
commit
84fdd404c9
14 files changed
+637
-12
No files matched your search
+19
-8
@@ -571,6 +571,7 @@ import {
|
||||
type MoritaniRetention,
|
||||
} from './moritani-retention';
|
||||
import { ordinaryCardAvailability } from './card-availability';
|
||||
import { IxStartingCardError, ixStartingCardPoolSize, ixStartingCardDealPlan } from './ixian-starting-cards';
|
||||
import { ecazDukeRevivalBlock, quoteEcazDukeRevival } from './ecaz-duke-revival';
|
||||
import {
|
||||
reserveShipmentCost,
|
||||
@@ -10897,11 +10898,18 @@ function initializeStartingForces(g: Game) {
|
||||
if (g.homeworlds)
|
||||
g.homeworlds.custody = createHomeworldCustody(homeworldContext(g));
|
||||
}
|
||||
function ixStartingRule<T>(operation: () => T): T {
|
||||
try { return operation(); } catch (error) {
|
||||
if (error instanceof IxStartingCardError) throw new RuleError(error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
function dealStartingTreachery(g: Game) {
|
||||
const ixians = byFaction(g, 'ixians');
|
||||
if (ixians) {
|
||||
g.ixSetupCards = [];
|
||||
for (const _player of g.players) {
|
||||
const poolSize = ixStartingRule(() => ixStartingCardPoolSize(g));
|
||||
for (let index = 0; index < poolSize; index++) {
|
||||
const card = draw(g);
|
||||
if (card) g.ixSetupCards.push(card);
|
||||
}
|
||||
@@ -39997,23 +40005,26 @@ function applyActionInner(
|
||||
g.status === 'setup' && p.faction === 'ixians' && g.ixSetupCards,
|
||||
'Ixian starting-card selection is not available.',
|
||||
);
|
||||
const plan = ixStartingRule(() => ixStartingCardDealPlan(g, g.ixSetupCards!.length));
|
||||
const card = g.ixSetupCards.find((c) => c.id === action.card);
|
||||
requireRule(card, 'Choose one of the starting cards.');
|
||||
p.hand.push(card);
|
||||
const remainder = shuffle(g.ixSetupCards.filter((c) => c.id !== card.id));
|
||||
for (const other of g.players.filter((other) => other.id !== p.id)) {
|
||||
const dealt = remainder.shift();
|
||||
if (dealt) other.hand.push(dealt);
|
||||
for (const seat of plan.seats) {
|
||||
const other = getPlayer(g, seat.player);
|
||||
for (let count = 0; count < seat.amount; count++) {
|
||||
const dealt = remainder.shift();
|
||||
if (dealt) other.hand.push(dealt);
|
||||
}
|
||||
}
|
||||
const harkonnen = byFaction(g, 'harkonnen');
|
||||
if (harkonnen) {
|
||||
if (plan.deckBonus) {
|
||||
const bonus = draw(g);
|
||||
if (bonus) harkonnen.hand.push(bonus);
|
||||
if (bonus) getPlayer(g, plan.deckBonus).hand.push(bonus);
|
||||
}
|
||||
g.ixSetupCards = null;
|
||||
log(
|
||||
g,
|
||||
`${p.name} selected a starting card. The other starting cards were shuffled and dealt privately.`,
|
||||
`${p.name} selected a starting card. The other starting cards were shuffled and dealt privately.${plan.kind === 'advanced-pool' ? ' Harkonnen received both starting cards from that inspected pool.' : ''}`,
|
||||
);
|
||||
} else if (decision.kind === 'ixAuction') {
|
||||
requireRule(
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { FACTIONS } from './catalog';
|
||||
|
||||
export type IxStartingCardGame = {
|
||||
advanced: boolean;
|
||||
players: readonly { id: string; faction: string }[];
|
||||
};
|
||||
export type IxStartingCardDealPlan = {
|
||||
kind: 'advanced-pool' | 'legacy-deck-bonus';
|
||||
seats: { player: string; amount: number }[];
|
||||
deckBonus: string | null;
|
||||
};
|
||||
export class IxStartingCardError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'IxStartingCardError';
|
||||
}
|
||||
}
|
||||
|
||||
function validateStartingRoster(game: IxStartingCardGame): void {
|
||||
if (!game || typeof game !== 'object' || typeof game.advanced !== 'boolean' ||
|
||||
!Array.isArray(game.players) || game.players.length < 2 || game.players.length > 6)
|
||||
throw new IxStartingCardError('Ixian starting cards require a two-to-six-player native roster and rules mode.');
|
||||
const ids = new Set<string>();
|
||||
const factions = new Set<string>();
|
||||
for (const player of game.players) {
|
||||
if (!player || typeof player !== 'object' || typeof player.id !== 'string' ||
|
||||
!player.id || player.id.trim() !== player.id || ids.has(player.id) ||
|
||||
!FACTIONS.some(faction => faction.id === player.faction) || factions.has(player.faction))
|
||||
throw new IxStartingCardError('Ixian starting cards require distinct registered factions and player identities.');
|
||||
ids.add(player.id);
|
||||
factions.add(player.faction);
|
||||
}
|
||||
if (!factions.has('ixians'))
|
||||
throw new IxStartingCardError('The starting-card pool requires the native Ixian faction.');
|
||||
}
|
||||
|
||||
/** Fresh supplied Advanced p34 includes Harkonnen's second card in Ixians' pool. */
|
||||
export function ixStartingCardPoolSize(game: IxStartingCardGame): number {
|
||||
validateStartingRoster(game);
|
||||
return game.players.length +
|
||||
(game.advanced && game.players.some(player => player.faction === 'harkonnen') ? 1 : 0);
|
||||
}
|
||||
|
||||
/** poolCount is the recorded original pool, including the card Ixians will keep.
|
||||
* Old N-card and historical short pools retain the original deck-bonus suffix.
|
||||
*/
|
||||
export function ixStartingCardDealPlan(game: IxStartingCardGame, poolCount: number): IxStartingCardDealPlan {
|
||||
const maximum = ixStartingCardPoolSize(game);
|
||||
if (!Number.isSafeInteger(poolCount) || poolCount < 1 || poolCount > maximum)
|
||||
throw new IxStartingCardError('The recorded Ixian starting-card pool has an invalid size.');
|
||||
const harkonnen = game.players.find(player => player.faction === 'harkonnen');
|
||||
const advancedPool = game.advanced && !!harkonnen && poolCount === game.players.length + 1;
|
||||
return {
|
||||
kind: advancedPool ? 'advanced-pool' : 'legacy-deck-bonus',
|
||||
seats: game.players.filter(player => player.faction !== 'ixians').map(player => ({
|
||||
player: player.id,
|
||||
amount: advancedPool && player.faction === 'harkonnen' ? 2 : 1,
|
||||
})),
|
||||
deckBonus: advancedPool ? null : harkonnen?.id ?? null,
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -2075,7 +2075,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
summary:
|
||||
'Private starting-card selection, auction preparation, advanced substitution and allied purchase replacement.',
|
||||
steps: [
|
||||
'Before starting treachery cards are dealt, Ixians inspect one card per faction and choose one to keep. Shuffle and privately deal the remainder, one per other faction. Harkonnen receives its additional card from the deck afterward.',
|
||||
'Before starting Treachery Cards are dealt, Ixians choose one from the original inspected pool. Fresh Advanced includes one extra card when Harkonnen is seated: shuffle the remainder and deal two to Harkonnen, one to each other faction, without an additional deck draw. Basic retains one card per faction in the pool and a separate Harkonnen top-deck bonus. An already-opened old-count Advanced draft keeps its recorded procedure without a redeal.',
|
||||
'Before the auction is prepared, everyone receives a Karama response window. Allowing Ixian inspection draws one extra card above the number up for bid. Ixians privately return one to the top or bottom of the deck, then shuffle the rest for auction. Canceling leaves only the ordinary auction cards and reveals none to Ixians.',
|
||||
'A full Ixian hand does not prevent its inspection advantage. The number of auction cards is determined by factions able to bid. The private pool record is unordered and includes cards already auctioned; it is not a view of another player’s hand or the upcoming card.',
|
||||
'In advanced play, before bidding on a card and before Atreides gets its peek, Ixians may offer one hand card for the upcoming unseen card. Declining saves the option for a later card. Declaring an exchange spends the once-per-round attempt and opens a separate Karama response; cancellation keeps both cards where they are.',
|
||||
|
||||
Reference in new issue
Block a user