Support conditional Truthtrance before storm and spice
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
e529cc2d7a
commit
0e9209531a
7 files changed
+172
-10
No files matched your search
+13
-1
@@ -38,6 +38,14 @@ the current group model deliberately caps membership at three. The combined
|
||||
earlier batch passed 189 cases; the shipment batch passed 58 cases. The latest
|
||||
focused funding, Nexus, shipment and Truthtrance union passed 61 cases.
|
||||
|
||||
**Conditional future-battle Truthtrance in Basic Storm and Spice Blow —
|
||||
28 October. Prototyped / Partial.** A named-leader question can use the quiet
|
||||
post-dial Storm window or the pre-draw Spice window. Real Storm casualties,
|
||||
the Spice draw, first battle and no-battle expiry retain their native paths.
|
||||
Active Storm resolution, Nexus and worm/Spice continuations remain guarded.
|
||||
The related eight-file batch passes 48 cases, including legal replies from
|
||||
all four existing AI profiles; this does not certify other combinations.
|
||||
|
||||
**Conditional future-battle Truthtrance in Basic Bidding — 28 October.
|
||||
Prototyped / Partial.** The named-leader question also works during a quiet
|
||||
ordinary auction. Its existing current-leader answer, first actual battle and
|
||||
@@ -128,7 +136,11 @@ extra visitor remains unproven. Gitea run 4 subsequently passed all 69 HTTP
|
||||
cases and published verified image `sha-8fb9f591060ba501ff9def8e32f8aa6e1786fc7d`.
|
||||
Run 5 for `e896570` also passed its 25 administrator checks, all 69 HTTP
|
||||
cases, restart/replacement and origin verification, and published both its
|
||||
revision-specific image and `checkpoint`. Deployment remains unverified.
|
||||
revision-specific image and `checkpoint`. Run 6 for `cea310b` succeeded and
|
||||
published its revision tag and `checkpoint` with digest
|
||||
`sha256:74340835931011be1d0be907c8db688719f05a1a1b949ba0633eff4c5fb55248`.
|
||||
Run 7 was canceled before starting; run 8 for `e529cc2` is running.
|
||||
Deployment remains unverified.
|
||||
Existing NAS deployment and its persistent storage remain untouched.
|
||||
|
||||
**Semuta before Basic Moritani ally retention — 28 October. Prototyped /
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Conditional next-battle leader question — 28 October 2026
|
||||
|
||||
**Prototyped / Partial.** In a quiet Basic Charity, Bidding, Revival or Shipment and Movement window with
|
||||
**Prototyped / Partial.** In a quiet Basic Storm, Spice Blow, Charity, Bidding, Revival or Shipment and Movement window with
|
||||
classic factions and no optional modules, a player can ask whether another
|
||||
player will use a named native leader in that player's first later battle this
|
||||
turn, if one occurs. A definite answer creates a waiting receipt; it does not
|
||||
@@ -13,7 +13,9 @@ ends. An independently impossible answer is released by the existing battle
|
||||
promise reconciliation. Another structured action answer for that respondent
|
||||
is guarded while the conditional answer is active; broader promise composition,
|
||||
expansion factions, optional modules and Advanced are not supported here.
|
||||
During Charity, Bidding and Revival, definite answers use currently available native leaders or
|
||||
Storm requires its original post-dial, pre-movement window; Spice Blow requires
|
||||
the original pre-draw window with no Nexus or worm continuation. During these
|
||||
windows, Charity, Bidding and Revival, definite answers use currently available native leaders or
|
||||
Cheap Heroes; they do not forecast a future revival or spend its resources.
|
||||
|
||||
The publisher's [November 2020 FAQ](https://www.gf9games.com/dune/wp-content/uploads/2020/11/Dune-FAQ-Nov-2020.pdf)
|
||||
@@ -21,7 +23,8 @@ permits game-related current-turn action questions and requires honoring an
|
||||
answer while possible. The first-battle binding and no-opportunity expiry are
|
||||
provisional online interpretations, not specific publisher rulings. The
|
||||
[native runtime tests](../tests/truth-future-battle-leader-runtime.test.ts) and
|
||||
[Bidding continuation tests](../tests/truth-future-battle-bidding.test.ts)
|
||||
[Bidding continuation tests](../tests/truth-future-battle-bidding.test.ts) and
|
||||
[Storm/Spice continuation tests](../tests/truth-future-battle-early.test.ts)
|
||||
exercise both answers and expiry without inventing a battle.
|
||||
|
||||
## Printed flight and pre-leader cards — 27 October 2026
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { createTwoPlayerControllers, validateTwoPlayerControllersLobby, validateTwoPlayerControllersComplete,
|
||||
addTwoPlayerControlledSeat, TwoPlayerControllersError, type TwoPlayerControllers } from './two-player-controllers';
|
||||
import { createFutureBattleLeader, bindFutureBattleLeader, settleFutureBattleLeader, releaseFutureBattleLeader,
|
||||
expireFutureBattleLeader, type FutureBattleLeaderReceipt } from './future-battle-leader';
|
||||
expireFutureBattleLeader, futureBattleLeaderTiming, type FutureBattleLeaderReceipt } from './future-battle-leader';
|
||||
import { createFactionDraft, validateFactionDraft, currentFactionDrafter, chooseFaction,
|
||||
FactionDraftError, type FactionDraft } from './faction-draft';
|
||||
import { createFactionSwap, validateFactionSwap, answerFactionSwap, cancelFactionSwap,
|
||||
@@ -31695,7 +31695,7 @@ function futureBattleLeaderTruthAnswers(g: Game, p: Player, q: FutureBattleLeade
|
||||
...(g.bidPromises ?? []), ...(g.phaseBidPromises ?? []), ...(g.richeseBidPromises ?? []),
|
||||
...(g.earlyRicheseBidPromises ?? []), ...(g.alliancePromises ?? []), ...(g.allianceExpressionPromises ?? []),
|
||||
];
|
||||
if (q.turn !== g.turn || q.target !== p.id || ![2, 3, 4, 5].includes(g.phase) ||
|
||||
if (q.turn !== g.turn || q.target !== p.id || !futureBattleLeaderTiming(g) ||
|
||||
(g.futureBattleLeaderPromises ?? []).some(row => row.turn === g.turn && row.player === p.id &&
|
||||
(row.status === 'waiting' || row.status === 'bound')) ||
|
||||
otherPromises.some(row => row.player === p.id && row.turn === g.turn && !row.fulfilled && !row.released) ||
|
||||
@@ -45802,7 +45802,7 @@ export function viewGame(state: Game, id: string, now?: number) {
|
||||
truthFutureBattleLeaderAnswers: g.truthtrance?.stage === 'answer' &&
|
||||
g.truthtrance.question?.target === id && g.truthtrance.question.kind === 'futureBattleLeader'
|
||||
? futureBattleLeaderTruthAnswers(g, me, g.truthtrance.question) : null,
|
||||
truthFutureBattleLeaderTargets: g.status === 'playing' && !g.advanced && [2, 3, 4, 5].includes(g.phase) &&
|
||||
truthFutureBattleLeaderTargets: g.status === 'playing' && !g.advanced && futureBattleLeaderTiming(g) &&
|
||||
!g.expansions.length && !g.techTokens && !g.strongholdCards && !g.nexusCards && !g.homeworlds &&
|
||||
!g.leaderSkills && !g.discoveryEnabled && !g.ecazTreachery && !g.battle && !g.response &&
|
||||
!g.decision && !g.phaseOpening && !g.pendingRevival && !g.pendingKarama && !g.pendingTreacheryDiscard
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
import type { Game } from './engine';
|
||||
|
||||
/** Early questions use the native pre-resolution Storm or pre-draw Spice
|
||||
* window. Other phases retain their existing quiet-interaction checks. */
|
||||
export function futureBattleLeaderTiming(g: Pick<Game, 'phase' | 'stormPending' | 'stormResolution' | 'spiceSequence' |
|
||||
'spiceWindow' | 'spiceResolution' | 'nexus' | 'summonedWorm' | 'wormRides'>): boolean {
|
||||
if (g.phase === 0) return Number.isInteger(g.stormPending) && !g.stormResolution;
|
||||
if (g.phase === 1) return !g.spiceSequence && !g.spiceWindow && !g.spiceResolution &&
|
||||
!g.nexus && !g.summonedWorm && !g.wormRides.length;
|
||||
return [2, 3, 4, 5].includes(g.phase);
|
||||
}
|
||||
|
||||
/** A conditional commitment for the respondent's first later native battle.
|
||||
* Waiting does not require a battle to occur. The engine owns plan legality,
|
||||
* answer feasibility and the decision that an obligation became impossible. */
|
||||
|
||||
+1
-1
@@ -3305,7 +3305,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
category: 'Cards', coverage: 'Partial', developmentStage: 'Prototyped',
|
||||
summary: 'Combine printed Karama shipment pricing and current-lot costs, original-window Weather/Atomics/Harvester plays, native Voice/Prescience declarations, future printed Hajr/Ghola plays, current battle decisions, final Nexus membership, ordinary bids, native Revival, future payments and physical transport in one whole current-turn answer, with optional frozen facts.',
|
||||
steps: [
|
||||
'In a quiet Basic Charity, Bidding, Revival or Shipment and Movement window, ask whether a player will use a named native leader in their first later battle this turn, if one occurs. The answer waits for an actual battle; if none occurs, it expires. This separate provisional question currently supports classic factions without optional modules and uses only currently available leaders for its answer.',
|
||||
'In a quiet Basic Storm, Spice Blow, Charity, Bidding, Revival or Shipment and Movement window, ask whether a player will use a named native leader in their first later battle this turn, if one occurs. Storm must have a pending movement after the dials; Spice Blow must be before the draw. The answer waits for an actual battle; if none occurs, it expires. This separate provisional question currently supports classic factions without optional modules and uses only currently available leaders for its answer.',
|
||||
'Choose Combine printed card plays, native battle powers and this turn’s other actions. Full AND/OR groups share sixteen logical leaves and four grouping levels, with optional current facts. This is a provisional composition of original-window card plays, native power declarations, printed Hajr/Ghola use, current battle decisions, final Nexus membership, current-lot bids, ordinary Revival, funded payments and owned-stage travel; wider action families and optional combinations remain unfinished.',
|
||||
'A final-alliance clause names another current participant during this original quiet native Nexus. Existing invitations and reciprocal formation remain authoritative. Offers, temporary membership and your own Ready do not settle the clause; final actual membership is fixed only when the Nexus closes.',
|
||||
'Whole No can permit a temporary paid pair and later require a break. Own Ready must preserve a completion after final membership is fixed; remaining payment or physical-action alternatives keep their later deadlines. Definite answers, including constants, uniformly reopen own Nexus readiness; later ordinary payments require confirmation again.',
|
||||
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
import { liveCharityPromises } from './charity-promises';
|
||||
import { futureBattleLeaderTiming } from './future-battle-leader';
|
||||
import { createSaphoTruthOrder, passSaphoTruthOrder, SaphoTruthOrderError, type SaphoTruthOrderFrame } from './sapho-truth-order';
|
||||
import { parseTravelActionClaim, type TravelClaim } from './travel-expression-promises';
|
||||
import { parseTurnActionLeaf, TURN_ACTION_MAX_DEPTH, TURN_ACTION_MAX_LEAVES,
|
||||
@@ -602,11 +603,11 @@ function parseQuestion(g: Game, asker: string, value: unknown): TruthQuestion {
|
||||
check(Object.keys(v).sort().join(',') === 'kind,leader,target,turn' &&
|
||||
v.turn === g.turn && typeof v.leader === 'string' &&
|
||||
leaders(target.faction).some(leader => leader.id === v.leader) &&
|
||||
g.status === 'playing' && !g.advanced && [2, 3, 4, 5].includes(g.phase) && !g.expansions.length &&
|
||||
g.status === 'playing' && !g.advanced && futureBattleLeaderTiming(g) && !g.expansions.length &&
|
||||
!g.techTokens && !g.strongholdCards && !g.nexusCards && !g.homeworlds &&
|
||||
!g.leaderSkills && !g.discoveryEnabled && !g.ecazTreachery && !g.battle &&
|
||||
!g.response && !g.decision && !g.phaseOpening && !g.pendingRevival && !g.pendingKarama && !g.pendingTreacheryDiscard,
|
||||
'Choose a native leader in a quiet Basic Charity, Bidding, Revival or Shipment and Movement window.');
|
||||
'Choose a native leader in a quiet Basic Storm, Spice Blow, Charity, Bidding, Revival or Shipment and Movement window.');
|
||||
return { kind: 'futureBattleLeader', target: v.target, turn: g.turn, leader: v.leader };
|
||||
}
|
||||
if (v.kind === 'battlePlan') {
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame, type Action, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { futureBattleLeaderTiming } from '../game/future-battle-leader';
|
||||
import { advanceTruthRevival, nextTruthRevival, truthRevivalClean, truthRevivalInventory, truthRevivalPlayer } from './fixture-truth-revival';
|
||||
import {
|
||||
actFutureBattleCharity as act, askFutureBattleCharity as ask,
|
||||
createFutureBattleCharityFixture, futureBattleCharityRecords as records,
|
||||
reachFutureBattleCharityBattle as reachBattle, reachFutureBattleCharityWindow as reachMovement,
|
||||
} from './fixture-truth-future-battle-charity';
|
||||
|
||||
/** Original turn-two Storm and its pre-Spice draw window: no played hand,
|
||||
* army, phase or promise is installed to manufacture these opportunities. */
|
||||
function fixture(phase: 0 | 1) {
|
||||
const source = createFutureBattleCharityFixture();
|
||||
const game = structuredClone(phase === 0 ? source.base.storm.before : source.base.storm.after);
|
||||
assert.equal(game.phase, phase);
|
||||
assert.equal(game.turn, source.game.turn);
|
||||
assert.ok(truthRevivalClean(game));
|
||||
assert.ok(truthRevivalPlayer(game, source.asker).hand.some(card => card.id === source.truth));
|
||||
assert.deepEqual(truthRevivalInventory(game), source.originalCards);
|
||||
assert.ok(viewGame(game, source.asker).truthFutureBattleLeaderTargets.includes(source.target));
|
||||
return { ...source, game };
|
||||
}
|
||||
|
||||
function finishBattle(game: Game, f: ReturnType<typeof fixture>, answer: 'yes' | 'no') {
|
||||
const plan = (leader: string): Action => ({type: 'battlePlan', leader, dial: 0, support: 0, weapon: null, defense: null});
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => act(game, f.target, plan(answer === 'yes' ? f.alternative : f.leader)));
|
||||
assert.deepEqual(game, before);
|
||||
game = act(game, f.target, plan(answer === 'yes' ? f.leader : f.alternative));
|
||||
const opponent = game.battle!.attacker === f.target ? game.battle!.defender : game.battle!.attacker;
|
||||
const other = truthRevivalPlayer(game, opponent).leaders.find(leader =>
|
||||
!leader.dead && !leader.capturedBy && !leader.gholaBy && !leader.usedAt);
|
||||
assert.ok(other);
|
||||
game = act(game, opponent, plan(other.id));
|
||||
assert.ok(game.battle?.revealed);
|
||||
assert.equal(records(game)[0].status, 'settled');
|
||||
return game;
|
||||
}
|
||||
|
||||
for (const phase of [0, 1] as const) {
|
||||
const window = phase === 0 ? 'Storm' : 'Spice Blow';
|
||||
for (const answer of ['yes', 'no'] as const) {
|
||||
void test(`Basic ${window} future-battle ${answer} preserves its native continuation and binds the later battle`, () => {
|
||||
const f = fixture(phase);
|
||||
const storm = f.game.storm, pending = f.game.stormPending;
|
||||
const spiceDeck = structuredClone(f.game.spiceDeck);
|
||||
const forces = structuredClone(truthRevivalPlayer(f.game, f.target).forces);
|
||||
let game = act(ask(f), f.target, {type: 'truthAnswer', answer});
|
||||
assert.equal(game.phase, phase); assert.equal(game.storm, storm); assert.equal(game.stormPending, pending);
|
||||
assert.deepEqual(game.spiceDeck, spiceDeck);
|
||||
assert.deepEqual(truthRevivalPlayer(game, f.target).forces, forces);
|
||||
assert.equal(records(game)[0].status, 'waiting');
|
||||
game = reachMovement(game, f);
|
||||
assert.equal(truthRevivalPlayer(game, f.target).forces[f.base.location], undefined);
|
||||
assert.equal(truthRevivalPlayer(game, f.target).tanks, 3);
|
||||
assert.ok(game.spiceDeck.length < spiceDeck.length, 'The original Spice Blow must actually draw.');
|
||||
game = reachBattle(game, f);
|
||||
assert.equal(records(game)[0].status, 'bound');
|
||||
game = finishBattle(game, f, answer);
|
||||
assert.equal(game.discard.filter(card => card.id === f.truth).length, 1);
|
||||
assert.deepEqual(truthRevivalInventory(game), f.originalCards);
|
||||
});
|
||||
}
|
||||
|
||||
void test(`Basic ${window} conditional leader answer expires without a later battle`, () => {
|
||||
const f = fixture(phase);
|
||||
let game = act(ask(f), f.target, {type: 'truthAnswer', answer: 'yes'});
|
||||
game = reachMovement(game, f);
|
||||
assert.ok(Object.values(truthRevivalPlayer(game, f.target).forces).every(amount => amount === 0));
|
||||
for (let count = 0; count < 1200 && game.turn === f.game.turn && game.phase <= 6 && game.status === 'playing'; count++) {
|
||||
if (game.phase === 5 && game.active && truthRevivalClean(game)) {
|
||||
game = act(game, game.active, {type: 'endMovement'}); continue;
|
||||
}
|
||||
const next = nextTruthRevival(game); assert.ok(next);
|
||||
game = act(game, next.actor, next.action);
|
||||
}
|
||||
assert.equal(records(game)[0].status, 'expired');
|
||||
assert.equal(game.discard.filter(card => card.id === f.truth).length, 1);
|
||||
});
|
||||
|
||||
void test(`each AI difficulty can legally answer the Basic ${window} question`, () => {
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const f = fixture(phase);
|
||||
const asked = ask(f);
|
||||
truthRevivalPlayer(asked, f.target).bot = difficulty;
|
||||
const answer = botActions(viewGame(asked, f.target)).find(action => action.type === 'truthAnswer');
|
||||
assert.ok(answer);
|
||||
const game = act(asked, f.target, answer);
|
||||
assert.equal(records(game)[0].status, 'waiting');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void test('early timing rejects controlled unsupported Storm and Spice parents', () => {
|
||||
const cases: {phase: 0 | 1; name: string; set: (game: Game) => void}[] = [
|
||||
{phase: 0, name: 'unresolved Storm dials', set: game => { game.stormPending = null; }},
|
||||
{phase: 0, name: 'Storm resolution', set: game => {
|
||||
game.stormResolution = {from: game.storm, distance: 1, traversed: 0, pending: []};
|
||||
}},
|
||||
{phase: 1, name: 'Spice resolution', set: game => { game.spiceResolution = {skipped: []}; }},
|
||||
{phase: 1, name: 'Spice sequence', set: game => { game.spiceSequence = {pile: 0, skipped: []}; }},
|
||||
{phase: 1, name: 'active Spice window', set: game => {
|
||||
game.spiceWindow = {territory: 'red_chasm', sector: 7, amount: 1, harvested: false};
|
||||
}},
|
||||
{phase: 1, name: 'Nexus', set: game => { game.nexus = true; }},
|
||||
{phase: 1, name: 'worm ride', set: game => { game.wormRides = ['red_chasm']; }},
|
||||
];
|
||||
for (const entry of cases) {
|
||||
const f = fixture(entry.phase);
|
||||
// Controlled parent probes: these fields represent continuations that are
|
||||
// outside the permitted quiet window, not an original full-game journey.
|
||||
entry.set(f.game);
|
||||
assert.equal(futureBattleLeaderTiming(f.game), false, entry.name);
|
||||
}
|
||||
});
|
||||
|
||||
void test('real pre-dial Storm and active Spice Blow hide targets and reject the early question', () => {
|
||||
const source = fixture(0);
|
||||
const storm = advanceTruthRevival(structuredClone(source.base.continuation.before), state =>
|
||||
state.turn === source.game.turn && state.phase === 0 && state.stormPending === null && truthRevivalClean(state));
|
||||
const spiceSource = fixture(1);
|
||||
const spice = advanceTruthRevival(structuredClone(spiceSource.game), state =>
|
||||
state.turn === spiceSource.game.turn && state.phase === 1 && !!state.spiceWindow && truthRevivalClean(state));
|
||||
for (const [f, game] of [[source, storm], [spiceSource, spice]] as const) {
|
||||
assert.deepEqual(viewGame(game, f.asker).truthFutureBattleLeaderTargets, []);
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => ask({ ...f, game }), /quiet Basic/);
|
||||
assert.deepEqual(game, before);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user