import type { IntroductionState } from '@/game/introduction'; import { introductionStorm, introductionSpiceBlow, PRACTICE_PREVIOUS_BLOW } from '@/game/introduction-opening'; import { splitLocation, territory } from '@/game/board'; import { BattleWheel } from './battle-wheel'; import { SpiceCardInspector, spiceCardTitle } from './spice-card-inspector'; import styles from './introduction.module.css'; type Props = { state: IntroductionState; update: (patch: Partial) => void }; const spicePlaces = (spice: Record) => Object.entries(spice).map(([key, amount]) => `${territory(splitLocation(key).territory).name}, sector ${splitLocation(key).sector}: ${amount} spice`).join('; ') || 'No spice on the board'; export function StormLesson({ state, update }: Props) { const result = introductionStorm(state); const first = state.stormExample === 'first'; return <>

Two players secretly dial the storm’s distance on their Battle Wheels. On the first turn, the players nearest the Storm Start dial 0–20. On later Basic turns, the last two players to use the wheels in battle dial 1–3; if no battle occurred, the previous pair stays. This example has no Weather Control or other reacting powers.

{ if (Number.isInteger(stormDial) && stormDial >= (first ? 0 : 1) && stormDial <= (first ? 20 : 3)) update({ stormDial }); }} />

{state.stormStage === 'resolved' ? 'Storm movement complete' : state.stormStage === 'revealed' ? 'Both dials revealed' : state.stormStage === 'sealed' ? 'Your dial is sealed' : 'Prepare a secret dial'}

The storm is at sector {result.sector}.

{result.distance === null ?

The other dial stays hidden. Sealing does not move the storm.

:

Your {state.stormDial} plus the other player’s {result.otherDial} makes {result.distance} sectors counterclockwise.

} {state.stormStage === 'revealed' &&

At a live table, players may use eligible storm cards before movement. This example has none.

} {state.stormStage === 'resolved' &&

{result.crossed.length ? `Crossed sectors: ${result.crossed.join(' → ')}.` : 'Zero movement crosses no sector.'}

}
    {result.locations.map(place =>
  • {place.name}, sector {place.sector}: {place.after} forces{state.stormStage === 'resolved' ? ` (${place.before - place.after} lost)` : ''} · {place.sheltered ? 'sheltered' : 'exposed sand'}
  • )}

Your reserves: {result.reserves}. Your Tanks: {result.tanks}.

{spicePlaces(result.spice)}.

Storm crosses every intervening sector and wraps from 18 to 1. Exposed forces go to the Tanks and crossed spice disappears. Rock, strongholds and Polar Sink shelter forces; the intact Shield Wall also shelters Imperial Basin. The first example starts with sheltered Atreides forces. Later examples are separate prepared positions.

; } export function SpiceBlowLesson({ state, update }: Props) { const result = introductionSpiceBlow(state); return <>

A territory card places its printed spice in one sector. When the preceding eligible discard is a territory card, Shai-Hulud affects that whole territory, then drawing continues to a new territory card. In these separate examples, no Fremen forces, allies, Harvester or other reacting powers are present.

Turn {result.turn} · storm sector {result.storm}.

{result.turn > 1 && <>

Previous territory card: {spiceCardTitle(PRACTICE_PREVIOUS_BLOW)}. Three Emperor forces occupy that territory.

}

{state.blowStage === 'draft' ? 'The next draw is face down' : result.nexus ? 'Nexus: negotiate alliances' : state.blowStage === 'settled' ? 'Continue to CHOAM Charity' : 'Spice sequence revealed'}

{result.revealed.map((card, index) =>

{index + 1}. {spiceCardTitle(card)}

)}

{spicePlaces(result.spice)}.

Emperor forces in The Great Flat: {result.forces}. Tanks: {result.tanks}. Reserves: {result.reserves}.

{result.added !== null &&

{result.added ? `${result.added} spice was added to Broken Land.` : 'Broken Land is in storm: its card adds no spice.'}

} {state.blowExample === 'worm' && state.blowStage !== 'draft' &&

Shai-Hulud consumed all six spice and three Emperor forces in The Great Flat. It did not attack the new Broken Land blow. {state.blowStage === 'settled' ? 'The replacement blow is finished and the Nexus is open.' : 'Finish the new blow before starting alliance negotiations.'}

} {result.skippedWorm &&

{state.blowStage === 'settled' ? 'The ignored worm is shuffled back after this first-turn blow.' : 'On turn one, set the worm aside and draw a replacement. It causes no destruction or Nexus.'}

} {result.nexus &&

The next lesson lets you practice an alliance offer, acceptance or withdrawal. Once negotiations finish, eligible Fremen may ride before Charity; this example has no Fremen to move.

}

Ordinary worms consume forces and spice throughout the previous territory, across its sectors. Fremen normally survive and may protect allies or ride, subject to their response windows. Advanced double blows and expansion cards have separate rules.

; }