Support staged group Fremen revival grants
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
9081249574
commit
d8d2e7207b
9 files changed
+277
-15
No files matched your search
@@ -3426,6 +3426,9 @@ function policyActions(g: GameView): Action[] {
|
||||
}
|
||||
if (g.fremenRevivalGrant && !g.fremenRevivalGrant.granted && !g.fremenRevivalGrant.blocked)
|
||||
actions.push({ type: 'grantRevival' });
|
||||
for (const grant of g.fremenGroupRevivalGrant ?? [])
|
||||
if (!grant.granted && !grant.blocked)
|
||||
actions.push({type:'grantRevival', target:grant.recipient});
|
||||
const emperorGroup = g.coalitionEmperor;
|
||||
const emperorAllies = me.ally ? [me.ally] : emperorGroup?.allies ?? [];
|
||||
if (me.faction === 'emperor' && level > 0) for (const allyId of emperorAllies) {
|
||||
|
||||
+5
-3
@@ -656,7 +656,7 @@ import { quoteEcazPoisonIncome, EcazPoisonIncomeError, type EcazPoisonDiscard }
|
||||
import { resolveBattleWeapons } from './effective-weapons';
|
||||
import { choamGholaEvent, choamGholaSkillOfferEvent, choamGholaSkillSignature, choamMarketGholaError, choamSaleGholaTiming, type ChoamMarketGhola } from './choam-market-ghola';
|
||||
import { quoteAdvisorPreparationWindow, quoteAdvisorPreparation, validateAdvisorPreparation, projectAdvisorPreparationWindow, AdvisorPreparationError, type AdvisorPreparationWindow, type AdvisorPreparationSource } from './advisor-preparation-window';
|
||||
import { quoteFremenRevivalGrant, validateFremenRevivalGrant, projectFremenRevivalGrant, FremenRevivalGrantError, type FremenRevivalGrantSource } from './fremen-revival-grant';
|
||||
import { quoteFremenRevivalGrant, validateFremenRevivalGrant, projectFremenRevivalGrant, projectFremenGroupRevivalGrant, FremenRevivalGrantError, type FremenRevivalGrantSource } from './fremen-revival-grant';
|
||||
import { splitOrdinaryPayments, type OrdinaryPayment } from './ordinary-payment-plan';
|
||||
import {
|
||||
charityAmount,
|
||||
@@ -44722,10 +44722,11 @@ function applyActionInner(
|
||||
return g;
|
||||
}
|
||||
if (t === 'grantRevival') {
|
||||
requireRule(g.phase === 4 && p.faction === 'fremen' && p.ally,
|
||||
requireRule(g.phase === 4 && p.faction === 'fremen',
|
||||
'Only Fremen may grant their ally free revival during revival.');
|
||||
requireRule(!recruitsInteractionPending(g), 'Finish the current revival transaction or priority window before granting allied revival.');
|
||||
const source = fremenRevivalGrantRule(() => quoteFremenRevivalGrant(g, id));
|
||||
const target = projectFremenGroupRevivalGrant(g, id) ? stringField(action.target) : undefined;
|
||||
const source = fremenRevivalGrantRule(() => quoteFremenRevivalGrant(g, id, target));
|
||||
g.response = { kind: 'fremenRevivalGrant', owner: source.owner,
|
||||
recipient: source.recipient, fremenGrant: source, passed: [] };
|
||||
return g;
|
||||
@@ -46979,6 +46980,7 @@ export function viewGame(state: Game, id: string, now?: number) {
|
||||
freeRevival: g.freeRevival ?? [],
|
||||
recruitsPreview: recruitsPreview(g, me),
|
||||
fremenRevivalGrant: projectFremenRevivalGrant(g, id),
|
||||
fremenGroupRevivalGrant: projectFremenGroupRevivalGrant(g, id),
|
||||
advisorPreparation: advisorPreparationView(g, id),
|
||||
revivalRules: {
|
||||
...(g.revivalRules ?? newRevivalRules()),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Game } from './engine';
|
||||
import { coalitionOf, CoalitionAllianceError } from './coalition-alliance';
|
||||
|
||||
export type FremenRevivalGrantSource = {
|
||||
turn: number;
|
||||
@@ -9,7 +10,8 @@ export type FremenRevivalGrantSource = {
|
||||
export type FremenRevivalGrantContext = Pick<
|
||||
Game,
|
||||
'status' | 'phase' | 'turn' | 'players' | 'freeRevival' | 'revivalRules' | 'recruits'
|
||||
>;
|
||||
> & Partial<Pick<Game, 'coalitionState' | 'advanced' | 'expansions' | 'homeworlds' |
|
||||
'nexusCards' | 'leaderSkills' | 'techTokens' | 'strongholdCards' | 'ecazTreachery' | 'discoveryEnabled'>>;
|
||||
|
||||
export class FremenRevivalGrantError extends Error {
|
||||
constructor(message: string) {
|
||||
@@ -23,12 +25,30 @@ const id = (value: unknown): value is string =>
|
||||
const count = (value: unknown): value is number =>
|
||||
Number.isSafeInteger(value) && (value as number) >= 0;
|
||||
|
||||
/** Match the unexposed Basic classic group profile; other profiles retain the
|
||||
* existing mutual-pair path until their interactions are connected. */
|
||||
function groupRecipients(g: FremenRevivalGrantContext, owner: string): string[] | null {
|
||||
if (!g.coalitionState || g.status !== 'playing' || g.advanced !== false ||
|
||||
!Array.isArray(g.expansions) || g.expansions.length || g.homeworlds || g.nexusCards ||
|
||||
g.leaderSkills || g.techTokens || g.strongholdCards || g.ecazTreachery || g.discoveryEnabled ||
|
||||
!g.players.every(player => ['atreides', 'beneGesserit', 'emperor', 'fremen', 'guild', 'harkonnen'].includes(player.faction)))
|
||||
return null;
|
||||
try {
|
||||
const group = coalitionOf(g.players, g.coalitionState, owner);
|
||||
return group?.members.length === 3 ? group.members.filter(member => member !== owner) : null;
|
||||
} catch (error) {
|
||||
if (error instanceof CoalitionAllianceError) throw new FremenRevivalGrantError(error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/** Permission only. The engine owns the response and commits or denies this
|
||||
* source once; normal revival quotes retain rates, prior usage and payment.
|
||||
* No busy gate belongs here because settlement validates its own response. */
|
||||
export function fremenRevivalGrantBlock(
|
||||
g: FremenRevivalGrantContext,
|
||||
owner: string,
|
||||
selectedRecipient?: string,
|
||||
): string | null {
|
||||
if (g.status !== 'playing' || g.phase !== 4 || !count(g.turn) || g.turn < 1)
|
||||
return 'Grant Fremen allied free revival during the current Revival phase.';
|
||||
@@ -39,10 +59,13 @@ export function fremenRevivalGrantBlock(
|
||||
if (owners.length !== 1 || fremen.faction !== 'fremen' ||
|
||||
g.players.filter(player => player.faction === 'fremen').length !== 1)
|
||||
return 'The grant needs the current native Fremen owner.';
|
||||
const recipients = g.players.filter(player => player.id === fremen.ally);
|
||||
const group = groupRecipients(g, owner);
|
||||
const recipientId = group ? selectedRecipient : fremen.ally;
|
||||
const recipients = g.players.filter(player => player.id === recipientId);
|
||||
const recipient = recipients[0];
|
||||
if (!id(fremen.ally) || recipients.length !== 1 ||
|
||||
recipient.id === owner || recipient.ally !== owner)
|
||||
if (!id(recipientId) || recipients.length !== 1 || recipient.id === owner ||
|
||||
(group ? !group.includes(recipientId) : recipient.ally !== owner ||
|
||||
(selectedRecipient !== undefined && selectedRecipient !== fremen.ally)))
|
||||
return 'The grant needs the Fremen owner’s current mutual ally.';
|
||||
if (!Array.isArray(g.freeRevival))
|
||||
return 'The current Fremen revival permissions are invalid.';
|
||||
@@ -67,11 +90,12 @@ export function fremenRevivalGrantBlock(
|
||||
export function quoteFremenRevivalGrant(
|
||||
g: FremenRevivalGrantContext,
|
||||
owner: string,
|
||||
recipient?: string,
|
||||
): FremenRevivalGrantSource {
|
||||
const blocked = fremenRevivalGrantBlock(g, owner);
|
||||
const blocked = fremenRevivalGrantBlock(g, owner, recipient);
|
||||
if (blocked) throw new FremenRevivalGrantError(blocked);
|
||||
const fremen = g.players.find(player => player.id === owner)!;
|
||||
return { turn: g.turn, owner, recipient: fremen.ally! };
|
||||
return { turn: g.turn, owner, recipient: groupRecipients(g, owner) ? recipient! : fremen.ally! };
|
||||
}
|
||||
|
||||
/** Bind the original relationship and turn before either response outcome.
|
||||
@@ -85,11 +109,26 @@ export function validateFremenRevivalGrant(
|
||||
!count(source.turn) || source.turn !== g.turn ||
|
||||
!id(source.owner) || !id(source.recipient))
|
||||
throw new FremenRevivalGrantError('The Fremen allied revival grant has lost its original source.');
|
||||
const current = quoteFremenRevivalGrant(g, source.owner);
|
||||
const current = quoteFremenRevivalGrant(g, source.owner, source.recipient);
|
||||
if (current.recipient !== source.recipient)
|
||||
throw new FremenRevivalGrantError('The Fremen allied revival grant no longer matches its original ally.');
|
||||
}
|
||||
|
||||
/** Separate owner-only projection avoids changing the established pair API. */
|
||||
export function projectFremenGroupRevivalGrant(
|
||||
g: FremenRevivalGrantContext,
|
||||
viewer: string,
|
||||
): Array<{recipient: string; granted: boolean; blocked: string | null}> | null {
|
||||
if (!id(viewer) || !Array.isArray(g.players) ||
|
||||
g.players.filter(player => player.id === viewer && player.faction === 'fremen').length !== 1)
|
||||
return null;
|
||||
const recipients = groupRecipients(g, viewer);
|
||||
if (!recipients) return null;
|
||||
return recipients.map(recipient => ({recipient,
|
||||
granted: Array.isArray(g.freeRevival) && g.freeRevival.includes(recipient),
|
||||
blocked: fremenRevivalGrantBlock(g, viewer, recipient)}));
|
||||
}
|
||||
|
||||
/** Owner-only status. No wallet, Tanks, prior counts or quoted free amount is
|
||||
* projected; the existing normal revival panel owns numerical allowances. */
|
||||
export function projectFremenRevivalGrant(
|
||||
|
||||
+6
-4
@@ -6538,7 +6538,8 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
"During Revival, Fremen may offer the current ally its ordinary free-force rate of three. The grant replaces the native ordinary rate; current bonuses, total limits and separate special-force limits still apply. It never moves counters by itself.",
|
||||
"The original owner, ally and turn remain bound while another player may prevent the grant with Karama. All-pass commits once. Under the provisional prevention policy, cancellation denies this grant for the current Revival, preserving the ally’s intrinsic free rate, Recruits and completed returns/payments. The next native Revival clears the denial.",
|
||||
"Recruits doubles the current granted rate including applicable bonuses before caps. With no other bonus, three becomes six. Provisionally accept grants before or after the card with the same calculation: deduct recorded free usage, preserve all earlier total returns and paid classifications, and give no refund. Finish a pending transaction before declaring a grant.",
|
||||
"The Fremen panel keeps status and unavailable reasons visible. Your ally’s normal revival panel supplies the actual remaining amount. Complete modes, broader combinations and final acceptance remain unfinished."
|
||||
"The Fremen panel keeps status and unavailable reasons visible. Your ally’s normal revival panel supplies the actual remaining amount. Complete modes, broader combinations and final acceptance remain unfinished.",
|
||||
"In an unexposed staged Basic three-member group, Fremen can provisionally grant each ally the rate separately; each named declaration has its own Karama response. Group games remain unavailable in the lobby."
|
||||
],
|
||||
"related": [
|
||||
"revival",
|
||||
@@ -6550,7 +6551,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
{
|
||||
"area": "Implementation",
|
||||
"status": "Partial",
|
||||
"detail": "Original owner/mutual-ally/turn source validates before cancellation cost and both outcomes. Only all-pass commits; cancellation marks a phase-scoped denied grant without general free prevention. Existing native arithmetic retains limits, prices, bonuses and prior usage.",
|
||||
"detail": "Original owner/ally/turn source validates before cancellation cost and both outcomes. Staged Basic groups bind an explicit recipient. Only all-pass commits; cancellation marks a recipient-specific phase denial. Existing native arithmetic retains limits, prices, bonuses and prior usage.",
|
||||
"evidence": [
|
||||
"game/fremen-revival-grant.ts",
|
||||
"game/engine.ts",
|
||||
@@ -6562,7 +6563,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
{
|
||||
"area": "Player controls",
|
||||
"status": "Partial",
|
||||
"detail": "Actual grant callback, native response and persistent status/policy guidance are integrated. Busy, interrupted, already granted and denied actions stay disabled; no private balance, Tanks or usage count is read.",
|
||||
"detail": "Actual pair and staged group grant callbacks, native response and persistent status/policy guidance are integrated. Busy, interrupted, already granted and denied actions stay disabled; no private balance, Tanks or usage count is read.",
|
||||
"evidence": [
|
||||
"components/fremen-revival-grant.tsx",
|
||||
"components/game-table.tsx",
|
||||
@@ -6572,7 +6573,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
{
|
||||
"area": "AI",
|
||||
"status": "Partial",
|
||||
"detail": "Sixteen existing-profile native/Recruits paths accept80 legal own-view grant/response/return proposals. The same projected permission prevents repeat grants. Strategy and strength calibration remain deferred.",
|
||||
"detail": "Existing-profile native/Recruits paths and staged group grants use the same projected legal permissions. Strategy and strength calibration remain deferred.",
|
||||
"evidence": [
|
||||
"game/bots.ts",
|
||||
"tests/recruits-controls.test.tsx",
|
||||
@@ -6594,6 +6595,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
"status": "Partial",
|
||||
"detail": "14 files305/305, types/lint/build, twelve actual control/gameplay/GameTable SSR journeys and sixteen minimal-policy paths pass. Genuine75–77 Basic/92–94 Advanced actions earn casualties and form a native Nexus alliance; Recruits is originally dealt and prior returns paid normally. Next actual Revival resets denial. Complete modes, visual/deployed and comprehensive assurance remain open.",
|
||||
"evidence": [
|
||||
"tests/coalition-fremen-revival.test.ts",
|
||||
"tests/fixture-fremen-revival-grant.ts",
|
||||
"tests/fremen-revival-grant-engine.test.ts",
|
||||
"tests/fremen-revival-grant-quote.test.ts",
|
||||
|
||||
Reference in new issue
Block a user