Expand the internal introduction to nine lessons and seven Basic exercises. The four-player Nexus example teaches incoming consent, outgoing accepted and unanswered offers, withdrawal, unilateral breaks and the ordinary three-solo/four-allied stronghold target. Formation shows prospective progress; victory appears only at an explicit later Mentat check on the unchanged teaching board. Fixed responses demonstrate legal choices without refining live AI. Extract ordinary pairing into quoteNexusAlliance used by both practice and engine. Keep phase and Homeworld checks, alliance dates, Nexus-card forfeiture, readiness and history in the authoritative engine. Use production strongholdProgress and quoteVictory for the example; physical forces remain separately owned. No mode, expansion or publication gate changes. Version 4 practice saves migrate all five v1, seven v2 and eight v3 lesson identities and preserve prior auction, movement, combat, Traitor and collection choices. Alliance inputs are whitelisted and semantically replayed, with impossible action sequences and phase order rejected. Nine new regression tests cover helper immutability, live engine side effects, all 24 offered practice combinations, prior-version migration and saved continuation. Existing Nexus-card forfeiture tests also pass. Independent review found no material defect. Required types, lint, 4934 offline tests and production build pass on stable final source. Browser checks confirm old completed-auction migration, keyboard consent, formed and pending refresh restoration, withdrawal/break, closed-Nexus controls, three-stronghold shortfall and four-stronghold joint win with saved result. Controls fit a 390px viewport at 309px wide and at least44px high; viewport reset. No external lesson links. Screenshot capture remains unavailable, so visual screenshot acceptance remains unverified. Fixed lessons do not certify the full curriculum, live AI strength or all rules interactions. The local development server was confirmed stopped by refused localhost connection and no matching processes. Started it once against the existing database without migration/reset. The existing K2V47CSN QA seat restored in setup with owning Atreides private hand/Traitors and rival privacy intact; its Hard Guild and circle6 remained visible. All 1337 rooms, 2805 seats and every original recovery/entry/handover/delegation/migration row are unchanged. SQLite integrity and foreign keys pass. No human play was interrupted, no recurring automation installed and no usage check or AI calibration run. HTTP/auth/schema source did not change; offline checks include recovery tests, and no broad HTTP suite was rerun. Private evidence: 20260921-alliance-practice. Full rules, expansion, multiplayer and presentation goal remains incomplete. Verified source tree: 9c74a3141aca4e049e0734209b96f68ee0e6d0e127a155a2a7562a79ea8a1a6b
73 lines
5.8 KiB
TypeScript
73 lines
5.8 KiB
TypeScript
import type { IntroductionState } from '@/game/introduction';
|
||
import { introductionAlliance } from '@/game/introduction-alliance';
|
||
import { TERRITORIES } from '@/game/board';
|
||
import styles from './introduction.module.css';
|
||
|
||
export function AllianceLesson({ state, update }: {
|
||
state: IntroductionState; update: (patch: Partial<IntroductionState>) => void;
|
||
}) {
|
||
const practice = introductionAlliance(state);
|
||
const reset = (patch: Partial<IntroductionState> = {}) => update({
|
||
allianceActions: [], allianceClosed: false, allianceChecked: false, ...patch,
|
||
});
|
||
return <>
|
||
<p>Shai-Hulud has brought a Nexus during the Spice Blow phase of turn 2.
|
||
This separate four-player Basic example starts after the worm and spice effects.
|
||
You are Atreides; the Emperor is a possible partner. Harkonnen and the Guild stay unallied.</p>
|
||
<div className={styles.columns}>
|
||
<div className={styles.controls}>
|
||
<label htmlFor="learn-alliance-scenario">Emperor’s practice response</label>
|
||
<select id="learn-alliance-scenario" value={state.allianceScenario}
|
||
onChange={e => reset({ allianceScenario: e.target.value as IntroductionState['allianceScenario'] })}>
|
||
<option value="invited">Offers you an alliance first</option>
|
||
<option value="accepts">Accepts your offer</option>
|
||
<option value="waits">Stays unallied</option>
|
||
</select>
|
||
<label htmlFor="learn-alliance-holdings">Emperor’s strongholds</label>
|
||
<select id="learn-alliance-holdings" value={state.allianceHoldings}
|
||
onChange={e => reset({ allianceHoldings: Number(e.target.value) as 1 | 2 })}>
|
||
<option value="2">Sietch Tabr and Habbanya Sietch · 2</option>
|
||
<option value="1">Sietch Tabr only · 1</option>
|
||
</select>
|
||
<p className="fine">These fixed responses demonstrate consent, not an AI’s strategy. Changing an option starts a new example.</p>
|
||
<button disabled={!practice.canOffer} onClick={() => update({ allianceActions: ['offer'] })}>
|
||
{practice.incoming ? 'Accept Emperor’s offer' : 'Offer alliance to Emperor'}
|
||
</button>
|
||
<button disabled={!practice.canLeave} onClick={() => update({ allianceActions: [...state.allianceActions, 'leave'] })}>
|
||
{practice.allied ? 'Break practice alliance' : practice.outgoing ? 'Withdraw your offer' : 'Remain unallied'}
|
||
</button>
|
||
<button disabled={state.allianceClosed} onClick={() => update({ allianceClosed: true })}>Finish practice Nexus</button>
|
||
<button disabled={!state.allianceClosed || state.allianceChecked} onClick={() => update({ allianceChecked: true })}>Check the same board at Mentat Pause</button>
|
||
<button onClick={() => reset()}>Restart alliance example</button>
|
||
</div>
|
||
<div className={styles.result} aria-live="polite">
|
||
<h3>{state.allianceChecked ? practice.winner.length ? 'You win together' : 'No stronghold victory yet'
|
||
: state.allianceClosed ? 'Nexus finished' : practice.allied ? 'Your alliance is public'
|
||
: state.allianceActions.includes('leave') ? 'You remain unallied' : 'Choose whether to ally'}</h3>
|
||
<p>{practice.allied ? 'You and the Emperor are allied. The ordinary alliance limit is two players.'
|
||
: practice.outgoing ? `${state.allianceClosed ? 'Your offer was not accepted.' : 'Your offer is pending.'} You and the Emperor still count separately.`
|
||
: practice.incoming && !state.allianceActions.length && !state.allianceClosed ? 'The Emperor has offered. You decide whether to accept.' : 'You are unallied.'}</p>
|
||
{state.allianceActions.includes('leave') && !state.allianceClosed && <p>In a live Nexus you may continue negotiating. Restart this example to try another choice.</p>}
|
||
<h3>Your stronghold progress</h3>
|
||
<p><strong>{practice.progress.strongholds.length} / {practice.progress.target} strongholds</strong>
|
||
{practice.allied ? ' held by your alliance' : ' held by you'}.</p>
|
||
<ul>{practice.progress.strongholds.map(id => <li key={id}>{TERRITORIES.find(t => t.id === id)!.name}</li>)}</ul>
|
||
<p>You occupy Arrakeen and Carthag. The Emperor occupies {state.allianceHoldings === 2 ? 'Sietch Tabr and Habbanya Sietch' : 'Sietch Tabr'}.
|
||
Harkonnen holds Tuek’s Sietch; the Guild has a force in the Polar Sink.</p>
|
||
{!state.allianceChecked && <p>This is prospective progress. Forming an alliance does not end the game during the Nexus.</p>}
|
||
{state.allianceClosed && <p>Ordinary alliances cannot now change until another Nexus. For this exercise, the later phases leave every force in place; check this unchanged position at the Mentat Pause.</p>}
|
||
{state.allianceChecked && <p>{practice.winner.length
|
||
? 'Atreides and the Emperor jointly hold four strongholds at the Mentat Pause and share the victory.'
|
||
: `You need ${practice.progress.target - practice.progress.strongholds.length} more stronghold${practice.progress.target - practice.progress.strongholds.length === 1 ? '' : 's'} for an ordinary ${practice.allied ? 'alliance' : 'solo'} victory. The game continues.`}</p>}
|
||
<h3>Nexus history</h3>
|
||
{practice.transcript.length ? <ol>{practice.transcript.map((entry, i) => <li key={i}>{entry}</li>)}</ol>
|
||
: <p>No offers yet. You may offer an alliance or stay unallied.</p>}
|
||
</div>
|
||
</div>
|
||
<p>Both players must agree to form an alliance. Either partner may break it during a Nexus.
|
||
In this ordinary four-player game, a solo player needs three strongholds and an alliance needs four at the Mentat Pause.</p>
|
||
<p>An alliance shares the victory and grants the faction’s listed alliance power, not every faction advantage.
|
||
Forces and cards remain owned by their players. Funding, movement restrictions, special victories and expansion exceptions are separate rules in the reference.</p>
|
||
</>;
|
||
}
|