From ac7db3cb35d2da87ed930f7e8d5416b7bdfdf7ec Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:18:48 +1100 Subject: [PATCH] Preserve Fremen revival grant opportunity for bot allies --- components/fremen-revival-grant.tsx | 5 + docs/CURRENT_STATUS.md | 11 ++ docs/FREMEN_REVIVAL_GRANT.md | 13 ++ game/bots.ts | 20 +++ game/reference.ts | 6 +- .../fremen-revival-grant-bot-priority.test.ts | 140 ++++++++++++++++++ tests/fremen-revival-grant-controls.test.tsx | 8 + 7 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 tests/fremen-revival-grant-bot-priority.test.ts diff --git a/components/fremen-revival-grant.tsx b/components/fremen-revival-grant.tsx index dcf8c297..895b9673 100644 --- a/components/fremen-revival-grant.tsx +++ b/components/fremen-revival-grant.tsx @@ -40,6 +40,7 @@ export function FremenRevivalGrant({ if (!grant || game.status !== 'playing' || game.phase !== 4 || me?.faction !== 'fremen' || me.ally !== grant.recipient) return null; const recipient = game.players.find((player) => player.id === grant.recipient)?.name ?? 'your ally'; + const automatedAlly = game.players.find(player => player.id === grant.recipient); const disabled = busy || !!game.truthtrance || !!game.nexusTraitors?.pending || !!grant.blocked || grant.granted; return ( @@ -50,6 +51,10 @@ export function FremenRevivalGrant({ replaces their native ordinary rate rather than adding three. Ordinary total revival and special-force limits still apply.
+ {!game.advanced && !game.revivalOrder && !group && + (automatedAlly?.bot || automatedAlly?.autopilot) && !grant.granted && !grant.blocked && ( +When this grant would help your automated ally, it waits to revive forces until you grant it or select Ready for next phase.
+ )}
Provisional current-rate policy: Recruits doubles the current granted rate, including applicable bonuses,
before caps. Without other bonuses, three becomes six.
diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md
index cd409bf7..cdbb4aab 100644
--- a/docs/CURRENT_STATUS.md
+++ b/docs/CURRENT_STATUS.md
@@ -17,6 +17,17 @@ open mode gates.
## Current checkpoint and work
+**Basic Fremen ally revival scheduling — 29 October. Prototyped / Partial.**
+An automated mutual ally now waits for a useful human Fremen free-revival grant
+in concurrent Basic Revival, until the grant resolves or the Fremen selects the
+existing Ready action. Karama responses remain active; other bot actions do not
+wait. The Fremen panel explains the choice. Genuine setup, battle casualties,
+Nexus alliance, grant/prevention and all four automated profiles are covered;
+the four-file related batch passes 40 cases, and type checking, lint and the
+production build pass. Ordered Revival, larger alliances,
+other opportunity races and deployed acceptance remain open.
+[Scope](FREMEN_REVIVAL_GRANT.md#human-fremen-and-an-automated-ally--29-october-2026).
+
**Opt-in CHOAM market continuation — 29 October. Prototyped / Partial.**
In supported Basic CHOAM/classic games, a human can finish a market and choose
to skip later markets for that turn; an owner-only persistent control revokes
diff --git a/docs/FREMEN_REVIVAL_GRANT.md b/docs/FREMEN_REVIVAL_GRANT.md
index 7ff0c6e0..8e7d8fc2 100644
--- a/docs/FREMEN_REVIVAL_GRANT.md
+++ b/docs/FREMEN_REVIVAL_GRANT.md
@@ -1,5 +1,18 @@
# Fremen allied free revival
+## Human Fremen and an automated ally — 29 October 2026
+
+**Prototyped / Partial.** During concurrent Basic Revival, an automated mutual
+ally waits to make its ordinary force return or finish Revival while a human
+Fremen has an eligible grant that would increase that ally's remaining free
+allowance and the ally has forces in the Tanks. The Fremen may use the existing
+grant action; its ordinary Karama response still resolves before the ally acts.
+The Fremen's existing Ready for next phase also releases the wait, as do an
+accepted or prevented grant. Other bot actions and responses remain available.
+The Fremen panel explains this scheduling choice. This is a provisional online
+priority policy, not a printed rule or a new mandatory confirmation. Ordered
+Revival, larger alliances and other races are not covered by this policy.
+
## Staged three-member alliance — 28 October 2026
In the unexposed Basic classic group prototype, Fremen may select each of its
diff --git a/game/bots.ts b/game/bots.ts
index 097aa3cf..bed554fb 100644
--- a/game/bots.ts
+++ b/game/bots.ts
@@ -2,6 +2,7 @@ import { liveMovementExpressionPromises } from './movement-expression-promises';
import { isAlliedShipmentVisit } from './allied-separation';
import { discoveryBotActions } from './discovery-options';
import { normalAuctionHandBlock } from './normal-auction';
+import { fremenRevivalGrantBlock } from './fremen-revival-grant';
import { atreidesFullPlanAction } from './atreides-full-plan';
import { liveMovementPromises } from './movement-promises';
import { ecazOccupyDialChoices, ecazOccupyOwnProfile, ecazOccupyPolicyActions } from './ecaz-occupy-options';
@@ -60,6 +61,8 @@ import { strongholdBattleEffects } from './stronghold-battle';
import {
eliteRevivalRemaining,
forceRevivalPrice,
+ freeRevivalRemaining,
+ revivalPrevented,
paidForceRevivalCost,
} from './revival';
import { controlsLeader, nativeAvailable } from './leader-control';
@@ -4661,6 +4664,21 @@ 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. */
+function waitsForHumanFremenGrant(g: Game, player: Game['players'][number]): boolean {
+ if (g.status !== 'playing' || g.advanced || g.phase !== 4 || g.revivalOrder ||
+ g.coalitionState || player.tanks <= 0 || revivalPrevented(g, player.id)) return false;
+ const fremen = g.players.find(seat => seat.faction === 'fremen');
+ if (!fremen || fremen.bot || fremen.autopilot || fremen.ally !== player.id ||
+ player.ally !== fremen.id || g.ready.includes(fremen.id) ||
+ fremenRevivalGrantBlock(g, fremen.id) !== null) return false;
+ const current = freeRevivalRemaining(g, player);
+ const granted = freeRevivalRemaining({ ...g, freeRevival: [...g.freeRevival, player.id] }, player);
+ return Math.min(player.tanks, granted) > Math.min(player.tanks, current);
+}
+
export function runBots(state: Game, limit = 96): Game {
const supplier = state.players.find((p) => p.faction === 'richese');
const unopenedAllyWindow =
@@ -4700,6 +4718,8 @@ export function runBots(state: Game, limit = 96): Game {
actors.sort((a, b) => Number(b.faction === 'guild') - Number(a.faction === 'guild'));
for (const p of actors) {
for (const action of botActions(viewGame(g, p.id))) {
+ if ((action.type === 'revive' || action.type === 'ready') && waitsForHumanFremenGrant(g, p))
+ continue;
try {
next = applyAction(g, p.id, action);
break;
diff --git a/game/reference.ts b/game/reference.ts
index 1df759a8..82c36541 100644
--- a/game/reference.ts
+++ b/game/reference.ts
@@ -6553,7 +6553,7 @@ 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. 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.",
"In the Basic group preview, Fremen can provisionally grant each ally the rate separately; each named declaration has its own Karama response. Group interactions remain incomplete."
],
"related": [
@@ -6588,9 +6588,10 @@ export const RULE_TOPICS: RuleTopic[] = [
{
"area": "AI",
"status": "Partial",
- "detail": "Existing-profile native/Recruits paths and staged group grants use the same projected legal permissions. Strategy and strength calibration remain deferred.",
+ "detail": "Existing-profile native/Recruits paths and staged group grants use the same projected legal permissions. In concurrent Basic Revival a mutual automated ally waits for a useful human Fremen grant until it is resolved or Fremen is Ready. Strategy and strength calibration remain deferred.",
"evidence": [
"game/bots.ts",
+ "tests/fremen-revival-grant-bot-priority.test.ts",
"tests/recruits-controls.test.tsx",
"docs/FREMEN_REVIVAL_GRANT.md"
]
@@ -6615,6 +6616,7 @@ export const RULE_TOPICS: RuleTopic[] = [
"tests/fremen-revival-grant-engine.test.ts",
"tests/fremen-revival-grant-quote.test.ts",
"tests/fremen-revival-grant-controls.test.tsx",
+ "tests/fremen-revival-grant-bot-priority.test.ts",
"tests/recruits-engine.test.ts",
"tests/recruits-controls.test.tsx"
]
diff --git a/tests/fremen-revival-grant-bot-priority.test.ts b/tests/fremen-revival-grant-bot-priority.test.ts
new file mode 100644
index 00000000..ff10021f
--- /dev/null
+++ b/tests/fremen-revival-grant-bot-priority.test.ts
@@ -0,0 +1,140 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { viewGame, type Game } from '../game/engine';
+import { botActions, runBots } from '../game/bots';
+import { DIFFICULTIES, type Difficulty } from '../game/bot-profiles';
+import { actFremenRevivalGrant as act, advanceFremenRevivalGrant as advance,
+ createFremenRevivalGrantFixture, fremenRevivalGrantClean as clean,
+ fremenRevivalGrantPlayer as player, passFremenRevivalGrantResponses as pass } from './fixture-fremen-revival-grant';
+
+let fixture: ReturnType