LEARN TO PLAY · BASIC GAME
+A first turn on Arrakis
+Try three small examples, then take a seat at a real table. These are separate practice positions; they do not change any saved game.
+diff --git a/README.md b/README.md
index a8522c02..1f909b71 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,10 @@ npm run dev -- --host 0.0.0.0
Open `http://localhost:3000`. Create a room, share its eight-character invitation, and add human or AI seats. The host can configure Easy, Medium, Hard and Brutal AI opponents. Current calibration demonstrates a substantially weaker Easy policy; it does not yet demonstrate a consistent ordering among the upper three levels.
+Choose **Learn to play** for the [interactive introduction](docs/INTERACTIVE_INTRODUCTION.md)
+at `/learn`. Practice shipping, battle plans and collection before entering a
+room; lesson progress stays in your browser and does not change saved games.
+
Local games persist in `.wrangler/state`. Keep this directory across restarts. Apply new additive migrations before running code that depends on them. Never delete the local database to resolve a connection or rules problem. `.openai/hosting.json` belongs to the existing Sites project; do not recreate the hosting project.
## Saved seats and reconnecting
diff --git a/app/learn/page.tsx b/app/learn/page.tsx
new file mode 100644
index 00000000..c7facca1
--- /dev/null
+++ b/app/learn/page.tsx
@@ -0,0 +1,5 @@
+import { Introduction } from '@/components/introduction';
+
+export default function Learn() {
+ return
LEARN TO PLAY · BASIC GAME
+Try three small examples, then take a seat at a real table. These are separate practice positions; they do not change any saved game.
+{state.step + 1} / {INTRODUCTION_STEPS.length}
+Each faction has its own powers. You will practice as Atreides with ordinary forces in the Basic game. Advanced combat and expansion powers come later.
+Everyone can see the board, forces, storm and revealed cards. Your spice, Treachery hand and Traitors stay private unless a rule reveals them.
Normally, hold three strongholds alone or four together with your ally at the Mentat Pause. Player-count and faction victory rules can change that target; the live table shows your current victory progress.
The table tells you who must act. Your decisions can wait while another player seals a plan or answers a power. Automatic effects show in the chronicle.
+ >} + {state.step === 1 && <> +You have 8 spice and six available reserve forces. Ship from reserves before moving on the board. This example has no discount, storm, occupancy restriction or reacting faction power.
+{state.shipment} forces cost {shipment.cost} spice.
+ {shipment.affordable ?{state.shipped ? 'You have' : 'You would have'} {shipment.remaining} spice left.
+ :You need {shipment.cost - 8} more spice. Reduce the force count or choose the stronghold.
} + {state.shipped &&{state.shipment} forces arrived; {6 - state.shipment} of your six available reserves remain. You could now move one group already on the board, including these newly shipped forces.
} +Ordinary movement reaches an adjacent territory. Ornithopters and faction powers can extend its range. Inspect the live destination guide before committing.
+ >} + {state.step === 2 && <> +A separate battle: you are the aggressor in Wind Pass with six ordinary forces and {INTRODUCTION_LEADERS.you.name}, strength {INTRODUCTION_LEADERS.you.strength}. Your opponent also has six forces. Neither side calls a Traitor or uses a faction power in this example.
+Choose how much to risk. In a normal victory you lose the forces you dial; in defeat you lose all your forces in this battle. In Basic play, ordinary forces need no spice support.
Your selected leader contributes strength only if they survive the weapon exchange. Plans reveal together.
> + : <>The opponent dialed 3 with {INTRODUCTION_LEADERS.opponent.name} (strength {INTRODUCTION_LEADERS.opponent.strength}), a {INTRODUCTION_CARDS.weapon.name} and no defense.
+{battle.leaderDeaths.attacker ? 'Your Snooper stops poison, so it cannot stop this projectile weapon. Your leader dies and adds no strength.' : 'Your Shield stops the projectile weapon. Your leader survives and adds five strength.'}
+Your total: {battle.scores!.attacker}. Opponent: {battle.scores!.defender}. {battle.scores!.attacker === battle.scores!.defender && 'The aggressor wins this ordinary tie.'}
+{6 - survivingForces} of your forces go to the Tanks; {survivingForces} remain in Wind Pass. {won ? 'The opponent loses all six forces.' : 'The opponent loses the three dialed forces and keeps three.'}
+{won ? 'A normal winner may keep or discard the cards they used.' : 'The defeated player discards the cards used in the plan.'} {battle.bounty?.amount ? `The winner also collects ${battle.bounty.amount} spice for the killed leader.` : 'Neither leader died, so there is no leader bounty.'}
+ + >} +A fresh position has 8 spice in Wind Pass, sector 14. You have surviving forces in that same sector, outside the storm. Collection uses the forces still on the board after battles.
+Each ordinary force can collect {state.city ? 3 : 2} spice{state.city ? ' because you occupy Arrakeen' : ''}.
+{state.collected ? 'You collected' : 'You can collect'} {collection.amount} spice. {collection.remaining} {state.collected ? 'remains' : 'would remain'} on the board.
+Spice beyond your capacity stays for a later turn. In a real table this calculation is automatic.
+You can revisit any example. This introduction covers the core resource and battle choices; it does not teach every faction, card, alliance, storm exception or expansion.
+Refresh restores a live game through its saved seat. If a request has an uncertain result, use the offered retry rather than repeating the game action.
+New to Dune? Try the interactive introduction →
This reference is growing with the game. Advanced play and expansion
rules are unfinished.{' '}
diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md
index a5356002..00f86577 100644
--- a/docs/CURRENT_STATUS.md
+++ b/docs/CURRENT_STATUS.md
@@ -6,13 +6,20 @@ gated.** Prototypes do not open completion or publication gates.
## Current checkpoint and work
-Latest delivered checkpoint: `bfa24b0`, [optional table sounds](TABLE_SOUNDS.md).
-Types, lint, **4,856 offline tests** and production build passed. Browser controls
-verified mute, keyboard volume, refresh and cross-tab preferences; physical audio
-and screenshot acceptance remain open. All 1,072 games and original seat/recovery
-records were preserved, and the server remained healthy after the outage.
+Latest delivered checkpoint: `9b67a78`, [early Sapho aggressor](SAPHO_AGGRESSOR.md).
+Types, lint, **4,866 offline tests**, production build and **43 HTTP tests** passed.
+Two genuine CHOAM/Richese samples completed; focused and browser evidence exercised
+the new priority effect. After the next outage all 1,106 rooms were present, the
+original baseline rows and latest QA save were unchanged, and a private seat
+restored after browser refresh. The healthy server was reused without a reset.
Earlier evidence remains in the [implementation history](IMPLEMENTATION_STATUS.md).
+Current work: the [interactive introduction](INTERACTIVE_INTRODUCTION.md) adds
+five lessons with shared-rule shipping, sealed battle and collection exercises,
+card inspection and browser-local continuation. Broader faction/expansion
+teaching remains unfinished. Final verification and delivery belong to the
+source-bound report and Git message.
+
The connected [Harass & Withdraw prototype](HARASS_WITHDRAW_RUNTIME.md) supports either
battle-card slot, private category inspections, exact undialed normal/elite
returns, opponent-Traitor cancellation, mandatory disposal, controls and all
@@ -23,7 +30,7 @@ card-specific discard precedence are explicitly labeled inferences. The Stone
Burner timing question is pending; no user answer is assumed. Reinforcements
remains unfinished. This does not open variant, mode or publication gates.
-Current checkpoint: [early Sapho aggressor](SAPHO_AGGRESSOR.md) connects the
+The preceding [early Sapho aggressor](SAPHO_AGGRESSOR.md) connects the
printed battle-priority effect during the existing shared pre-plan opportunity.
Physical attacker/defender/chooser stay stable; Habbanya takes precedence in either
slot. Shared priority feeds Stone admission, ordinary/Stone resolution, public
@@ -38,6 +45,11 @@ cannot safely depend on hidden custody or alter the printed deck. Mirror remains
gated pending the existing disposal ruling and copied-choice work. Continue with
independent missing faction/card functions, then integrated play and refinement.
+The Smuggler follow-up review supports a saved reveal-time receipt with settlement
+only on leader survival, but located no official ruling on modified leader
+strength. The user has been asked about Kwisatz/trainer/Stunner composition; no
+answer is assumed. See the [source follow-up](LEADER_SKILLS_RULES.md#20-september-smuggler-follow-up).
+
## Authoritative navigation
- [Rules checklist](../game/reference.ts), topic `implementation-checklist` and
diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md
index 4817815c..5ddc8bba 100644
--- a/docs/IMPLEMENTATION_STATUS.md
+++ b/docs/IMPLEMENTATION_STATUS.md
@@ -1,5 +1,20 @@
# Dune implementation status
+## 20 September 2026 — Interactive introduction prototype
+
+[Five guided lessons](INTERACTIVE_INTRODUCTION.md) connect shipping budgets,
+sealed battle plans and spice collection to shared production rule quotes, with
+keyboard controls, card inspection and browser-local saved progress. The fixed
+opponent is a teaching example, not an AI match. The wider faction, movement,
+bidding, Advanced and expansion curriculum remains unfinished. Focused checks
+cover combat outcomes and saved-choice validation; final browser/check/build,
+preservation and Git evidence belong to the private source-bound report and
+commit message. No mode or publication gate is opened.
+
+Independent Smuggler review supports the deferred survival receipt but found no
+official modifier-strength ruling. That single new question remains explicit in
+the decision register; no pending answer was inferred to enable collection.
+
## 20 September 2026 — Sapho early battle aggressor
[Early aggressor](SAPHO_AGGRESSOR.md) connects the held physical card, existing
diff --git a/docs/INTERACTIVE_INTRODUCTION.md b/docs/INTERACTIVE_INTRODUCTION.md
new file mode 100644
index 00000000..a702d430
--- /dev/null
+++ b/docs/INTERACTIVE_INTRODUCTION.md
@@ -0,0 +1,44 @@
+# Interactive introduction
+
+20 September 2026. **Prototyped; partial teaching coverage.** `/learn` is linked
+from the lobby and rules reference. It adds the previously missing guided
+introduction alongside the reference's existing standalone practice wheel.
+
+Five navigable lessons cover public/private table information and the phase
+sequence, shipment budgets, sealed battle plans, spice collection and joining or
+recovering a saved table. Three independent Basic practice positions let a player
+change inputs, commit, inspect results and retry. These positions are deliberately
+separate examples rather than a complete game or an authoritative live room.
+
+The examples call `reserveShipmentCost`, `quoteBattleResolution` and
+`quoteSpiceCollection`. Battle uses six ordinary forces, real Atreides/Guild
+leaders and canonical base-deck cards. The fixed opponent's dial, leader and
+projectile weapon appear only after the player's seal action. The resulting
+weapon survival, scores, ordinary aggressor tie, force losses and bounty come
+from the production combat quote. There are no Traitor calls or faction powers
+in this teaching position. It is not an AI policy or hidden-information test of
+the four live difficulty levels.
+
+The existing wheel, keyboard controls and card inspectors remain available.
+Lesson changes focus the heading; responsive columns stack on small screens.
+Browser storage contains only a versioned, validated set of lesson choices under
+`dune-introduction-v1`. It includes the sealed/revealed state. Unknown versions,
+out-of-range values and impossible saved shipments fall back to the first lesson.
+Unknown fields are dropped. Storage failure leaves the exercise usable in memory
+and displays that refresh may reset it. Separate tabs may save different progress;
+the last write is restored on the next visit. No multiplayer API or seat token is
+used, and no server restart or database migration is required.
+
+`tests/introduction.test.ts` checks unaffordable shipping, protected ties, wrong
+defense and leader death, every offered dial/defense pair, collection capacity,
+save round trips and malformed state. Browser interaction checks confirmed the
+unaffordable-shipment explanation, a legal shipment, card inspection, a protected
+8–8 tie, the wrong defense causing leader death, saved reveal after refresh,
+city collection and lobby return/re-entry. At a 390px viewport the battle controls
+and new lobby link stayed within the page bounds. Screenshot capture timed out,
+so visual screenshot acceptance remains open; DOM bounds are narrower evidence.
+Final source-bound reports and Git delivery record required check/build results.
+
+This does not replace a full interactive curriculum. Movement paths, bidding,
+Traitor calls, alliances, faction lessons, Advanced support and expansion lessons
+remain future work. No rules mode, expansion or publication gate is opened.
diff --git a/docs/LEADER_SKILLS_RULES.md b/docs/LEADER_SKILLS_RULES.md
index 9969343c..3602c0b2 100644
--- a/docs/LEADER_SKILLS_RULES.md
+++ b/docs/LEADER_SKILLS_RULES.md
@@ -184,6 +184,29 @@ designer expressly presents the latter as useful to Fremen, but does not
publish a general stacking order for every expansion movement modifier
([designer example, 19:13](https://www.youtube.com/watch?v=XT_azRVLq_0&t=1153s)).
+## 20 September Smuggler follow-up
+
+A bounded renewed check of the GF9 CHOAM & Richese rules/Q&A, the
+[core FAQ](https://www.gf9games.com/dune/wp-content/uploads/2020/11/Dune-FAQ-Nov-2020.pdf),
+Future Pastimes expansion page and Jack Reda walkthrough found no ruling defining
+Smuggler's leader strength with Kwisatz, another face-up trainer or Stunner. The
+walkthrough at 12:19–12:46 repeats reveal timing and survival with an unmodified
+strength-five example. It does not settle those modifiers. A user question now
+asks whether to keep those combinations gated, use printed/copied disc value,
+or include leader bonuses with a stunned leader counted as zero. No answer is
+assumed; this is not a publisher ruling.
+
+The prior pending-receipt contract remains viable: bind the original battle,
+controller, leader and spice pile at reveal, then transfer only after survival is
+known. The printed card has no victory requirement; the designer's winning
+example does not add one. A captive's lower effect belongs to its controller.
+Resolving this reveal-time collection logically before Sandmaster's after-win
+addition is an explicit timing inference. A drained pile leaves no existing spice
+for Sandmaster to augment. Multiple-pile allocation remains unresolved. Pending
+spice is not spendable personal custody; this provisional timing must remain
+explicit when the collection function is connected. Runtime collection is still
+missing; the independent introduction proceeded while the modifier answer waits.
+
## Implementation readiness contract
A bounded prototype can now use the exact fourteen-card inventory, modified
diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md
index 0f702099..e63cfb25 100644
--- a/docs/RULE_DECISIONS.md
+++ b/docs/RULE_DECISIONS.md
@@ -57,6 +57,7 @@ These are unresolved in the cited records. Do not infer an answer from elapsed t
| Mirror Weapon | Whether copied Tooth/Artillery mandatory-discard rules attach to the physical Mirror remains unanswered. Every genuine Richese setup includes those cards and Stone Burner; an ordinary-copy-only admission gate cannot depend on concealed custody or alter the printed deck. | [Engine audit](MIRROR_WEAPON_ENGINE_AUDIT.md) |
| Semuta Drug | Full-hand take-before-discard and the privacy-neutral immediate reaction protocol remain unresolved. Typed discard continuations already exist; they are not activation. | [Source update](SEMUTA_SOURCE_UPDATE_20260907.md), [privacy review](SEMUTA_PRIVACY_REVIEW.md) |
| Leader Skills edge cases | Capture/replacement eligibility, Banker escrow custody (user question sent), Smuggler survival settlement, Mentat empty-hand fallback (private historical observations persist after death), Diplomat lower retreat ordering, modified-defense inheritance and remaining simultaneous optional timing need resolution before those cases are enabled. These are recorded source gaps, not answered user rulings. | [Material boundaries](LEADER_SKILLS_RULES.md#material-boundaries-not-settled-by-the-sources), [Diplomat boundary](DIPLOMAT_DEFENSE.md#evidence-and-remaining-scope) |
+| Smuggler collection strength | A fresh primary-source check found no modifier ruling. The user was asked whether to keep affected combinations gated, use printed/copied disc value, or include leader bonuses with Stunner zero. No answer is assumed. The reveal-bound deferred receipt is supported as an explicit timing inference; lower collection remains unimplemented. | [Smuggler follow-up](LEADER_SKILLS_RULES.md#20-september-smuggler-follow-up) |
| Suk Graduate / Atreides | Whether rescued dialed casualties count toward the seven-loss Kwisatz Haderach threshold remains unresolved. The user question has been sent; Advanced Atreides rescue stays guarded. | [Source and runtime boundary](SUK_GRADUATE_RULES.md) |
| Ordinary Guild cancellation | Unaffordable repricing, replacement/retry/decline, consumed opportunity, contributor adjustment and ordering with special shipment-stop. The affordability question was already sent. | [Material timing](GUILD_RATE_KARAMA_RULES.md#material-unresolved-timing) |
| Base special Karama | Unfunded auction recovery, stopped-shipment settlement and Atreides full-plan commitment order remain provisional. Native full-plan/Voice/Prescience/Truthtrance/Ghola interactions now have genuine-setup focused evidence, but the source audit does not settle the special offer/deadline or target-first interpretation. | [Explicit base interpretations](BASE_ADVANCED_READINESS_20260907.md#explicit-unresolved-base-interpretations), [full-plan interaction audit](ATREIDES_FULL_PLAN_TIMING.md) |
diff --git a/game/introduction.ts b/game/introduction.ts
new file mode 100644
index 00000000..65d41e6b
--- /dev/null
+++ b/game/introduction.ts
@@ -0,0 +1,82 @@
+import { baseDeck, leaders } from './cards';
+import { reserveShipmentCost } from './shipment-price';
+import { quoteBattleResolution, type ResolutionCombatant } from './battle-resolution-quote';
+import { quoteSpiceCollection } from './board-resolution-quote';
+
+/** Fixed teaching examples, never a room, seat or multiplayer save. */
+export const INTRODUCTION_STORAGE_KEY = 'dune-introduction-v1';
+export const INTRODUCTION_STEPS = [
+ { title: 'Your place at the table', topic: 'setup' },
+ { title: 'Ship within your budget', topic: 'movement' },
+ { title: 'Seal a battle plan', topic: 'battle' },
+ { title: 'Collect the spice', topic: 'collection' },
+ { title: 'Join a table', topic: 'privacy' },
+] as const;
+export type IntroductionState = {
+ version: 1;
+ step: number;
+ shipment: number;
+ destination: 'stronghold' | 'sand';
+ shipped: boolean;
+ dial: number;
+ defense: 'shield' | 'snooper';
+ revealed: boolean;
+ collectors: number;
+ city: boolean;
+ collected: boolean;
+};
+export function newIntroduction(): IntroductionState {
+ return { version: 1, step: 0, shipment: 4, destination: 'sand', shipped: false,
+ dial: 2, defense: 'shield', revealed: false, collectors: 3, city: false, collected: false };
+}
+const integer = (value: unknown, min: number, max: number): value is number =>
+ Number.isSafeInteger(value) && (value as number) >= min && (value as number) <= max;
+export function restoreIntroduction(raw: string | null): IntroductionState | null {
+ if (!raw) return null;
+ try {
+ const s = JSON.parse(raw) as IntroductionState;
+ if (!s || s.version !== 1 || !integer(s.step, 0, INTRODUCTION_STEPS.length - 1) ||
+ !integer(s.shipment, 1, 6) || !['stronghold', 'sand'].includes(s.destination) ||
+ !integer(s.dial, 0, 6) || !['shield', 'snooper'].includes(s.defense) ||
+ !integer(s.collectors, 0, 6) ||
+ ![s.shipped, s.revealed, s.city, s.collected].every(v => typeof v === 'boolean') ||
+ (s.shipped && introductionShipment(s).cost > 8)) return null;
+ // Whitelist fields; never retain credentials or arbitrary imported properties.
+ return { version: 1, step: s.step, shipment: s.shipment, destination: s.destination,
+ shipped: s.shipped, dial: s.dial, defense: s.defense, revealed: s.revealed,
+ collectors: s.collectors, city: s.city, collected: s.collected };
+ } catch { return null; }
+}
+export function introductionShipment(s: Pick