Connect Advanced allied reserve transit and legal departure
This commit is contained in:
1 parent
869412bebd
commit
7cef1a3412
10 files changed
+118
-44
No files matched your search
@@ -12,10 +12,9 @@ export type BasicAlliedShipment = Readonly<{
|
||||
baseline: Readonly<{ normal: number; elite: number }>;
|
||||
}>;
|
||||
|
||||
/** Ordinary Basic reserve arrivals only; movement endpoints keep their guard. */
|
||||
export function isBasicAlliedShipmentVisit(
|
||||
/** Ordinary reserve arrivals only; movement endpoints keep their guard. */
|
||||
export function isAlliedShipmentVisit(
|
||||
context: Readonly<{
|
||||
advanced: boolean;
|
||||
phase: number;
|
||||
active: string | null;
|
||||
players: readonly OccupancySeat[];
|
||||
@@ -23,7 +22,7 @@ export function isBasicAlliedShipmentVisit(
|
||||
player: OccupancySeat & Readonly<{ shipped?: boolean; moved?: number }>,
|
||||
to: string,
|
||||
): boolean {
|
||||
if (context.advanced || context.phase !== 5 || context.active !== player.id ||
|
||||
if (context.phase !== 5 || context.active !== player.id ||
|
||||
player.shipped !== false || player.moved !== 0 || to === 'polar_sink' ||
|
||||
to === MOBILE_STRONGHOLD) return false;
|
||||
const ally = context.players.find(seat => seat.id === player.ally);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
type PlanetologistMoveMode,
|
||||
} from './planetologist-movement';
|
||||
import { territoryEntryBlock, strongholdPathBlocked } from './occupancy';
|
||||
import { isBasicAlliedShipmentVisit } from './allied-separation';
|
||||
import { isAlliedShipmentVisit } from './allied-separation';
|
||||
import {
|
||||
distance,
|
||||
gameDistance,
|
||||
@@ -67,7 +67,7 @@ export function botEntryAllowed(
|
||||
return false;
|
||||
try {
|
||||
return !territoryEntryBlock(g.players, p.id, to, advisors,
|
||||
kind === 'ship' && isBasicAlliedShipmentVisit(g, p, to));
|
||||
kind === 'ship' && isAlliedShipmentVisit(g, p, to));
|
||||
} catch (error) {
|
||||
if (error instanceof EcazOccupancyError) return false;
|
||||
throw error;
|
||||
|
||||
+8
-5
@@ -3485,6 +3485,7 @@ function policyActions(g: GameView): Action[] {
|
||||
const retained =
|
||||
level > 0 &&
|
||||
g.basicAlliedShipment?.territory !== territory(origin).name &&
|
||||
!g.advancedAllySeparation?.territories.includes(territory(origin).name) &&
|
||||
territory(origin).type === 'stronghold' &&
|
||||
!isAdvisor(me, origin)
|
||||
? 1
|
||||
@@ -4128,13 +4129,15 @@ export function botActions(g: GameView): Action[] {
|
||||
return sourced && botHomeworldShipmentPaymentAllowed(g, sourced) && !homeworldRevivalActionBlock(g, sourced) && !botArrivalBlock(g, sourced) ? [sourced] : [];
|
||||
});
|
||||
const me = g.players.find((p) => p.id === g.me)!;
|
||||
if (g.basicAlliedShipment && g.phase === 5 && g.active === me.id &&
|
||||
me.shipped && me.moved === 0) {
|
||||
// Do not choose an unrelated move or retain a city sentinel while visitors
|
||||
// owe departure. Native response/decision branches do not offer these moves.
|
||||
const advancedDepartures = g.advancedAllySeparation?.territories;
|
||||
if ((g.basicAlliedShipment || advancedDepartures?.length) &&
|
||||
g.phase === 5 && g.active === me.id && me.shipped && me.moved === 0) {
|
||||
// Depart from shared groups before unrelated moves; do not retain a
|
||||
// sentinel that the ending alliance constraint would send to Tanks.
|
||||
const departures = actions.filter(action => action.type === 'move' &&
|
||||
typeof action.from === 'string' &&
|
||||
territory(splitLocation(action.from).territory).name === g.basicAlliedShipment!.territory);
|
||||
(territory(splitLocation(action.from).territory).name === g.basicAlliedShipment?.territory ||
|
||||
advancedDepartures?.includes(territory(splitLocation(action.from).territory).name)));
|
||||
if (departures.length) return departures;
|
||||
}
|
||||
const ghola = standaloneGholaAction(g, actions);
|
||||
|
||||
+4
-4
@@ -567,7 +567,7 @@ import {
|
||||
settleAdvisors,
|
||||
arrivalAsAdvisor,
|
||||
} from './advisors';
|
||||
import { quoteAlliedSeparation, isBasicAlliedShipmentVisit, alliedShipmentPhysicalPool,
|
||||
import { quoteAlliedSeparation, isAlliedShipmentVisit, alliedShipmentPhysicalPool,
|
||||
type BasicAlliedShipment } from './allied-separation';
|
||||
import { casualtyOptions, maxCombatDial, maxCombatSupport, validCombatForces, type Casualties, type CombatForces } from './combat';
|
||||
import { FACTIONS, faction, type FactionId } from './catalog';
|
||||
@@ -25095,7 +25095,7 @@ function validatePhysicalShipment(g: Game, shipment: PendingShipment) {
|
||||
shipment.sector,
|
||||
!shipment.guildSecretEvent && g.advanced && p.faction === 'fremen',
|
||||
shipment.advisors,
|
||||
!shipment.noField && !shipment.alliedNoField && isBasicAlliedShipmentVisit(g, p, shipment.territory),
|
||||
!shipment.noField && !shipment.alliedNoField && isAlliedShipmentVisit(g, p, shipment.territory),
|
||||
);
|
||||
if (p.faction === 'fremen' && !shipment.guildSecretEvent)
|
||||
requireRule(
|
||||
@@ -25594,7 +25594,7 @@ function commitShipment(g: Game, shipment: PendingShipment, settlement?: {recipi
|
||||
}
|
||||
let homeworldOrigins = '';
|
||||
if (!shipment.noField) {
|
||||
if (!shipment.alliedNoField && n > 0 && isBasicAlliedShipmentVisit(g, p, to))
|
||||
if (!g.advanced && !shipment.alliedNoField && n > 0 && isAlliedShipmentVisit(g, p, to))
|
||||
g.basicAlliedShipment = {
|
||||
player: p.id, ally: p.ally!, turn: g.turn, territory: to,
|
||||
baseline: alliedShipmentPhysicalPool(p, to),
|
||||
@@ -30940,7 +30940,7 @@ function applyActionInner(
|
||||
'Only Ixians may ship directly into the mobile stronghold.',
|
||||
);
|
||||
allowedEntry(g, p, to, s, !guildSecretEvent && g.advanced && p.faction === 'fremen', advisors,
|
||||
action.noField === undefined && isBasicAlliedShipmentVisit(g, p, to));
|
||||
action.noField === undefined && isAlliedShipmentVisit(g, p, to));
|
||||
let noField: PendingShipment['noField'];
|
||||
if (action.noField !== undefined) {
|
||||
requireRule(
|
||||
|
||||
+7
-5
@@ -145,7 +145,7 @@ const phaseDetails = [
|
||||
'For movement, select the source forces. Enable Combine sectors to include several groups from the same territory.',
|
||||
'A Fremen move using the two-territory advantage has a Karama response. Cancellation leaves the forces in place and limits this move to one territory; city ornithopters and a later extra move keep their normal rules.',
|
||||
'Guild transport can move one exact physical group across the planet or return it to reserves. In classic games its normal half-price rate can be canceled separately from its transport permission. Ordinary movement is checked against range, storm and stronghold occupancy.',
|
||||
'You may pass through an ally’s territory on a legal route that ends elsewhere. Basic ordinary physical reserve shipments may also enter allied territory before movement, provided the visiting group departs during that turn. Storm and stronghold capacity remain binding. The table visibly labels its provisional no-exit policy: excess new visitors still present when the turn ends go to Tanks, without destroying the ally or granting an extra move. Concealed No-Fields, optional arrivals and Advanced entry remain separate.',
|
||||
'You may pass through an ally’s territory on a legal route that ends elsewhere. Ordinary physical reserve shipments in both modes may enter allied territory before movement, then depart during that turn. Storm and stronghold capacity remain binding. Basic visibly labels its provisional no-exit policy: excess new visitors still present go to Tanks. Advanced follows supplied p15: all the ending player’s shared forces go to Tanks, including old forces, subject to advisor/Polar/Ecaz exemptions. Neither grants an extra move. Concealed No-Fields and optional arrivals remain separate.',
|
||||
'Ecaz and its reciprocal ally may share territories and count as one faction toward stronghold capacity, without extra movement or storm/Homeworld exceptions. Their three-joint-stronghold victory and desert allocation are connected. Fremen/Ecaz cooccupation permits the Fremen special win in Sietch Tabr in Basic; supplied Advanced p32 permits both Tabr and Habbanya Sietch. Other victory conditions and exceptional combined interactions remain separate.',
|
||||
'A restored pending physical shipment is checked again before forces or spice are committed. If its saved turn, force allocation, destination, price or authorized contribution no longer matches the table, it is rejected without changing the saved declaration.',
|
||||
'Finish shipment and movement explicitly. Any Bene Gesserit free-shipment decision and its response must finish first.',
|
||||
@@ -3144,18 +3144,20 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
title: 'Advanced alliances: ending shared occupation',
|
||||
category: 'Advanced & expansions',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Prototyped',
|
||||
summary: 'Each ending player loses its own shared fighters; advisor coexistence, Polar Sink and native Ecaz coexistence are exempt.',
|
||||
steps: [
|
||||
'Under the adopted unofficial Advanced revision, finish each player’s Shipment and Movement separately. Your own fighters still sharing a territory with your ally go to Tanks when your turn ends. Your ally’s fighters stay; its later turn does not postpone your loss. An alliance formed this turn has no Advanced grace period.',
|
||||
'Polar Sink is exempt. If either allied group is Bene Gesserit advisors, coexistence sends neither group to Tanks. Bene Gesserit fighters have no advisor exemption. Native Ecaz peaceful coexistence keeps its own conditions, including applicable Karama effects.',
|
||||
'Before an ordinary finish, the table warns which public territories would lose your forces. It does not reveal force amounts or concealed No-Field values. Finish movement uses the existing action, without another loss confirmation. If no legal escape is available, you may still finish and accept the mandatory consequence.',
|
||||
'Complete any pending shipment, movement, arrival or response before ordinary finish controls become available. Extra-move and deferred-turn endings use the same separation rule; they do not replay the arrival or charge shipment again.',
|
||||
'Basic still uses its existing later-player deadline and formation-turn exemption, without this advisor exception. Homeworlds are outside this Dune-territory rule and keep their native allied-entry restrictions. The rule grants no new allied-entry permission or alliance formation window, and completed turns are not reevaluated.',
|
||||
'An ordinary physical reserve shipment may enter an allied territory before your movement on your own unused turn. Ship, then move out; stronghold capacity and storm still apply. The ending consequence covers your entire shared group, not only new visitors. No-Fields and optional Homeworld/mobile transport keep their separate legality.',
|
||||
'Basic still uses its older later-player deadline and formation-turn exemption, plus its visibly provisional new-visitor consequence. Homeworlds keep their native allied-entry restrictions. The rule grants no extra shipment, movement or alliance formation window; completed turns are not reevaluated.',
|
||||
],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'One canonical separation quote distinguishes Basic from Advanced timing and exceptions; native ending-group custody remains responsible for physical losses.', evidence: ['game/allied-separation.ts', 'game/engine.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'The own clean-turn warning contains public loss territories only; forced loss adds no action or confirmation and shared pending-control locks retain priority.', evidence: ['components/game-table.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'All four legal policies use native projected movement/ending availability and can finish with mandatory loss; no duplicate separation calculator or strategic alliance calibration was added.', evidence: ['game/bots.ts', 'tests/bot-advanced-source.test.ts'] },
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'A shared ordinary reserve-entry quote connects native preflight and commit. Canonical separation distinguishes Basic from Advanced timing and exceptions; native ending-group custody removes all Advanced shared forces.', evidence: ['game/allied-separation.ts', 'game/engine.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Pre-shipment notice explains the mode-specific consequence; own clean-turn warning contains public loss territories only. Mandatory loss adds no confirmation and pending-control locks retain priority.', evidence: ['components/game-table.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'All four legal policies use projected movement/ending availability. Native departure takes priority, without retaining a doomed city sentinel; a Medium typed-arrival/departure smoke passes. No separate separation calculator or strategic calibration was added.', evidence: ['game/bots.ts', 'game/bot-mobility.ts', 'tests/bot-advanced-source.test.ts'] },
|
||||
{ area: 'Documentation', status: 'Implemented', detail: 'The unofficial per-player/advisor departures are distinguished from the earlier publisher/FAQ and unchanged Basic policy.', evidence: ['docs/ALLIED_TRANSIT_RULES.md', 'docs/RULE_DECISIONS.md', 'docs/BASE_ADVANCED_READINESS_20260907.md'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Genuine native overlaps cover first/later newly allied ends, exact normal/elite custody, Hajr, advisor/Ecaz/Polar exceptions and unchanged Basic policy. Authenticated SQLite covers restart, races, privacy and immutable rejects. Actual phone ending moved only the ending group to Tanks and refresh retained it; wider combinations, full modes and deployment remain open.', evidence: ['tests/allied-separation.test.ts', 'tests/advanced-source-engine.test.ts', 'tests/advanced-source-recovery.test.ts', 'tests/bot-advanced-source.test.ts', 'docs/ALLIED_TRANSIT_RULES.md'] },
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user