Enforce Basic Truthtrance ground-movement promises
Add a structured future single-movement predicate and persistent authoring, feasible-answer and promise controls for bare native Basic active post-shipment turns. Yes requires one matching physical movement while possible; No survives every later Hajr/Ornithopter opportunity until actual turn closure. Matching physical commitment precedes disposal and arrival effects. Preserve compatible promises after opposing loss and reject voluntary destruction of the last owned completion. Reuse the actual movement declaration and physical commitment in a focused owned-action search rather than a strategic shortlist. Retain typed groups, original two-group cohorts, held Hajr, both Ornithopter modes, city access and current Kulon bonus. Deterministic preparation on private clones retains physical cost semantics without inventing response/discard events in reachability. Existing native/optional/Advanced movement retains its actual controls. Verify 17-file218/218, types/lint/build, seven original-action gameplay/control/table-rendering programmes and all four existing legal profiles. Original public classic, paid cache Ornithopter and native allowed-Kulon sources use only unopened positions and scalar lotteries; extra-card/force-loss edges are explicitly controlled. Repair nearby stale fixture continuations to acknowledge Semuta/battle outcomes and put pre-movement Hajr in Basic. Full modes, wider preparation/timing/module coverage, comprehensive assurance and deployed acceptance remain open. No strategic tuning or saved-room/reset/restart/deployment operation.
This commit is contained in:
1 parent
b4a556d5d3
commit
edbcbe28de
23 files changed
+1471
-218
No files matched your search
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import { MovementPromises } from './movement-promises';
|
||||
import { integerInputValue } from '../lib/integer-input';
|
||||
import { EcazSetup } from './ecaz-setup';
|
||||
import { StormCardInspector, StormCardLogInspector } from './storm-cards';
|
||||
@@ -1959,6 +1960,7 @@ export function GameTable({
|
||||
</div>
|
||||
{g.battle?.revealed && <a className="battle-display-link" href="#revealed-battle-plans">Compare revealed battle plans</a>}
|
||||
<PrivateBattlePlan game={g} />
|
||||
<MovementPromises game={g} act={act} busy={reactionBusy} />
|
||||
{!reactionOwnsControls && g.semutaReaction && <SemutaReaction game={g} act={act} busy={reactionBusy} />}
|
||||
{g.karamaBlocked && <p className="notice" role="status">{g.karamaBlocked}</p>}
|
||||
{g.karamaPurposeBlocks?.map(({purpose, message}) => <p key={purpose} className="notice" role="status">{message}</p>)}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
import { TERRITORIES } from '@/game/board';
|
||||
import { validMovementClaim, type MovementClaim } from '@/game/movement-promises';
|
||||
import { Input } from './ui/input';
|
||||
|
||||
export function MovementClaimFields({ id, value, onChange }: {
|
||||
id: string; value: MovementClaim; onChange: (value: MovementClaim) => void;
|
||||
}) {
|
||||
return <fieldset className="grid gap-2">
|
||||
<legend>Ground-movement condition</legend>
|
||||
<label htmlFor={`${id}-territory`}>Ground-movement destination</label>
|
||||
<select id={`${id}-territory`} value={value.territory}
|
||||
onChange={event => onChange({ ...value, territory: event.target.value })}>
|
||||
{TERRITORIES.map(destination => <option key={destination.id} value={destination.id}>{destination.name}</option>)}
|
||||
</select>
|
||||
<label htmlFor={`${id}-minimum`}>Minimum physical forces in one ground move</label>
|
||||
<Input id={`${id}-minimum`} type="number" min={1} max={20} step={1} required
|
||||
value={Number.isNaN(value.minimum) ? '' : value.minimum} aria-invalid={!validMovementClaim(value)}
|
||||
onChange={event => onChange({ ...value, minimum: event.currentTarget.valueAsNumber })} />
|
||||
</fieldset>;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
import type { Action, GameView } from '@/game/engine';
|
||||
import { liveMovementPromises, movementClaimText } from '@/game/movement-promises';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
export function MovementPromises({ game, act, busy }: { game: GameView; act: (action: Action) => void; busy: boolean }) {
|
||||
const own = liveMovementPromises(game.movementPromises ?? [], game.me, game.turn);
|
||||
if (!own.length) return null;
|
||||
const next = game.movementPromisePolicy?.[0];
|
||||
const amount = next?.forces && typeof next.forces === 'object'
|
||||
? Object.values(next.forces as Record<string, number>).reduce((sum, count) => sum + count, 0) : typeof next?.amount === 'number' ? next.amount : 0;
|
||||
const destination = next && game.combatLocations.find(location => location.id === next.territory)?.name;
|
||||
const destinationLabel = destination ?? (typeof next?.territory === 'string' ? next.territory : 'the chosen destination');
|
||||
const sector = typeof next?.sector === 'number' ? next.sector : 0;
|
||||
const label = next?.type === 'card' ? 'Play Hajr to prepare the required movement'
|
||||
: next?.type === 'endMovement' ? 'Finish movement while honoring No'
|
||||
: `${next?.movementCard ? 'Use Ornithopter and move' : 'Move'} ${amount} forces to ${destinationLabel}, sector ${sector}`;
|
||||
return <section className="notice space-y-3" aria-label="Your ground-movement promises">
|
||||
<h4>Your ground-movement promises</h4>
|
||||
{own.map((promise, index) => <p key={index}>{promise.answer ? 'Yes: you must' : 'No: you must not'} {movementClaimText(promise)} before this Movement turn ends.</p>)}
|
||||
<p className="fine">One qualifying move fulfills Yes. No remains binding through every later Hajr or Ornithopter move. Opposing effects release an answer only if it becomes impossible.</p>
|
||||
{next && <Button className="game-action min-h-11 whitespace-normal" disabled={busy} onClick={() => { if (!busy) act(next); }}>{label}</Button>}
|
||||
</section>;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
'use client';
|
||||
import { MovementClaimFields } from './movement-claim-fields';
|
||||
import { validMovementClaim, type MovementClaim } from '@/game/movement-promises';
|
||||
import { ShipmentClaimFields, invalidShipmentClause, type ShipmentClauseInput } from './shipment-claim-fields';
|
||||
import { BattleClaimFields } from './battle-promises';
|
||||
import {
|
||||
@@ -50,8 +52,9 @@ export function Truthtrance({
|
||||
g.players.find((p) => p.id !== me.id)!.id,
|
||||
);
|
||||
const [kind, setKind] = useState<
|
||||
'fact' | 'freeform' | 'battlePlan' | 'shipment' | 'mixedShipment'
|
||||
'fact' | 'freeform' | 'battlePlan' | 'shipment' | 'mixedShipment' | 'movement'
|
||||
>('fact');
|
||||
const [movement, setMovement] = useState<MovementClaim>({ territory: 'carthag', minimum: 6 });
|
||||
const [shipmentJoin, setShipmentJoin] = useState<'single' | 'and' | 'or'>('single');
|
||||
const [shipmentClauses, setShipmentClauses] = useState<ShipmentClauseInput[]>([
|
||||
{ territory: 'carthag', minimum: 6 },
|
||||
@@ -105,6 +108,7 @@ export function Truthtrance({
|
||||
? planClauses[0]
|
||||
: { kind: join, terms: planClauses },
|
||||
}
|
||||
: kind === 'movement' ? { kind, target, ...movement }
|
||||
: kind === 'shipment'
|
||||
? {
|
||||
kind,
|
||||
@@ -212,6 +216,13 @@ export function Truthtrance({
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : w.question!.kind === 'movement' ? (
|
||||
<>
|
||||
<p className="notice">Yes requires one matching ground movement before your turn ends, while possible. No forbids every matching move through later Hajr or Ornithopter opportunities. Separate smaller groups do not add together. The No-Field marker itself is not a physical force counter.</p>
|
||||
{(g.truthMovementAnswers ?? []).map(answer => (
|
||||
<div key={answer}>{button(answer === 'unknown' ? 'I don’t know yet' : `Answer ${answer === 'yes' ? 'Yes' : 'No'}`, { type: 'truthAnswer', answer })}</div>
|
||||
))}
|
||||
</>
|
||||
) : w.question!.kind === 'battlePlan' ? (
|
||||
<>
|
||||
<p className="notice">
|
||||
@@ -319,6 +330,7 @@ export function Truthtrance({
|
||||
<option value="fact">
|
||||
Verified current facts
|
||||
</option>
|
||||
<option value="movement">Bind ground movement this turn</option>
|
||||
<option value="shipment">
|
||||
Bind a shipment from reserves
|
||||
</option>
|
||||
@@ -333,7 +345,15 @@ export function Truthtrance({
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
{kind === 'shipment' || kind === 'mixedShipment' ? (
|
||||
{kind === 'movement' ? (
|
||||
<>
|
||||
<MovementClaimFields id={`${factId}-movement`} value={movement} onChange={setMovement} />
|
||||
<p className="fine">One ground movement of the chosen physical minimum, from now until the respondent’s Movement turn ends. Shipment, HMS relocation and Ambassador relocation do not count. Owned Hajr and Ornithopter can prepare a later move. No stays binding after earlier nonmatching moves.</p>
|
||||
{!g.truthMovementTargets.includes(target) && <p className="notice">Choose the active Basic player after shipment and finish pending decisions. Optional-module and Advanced movement promises remain unfinished.</p>}
|
||||
<p className="notice" aria-label="Public movement question preview">{truthQuestionText(question, leaderName, locationName)}</p>
|
||||
</>
|
||||
) : kind === 'shipment' || kind === 'mixedShipment' ? (
|
||||
|
||||
<>
|
||||
{kind === 'mixedShipment' ? (
|
||||
<MixedShipmentFields id={`${factId}-mixed`} value={mixed}
|
||||
@@ -472,6 +492,7 @@ export function Truthtrance({
|
||||
{button(
|
||||
'Ask publicly',
|
||||
{ type: 'truthAsk', question },
|
||||
(kind === 'movement' && (!validMovementClaim(movement) || !g.truthMovementTargets.includes(target))) ||
|
||||
(kind === 'shipment' &&
|
||||
(invalidShipment || !shipmentAvailable)) ||
|
||||
(kind === 'mixedShipment' &&
|
||||
|
||||
Reference in new issue
Block a user