Recognize group Guild shipment promises
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 03:50:33 +11:00
1 parent 6eb417ff32
commit 44e7cf8001
4 files changed
+145 -7

No files matched your search

+9 -6
View File
@@ -17,6 +17,15 @@ open mode gates.
## Current checkpoint and work ## Current checkpoint and work
**Group Fremen–Guild shipment promise — 29 October. Prototyped / Partial.**
In a staged Basic group, the Truthtrance shipment search now recognizes a legal
Fremen reserve shipment through its Guild ally. A focused journey forms the
group through Nexus, stages Movement, then completes the real question and
paid shipment; all four bot profiles have a legal path. Twenty-five related
cases, typecheck, lint and the production build pass. Group lobby admission
and full rules acceptance remain open.
[Scope](OPTIONAL_ALLIANCE_VICTORY.md#group-fremenguild-shipment-answer--29-october-2026).
**Truthtrance card types — 29 October. Prototyped / Partial.** Verified **Truthtrance card types — 29 October. Prototyped / Partial.** Verified
current-hand questions can distinguish primary projectile and poison weapons current-hand questions can distinguish primary projectile and poison weapons
and primary projectile and poison defenses. Physical hybrid cards count for and primary projectile and poison defenses. Physical hybrid cards count for
@@ -25,12 +34,6 @@ focused cases pass, including human and bot answers; typecheck, lint and the
production build pass. Full Truthtrance and mode acceptance remain open. production build pass. Full Truthtrance and mode acceptance remain open.
[Scope](TRUTHTRANCE_HAND_INVENTORY.md#primary-attack-and-defense-types--29-october-2026). [Scope](TRUTHTRANCE_HAND_INVENTORY.md#primary-attack-and-defense-types--29-october-2026).
**Group-alliance lobby remains unexposed.** A source inspection found that
the Basic shipment-promise search checks the scalar Fremen–Guild alliance
even though a three-member coalition clears that scalar. The native transport
permission has a group-aware path. This is a candidate legal-action gap to
reproduce and resolve before exposing group play; no group lobby was added.
**Whole-board Truthtrance forces — 29 October. Prototyped / Partial.** **Whole-board Truthtrance forces — 29 October. Prototyped / Partial.**
Players can ask a verified physical-force question across the current Arrakis Players can ask a verified physical-force question across the current Arrakis
board. The room's current locations include a placed Mobile Stronghold and board. The room's current locations include a placed Mobile Stronghold and
+19
View File
@@ -1,5 +1,24 @@
# Optional three-stronghold alliance victory # Optional three-stronghold alliance victory
## Group Fremen–Guild shipment answer — 29 October 2026
**Prototyped / Partial.** In the staged Basic group, a Fremen member can
answer Yes to a Truthtrance reserve-shipment question when its actual Guild
ally can transport the group to the named territory. The completion search
uses the same group-aware native Guild permission as the transport itself,
including its cancellation state, rather than relying on the pair-only ally
field cleared by a larger coalition. The printed transport, two native Karama
response windows, actual cost and physical force transfer remain unchanged.
The [focused journey](../tests/coalition-fremen-guild-truth-shipment.test.ts)
forms a group through actual Nexus actions after staging an empty ledger; it
then stages the Movement phase, conserved force pools and wallets and transfers
the actual Truthtrance and Karama from the undealt deck. It verifies the legal
Guild route to Carthag, a real question and fulfillment, and minimal legal
responses across four bot profiles. Group consent and timing are provisional
online interpretations; this does not expose a lobby option or certify full
group play, Advanced combinations or other shipment continuations.
## Self-funded Guild transport with a live promise — 29 October 2026 ## Self-funded Guild transport with a live promise — 29 October 2026
In staged Basic and Advanced group games, a Guild ally's transport quote now In staged Basic and Advanced group games, a Guild ally's transport quote now
+1 -1
View File
@@ -32347,7 +32347,7 @@ function findShipmentCompletion(
if (!(error instanceof RuleError)) throw error; if (!(error instanceof RuleError)) throw error;
} }
} }
if (p.faction === 'fremen' && byFaction(g, 'guild')?.id === p.ally) { if (p.faction === 'fremen' && nativeGuildTransportPermission(g, p)) {
const action: Action = { const action: Action = {
type: 'guildShip', type: 'guildShip',
from: 'reserves', from: 'reserves',
@@ -0,0 +1,116 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { baseDeck } from '../game/cards';
import { botActions } from '../game/bots';
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 player = (game: Game, id: string) => game.players.find(seat => seat.id === id)!;
const shipment: Action = {type:'guildShip', from:'reserves', territory:'carthag', sector:11, amount:3, elite:0, allyPayment:0};
/** The unexposed group mode requires a staged empty ledger and later Movement
* phase; the pair and third member join using actual Nexus actions. Forces and
* wallets are staged with physical totals intact, and the actual Truthtrance
* card is transferred from the undealt deck. No saved room is touched. */
function fixture() {
let game = createGame('GROUPFREMENSHIP', newPlayer('g', 'Guild', 'guild'), false);
game.players.push(newPlayer('e', 'Emperor', 'emperor'), newPlayer('f', 'Fremen', 'fremen'),
newPlayer('h', 'Harkonnen', 'harkonnen'));
Object.assign(game, {status:'playing', phase:1, turn:2, nexus:true, order:['g','e','f','h'],
ready:[], storm:18, deck:baseDeck(), response:null, decision:null, phaseOpening:null});
game.coalitionState = emptyCoalitionState();
game = act(game, 'g', {type:'alliance', target:'e'});
game = act(game, 'e', {type:'alliance', target:'g'});
const pair = coalitionOf(game.players, game.coalitionState, 'g'); assert.ok(pair);
game = act(game, 'g', {type:'coalitionPropose', groupId:pair.id, entrant:'f',
expectedRevision:game.coalitionState!.revision});
for (const id of ['e','f']) {
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});
}
assert.equal(player(game,'f').ally, null, 'A three-member group does not use the pair-only scalar.');
Object.assign(game, {phase:5, nexus:false, active:'f', ready:[],
movementRemaining:['f','g','e','h'], guildTimingGranted:false, guildTimingLocked:false});
for (const seat of game.players) {
seat.forces = {}; seat.reserves = 20; seat.tanks = 0; seat.spice = 10;
seat.hand = []; seat.traitors = []; seat.traitorChoices = []; seat.shipped = false; seat.moved = 0;
}
const index = game.deck.findIndex(card => card.effect === 'truthtrance'); assert.ok(index >= 0);
const [truth] = game.deck.splice(index,1); player(game,'h').hand.push(truth);
// A distinct undealt Karama gives the outside player a real opportunity to
// counter the native permission and rate; it stays held when passed.
const karama = game.deck.findIndex(card => card.effect === 'karama'); assert.ok(karama >= 0);
player(game,'h').hand.push(...game.deck.splice(karama,1));
return {game, truth};
}
function settle(game: Game): Game {
for (let n = 0; game.response && n < 32; n++) {
const seat = game.players.find(candidate => !game.response!.passed.includes(candidate.id)); assert.ok(seat);
game = act(game,seat.id,{type:'passResponse'});
}
assert.equal(game.response ?? null,null);
return game;
}
function ask() {
const {game:before, truth} = fixture();
let game = act(before,'h',{type:'card',card:truth.id});
while (game.truthtrance?.stage === 'priority') {
const seat = game.players.find(candidate => !game.truthtrance!.passed.includes(candidate.id)); assert.ok(seat);
game = act(game,seat.id,{type:'truthPass'});
}
game = act(game,'h',{type:'truthAsk',question:{kind:'shipment',target:'f',turn:game.turn,
territory:'carthag',minimum:3}});
assert.equal(game.truthtrance?.stage,'answer');
return {before,game,truth};
}
void test('group-allied Fremen can actually ship from reserves to Carthag with native Guild permission', () => {
let {game} = fixture();
// Carthag is outside the Fremen southern reinforcement area. The native
// group-aware Guild path is the real legal transport for this location.
assert.throws(() => act(game,'f',{type:'ship',territory:'carthag',sector:11,amount:3,elite:0}));
game = act(game,'f',shipment);
assert.equal(game.response?.kind,'guildTransportPermission');
assert.equal(player(game,'f').reserves,20);
game = settle(game);
assert.equal(player(game,'f').forces['carthag:11'],3);
assert.equal(player(game,'f').reserves,17);
assert.equal(player(game,'f').spice,8);
assert.equal(player(game,'g').spice,12);
});
void test('a group-allied Fremen can answer Yes to the real Carthag reserve-shipment question and fulfill it', () => {
const {game:asked,truth} = ask();
assert.ok(viewGame(asked,'f').truthShipmentAnswers?.includes('yes'));
let game = act(asked,'f',{type:'truthAnswer',answer:'yes'});
assert.ok(game.shipmentPromises?.some(promise => promise.player === 'f' && promise.answer && !promise.released));
assert.equal(game.discard.filter(card => card.id === truth.id).length,1);
assert.throws(() => act(game,'f',{type:'endMovement'}),/Truthtrance/i);
game = act(game,'f',shipment);
assert.equal(game.response?.kind,'guildTransportPermission');
game = settle(game);
assert.equal(player(game,'f').forces['carthag:11'],3);
assert.equal(player(game,'f').spice,8);
assert.equal(player(game,'g').spice,12);
assert.ok(game.shipmentPromises?.some(promise => promise.player === 'f' && promise.fulfilled && !promise.released));
});
void test('all four bot profiles can answer and follow the original group Guild shipment witness', () => {
const {game:asked} = ask();
for (const difficulty of ['Easy','Medium','Hard','Brutal'] as const) {
const answerView = viewGame(asked,'f');
answerView.players.find(seat => seat.id === 'f')!.bot = difficulty;
const answer = botActions(answerView).find(action => action.type === 'truthAnswer'); assert.ok(answer);
assert.equal(answer.answer,'yes');
let game = act(structuredClone(asked),'f',answer);
const movementView = viewGame(game,'f');
movementView.players.find(seat => seat.id === 'f')!.bot = difficulty;
const next = botActions(movementView)[0]; assert.equal(next?.type,'guildShip');
assert.equal(next.from,'reserves'); assert.equal(next.territory,'carthag');
game = act(game,'f',next);
game = settle(game);
assert.ok(game.shipmentPromises?.some(promise => promise.player === 'f' && promise.fulfilled && !promise.released));
}
});