Add movement and Traitor practice to the introduction

Expand /learn from five lessons/three exercises to seven lessons/five exercises. Movement practice uses shared board geometry and occupancy rules to teach ordinary range, separate-city ornithopters, storm blocking, full strongholds, one-group movement and conserved force counts. Traitor practice uses the production combat quote for an optional post-reveal call, a declined losing plan, a sole successful call and mutual betrayal, with the existing full private Traitor inspector. The fixed opponent scenario is explicitly teaching data, not live private information or an AI match.

Migrate every v1 saved lesson to its same identity after insertion, preserving previous choices and outcomes under the existing browser key. Validate v2 movement commitments and reveal/call stages, whitelist saved fields and default only new choices. Browser testing exposed stale pre-update React state surviving hot refresh; a schema-keyed lesson remount runs the migration for development updates too. No live rules, AI strategy, room persistence or server scheduling changed.

Eleven focused cases include all 96 movement combinations against actual authoritative actions, exact force/spice preservation, sole/mutual/declined Traitor outcomes, all five old lesson mappings and malformed or impossible save rejection. Independent review found no material teaching, privacy or restore issue. Types, lint, all 4899 offline tests and production build pass on the unchanged source fingerprint. HTTP and complete-game runs were not repeated for this browser-local guidance change.

Browser checks verified range, storm and full-stronghold explanations, committed movement after refresh, private Traitor inspection, decline and successful-call outcomes, and mutual calls after a refresh at the revealed-choice stage. At a 390px viewport both new lessons fit within the 375px client width without horizontal overflow; keyboard movement selection committed four arrivals and one source remainder. Screenshot acceptance remains open. The viewport was reset and /learn retained as the deliverable.

All 1174 rooms, 2456 seats and every original recovery/entry/handover row remain unchanged. SQLite integrity and foreign keys pass. No new live room, migration, reset, restart, usage check or automation was needed. Broader curriculum, rules/mode acceptance and publication remain unfinished. The status dashboard identifies durable unattended bot recovery as the next independent backend capability; pending faction-rule questions were preserved.

Verified source tree: 6f25e31c2d8865dac823762d7da782f21f7538034c9d3c0e853872d796ced6f7
This commit is contained in:
Quintine committed 2026-09-20 15:44:14 +10:00
1 parent 54c8030b9b
commit e13ad454e2
11 files changed
+357 -53

No files matched your search

+10 -6
View File
@@ -5,6 +5,7 @@ import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
import { BattleWheel } from './battle-wheel';
import { CardInspector, CardRules } from './card-inspector';
import { PHASES } from '@/game/catalog';
import { MovementLesson, TraitorLesson } from './introduction-practice';
import {
INTRODUCTION_CARDS, INTRODUCTION_LEADERS, INTRODUCTION_STEPS, INTRODUCTION_STORAGE_KEY,
introductionBattle, introductionCollection, introductionShipment, newIntroduction, restoreIntroduction,
@@ -23,9 +24,10 @@ export function Introduction() {
<Link href="/">← Return to Arrakis</Link>
<p className="eyebrow">LEARN TO PLAY · BASIC GAME</p>
<h1>A first turn on Arrakis</h1>
<p>Try three small examples, then take a seat at a real table. These are separate practice positions; they do not change any saved game.</p>
<p>Practice shipping, movement, battle plans, Traitor calls and collection, then take a seat at a real table. These are separate examples; they do not change any saved game.</p>
</header>
{hydrated ? <IntroductionLessons /> : <output>Restoring your lesson…</output>}
{/* Remount on schema changes so a development hot update also migrates old state. */}
{hydrated ? <IntroductionLessons key="v2" /> : <output>Restoring your lesson…</output>}
</main>;
}
@@ -97,7 +99,8 @@ function IntroductionLessons() {
</div>
<p className="fine">Ordinary movement reaches an adjacent territory. Ornithopters and faction powers can extend its range. Inspect the live destination guide before committing.</p>
</>}
{state.step === 2 && <>
{state.step === 2 && <MovementLesson state={state} update={update} />}
{state.step === 3 && <>
<p>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.</p>
<div className={styles.columns}>
<div className={styles.controls}>
@@ -125,7 +128,8 @@ function IntroductionLessons() {
</div>
</div>
</>}
{state.step === 3 && <>
{state.step === 4 && <TraitorLesson state={state} update={update} />}
{state.step === 5 && <>
<p>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.</p>
<div className={styles.columns}>
<div className={styles.controls}>
@@ -141,8 +145,8 @@ function IntroductionLessons() {
</div>
</div>
</>}
{state.step === 4 && <>
<p>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.</p>
{state.step === 6 && <>
<p>You can revisit any example. This introduction covers core resources, movement and battle choices; it does not teach bidding or every faction, card, alliance, storm exception or expansion.</p>
<ul>
<li>Create a private room, choose a faction, and add friends or AI players. Share the room invitation.</li>
<li>Mark yourself ready after reviewing the selected rules. The host begins once all seats are ready.</li>