Coordinate Fremen grants in ordered revival
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 09:19:17 +11:00
1 parent 64706cc7e0
commit 220d78c779
11 files changed
+429 -12

No files matched your search

+10 -5
View File
@@ -4665,14 +4665,19 @@ export function botActions(g: GameView): Action[] {
: [{ type: 'endMovement' }];
}
/** In concurrent Basic Revival, let a human Fremen decide its existing grant
* before its automated ally spends or finishes. Ready is the normal way to
* finish that decision; responses and unrelated actions remain available. */
/** Let a human Fremen decide a useful grant before an automated ally spends or
* finishes. Ordered Advanced uses a per-recipient release; concurrent Basic
* retains its normal Ready release. Responses and unrelated actions continue. */
function waitsForHumanFremenGrant(g: Game, player: Game['players'][number]): boolean {
if (g.status !== 'playing' || g.advanced || g.phase !== 4 || g.revivalOrder ||
const ordered = g.advanced && !!g.revivalOrder;
if (g.status !== 'playing' || g.phase !== 4 || (g.advanced && !ordered) ||
(!g.advanced && !!g.revivalOrder) ||
(ordered && g.revivalOrder!.remaining[0] !== player.id) ||
player.tanks <= 0 || revivalPrevented(g, player.id)) return false;
const fremen = g.players.find(seat => seat.faction === 'fremen');
if (!fremen || fremen.bot || fremen.autopilot || g.ready.includes(fremen.id) ||
if (!fremen || fremen.bot || fremen.autopilot || (!ordered && g.ready.includes(fremen.id)) ||
(ordered && g.fremenRevivalWaitRelease?.turn === g.turn &&
g.fremenRevivalWaitRelease.recipients.includes(player.id)) ||
fremenRevivalGrantBlock(g, fremen.id, player.id) !== null) return false;
const current = freeRevivalRemaining(g, player);
const granted = freeRevivalRemaining({ ...g, freeRevival: [...g.freeRevival, player.id] }, player);
+20
View File
@@ -1501,6 +1501,8 @@ export type Game = {
mentatVictoryAfterActions?: number;
charityPromises?: CharityPromise[];
revivalOrder?: RevivalOrder;
/** Owner's per-ally release of the provisional ordered bot wait. */
fremenRevivalWaitRelease?: { turn: number; recipients: string[] };
charityOrder?: CharityOrder;
pendingAutomaticAdvisorFlips?: AutomaticAdvisorFlipSource[];
pendingAutomaticAdvisorTransport?: AutomaticAdvisorTransport;
@@ -30531,6 +30533,7 @@ function beginPhase(g: Game) {
}
}
if (g.phase === 4) {
delete g.fremenRevivalWaitRelease;
g.revivalRules = newRevivalRules();
g.revivalRequests = {};
g.freeRevival = [];
@@ -46752,6 +46755,23 @@ function applyActionInner(
};
return g;
}
if (t === 'releaseFremenRevivalWait') {
requireRule(Object.keys(action).sort().join(',') === 'target,type' &&
g.phase === 4 && g.advanced && g.revivalOrder && p.faction === 'fremen',
'Release an automated ally only during ordered Advanced Revival.');
requireRule(!recruitsInteractionPending(g), 'Finish the current revival transaction or priority window first.');
const target = stringField(action.target);
const recipient = getPlayer(g, target);
requireRule(!!(recipient.bot || recipient.autopilot) && g.revivalOrder.remaining.includes(target),
'Choose an automated ally whose Revival turn remains.');
fremenRevivalGrantRule(() => quoteFremenRevivalGrant(g, id, target));
const released = g.fremenRevivalWaitRelease?.turn === g.turn
? g.fremenRevivalWaitRelease.recipients : [];
requireRule(!released.includes(target), 'This ally has already been released from the wait.');
g.fremenRevivalWaitRelease = {turn: g.turn, recipients: [...released, target]};
log(g, `${p.name} released ${recipient.name} to continue Revival without waiting for a Fremen grant.`);
return g;
}
if (t === 'grantRevival') {
requireRule(g.phase === 4 && p.faction === 'fremen',
'Only Fremen may grant their ally free revival during revival.');
+9 -4
View File
@@ -11,7 +11,8 @@ 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'>>;
'nexusCards' | 'leaderSkills' | 'techTokens' | 'strongholdCards' | 'ecazTreachery' | 'discoveryEnabled' |
'fremenRevivalWaitRelease'>>;
export class FremenRevivalGrantError extends Error {
constructor(message: string) {
@@ -118,7 +119,7 @@ export function validateFremenRevivalGrant(
export function projectFremenGroupRevivalGrant(
g: FremenRevivalGrantContext,
viewer: string,
): Array<{recipient: string; granted: boolean; blocked: string | null}> | null {
): Array<{recipient: string; granted: boolean; blocked: string | null; waitReleased?: boolean}> | null {
if (!id(viewer) || !Array.isArray(g.players) ||
g.players.filter(player => player.id === viewer && player.faction === 'fremen').length !== 1)
return null;
@@ -126,7 +127,9 @@ export function projectFremenGroupRevivalGrant(
if (!recipients) return null;
return recipients.map(recipient => ({recipient,
granted: Array.isArray(g.freeRevival) && g.freeRevival.includes(recipient),
blocked: fremenRevivalGrantBlock(g, viewer, recipient)}));
blocked: fremenRevivalGrantBlock(g, viewer, recipient),
...(g.fremenRevivalWaitRelease?.turn === g.turn &&
g.fremenRevivalWaitRelease.recipients.includes(recipient) ? {waitReleased: true} : {})}));
}
/** Owner-only status. No wallet, Tanks, prior counts or quoted free amount is
@@ -134,7 +137,7 @@ export function projectFremenGroupRevivalGrant(
export function projectFremenRevivalGrant(
g: FremenRevivalGrantContext,
viewer: string,
): { recipient: string; granted: boolean; blocked: string | null } | null {
): { recipient: string; granted: boolean; blocked: string | null; waitReleased?: boolean } | null {
if (!id(viewer) || !Array.isArray(g.players)) return null;
const owners = g.players.filter(player => player.id === viewer);
const owner = owners[0];
@@ -146,5 +149,7 @@ export function projectFremenRevivalGrant(
recipient: owner.ally,
granted: Array.isArray(g.freeRevival) && g.freeRevival.includes(owner.ally),
blocked: fremenRevivalGrantBlock(g, viewer),
...(g.fremenRevivalWaitRelease?.turn === g.turn &&
g.fremenRevivalWaitRelease.recipients.includes(owner.ally) ? {waitReleased: true} : {}),
};
}
+6 -3
View File
@@ -6566,6 +6566,7 @@ export const RULE_TOPICS: RuleTopic[] = [
"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. In concurrent Basic Revival an automated ally can wait for a useful grant until the Fremen grants it or finishes the phase; the existing Karama response still applies. Your ally’s normal revival panel supplies the actual remaining amount. Complete modes, broader combinations and final acceptance remain unfinished.",
"During ordered Advanced Revival, an automated ally whose turn would benefit may wait for a human Fremen's grant. Fremen can release that ally to continue without waiting, before or during its turn, and may still grant later if possible. Each eligible ally has a separate release; the choice reveals no Tanks. This is a provisional online priority.",
"In the Basic group preview, Fremen can provisionally grant each ally the rate separately; each named declaration has its own Karama response. Automated allies wait individually when a human Fremen's available grant would help, until it resolves or Fremen is ready. Group interactions remain incomplete."
],
"related": [
@@ -6590,20 +6591,22 @@ export const RULE_TOPICS: RuleTopic[] = [
{
"area": "Player controls",
"status": "Partial",
"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.",
"detail": "Actual pair and staged group grant callbacks, native response and persistent status/policy guidance are integrated. Ordered automated allies have a separate per-recipient release. 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",
"tests/fremen-revival-grant-controls.test.tsx"
"tests/fremen-revival-grant-controls.test.tsx",
"tests/fremen-ordered-revival-controls.test.tsx"
]
},
{
"area": "AI",
"status": "Partial",
"detail": "Existing-profile native/Recruits paths and group grants use the same projected legal permissions. In concurrent Basic Revival each eligible automated pair or group ally waits for a useful human Fremen grant until it is resolved or Fremen is Ready. Strategy and strength calibration remain deferred.",
"detail": "Existing-profile native/Recruits paths and group grants use the same projected legal permissions. In concurrent Basic Revival each eligible automated pair or group ally waits for a useful human Fremen grant until it is resolved or Fremen is Ready. Ordered Advanced allies use a recipient-specific release. Strategy and strength calibration remain deferred.",
"evidence": [
"game/bots.ts",
"tests/fremen-revival-grant-bot-priority.test.ts",
"tests/fremen-ordered-revival-bot-priority.test.ts",
"tests/coalition-fremen-bot-grant.test.ts",
"tests/recruits-controls.test.tsx",
"docs/FREMEN_REVIVAL_GRANT.md"