Support staged group Emperor aid
Verify and publish checkpoint container / container (push) Successful in 10m13s
Verify and publish checkpoint container / container (push) Successful in 10m13s
This commit is contained in:
1 parent
b89c225d5b
commit
9081249574
10 files changed
+413
-17
No files matched your search
+9
-6
@@ -3426,8 +3426,11 @@ function policyActions(g: GameView): Action[] {
|
||||
}
|
||||
if (g.fremenRevivalGrant && !g.fremenRevivalGrant.granted && !g.fremenRevivalGrant.blocked)
|
||||
actions.push({ type: 'grantRevival' });
|
||||
if (me.faction === 'emperor' && me.ally && level > 0) {
|
||||
const ally = g.players.find((p) => p.id === me.ally)!;
|
||||
const emperorGroup = g.coalitionEmperor;
|
||||
const emperorAllies = me.ally ? [me.ally] : emperorGroup?.allies ?? [];
|
||||
if (me.faction === 'emperor' && level > 0) for (const allyId of emperorAllies) {
|
||||
const ally = g.players.find((p) => p.id === allyId)!;
|
||||
const available = emperorGroup?.availableSpice ?? me.spice ?? 0;
|
||||
const extra =
|
||||
g.revivalPrevention?.player === ally.id
|
||||
? 0
|
||||
@@ -3439,7 +3442,7 @@ function policyActions(g: GameView): Action[] {
|
||||
eliteRevivalRemaining(ally, g.advanced),
|
||||
),
|
||||
3 - (g.emperorExtra[ally.id] ?? 0),
|
||||
Math.floor(Math.max(0, (me.spice ?? 0) - 3) / 2),
|
||||
Math.floor(Math.max(0, available - 3) / 2),
|
||||
);
|
||||
for (let amount = extra; amount > 0; amount--) {
|
||||
const minimumElite = Math.max(
|
||||
@@ -3450,8 +3453,8 @@ function policyActions(g: GameView): Action[] {
|
||||
Math.min(amount, eliteRevivalRemaining(ally, g.advanced)),
|
||||
minimumElite,
|
||||
]))
|
||||
if (paidForceRevivalCost(ally, amount, elite) <= (me.spice ?? 0) - 3)
|
||||
actions.push({ type: 'emperorRevival', amount, elite });
|
||||
if (paidForceRevivalCost(ally, amount, elite) <= available - 3)
|
||||
actions.push({ type: 'emperorRevival', amount, elite, ...(emperorGroup ? {target: ally.id} : {}) });
|
||||
}
|
||||
}
|
||||
const freeRemaining = g.revival.freeRemaining;
|
||||
@@ -3505,7 +3508,7 @@ function policyActions(g: GameView): Action[] {
|
||||
return !g.revival.orderBlocked &&
|
||||
(action.type === 'ready' || action.type === 'cancelLeaderRequest' || !g.revival.prevented);
|
||||
if (action.type === 'emperorRevival' && g.revivalOrder)
|
||||
return g.revivalOrder.remaining[0] === me.ally;
|
||||
return g.revivalOrder.remaining[0] === (action.target ?? me.ally);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
+20
-6
@@ -16060,6 +16060,14 @@ function coalitionFundingAvailable(g: Game, p: Player, committedBid = false): bo
|
||||
const group = coalitionRule(() => coalitionOf(g.players, g.coalitionState, p.id));
|
||||
return group?.members.length === 3;
|
||||
}
|
||||
function coalitionEmperorGroup(g: Game, p: Player) {
|
||||
if (!g.coalitionState || p.faction !== 'emperor' || g.status !== 'playing' || g.advanced ||
|
||||
g.expansions.length || g.homeworlds || g.nexusCards || g.leaderSkills || g.techTokens ||
|
||||
g.strongholdCards || g.ecazTreachery || g.discoveryEnabled ||
|
||||
!g.players.every(seat => ['atreides', 'beneGesserit', 'emperor', 'fremen', 'guild', 'harkonnen'].includes(seat.faction))) return null;
|
||||
const group = coalitionRule(() => coalitionOf(g.players, g.coalitionState, p.id));
|
||||
return group?.members.length === 3 ? group : null;
|
||||
}
|
||||
function guildGroupAlly(g: Game, p: Player): boolean {
|
||||
return coalitionRule(() => groupGuildShipmentRate(g.players, p.id, g.coalitionState));
|
||||
}
|
||||
@@ -44640,11 +44648,12 @@ function applyActionInner(
|
||||
return g;
|
||||
}
|
||||
if (t === 'emperorGift' || t === 'emperorRevival') {
|
||||
requireRule(
|
||||
p.faction === 'emperor' && p.ally,
|
||||
'Only the Emperor may use this power for an ally.',
|
||||
);
|
||||
const recipient = getPlayer(g, p.ally);
|
||||
const group = coalitionEmperorGroup(g, p);
|
||||
const recipientId = group ? stringField(action.target) : p.ally;
|
||||
requireRule(p.faction === 'emperor' && recipientId &&
|
||||
(group ? recipientId !== id && group.members.includes(recipientId) : recipientId === p.ally),
|
||||
'Only the Emperor may use this power for an ally.');
|
||||
const recipient = getPlayer(g, recipientId);
|
||||
if (t === 'emperorRevival') {
|
||||
requireRule(
|
||||
!revivalPrevented(g, recipient.id),
|
||||
@@ -44685,7 +44694,7 @@ function applyActionInner(
|
||||
);
|
||||
if (t === 'emperorRevival')
|
||||
requireRule(
|
||||
p.spice >= paidForceRevivalCost(recipient, amount, elite),
|
||||
uncommittedSpice(g, p) >= paidForceRevivalCost(recipient, amount, elite),
|
||||
'Not enough spice for these extra revivals.',
|
||||
);
|
||||
if (liveShipmentPromises(g.shipmentPromises ?? [], p.id, g.turn).length ||
|
||||
@@ -47019,6 +47028,11 @@ export function viewGame(state: Game, id: string, now?: number) {
|
||||
? g.aid[me.ally].amount
|
||||
: 0,
|
||||
},
|
||||
coalitionEmperor: (() => {
|
||||
const group = coalitionEmperorGroup(g, me);
|
||||
return group ? {allies: group.members.filter(member => member !== id),
|
||||
availableSpice: Math.max(0, uncommittedSpice(g, me))} : null;
|
||||
})(),
|
||||
coalitionFunding: coalitionFundingAvailable(g, me) ? (() => {
|
||||
const group = coalitionRule(() => coalitionOf(g.players, g.coalitionState, id))!;
|
||||
const pledges = g.coalitionEscrow!.pledges;
|
||||
|
||||
+4
-4
@@ -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 alliance target shares the progress and actual victory calculation; default and two-controller targets remain unchanged.', evidence: ['game/alliance-victory-option.ts', 'game/victory-progress.ts', 'game/victory-quote.ts', 'game/engine.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'The lobby offers the option and the active table displays its scope.', evidence: ['components/game-table.tsx', 'tests/short-alliance-victory-controls.test.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'Existing legal alliance and victory paths use the same shared target; strategic tuning remains deferred.', evidence: ['game/bots.ts', 'game/victory-progress.ts'] },
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'A host-selectable shorter two-member target shares victory calculation. Unexposed staged Basic groups connect Nexus, selected funding and the Emperor’s targeted aid; other group benefits remain open.', evidence: ['game/alliance-victory-option.ts', 'game/victory-progress.ts', 'game/victory-quote.ts', 'game/engine.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 target, lobby, native Nexus and staged Mentat evidence; larger alliances and full combinations remain open.', evidence: ['tests/alliance-victory-option.test.ts', 'tests/short-alliance-victory-runtime.test.ts'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Focused pair and staged group evidence includes real Nexus actions and Emperor aid; 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'] },
|
||||
],
|
||||
related: [
|
||||
'mentat',
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
GuildAmbassadorContinuationError,
|
||||
} from './guild-ambassador-continuation';
|
||||
import type { Game, ResponseWindow } from './engine';
|
||||
import { coalitionOf, CoalitionAllianceError } from './coalition-alliance';
|
||||
import {
|
||||
homeworldMovementForesightBlock,
|
||||
homeworldSpiritualAdvisorAllowance,
|
||||
@@ -57,6 +58,7 @@ export type TerminalCancellationContext = Pick<
|
||||
| 'turn'
|
||||
| 'advanced'
|
||||
| 'players'
|
||||
| 'coalitionState'
|
||||
| 'auction'
|
||||
| 'richeseAuction'
|
||||
| 'stormCard'
|
||||
@@ -138,10 +140,21 @@ export function validateTerminalCancellation(
|
||||
case 'emperorRevival': {
|
||||
faction('emperor');
|
||||
amount();
|
||||
let allied = owner.ally === response.recipient;
|
||||
if (!allied && g.coalitionState) {
|
||||
try {
|
||||
const group = coalitionOf(g.players, g.coalitionState, owner.id);
|
||||
allied = !!group && group.members.length === 3 && !!response.recipient &&
|
||||
group.members.includes(response.recipient);
|
||||
} catch (error) {
|
||||
if (error instanceof CoalitionAllianceError) throw new TerminalCancellationError(error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
requireTerminal(
|
||||
recipient() &&
|
||||
response.recipient !== owner.id &&
|
||||
owner.ally === response.recipient,
|
||||
allied,
|
||||
'The canceled Emperor power needs its originally declared ally.',
|
||||
);
|
||||
const elite = response.elite ?? 0;
|
||||
|
||||
Reference in new issue
Block a user