Enable native Fremen Cunning with original Nexus components

This commit is contained in:
Quintine committed 2026-10-11 18:56:20 +11:00
1 parent 7571ffb2ef
commit bcabaaf596
8 files changed
+176 -4

No files matched your search

+14
View File
@@ -29,6 +29,20 @@ optional skill bands, combinations and public expansion starts remain open.
Rules implementation continues; comprehensive assurance and administration
remain deferred until all rules are implemented.
**Fremen Cunning / original components — 11 October.** Classic Nexus games
with original Tech Tokens and/or Advanced Stronghold Cards now receive the same
empty-worm native Cunning offer as Nexus alone. The isolated software guard is
removed; physical card/occurrence, provisional Karama response and one post-Nexus
remote ride remain original. Dynamic destinations, Great Maker, Skills,
Homeworlds, Discovery and expansion factions/decks stay separate.
Types/lint and 41 affected cases pass. Nine runtime scenarios cover the four
legal component configurations, four legal bots and printed Karama cancellation.
Real GameTable at 390px spends Cunning and moves two Wind Pass14 counters to
Polar Sink0; reserves8/spice3 and original component custody remain unchanged.
A labelled QA control advances the original post-Nexus suffix between real
spend and ride controls. No saved store or deployed game was targeted. See
[the native contract](NEXUS_FREMEN_RULES.md#original-tech-and-strongholds--11-october-2026).
**Semuta / native Intrusion — 11 October.** Final Ornithopter movement now
offers the retired card before the original Bene Gesserit Intrusion choice.
Forces and move counters stay committed; claim or decline cannot move twice or
+31
View File
@@ -1,5 +1,36 @@
# Dune implementation status
## 11 October 2026 — native Fremen Cunning with original Tech/Strongholds
The classic original-component Nexus envelope already permits Tech Tokens3+
and Advanced Stronghold Cards2+, independently or together. `beginWorm` still
suppressed native Cunning whenever either component was present. Removing that
software guard preserves all existing occurrence/card/source/arrival processing
and leaves other module/faction gates unchanged. No new dynamic destination,
Great Maker or Tleilaxu revival ruling is inferred.
The regression fails before and passes after the guard removal. Its genuine
setup and first-turn progression establish original Tech and Stronghold owners;
only conserved deck/card order and two reserve counters are staged at the actual
turn-two Spice Blow. It checks the public opportunity in a paired no-card state,
one physical card spend, exact remote movement, original wallet/reserves and
component custody, and rejection of a retired ride event.
TypeScript/lint and 41/41 affected cases pass. Nine isolated runtime scenarios
cover Basic Tech, Advanced Tech, Advanced Strongholds and both, all four legal
AI profiles, and printed Karama cancellation retaining the remote source.
At 390px, real GameTable shows Spend Fremen Cunning, then the original typed
desert/destination controls. Two Wind Pass14 counters reach Polar Sink0 while
reserves8/spice3 and original component custody remain unchanged; no horizontal
overflow. A labelled QA button runs the existing post-Nexus continuation policy
between the production spend and ride controls. Completed local notices were
dismissed explicitly; no production save, room or credential was used.
Temporary scripts/server/tab were removed or closed. This is local changed-path
proof, not full games, all optional combinations, recovery or deployed acceptance.
Contracts: [native Cunning](NEXUS_FREMEN_RULES.md),
[original component composition](NEXUS_CARD_RULES.md#classic-tech-and-stronghold-composition--4-october-2026).
## 11 October 2026 — Semuta before original Bene Gesserit Intrusion
Final Ornithopter movement already commits forces, counters and a typed
+9
View File
@@ -1067,6 +1067,15 @@ CHOAM winner inspection then excludes all actually played opposing cards,
including a retained one. Card spend, casualties, income and cleanup remain
single original events. Ordinary city Collection is separate from battle costs.
**11 October — native Fremen Cunning.** The original classic Tech/Stronghold
envelope now offers the same empty-worm native Cunning as Nexus alone.
Original card/occurrence and post-Nexus riding remain; a completed remote ride
does not spend normal movement or alter Tech/Stronghold ownership or income.
Nine bounded runtime scenarios and real 390px spend/typed-ride controls prove
the changed path. Great Maker, dynamic destinations, Skills/Homeworld/Discovery
and unresolved protection remain separate. See
[the current native contract](NEXUS_FREMEN_RULES.md#original-tech-and-strongholds--11-october-2026).
Borrowed Voice, Fremen worm-protection reaction policy, Tleilaxu revival
accounting and other unresolved interpretations remain guarded. This is
bounded composition, not complete Nexus/faction/module or release acceptance.
+24 -2
View File
@@ -81,14 +81,36 @@ Two interactions need a concrete decision before treating this as a complete fea
`game/nexus-fremen-cunning.ts` captures `{event, turn, territory, origin, pile?, parent?, initiallyEmpty}` at the accepted appearance before destruction, using public `presenceAt` across players. Advisers and public No-Field markers count without revealing marker denomination. `beginWorm` offers the same Fremen decision for an initially empty appearance whether or not that seat holds its own card; the owner-only card view controls the spend button. The server, not the client, issues the event and later `{event, turn, owner}` ride authorization. The actual Fremen card moves to the Nexus discard on acceptance.
The current runtime path covers natural and accepted additional worms in Basic/Advanced, plus Fremen special-Karama summoned worms in Advanced, with seated classic factions, Nexus alone and no other optional modules. The summon mints a separate server-side parent event when its physical Karama is spent; a new empty appearance binds the Cunning occurrence to that parent before devouring. A saved offer or pending ride must still belong to the active summon. A summon occurring inside another unresolved Cunning offer or ride retains its ordinary worm path without a second Cunning offer. Great Maker is not enabled for this Cunning path; Sandtrout-suppressed and first-turn ignored *drawn* worms never call the appearance hook, while a real special summon still does. `quoteFremenCunning` checks native unallied entitlement and lists typed forces in each occupied sand territory. After spending, `quoteFremenCunningRide` requires the saved event authorization and spent physical card; `validateFremenCunningSelection` requotes one source and a subset of normal/Fedaykin counters immediately before moving. The destination uses Arrakis entry, storm and occupation rules, not the desert-only source restriction. Ordinary worm destruction and the applicable Nexus happen first; `nextWormRide` schedules native rides before the remote ride afterward, without consuming normal movement. BG intrusion and territory-entry continuations use the native worm-ride path. JSON normalization and rejected-action checks cover a live offer, a spent response and the post-Nexus selection.
The current runtime path covers natural and accepted additional worms in Basic/Advanced, plus Fremen special-Karama summoned worms in Advanced, with seated classic factions and Nexus, optionally retaining original Tech Tokens3+ and/or Advanced Stronghold Cards2+. Skills, Homeworlds, Discovery and expansion factions/decks stay outside this Cunning boundary. The summon mints a separate server-side parent event when its physical Karama is spent; a new empty appearance binds the Cunning occurrence to that parent before devouring. A saved offer or pending ride must still belong to the active summon. A summon occurring inside another unresolved Cunning offer or ride retains its ordinary worm path without a second Cunning offer. Great Maker is not enabled for this Cunning path; Sandtrout-suppressed and first-turn ignored *drawn* worms never call the appearance hook, while a real special summon still does. `quoteFremenCunning` checks native unallied entitlement and lists typed forces in each occupied sand territory. After spending, `quoteFremenCunningRide` requires the saved event authorization and spent physical card; `validateFremenCunningSelection` requotes one source and a subset of normal/Fedaykin counters immediately before moving. The destination uses Arrakis entry, storm and occupation rules, not the desert-only source restriction. Ordinary worm destruction and the applicable Nexus happen first; `nextWormRide` schedules native rides before the remote ride afterward, without consuming normal movement. BG intrusion and territory-entry continuations use the native worm-ride path. JSON normalization and rejected-action checks cover a live offer, a spent response and the post-Nexus selection.
A special summon during an existing Cunning offer or Karama response saves and
restores that original control. When it interrupts a selected remote ride, the
unfinished ride returns to its queue so the new Nexus and ordinary worm rides
resolve first; no second Cunning offer is opened for that nested appearance.
One Karama response may cancel only this additional remote ride after the Nexus card is spent. The originally empty appearance still destroys/places spice as usual; the remote source is not destroyed, and unrelated native rides continue. This is a **provisional enhanced-native cancellation composition**, not an explicit publisher ruling about Nexus/Karama classification. Games requiring other modules, a second Cunning offer inside an already pending Cunning control, Great Maker reserve replacement or Sandmaster collection stay gated until their own adapters/rulings are verified. The accepted receipt is server-issued but not cryptographically signed; this path does not claim malicious-save authentication.
One Karama response may cancel only this additional remote ride after the Nexus card is spent. The originally empty appearance still destroys/places spice as usual; the remote source is not destroyed, and unrelated native rides continue. This is a **provisional enhanced-native cancellation composition**, not an explicit publisher ruling about Nexus/Karama classification. Games requiring modules beyond original Tech/Advanced Strongholds, a second Cunning offer inside an already pending Cunning control, Great Maker reserve replacement or Sandmaster collection stay gated until their own adapters/rulings are verified. The accepted receipt is server-issued but not cryptographically signed; this path does not claim malicious-save authentication.
### Original Tech and Strongholds — 11 October 2026
The existing classic Nexus component envelope now receives the same public
empty-worm offer as Nexus alone. Private card entitlement, original occurrence,
card cost, provisional Karama response and post-Nexus remote ride are unchanged.
No new dynamic destination or Great Maker adapter is inferred.
The reusable fixture completes genuine setup, first-Storm Tech assignment,
first-Mentat Stronghold claims and the actual turn-two Spice Blow. Only
conserved physical card/deck order and two reserve counters are staged.
Nine isolated runtime scenarios cover Basic Tech, Advanced Tech, Advanced
Strongholds and both; four legal AI profiles and printed Karama cancellation.
Each completed ride moves the original two counters once, preserves wallet,
reserves and original component custody, and spends one physical Nexus card.
At 390px, real GameTable spends Cunning and selects Wind Pass14→Polar Sink0:
two counters arrive, reserves stay8 and spice stays3, with no horizontal
overflow. A labelled QA control advances the original post-Nexus suffix;
the spend and final typed ride use production controls. No saved store or
deployed game was targeted; complete module/public acceptance remains open.
TypeScript/lint and the final 41-case affected union pass. The permanent regression
fails before the software guard removal and passes afterward.
## Secret Ally protection
-1
View File
@@ -10069,7 +10069,6 @@ function beginWorm(g: Game, t: string, origin: 'natural' | 'additional' | 'summo
!g.greatMaker && g.nexusCards?.cards &&
!g.expansions.length && !g.homeworlds && !g.leaderSkills &&
!g.discoveryEnabled && !g.discoveries && !g.discoveryStash &&
!g.techTokens && !g.strongholdCards &&
g.players.every(seat => ['atreides', 'harkonnen', 'emperor', 'guild', 'beneGesserit', 'fremen'].includes(seat.faction)) &&
fremen && !fremen.ally && g.phase === 1 &&
fremen.fremenNexusWormBlockedTurn !== g.turn &&
+1 -1
View File
@@ -4499,7 +4499,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Temporary Sardaukar use the applicable strength and support rules, including the Fremen exception, but remain physically ordinary counters. Their losses go to Tanks as ordinary counters, without changing starred reserves or Tanks. Existing inspected or promised plans remain binding. The role ends after this battle; fewer-than-five groups remain unavailable. The separate Secret Ally revival and normal bank-auction purchase have connected controls; seller-paid and forced Emperor payments remain unfinished.',
'Emperor Betrayal has a battle alternative in Advanced play: an unallied opponent holding the Emperor Nexus card can spend it during the ordinary pre-plan Sardaukar response to suppress actual Sardaukar strength for this battle. Every seat receives the same response opportunity even with no card, keeping hidden custody private. The starred pieces remain physical Sardaukar; no card grants a new force or cancels their separate support benefit.',
'With Fremen absent, an unallied holder may spend Fremen Secret Ally during Revival to return exactly three eligible forces from Tanks to reserves at no spice cost. Unlike Emperor Secret Ally, these three consume the ordinary force and free-force allowances, including the Advanced one-elite cap. This bounded base-faction path keeps prior-return and competing-revival guards; supported classic Tech/Stronghold composition invokes actual Axlotl activity after the batch, independently of allowance accounting. Both free returns collect Tech only at phase end, never as spendable advance spice. Partial returns, prior returns, higher free rates and separate sandworm protection remain unavailable.',
'Native Fremen Cunning has a bounded Nexus-only path: when a natural, accepted additional, or Advanced special-Karama summoned worm appears in a territory initially containing no forces, Fremen may spend their own Nexus card before the worm resolves. The special summon retains its original Spice Blow parent. After the applicable Nexus, choose some forces from one occupied desert territory and ride to a legal Arrakis destination without using normal movement. The choice appears even without the secret card, so declining does not disclose custody. Karama provisionally prevents only the additional remote ride; the original worm still resolves and the card remains spent. An interrupted existing Cunning control is restored, and a selected ride waits for the summoned Nexus without granting a second Cunning offer; Great Maker and combined optional modules await integration.',
'Native Fremen Cunning has a bounded classic Nexus path, optionally retaining original Tech Tokens and Advanced Stronghold Cards: when a natural, accepted additional, or Advanced special-Karama summoned worm appears in a territory initially containing no forces, Fremen may spend their own Nexus card before the worm resolves. The special summon retains its original Spice Blow parent. After the applicable Nexus, choose some forces from one occupied desert territory and ride to a legal Arrakis destination without using normal movement or changing original component custody/income. The choice appears even without the secret card, so declining does not disclose custody. Karama provisionally prevents only the additional remote ride; the original worm still resolves and the card remains spent. An interrupted existing Cunning control is restored, and a selected ride waits for the summoned Nexus without granting a second Cunning offer; Great Maker, dynamic destinations and other optional-module combinations await integration.',
'CHOAM Secret Ally has a working Collection trade: when CHOAM is absent, an unallied holder may discard one Worthless card and the Nexus card for two bank spice. After a battle victory, every winner in an eligible Nexus game receives the same use-or-continue window; an unallied CHOAM-card holder may instead spend that card to inspect one random unused card in the opposing combatant’s current hand. Played battle cards remain excluded even if retained. The inspected face is private, while use is public. The universal window protects concealed card custody; this is an interface interpretation, not a publisher-prescribed prompt. An ordinary Karama response to this Secret Ally inspection remains unestablished.',
'With CHOAM seated, an unallied holder may spend CHOAM Betrayal at a clean play boundary to discard one uniformly random held CHOAM Treachery Card without spice compensation. The target name is public, but its hand count is offered only during Bidding; the holder cannot pick or inspect the card before the ordinary discard reveal.',
'CHOAM Cunning spends its Nexus card to use one held Treachery Card for one chosen Worthless effect at that effect’s normal timing, in Basic and Advanced. Choose the physical card separately from the effect. Kulon, La La La, Baliset, Jubba Cloak and Trip to Gamont retain their existing runtime. The explicit new Nexus Kull profile adds Kull at the canonical Karama reaction only; it remains unavailable in other Nexus previews. The old printed Kull profile still excludes Nexus. See the source-aware Kull guide for cost custody, counters and pending verification.',
+59
View File
@@ -0,0 +1,59 @@
import assert from 'node:assert/strict';
import { applyAction, type Game } from '../game/engine';
import { presenceAt } from '../game/force-presence';
import {
initializeNexusModulePaymentsSetup, nextNexusModulePaymentsNativeStep,
} from './fixture-nexus-module-payments';
import { nexusReady, orderNexusSpice } from './fixture-nexus-cards';
/** Original setup, first-Storm Tech and first-Mentat Stronghold ownership precede
* the turn-two appearance. Only the physical card/deck order and two reserve
* counters are staged; no phase, wallet, owner or earned receipt is manufactured. */
export function nexusFremenComponentsAppearance(options: {
advanced: boolean; tech: boolean; strongholds: boolean;
}) {
let game = initializeNexusModulePaymentsSetup({
...options, ownerFaction: 'fremen',
});
for (let n = 0; n < 1600; n++) {
if (game.status === 'playing' && game.turn === 2 && game.phase === 1 &&
!game.phaseOpening && !game.response && !game.decision) break;
assert.ok(game.turn <= 2, 'Stop at the original second-turn Spice Blow.');
const step = nextNexusModulePaymentsNativeStep(game);
assert.ok(step, `Original continuation stopped at ${game.turn}/${game.phase}/${game.decision?.kind}`);
game = applyAction(game, step.actor, step.action);
}
assert.equal(game.turn, 2); assert.equal(game.phase, 1);
assert.ok(!game.phaseOpening && !game.decision && !game.response);
const fremen = game.players.find(player => player.faction === 'fremen')!;
assert.ok(fremen.reserves - (fremen.elites?.reserves ?? 0) >= 2);
const sourceSector = game.storm === 14 ? 15 : 14;
const source = `wind_pass:${sourceSector}`;
fremen.reserves -= 2;
fremen.forces[source] = (fremen.forces[source] ?? 0) + 2;
const cards = game.nexusCards!.cards!;
const cardIndex = cards.deck.indexOf('fremen');
assert.ok(cardIndex >= 0);
cards.deck.splice(cardIndex, 1);
cards.hands[fremen.id] = 'fremen';
orderNexusSpice(game, ['worm', 'land']);
const empty = game.spiceDeck.find(card => 'territory' in card &&
game.players.every(player => presenceAt(player, card.territory) === 0));
assert.ok(empty && 'territory' in empty);
game.spiceDeck.splice(game.spiceDeck.indexOf(empty), 1);
game.spiceDiscard[0].push(empty);
const before = structuredClone(game);
game = nexusReady(game);
return { game, before, actor: fremen.id, source, sourceSector };
}
export function completeNexusFremenComponentsRide(state: Game): Game {
let game = state;
for (let n = 0; n < 100 && game.decision?.kind !== 'nexusFremenCunningRide'; n++) {
const step = nextNexusModulePaymentsNativeStep(game);
assert.ok(step, 'The original post-Nexus ride needs a legal continuation.');
game = applyAction(game, step.actor, step.action);
}
assert.equal(game.decision?.kind, 'nexusFremenCunningRide');
return game;
}
+38
View File
@@ -9,6 +9,7 @@ import { captureFremenCunningOccurrence, fremenCunningDestinationBlock,
import { applyAction, normalizeAutomaticGame, viewGame, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { nexusFixture, nexusTurnTwo, nexusReady, nexusAllow, nexusInventory, nexusReload, orderNexusSpice } from './fixture-nexus-cards';
import { nexusFremenComponentsAppearance, completeNexusFremenComponentsRide } from './fixture-nexus-fremen-components';
function fixture() {
const g = nexusFixture({ advanced: true });
@@ -274,3 +275,40 @@ void test('an accepted Advanced additional-worm placement offers the distinct em
assert.equal(g.nexusCards!.cards!.hands.f, null);
nexusInventory(g);
});
void test('original Tech and Strongholds preserve the private empty-worm opportunity and one remote ride', () => {
const fixture = nexusFremenComponentsAppearance({ advanced: true, tech: true, strongholds: true });
const { actor, source } = fixture;
let g = fixture.game;
assert.equal(g.decision?.kind, 'nexusFremenCunningOffer');
const event = g.nexusFremenCunningOffer!.occurrence.event;
const components = { tech: structuredClone(g.techTokens), strongholds: structuredClone(g.strongholdCards) };
const fremen = g.players.find(player => player.id === actor)!;
const sourceBefore = fremen.forces[source];
const reserves = fremen.reserves, spice = fremen.spice;
const observer = g.players.find(player => player.id !== actor)!.id;
const absent = nexusReload(g);
absent.nexusCards!.cards!.hands[actor] = null;
absent.nexusCards!.cards!.deck.push('fremen');
assert.deepEqual(viewGame(absent, observer).decision, viewGame(g, observer).decision);
assert.throws(() => applyAction(absent, actor, { type: 'decision', event, accept: true }));
const declined = applyAction(absent, actor, { type: 'decision', event, accept: false });
assert.equal(declined.nexusFremenCunningOffer, null);
g = applyAction(g, actor, { type: 'decision', event, accept: true });
g = completeNexusFremenComponentsRide(g);
g = applyAction(g, actor, { type: 'decision', event, accept: true,
source: 'wind_pass', forces: { [source]: { normal: 2, elite: 0 } },
territory: 'polar_sink', sector: 0 });
const moved = g.players.find(player => player.id === actor)!;
assert.equal(moved.forces[source] ?? 0, sourceBefore - 2);
assert.equal(moved.forces['polar_sink:0'], 2);
assert.equal(moved.reserves, reserves);
assert.equal(moved.spice, spice);
assert.equal(g.nexusFremenCunningRides?.[0].stage, 'complete');
assert.equal(g.nexusCards!.cards!.discard.filter(card => card === 'fremen').length, 1);
assert.deepEqual(g.techTokens, components.tech);
assert.deepEqual(g.strongholdCards, components.strongholds);
assert.throws(() => applyAction(g, actor, { type: 'decision', event, accept: true,
source: 'wind_pass', forces: { [source]: { normal: 2, elite: 0 } },
territory: 'polar_sink', sector: 0 }));
});