Respect Fremen group revival grant for bots
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
e439f3f7b6
commit
f9ca7ed9a9
6 files changed
+203
-7
No files matched your search
@@ -23,6 +23,9 @@ export function FremenRevivalGrant({
|
||||
<h3 className="font-serif text-xl">Fremen group revival</h3>
|
||||
<p className="muted">Choose an ally to receive the ordinary Fremen free-revival rate of three for this phase. This replaces their native rate; normal revival limits still apply.</p>
|
||||
<p className="fine">The grant to each ally is a provisional interpretation of the larger-alliance rule. Each declaration opens a Karama response.</p>
|
||||
{!game.advanced && !game.revivalOrder && group.some(option => !option.granted && !option.blocked &&
|
||||
game.players.some(player => player.id === option.recipient && (player.bot || player.autopilot))) &&
|
||||
<p className="fine">When a grant would help an automated ally, it waits to revive forces until you grant it or select Ready for next phase.</p>}
|
||||
{group.map(option => {
|
||||
const name = game.players.find(player => player.id === option.recipient)?.name ?? 'Ally';
|
||||
return <div key={option.recipient}>
|
||||
|
||||
@@ -17,6 +17,19 @@ open mode gates.
|
||||
|
||||
## Current checkpoint and work
|
||||
|
||||
**Basic group Fremen revival scheduling — 29 October. Prototyped / Partial.**
|
||||
During concurrent Basic Revival, automated group allies wait individually for
|
||||
a human Fremen's eligible, useful free-revival grant. Grant, targeted Karama
|
||||
prevention, Fremen Ready or no added benefit releases the affected ally;
|
||||
unrelated bot actions and responses remain available. The Fremen panel explains
|
||||
the wait without exposing Tanks. Four related files pass 40 cases, including
|
||||
15 new cases across both allies and all four legal bot profiles; type checking,
|
||||
lint and production build pass. The tests form the group through real Nexus
|
||||
actions, with staged Revival and conserved Tanks. This is a provisional online
|
||||
priority, not strategic AI calibration; ordered Revival, other group interactions
|
||||
and deployed acceptance remain open.
|
||||
[Scope](FREMEN_REVIVAL_GRANT.md#human-fremen-and-automated-group-allies--29-october-2026).
|
||||
|
||||
**Basic dead-leader battle preparation — 29 October. Prototyped / Partial.**
|
||||
Fresh classic Basic battles without Harkonnen open the existing neutral
|
||||
pre-leader opportunity when a combatant has a dead leader and no available
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# Fremen allied free revival
|
||||
|
||||
## Human Fremen and automated group allies — 29 October 2026
|
||||
|
||||
**Prototyped / Partial.** In concurrent Basic Revival, each automated ally in
|
||||
the supported classic group preview waits for its own useful, available Fremen
|
||||
grant. Fremen may grant each ally independently, finish with the existing Ready
|
||||
action, or have a targeted grant prevented. Only that recipient's ordinary
|
||||
revival and Ready actions wait; unrelated bot actions and Karama responses
|
||||
continue. The shared grant quote and normal revival allowances determine when
|
||||
waiting is useful. The group panel explains this scheduling without showing
|
||||
allied Tanks or balances. This is a provisional online priority, not a printed
|
||||
timing rule or strategic AI calibration. The test uses genuine Nexus group
|
||||
formation with staged Revival and conserved Tanks; other group interactions,
|
||||
ordered Revival and deployed acceptance remain open. The later
|
||||
[Basic classic group lobby preview](OPTIONAL_ALLIANCE_VICTORY.md#basic-classic-group-lobby-preview--29-october-2026)
|
||||
supersedes older notes below that group starts were unavailable.
|
||||
|
||||
## Human Fremen and an automated ally — 29 October 2026
|
||||
|
||||
**Prototyped / Partial.** During concurrent Basic Revival, an automated mutual
|
||||
@@ -11,7 +27,8 @@ 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.
|
||||
Revival and other races are not covered by this policy. The group follow-up
|
||||
above extends it to supported larger alliances.
|
||||
|
||||
## Staged three-member alliance — 28 October 2026
|
||||
|
||||
|
||||
+3
-4
@@ -4670,11 +4670,10 @@ export function botActions(g: GameView): Action[] {
|
||||
* 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;
|
||||
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;
|
||||
if (!fremen || fremen.bot || fremen.autopilot || g.ready.includes(fremen.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);
|
||||
return Math.min(player.tanks, granted) > Math.min(player.tanks, current);
|
||||
|
||||
+3
-2
@@ -6562,7 +6562,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.",
|
||||
"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."
|
||||
"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": [
|
||||
"revival",
|
||||
@@ -6596,10 +6596,11 @@ 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. 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.",
|
||||
"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.",
|
||||
"evidence": [
|
||||
"game/bots.ts",
|
||||
"tests/fremen-revival-grant-bot-priority.test.ts",
|
||||
"tests/coalition-fremen-bot-grant.test.ts",
|
||||
"tests/recruits-controls.test.tsx",
|
||||
"docs/FREMEN_REVIVAL_GRANT.md"
|
||||
]
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { FremenRevivalGrant } from '../components/fremen-revival-grant';
|
||||
import { baseDeck } from '../game/cards';
|
||||
import { botActions, runBots } from '../game/bots';
|
||||
import { DIFFICULTIES, type Difficulty } from '../game/bot-profiles';
|
||||
import { coalitionOf, emptyCoalitionState } from '../game/coalition-alliance';
|
||||
import { applyAction, createGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
|
||||
|
||||
const act = (game:Game, id:string, action:Action) => applyAction(game,id,action);
|
||||
const seat = (game:Game, id:string) => {
|
||||
const player = game.players.find(candidate => candidate.id === id); assert.ok(player); return player;
|
||||
};
|
||||
|
||||
/** Real Nexus proposals, acceptance and group admission. The original ordinary
|
||||
* phase, force pools, wallets and holdings are staged at Revival, with tanks
|
||||
* moved from reserves and the counter card taken from the physical deck. */
|
||||
function fixture(difficulty:Difficulty, bot:'a'|'h' = 'a', grouped = true) {
|
||||
let game = createGame('GROUPFREMENBOT',newPlayer('f','Fremen','fremen'),false);
|
||||
game.players.push(newPlayer('a','Atreides','atreides'),newPlayer('h','Harkonnen','harkonnen'),
|
||||
newPlayer('g','Guild','guild'));
|
||||
Object.assign(game,{status:'playing',phase:1,turn:2,nexus:true,order:['f','a','h','g'],
|
||||
ready:[],storm:18,deck:baseDeck(),response:null,decision:null,phaseOpening:null});
|
||||
game.coalitionState = emptyCoalitionState();
|
||||
game = act(game,'f',{type:'alliance',target:'a'});
|
||||
game = act(game,'a',{type:'alliance',target:'f'});
|
||||
if (grouped) {
|
||||
const group = coalitionOf(game.players,game.coalitionState,'f'); assert.ok(group);
|
||||
game = act(game,'f',{type:'coalitionPropose',groupId:group.id,entrant:'h',
|
||||
expectedRevision:game.coalitionState!.revision});
|
||||
for (const id of ['a','h']) {
|
||||
const proposal = game.coalitionState!.proposal!;
|
||||
game = act(game,id,{type:'coalitionAnswer',event:proposal.event,nexus:proposal.nexus,
|
||||
turn:proposal.turn,accept:true,expectedRevision:game.coalitionState!.revision});
|
||||
}
|
||||
}
|
||||
Object.assign(game,{phase:4,nexus:false,ready:[],response:null,decision:null});
|
||||
for (const player of game.players) {
|
||||
player.forces = {}; player.reserves = 20; player.tanks = 0; player.spice = 10;
|
||||
player.hand = []; player.traitors = []; player.traitorChoices = [];
|
||||
}
|
||||
for (const id of ['a','h']) { seat(game,id).reserves = 16; seat(game,id).tanks = 4; }
|
||||
const index = game.deck.findIndex(card => card.effect === 'karama'); assert.ok(index >= 0);
|
||||
const [karama] = game.deck.splice(index,1); seat(game,'g').hand.push(karama);
|
||||
game = act(game,bot,{type:'setAutopilot',difficulty});
|
||||
return {game,bot,karama:karama.id};
|
||||
}
|
||||
|
||||
function pass(game:Game):Game {
|
||||
for (let count = 0; game.response && count < 24; count++) {
|
||||
const player = game.players.find(candidate => !game.response!.passed.includes(candidate.id)); assert.ok(player);
|
||||
game = act(game,player.id,{type:'passResponse'});
|
||||
}
|
||||
assert.equal(game.response ?? null,null); return game;
|
||||
}
|
||||
|
||||
for (const difficulty of DIFFICULTIES) for (const bot of ['a','h'] as const) void test(
|
||||
`${difficulty} group ally ${bot} waits for human Fremen and uses the granted free revival`, () => {
|
||||
let {game} = fixture(difficulty,bot);
|
||||
const priorSpice = seat(game,bot).spice, priorTanks = seat(game,bot).tanks;
|
||||
assert.equal(viewGame(game,bot).revival.freeRemaining,2);
|
||||
assert.equal(viewGame(game,'f').fremenGroupRevivalGrant?.find(row => row.recipient === bot)?.blocked,null);
|
||||
game = runBots(game,1);
|
||||
assert.equal(seat(game,bot).tanks,priorTanks);
|
||||
assert.equal(seat(game,bot).spice,priorSpice);
|
||||
assert.equal(seat(game,bot).revived,0);
|
||||
assert.ok(!game.ready.includes(bot));
|
||||
game = act(game,'f',{type:'grantRevival',target:bot});
|
||||
assert.equal(game.response?.kind,'fremenRevivalGrant');
|
||||
game = pass(game);
|
||||
assert.equal(viewGame(game,bot).revival.freeRemaining,3);
|
||||
assert.ok(botActions(viewGame(game,bot)).some(action => action.type === 'revive' && action.amount === 3));
|
||||
game = runBots(game,1);
|
||||
assert.equal(seat(game,bot).tanks,priorTanks - 3);
|
||||
assert.equal(seat(game,bot).spice,priorSpice);
|
||||
assert.equal(seat(game,bot).freeForcesRevived,3);
|
||||
},
|
||||
);
|
||||
|
||||
void test('Fremen Ready declines further group grants and releases the waiting ally', () => {
|
||||
const {game: initial,bot} = fixture('Easy'); let game = initial;
|
||||
game = runBots(game,1); assert.equal(seat(game,bot).revived,0);
|
||||
game = act(game,'f',{type:'ready'}); assert.ok(game.ready.includes('f'));
|
||||
game = runBots(game,1);
|
||||
assert.ok(seat(game,bot).revived > 0 || game.ready.includes(bot));
|
||||
});
|
||||
|
||||
void test('actual Karama prevents one group grant and releases that waiting ally', () => {
|
||||
const {game: initial,bot,karama} = fixture('Medium'); let game = initial;
|
||||
game = act(game,'h',{type:'setAutopilot',difficulty:'Hard'});
|
||||
game = act(game,'f',{type:'grantRevival',target:bot});
|
||||
assert.equal(game.response?.kind,'fremenRevivalGrant');
|
||||
game = act(game,'g',{type:'card',card:karama,mode:'cancel'});
|
||||
assert.equal(game.response ?? null,null);
|
||||
assert.ok(game.revivalRules?.fremenGrantBlocked?.includes(bot));
|
||||
assert.equal(viewGame(game,bot).revival.freeRemaining,2);
|
||||
game = runBots(game,1);
|
||||
assert.ok(seat(game,bot).revived > 0 || game.ready.includes(bot));
|
||||
assert.equal(seat(game,'h').revived,0);
|
||||
assert.ok(!game.ready.includes('h'));
|
||||
});
|
||||
|
||||
void test('an automated non-ally does not wait for a group grant it cannot receive', () => {
|
||||
let {game} = fixture('Easy');
|
||||
game = act(game,'g',{type:'setAutopilot',difficulty:'Hard'});
|
||||
seat(game,'g').reserves = 16; seat(game,'g').tanks = 4;
|
||||
game = runBots(game,1);
|
||||
assert.ok(seat(game,'g').revived > 0 || game.ready.includes('g'));
|
||||
assert.equal(seat(game,'a').revived,0);
|
||||
});
|
||||
|
||||
void test('a group ally with only its two already-free forces in Tanks does not wait', () => {
|
||||
const {game: initial,bot} = fixture('Easy'); let game = initial;
|
||||
// Move surplus counters back to the same native reserve pool.
|
||||
seat(game,bot).reserves += 2; seat(game,bot).tanks -= 2;
|
||||
assert.equal(viewGame(game,bot).revival.freeRemaining,2);
|
||||
game = runBots(game,1);
|
||||
assert.equal(seat(game,bot).tanks,0);
|
||||
assert.equal(seat(game,bot).revived,2);
|
||||
});
|
||||
|
||||
void test('a normal pair in a coalition-enabled game also waits for the human grant', () => {
|
||||
const {game: initial,bot} = fixture('Easy','a',false); let game = initial;
|
||||
assert.ok(game.coalitionState);
|
||||
assert.equal(seat(game,'f').ally,bot);
|
||||
game = runBots(game,1);
|
||||
assert.equal(seat(game,bot).revived,0);
|
||||
assert.ok(!game.ready.includes(bot));
|
||||
});
|
||||
|
||||
void test('two automated group allies both wait for the human decision', () => {
|
||||
let {game} = fixture('Easy','a');
|
||||
game = act(game,'h',{type:'setAutopilot',difficulty:'Hard'});
|
||||
game = runBots(game,12);
|
||||
for (const id of ['a','h']) {
|
||||
assert.equal(seat(game,id).revived,0);
|
||||
assert.equal(seat(game,id).tanks,4);
|
||||
assert.ok(!game.ready.includes(id));
|
||||
}
|
||||
game = act(game,'f',{type:'grantRevival',target:'a'});
|
||||
game = pass(game);
|
||||
game = runBots(game,1);
|
||||
assert.equal(seat(game,'a').revived,3);
|
||||
assert.equal(seat(game,'h').revived,0);
|
||||
assert.ok(!game.ready.includes('h'));
|
||||
});
|
||||
|
||||
void test('group grant guidance is visible only to Fremen and does not disclose an ally’s Tanks', () => {
|
||||
const {game} = fixture('Easy');
|
||||
const markup = (id:string) => renderToStaticMarkup(createElement(FremenRevivalGrant, {
|
||||
game:viewGame(game,id),busy:false,act() {},
|
||||
}));
|
||||
const before = markup('f');
|
||||
assert.match(before,/When a grant would help an automated ally, it waits to revive forces/);
|
||||
assert.match(before,/Grant Atreides free revival/);
|
||||
assert.doesNotMatch(before,/Tanks|four forces|4 forces/);
|
||||
seat(game,'a').reserves += 2; seat(game,'a').tanks -= 2;
|
||||
assert.equal(markup('f'),before);
|
||||
assert.equal(markup('a'),'');
|
||||
assert.equal(markup('g'),'');
|
||||
});
|
||||
Reference in new issue
Block a user