Recognize group Guild shipment promises
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
6eb417ff32
commit
44e7cf8001
4 files changed
+145
-7
No files matched your search
@@ -17,6 +17,15 @@ open mode gates.
|
||||
|
||||
## 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
|
||||
current-hand questions can distinguish primary projectile and poison weapons
|
||||
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.
|
||||
[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.**
|
||||
Players can ask a verified physical-force question across the current Arrakis
|
||||
board. The room's current locations include a placed Mobile Stronghold and
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# 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
|
||||
|
||||
In staged Basic and Advanced group games, a Guild ally's transport quote now
|
||||
|
||||
+1
-1
@@ -32347,7 +32347,7 @@ function findShipmentCompletion(
|
||||
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 = {
|
||||
type: 'guildShip',
|
||||
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));
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user