Counter each changing origin in a resident BG gather
Hold both physical groups until all required matching responses allow the original movement. Share normal packet denials across gather and ordinary moves, then resume one original movement suffix.
This commit is contained in:
1 parent
b2a20226d7
commit
0d8481e821
16 files changed
+987
-10
No files matched your search
@@ -0,0 +1,94 @@
|
||||
import { gameTerritories, splitLocation } from './board';
|
||||
import {
|
||||
AdvisorMatchingError,
|
||||
quoteAdvisorMatching,
|
||||
type AdvisorMatchingContext,
|
||||
type AdvisorMatchingQuote,
|
||||
} from './advisor-matching';
|
||||
|
||||
export type AdvisorGatherMatchingIntent = {
|
||||
owner: string;
|
||||
move: number;
|
||||
destination: string;
|
||||
sector: number;
|
||||
advisors: boolean;
|
||||
group: Record<string, number>;
|
||||
};
|
||||
|
||||
export class AdvisorGatherMatchingError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'AdvisorGatherMatchingError';
|
||||
}
|
||||
}
|
||||
|
||||
const identifier = (value: unknown): value is string =>
|
||||
typeof value === 'string' && value.trim().length > 0;
|
||||
const nonnegative = (value: unknown): value is number =>
|
||||
Number.isSafeInteger(value) && (value as number) >= 0;
|
||||
const positive = (value: unknown): value is number =>
|
||||
Number.isSafeInteger(value) && (value as number) > 0;
|
||||
const record = (value: unknown): value is Record<string, unknown> =>
|
||||
!!value && typeof value === 'object' && !Array.isArray(value);
|
||||
function requireMatching(value: unknown, message: string): asserts value {
|
||||
if (!value) throw new AdvisorGatherMatchingError(message);
|
||||
}
|
||||
|
||||
/** Resident gather changes each incoming origin independently. The original
|
||||
* Planetologist quote retains trainer, route and complete-order permission;
|
||||
* enemy-only and empty destinations retain their existing native policy. */
|
||||
export function quoteAdvisorGatherMatching(
|
||||
ctx: AdvisorMatchingContext,
|
||||
intent: AdvisorGatherMatchingIntent,
|
||||
): AdvisorMatchingQuote[] {
|
||||
if (ctx.advanced !== true || ctx.status !== 'playing' || ctx.phase !== 5 || !positive(ctx.turn)) return [];
|
||||
requireMatching(record(intent) && identifier(intent.owner), 'Choose the current BG gather owner.');
|
||||
const owners = ctx.players.filter((player) => player.id === intent.owner);
|
||||
const owner = owners[0];
|
||||
if (owners.length !== 1 || owner.faction !== 'beneGesserit' || ctx.active !== owner.id) return [];
|
||||
requireMatching(nonnegative(intent.move) && identifier(intent.destination) && nonnegative(intent.sector) &&
|
||||
typeof intent.advisors === 'boolean',
|
||||
'Choose the original gather movement and resident destination stance.');
|
||||
const residents = Object.entries(owner.forces).some(([key, count]) =>
|
||||
splitLocation(key).territory === intent.destination && count > 0);
|
||||
if (!residents) return [];
|
||||
const places = gameTerritories(ctx);
|
||||
requireMatching(places.some((place) => place.id === intent.destination && place.sectors.includes(intent.sector)),
|
||||
'Gather matching needs a current board destination sector.');
|
||||
requireMatching(record(intent.group) && Object.keys(intent.group).length > 0,
|
||||
'Select two nonempty physical board origins for gather matching.');
|
||||
const origins = new Map<string, Record<string, number>>();
|
||||
let total = 0;
|
||||
// Sorting also gives every public origin packet a stable physical-key order.
|
||||
for (const key of Object.keys(intent.group).sort()) {
|
||||
const point = splitLocation(key);
|
||||
requireMatching(key === `${point.territory}:${point.sector}` &&
|
||||
places.some((place) => place.id === point.territory && place.sectors.includes(point.sector)),
|
||||
'Gather matching needs physical counters from current canonical board sectors.');
|
||||
const count = intent.group[key];
|
||||
const held = owner.forces[key];
|
||||
requireMatching(positive(count) && nonnegative(held) && held >= count,
|
||||
'The selected gather packet is no longer held at its original board sources.');
|
||||
total += count;
|
||||
requireMatching(positive(total), 'The selected gather packet exceeds the supported physical count.');
|
||||
const group = origins.get(point.territory) ?? {};
|
||||
group[key] = count;
|
||||
origins.set(point.territory, group);
|
||||
}
|
||||
requireMatching(origins.size === 2, 'Resident gather matching requires exactly two selected origin territories.');
|
||||
const quotes: AdvisorMatchingQuote[] = [];
|
||||
try {
|
||||
for (const origin of [...origins.keys()].sort()) {
|
||||
const quote = quoteAdvisorMatching(ctx, {
|
||||
kind: 'movement', owner: owner.id, move: intent.move, origin,
|
||||
destination: intent.destination, sector: intent.sector, advisors: intent.advisors,
|
||||
group: origins.get(origin)!,
|
||||
});
|
||||
if (quote) quotes.push(quote);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AdvisorMatchingError) throw new AdvisorGatherMatchingError(error.message);
|
||||
throw error;
|
||||
}
|
||||
return quotes;
|
||||
}
|
||||
+8
-1
@@ -119,6 +119,7 @@ import { fighterCount, isAdvisor } from './advisors';
|
||||
import { quoteAdvisorMatching, AdvisorMatchingError } from './advisor-matching';
|
||||
import { quoteAdvisorAmbassadorMatching, AdvisorAmbassadorMatchingError } from './advisor-ambassador-matching';
|
||||
import { guildTransportQuote } from './transport-quote';
|
||||
import { quoteAdvisorGatherMatching, AdvisorGatherMatchingError } from './advisor-gather-matching';
|
||||
import { casualtyOptions, maxCombatDial, maxCombatSupport, type CombatForces } from './combat';
|
||||
import { reinforcementsBattleForces } from './reinforcements-homeworld';
|
||||
import type { NativeReserveSelections } from './homeworld-native-reserves';
|
||||
@@ -4087,7 +4088,7 @@ function standaloneGholaAction(g: GameView, ordinary: Action[]): Action | null {
|
||||
|
||||
/** Obligations apply across policy branches, including choosing to move first. */
|
||||
function botAdvisorMatchingAllowed(g:GameView,action:Action) {
|
||||
if(!g.advisorMatchingDenied?.length || !['move','ship'].includes(action.type) || action.planetologist==='gather') return true;
|
||||
if(!g.advisorMatchingDenied?.length || !['move','ship'].includes(action.type)) return true;
|
||||
const me=g.players.find(p=>p.id===g.me)!;
|
||||
if(!g.advanced || me.faction!=='beneGesserit' || g.phase!==5 || g.active!==me.id) return true;
|
||||
const destination=String(action.territory),sector=Number(action.sector);
|
||||
@@ -4095,6 +4096,12 @@ function botAdvisorMatchingAllowed(g:GameView,action:Action) {
|
||||
action.forces && typeof action.forces==='object' ? action.forces as Record<string,number> :
|
||||
{[String(action.from)]:Number(action.amount)};
|
||||
const origins=[...new Set(Object.keys(group).map(key=>splitLocation(key).territory))];
|
||||
if(action.type==='move'&&action.planetologist==='gather'){
|
||||
try{
|
||||
quoteAdvisorGatherMatching(g,{owner:me.id,move:me.moved??0,destination,sector,
|
||||
advisors:isAdvisor(me,destination),group});return true;
|
||||
}catch(error){if(!(error instanceof AdvisorGatherMatchingError))throw error;return false;}
|
||||
}
|
||||
if(action.type==='move' && origins.length!==1) return true;
|
||||
try {
|
||||
quoteAdvisorMatching(g,{kind:action.type==='ship'?'shipment':'movement',owner:me.id,
|
||||
|
||||
+92
-3
@@ -612,6 +612,7 @@ import { quoteAdvisorMatching, validateAdvisorMatching, advisorMatchingKey,
|
||||
AdvisorMatchingError, type AdvisorMatchingSource } from './advisor-matching';
|
||||
import { quoteAdvisorAmbassadorMatching, validateAdvisorAmbassadorMatching, advisorAmbassadorMatchingKey, AdvisorAmbassadorMatchingError, type AdvisorAmbassadorMatchingSource } from './advisor-ambassador-matching';
|
||||
import { quoteAdvisorTransportMatching, validateAdvisorTransportMatching, advisorTransportMatchingKey, AdvisorTransportMatchingError, type AdvisorTransportMatchingSource } from './advisor-transport-matching';
|
||||
import { quoteAdvisorGatherMatching, AdvisorGatherMatchingError } from './advisor-gather-matching';
|
||||
import { spiritualAdvisorTargetBlock, spiritualAdvisorArrival, validateSpiritualAdvisorSource,
|
||||
spiritualAdvisorMatchingKey, SpiritualAdvisorError, type SpiritualAdvisorSource } from './spiritual-advisor';
|
||||
import {
|
||||
@@ -1076,6 +1077,8 @@ export type ResponseWindow = {
|
||||
advisorMatchingSource?: AdvisorMatchingSource;
|
||||
advisorAmbassadorMatchingSource?: AdvisorAmbassadorMatchingSource;
|
||||
advisorTransportMatchingSource?: AdvisorTransportMatchingSource;
|
||||
advisorGatherMatchingSource?: AdvisorMatchingSource;
|
||||
advisorGatherMatchingPosition?: {current:number;total:number};
|
||||
spiritualAdvisorSource?: SpiritualAdvisorSource;
|
||||
fremenGrant?: FremenRevivalGrantSource;
|
||||
spiceBankerIncomePayment?: SpiceBankerIncomePayment;
|
||||
@@ -1299,6 +1302,13 @@ export type Game = {
|
||||
intent: {kind:'movement'; order:MovementOrder} | {kind:'shipment'; order:PendingShipment};
|
||||
};
|
||||
advisorMatchingDenied?: string[];
|
||||
pendingAdvisorGatherMatching?: {
|
||||
order: MovementOrder;
|
||||
receipts: AdvisorMatchingSource[];
|
||||
signature: string;
|
||||
index: number;
|
||||
approved: string[];
|
||||
};
|
||||
pendingAdvisorAmbassadorMatching?: {
|
||||
receipt: AdvisorAmbassadorMatchingSource;
|
||||
order: MovementOrder;
|
||||
@@ -14642,6 +14652,7 @@ function revivalCancellationQuote(g: Game, response: ResponseWindow) {
|
||||
}
|
||||
}
|
||||
function movementCancellationQuote(g: Game, response: ResponseWindow) {
|
||||
if (response.advisorGatherMatchingSource) { currentAdvisorGatherMatching(g,response); return null; }
|
||||
if (response.advisorTransportMatchingSource) { currentAdvisorTransportMatching(g,response); return null; }
|
||||
if (response.advisorAmbassadorMatchingSource) { currentAdvisorAmbassadorMatching(g,response); return null; }
|
||||
if (response.spiritualAdvisorSource) { currentSpiritualAdvisorResponse(g,response); return null; }
|
||||
@@ -16604,6 +16615,7 @@ function groundMovementProbe(source: Game, actor: string): Game {
|
||||
game.pendingFremenMove = null; game.pendingIxMove = null; game.pendingChoamMove = null;
|
||||
game.pendingAmbassador = null; game.pendingTerrorEntry = null; game.pendingArrivalOverlap = null;
|
||||
delete game.pendingAdvisorTransportMatching;
|
||||
delete game.pendingAdvisorGatherMatching;
|
||||
game.pendingArrivalReaction = []; game.pendingExchange = null;
|
||||
if (game.pendingShipment?.player === game.active && recoverableUnsupportedShipment(game.pendingShipment)) {
|
||||
const shipment = game.pendingShipment; game.pendingShipment = null;
|
||||
@@ -17011,9 +17023,12 @@ function declareGroundMovement(g: Game, p: Player, action: Action, quoteOnly = f
|
||||
);
|
||||
validateFlightSelection(g, p, move);
|
||||
const matching = quoteMatchingMovement(g, move);
|
||||
const gatherMatching=quoteGatherMatching(g,move);
|
||||
if (quoteOnly) return move;
|
||||
checkGroundMovementPromises(g, p, move);
|
||||
if (advisorGather) {
|
||||
if(gatherMatching.length){
|
||||
openAdvisorGatherMatching(g,move,gatherMatching);
|
||||
} else if (advisorGather) {
|
||||
g.response = {
|
||||
kind: 'advisorFlip', owner: id, location: to, advisors: false, amount: n, passed: [],
|
||||
advisorResume: 'planetologistGather', advisorGatherMove: { turn: g.turn, move: p.moved, order: move },
|
||||
@@ -20773,6 +20788,76 @@ function finishAdvisorAmbassadorMatching(g:Game,response:ResponseWindow,canceled
|
||||
{faction:'beneGesserit',name:'Ambassador group matching prevented'});
|
||||
}else offerChoamMovement(g,parent.order);
|
||||
}
|
||||
function gatherMatchingRule<T>(quote:()=>T):T{
|
||||
try{return quote();}catch(error){if(error instanceof AdvisorGatherMatchingError)throw new RuleError(error.message);throw error;}
|
||||
}
|
||||
function quoteGatherMatching(g:Game,move:MovementOrder){
|
||||
if(move.planetologist?.mode!=='gather')return [];
|
||||
return gatherMatchingRule(()=>quoteAdvisorGatherMatching(g,{owner:move.player,move:getPlayer(g,move.player).moved,
|
||||
destination:move.to,sector:move.sector,advisors:move.advisors,group:Object.fromEntries(move.group)}));
|
||||
}
|
||||
function nextAdvisorGatherMatching(g:Game){
|
||||
const parent=g.pendingAdvisorGatherMatching!,source=parent.receipts[parent.index];
|
||||
g.response={kind:'advisorFlip',owner:source.owner,location:source.destination,advisors:source.advisors,
|
||||
amount:Object.values(source.group).reduce((n,count)=>n+count,0),passed:[],advisorGatherMatchingSource:source,
|
||||
advisorGatherMatchingPosition:{current:parent.index+1,total:parent.receipts.length}};
|
||||
}
|
||||
function openAdvisorGatherMatching(g:Game,order:MovementOrder,quotes:Omit<AdvisorMatchingSource,'event'>[]){
|
||||
requireRule(!g.pendingAdvisorGatherMatching&&!g.response&&!g.decision&"es.length>0,
|
||||
'Finish the original gathered matching before another movement.');
|
||||
const receipts=quotes.map(quote=>({event:crypto.randomUUID(),...quote}));
|
||||
g.pendingAdvisorGatherMatching={order,receipts,signature:JSON.stringify([order,receipts]),index:0,approved:[]};
|
||||
nextAdvisorGatherMatching(g);
|
||||
}
|
||||
function currentAdvisorGatherMatching(g:Game,response:ResponseWindow){
|
||||
const parent=g.pendingAdvisorGatherMatching,source=response.advisorGatherMatchingSource;
|
||||
requireRule(parent&&source&&Number.isSafeInteger(parent.index)&&parent.index>=0&&parent.index<parent.receipts.length&&
|
||||
parent.approved.length===parent.index&&parent.approved.every((event,index)=>event===parent.receipts[index].event)&&
|
||||
parent.signature===JSON.stringify([parent.order,parent.receipts])&&parent.order.planetologist?.mode==='gather'&&
|
||||
JSON.stringify(parent.receipts[parent.index])===JSON.stringify(source)&&response.kind==='advisorFlip'&&
|
||||
response.owner===source.owner&&response.location===source.destination&&response.advisors===source.advisors&&
|
||||
response.amount===Object.values(source.group).reduce((n,count)=>n+count,0)&&
|
||||
response.advisorGatherMatchingPosition?.current===parent.index+1&&
|
||||
response.advisorGatherMatchingPosition?.total===parent.receipts.length&&
|
||||
!response.advisorResume&&!response.advisorMatchingSource&&!response.advisorAmbassadorMatchingSource&&
|
||||
!response.advisorTransportMatchingSource&&!response.spiritualAdvisorSource&&!response.automaticAdvisorFlip&&
|
||||
!response.advisorPreparationSource&&!response.advisorGatherMove,
|
||||
'The gathered matching response lost its original two-origin declaration or approvals.');
|
||||
matchingAdvisorRule(()=>validateAdvisorMatching(g,source));
|
||||
validateMovementOrder(g,getPlayer(g,source.owner),parent.order);
|
||||
const quotes=quoteGatherMatching(g,parent.order);
|
||||
requireRule(JSON.stringify(quotes)===JSON.stringify(parent.receipts.map(receipt=>Object.fromEntries(
|
||||
Object.entries(receipt).filter(([key])=>key!=='event')))),
|
||||
'The gathered matching responses no longer describe their original selected groups.');
|
||||
return parent;
|
||||
}
|
||||
function advisorGatherMatchingIntegrity(g:Game){
|
||||
if(!g.pendingAdvisorGatherMatching)return;
|
||||
const continuation=g.pendingTreacheryDiscard?.continuation;
|
||||
const controls=[g,g.pendingExchange,g.pendingNullentropy?.resume,g.pendingRicheseGift?.resume,
|
||||
g.pendingRichesePurchaseIncome?.resume,g.summonedWorm?.resume,g.pendingKull?.resume,
|
||||
continuation&&'resume'in continuation?continuation.resume:null];
|
||||
const responses=controls.flatMap(c=>c?.response?[c.response]:[]);
|
||||
for(const {pending}of savedKaramaContexts(g))if(pending.use.kind==='cancel')responses.push(pending.use.response);
|
||||
const matching=responses.filter(r=>r.advisorGatherMatchingSource);
|
||||
requireRule(matching.length>0,'Restore the original gathered matching response.');
|
||||
for(const response of matching)currentAdvisorGatherMatching(g,response);
|
||||
}
|
||||
function finishAdvisorGatherMatching(g:Game,response:ResponseWindow,canceled:boolean){
|
||||
const parent=currentAdvisorGatherMatching(g,response),source=parent.receipts[parent.index];
|
||||
if(canceled){
|
||||
delete g.pendingAdvisorGatherMatching;
|
||||
const key=advisorMatchingKey(source);if(!(g.advisorMatchingDenied??=[]).includes(key))g.advisorMatchingDenied.push(key);
|
||||
log(g,`${getPlayer(g,source.owner).name}'s gathered matching from ${territory(source.origin).name} in ${territory(source.destination).name} was prevented. Neither group moved or changed type, and the movement remains unused. Choose a legal replacement without this prevented packet.`,
|
||||
{faction:'beneGesserit',name:'Gathered group matching prevented'});
|
||||
}else{
|
||||
parent.approved.push(source.event);parent.index++;
|
||||
if(parent.index<parent.receipts.length){nextAdvisorGatherMatching(g);return;}
|
||||
delete g.pendingAdvisorGatherMatching;
|
||||
validateMovementOrder(g,getPlayer(g,source.owner),parent.order);
|
||||
offerChoamMovement(g,parent.order);
|
||||
}
|
||||
}
|
||||
function matchingAdvisorRule<T>(quote: () => T): T {
|
||||
try { return quote(); }
|
||||
catch(error) { if(error instanceof AdvisorMatchingError) throw new RuleError(error.message); throw error; }
|
||||
@@ -23305,7 +23390,7 @@ function decideSharedSpice(g: Game, id: string, action: Action) {
|
||||
function victory(g: Game, quote?: VictoryQuote) {
|
||||
requireRule(!g.pendingAutomaticAdvisorFlips?.length && !g.pendingAutomaticAdvisorTransport &&
|
||||
!g.pendingAutomaticAdvisorGamont && !g.pendingAutomaticAdvisorBattle && !g.pendingAutomaticAdvisorHomeworldTransport &&
|
||||
!g.pendingAutomaticAdvisorFaceDance && !g.pendingAutomaticAdvisorStorm && !g.pendingAutomaticAdvisorRelocation && !g.pendingAdvisorMatching && !g.pendingAdvisorAmbassadorMatching && !g.pendingAdvisorTransportMatching && !g.pendingSpiritualAdvisor,
|
||||
!g.pendingAutomaticAdvisorFaceDance && !g.pendingAutomaticAdvisorStorm && !g.pendingAutomaticAdvisorRelocation && !g.pendingAdvisorMatching && !g.pendingAdvisorAmbassadorMatching && !g.pendingAdvisorTransportMatching && !g.pendingAdvisorGatherMatching && !g.pendingSpiritualAdvisor,
|
||||
'Resolve mandatory advisor conversions before checking victory.');
|
||||
quote ??= currentVictoryQuote(g);
|
||||
applyAdvisorReleases(g, quote.released);
|
||||
@@ -29097,7 +29182,8 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
|
||||
);
|
||||
} else if (response.kind === 'advisorFlip') {
|
||||
const bg = getPlayer(g, response.owner);
|
||||
if (response.advisorTransportMatchingSource) finishAdvisorTransportMatching(g,response,canceled);
|
||||
if (response.advisorGatherMatchingSource) finishAdvisorGatherMatching(g,response,canceled);
|
||||
else if (response.advisorTransportMatchingSource) finishAdvisorTransportMatching(g,response,canceled);
|
||||
else if (response.advisorAmbassadorMatchingSource) finishAdvisorAmbassadorMatching(g,response,canceled);
|
||||
else if (response.spiritualAdvisorSource) finishSpiritualAdvisorResponse(g,response,canceled);
|
||||
else if (response.advisorMatchingSource) finishAdvisorMatching(g,response,canceled);
|
||||
@@ -32784,6 +32870,7 @@ export function applyAction(state: Game, id: string, action: Action): Game {
|
||||
treacheryDiscardIntegrity(state);
|
||||
shipmentPromiseIntegrity(state);
|
||||
saphoMovementIntegrity(state);
|
||||
advisorGatherMatchingIntegrity(state);
|
||||
advisorTransportMatchingIntegrity(state);
|
||||
advisorAmbassadorMatchingIntegrity(state);
|
||||
spiritualAdvisorIntegrity(state);
|
||||
@@ -33197,6 +33284,7 @@ export function normalizeAutomaticGame(state: Game): Game {
|
||||
treacheryDiscardIntegrity(state);
|
||||
shipmentPromiseIntegrity(state);
|
||||
saphoMovementIntegrity(state);
|
||||
advisorGatherMatchingIntegrity(state);
|
||||
advisorTransportMatchingIntegrity(state);
|
||||
advisorAmbassadorMatchingIntegrity(state);
|
||||
spiritualAdvisorIntegrity(state);
|
||||
@@ -37286,6 +37374,7 @@ export function viewGame(state: Game, id: string) {
|
||||
treacheryDiscardIntegrity(state);
|
||||
shipmentPromiseIntegrity(state);
|
||||
saphoMovementIntegrity(state);
|
||||
advisorGatherMatchingIntegrity(state);
|
||||
advisorTransportMatchingIntegrity(state);
|
||||
advisorAmbassadorMatchingIntegrity(state);
|
||||
spiritualAdvisorIntegrity(state);
|
||||
|
||||
@@ -194,6 +194,9 @@ const cancellationSources = {
|
||||
...(response.automaticAdvisorFlip.producer === 'spiritualArrival'
|
||||
? { spiritual:g.pendingSpiritualAdvisor?.source??null } : {}),
|
||||
group: g.players.find(p => p.id === response.owner)?.advisors?.[response.location ?? ''] ?? null }
|
||||
: response.advisorGatherMatchingSource
|
||||
? {matching:response.advisorGatherMatchingSource,
|
||||
pending:g.pendingAdvisorGatherMatching?.receipts[g.pendingAdvisorGatherMatching.index]??null}
|
||||
: response.advisorTransportMatchingSource
|
||||
? {matching:response.advisorTransportMatchingSource,pending:g.pendingAdvisorTransportMatching?.receipt??null}
|
||||
: response.advisorAmbassadorMatchingSource
|
||||
|
||||
@@ -309,6 +309,7 @@ export function quoteMovementCancellation(
|
||||
if (response.kind === 'advisorFlip' && response.advisorMatchingSource) return null;
|
||||
if (response.kind === 'advisorFlip' && response.advisorAmbassadorMatchingSource) return null;
|
||||
if (response.kind === 'advisorFlip' && response.advisorTransportMatchingSource) return null;
|
||||
if (response.kind === 'advisorFlip' && response.advisorGatherMatchingSource) return null;
|
||||
if (response.kind === 'advisorFlip' && response.spiritualAdvisorSource) return null;
|
||||
if (
|
||||
![
|
||||
|
||||
+26
-5
@@ -4317,6 +4317,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'A normal shipment enters as fighters unless joining existing advisors. Opposite-type normal ground arrivals and normal reserves joining advisors now offer Karama matching prevention before movement or payment. Residents keep their type. Provisionally stop a prevented packet into this territory for that action opportunity, leaving a legal replacement and the unused action available. Same-type moves need no flip. Advisors entering another occupied territory may keep their existing optional fighter choice.',
|
||||
'An allied Fremen Ambassador relocation also offers incoming matching before any selected force moves or collects spice. Residents keep their type. Provisionally, prevention restores the same relocation chooser while denying that same packet here for this Ambassador event. The already-triggered token and ordinary movement/shipment remain unchanged; another legal packet or destination stays available.',
|
||||
'Fresh Guild cross-transport and Junction board arrivals also offer incoming matching before their original permission, price, payment or transfer. Junction Homeworld groups provisionally enter fighter-side. Prevention leaves the original shipment and grant unused while that same packet here stays unavailable for this producer opportunity; choose another legal packet or destination. Junction permission and tariff remain immune. Secret Ally keeps its actual Nexus until the shipment commits.',
|
||||
'Planetologist gathering into your residents offers one matching response per incoming origin that must change type. Two changing origins need two separate responses. Provisionally, neither group moves or changes type until every required allowance; preventing any group leaves both origins and the movement unused. That prevented origin packet here is also unavailable to a normal move during the same opportunity. Choose another legal packet or destination.',
|
||||
'After Trip to Gamont completes during Mentat, the last outsider’s return or empty No-Field reveal can leave advisors alone. Karama can then prevent their conversion without undoing the completed effect or card use. The final CHOAM opportunity resumes afterward; victory waits for its original confirmation. Ordinary Mentat does not gain an extra final chooser.',
|
||||
'A committed Guild cross-shipment or return to reserves can also leave advisors alone. Original transport permission, rate and any Guild income resolve first; payment, force transfer and shipment use stay committed if the later conversion is prevented. The ordinary ground move remains unused. Cross-shipment arrival follows the conversion once; a reserve return has no arrival.',
|
||||
'After actual battle losses leave surviving bystanding advisors alone, Karama may prevent their conversion without undoing losses or costs. Mandatory card disposal, original skills and substitution finish first. Provisionally resolve this before optional winner-card cleanup and later aftermath, then resume the original cleanup once. An explosion that destroys the advisors creates no conversion.',
|
||||
@@ -4335,8 +4336,13 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
{
|
||||
"area": "Implementation",
|
||||
"status": "Partial",
|
||||
"detail": "Fresh Guild/native Secret Ally and Junction board/Homeworld arrival declarations now own precommit matching before their original permission/rate/settlement suffix. Original scope, funding and order stay private; prevention retains unused shipment and grant. Fremen Ambassador incoming matching now owns a separate precommit counter tied to the actual allied grant, with scoped denial and one original CHOAM/transfer/arrival suffix. Residents, consumed trigger and ordinary allowances are retained. Fresh Advanced Spiritual declarations share target legality and distinct shipping/matching/committed-alone stages. A public seven-field source binds a private original chooser/parent/order; late unavailability restores choice before placement, and committed callbacks never recheck withdrawn reserves. Ordinary/Guild continuation and prior alone holds stay intact. Basic and already-recorded legacy responses keep their old protocol.",
|
||||
"detail": "Resident Planetologist gathers partition incoming physical origins, queue one normal-format matching counter per changing group and retain a private original order/approval batch until atomic commitment. Fresh Guild/native Secret Ally and Junction board/Homeworld arrival declarations now own precommit matching before their original permission/rate/settlement suffix. Original scope, funding and order stay private; prevention retains unused shipment and grant. Fremen Ambassador incoming matching now owns a separate precommit counter tied to the actual allied grant, with scoped denial and one original CHOAM/transfer/arrival suffix. Residents, consumed trigger and ordinary allowances are retained. Fresh Advanced Spiritual declarations share target legality and distinct shipping/matching/committed-alone stages. A public seven-field source binds a private original chooser/parent/order; late unavailability restores choice before placement, and committed callbacks never recheck withdrawn reserves. Ordinary/Guild continuation and prior alone holds stay intact. Basic and already-recorded legacy responses keep their old protocol.",
|
||||
"evidence": [
|
||||
"game/advisor-gather-matching.ts",
|
||||
"components/planetologist-movement.tsx",
|
||||
"tests/advisor-gather-matching.test.ts",
|
||||
"tests/advisor-gather-matching-engine.test.ts",
|
||||
"tests/fixture-advisor-gather-matching.ts",
|
||||
"game/advisor-transport-matching.ts",
|
||||
"game/transport-quote.ts",
|
||||
"game/junction-transport-options.ts",
|
||||
@@ -4367,8 +4373,13 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
{
|
||||
"area": "Player controls",
|
||||
"status": "Partial",
|
||||
"detail": "Transport and Junction quotes share scoped packet denials and persistent before-payment matching guidance; twenty native GameTable SSR journeys and two controlled Truthtrance cases pass. Ambassador relocation drafts and persistent matching notices reuse the shared packet denial quote. Six native declaration/response/replacement GameTable SSR journeys pass; callback/hydration and browser acceptance remain separate. Shared projected target choices drive ordinary/Guild buttons and minimal bots. Persistent notices distinguish uncommitted shipping/matching from an already-arrived force awaiting conversion. Same prevented matching packet stays blocked when the original chooser returns. Fifteen native GameTable SSR journeys and two controlled late-stance cases pass; callback/hydration and browser acceptance remain separate.",
|
||||
"detail": "Gather drafts share normal/gather denial quotes; persistent progress identifies each required changing origin. Partial allowance keeps both groups in place. Seven native gather GameTable SSR journeys pass; callback/hydration/browser acceptance remains separate. Transport and Junction quotes share scoped packet denials and persistent before-payment matching guidance; twenty native GameTable SSR journeys and two controlled Truthtrance cases pass. Ambassador relocation drafts and persistent matching notices reuse the shared packet denial quote. Six native declaration/response/replacement GameTable SSR journeys pass; callback/hydration and browser acceptance remain separate. Shared projected target choices drive ordinary/Guild buttons and minimal bots. Persistent notices distinguish uncommitted shipping/matching from an already-arrived force awaiting conversion. Same prevented matching packet stays blocked when the original chooser returns. Fifteen native GameTable SSR journeys and two controlled late-stance cases pass; callback/hydration and browser acceptance remain separate.",
|
||||
"evidence": [
|
||||
"game/advisor-gather-matching.ts",
|
||||
"components/planetologist-movement.tsx",
|
||||
"tests/advisor-gather-matching.test.ts",
|
||||
"tests/advisor-gather-matching-engine.test.ts",
|
||||
"tests/fixture-advisor-gather-matching.ts",
|
||||
"game/advisor-transport-matching.ts",
|
||||
"game/transport-quote.ts",
|
||||
"game/junction-transport-options.ts",
|
||||
@@ -4388,8 +4399,13 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
{
|
||||
"area": "AI",
|
||||
"status": "Partial",
|
||||
"detail": "Transport policies filter prevented packets through the same quotes; sixteen native transport paths across existing profiles accept32 response/replacement actions. No strategic refinement or difficulty calibration. Fremen Ambassador candidates reuse the public packet quote to omit prevented retries. Eight native existing-profile paths accept sixteen response/replacement actions after Ambassador matching; no strategy refinement. Minimal policies consume projected legal targets, including all-blocked matching fallback to decline. Twenty-four paths across four existing profiles, two native parents and three stances accept72 choice/response actions. This is legal participation; no strategy refinement or difficulty calibration.",
|
||||
"detail": "Minimal gather candidates reuse the partition quote so normal/gather prevented packets cannot bypass one another. Eight native gather paths across existing profiles accept sixteen response/replacement actions; no strategy refinement or calibration. Transport policies filter prevented packets through the same quotes; sixteen native transport paths across existing profiles accept32 response/replacement actions. No strategic refinement or difficulty calibration. Fremen Ambassador candidates reuse the public packet quote to omit prevented retries. Eight native existing-profile paths accept sixteen response/replacement actions after Ambassador matching; no strategy refinement. Minimal policies consume projected legal targets, including all-blocked matching fallback to decline. Twenty-four paths across four existing profiles, two native parents and three stances accept72 choice/response actions. This is legal participation; no strategy refinement or difficulty calibration.",
|
||||
"evidence": [
|
||||
"game/advisor-gather-matching.ts",
|
||||
"components/planetologist-movement.tsx",
|
||||
"tests/advisor-gather-matching.test.ts",
|
||||
"tests/advisor-gather-matching-engine.test.ts",
|
||||
"tests/fixture-advisor-gather-matching.ts",
|
||||
"game/advisor-transport-matching.ts",
|
||||
"game/transport-quote.ts",
|
||||
"game/junction-transport-options.ts",
|
||||
@@ -4432,7 +4448,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
{
|
||||
"area": "Documentation",
|
||||
"status": "Partial",
|
||||
"detail": "Guild/Junction/World/Secret matching records provisional response priority, immune Junction tariff, private scope and fresh-declaration cutover. Source and native/control evidence remain separate from broader mode acceptance. Supplied physical28 redirects retained non-Polar fighters and starts Spiritual Advisors advisor-side, with no alone-conversion Polar exemption. Distinct physical17 counters, provisional Polar matching/restored-choice remedy, continuously-alone duration and the legacy source cutover are explicit. Wider combinations remain open.",
|
||||
"detail": "Resident gathering records one counter per changing origin, provisional atomic commitment, shared normal denials and preserved enemy-only/empty/legacy boundaries. Guild/Junction/World/Secret matching records provisional response priority, immune Junction tariff, private scope and fresh-declaration cutover. Source and native/control evidence remain separate from broader mode acceptance. Supplied physical28 redirects retained non-Polar fighters and starts Spiritual Advisors advisor-side, with no alone-conversion Polar exemption. Distinct physical17 counters, provisional Polar matching/restored-choice remedy, continuously-alone duration and the legacy source cutover are explicit. Wider combinations remain open.",
|
||||
"evidence": [
|
||||
"docs/SPIRITUAL_ADVISORS.md",
|
||||
"docs/ADVISOR_MATCHING.md",
|
||||
@@ -4445,8 +4461,13 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
{
|
||||
"area": "Verification",
|
||||
"status": "Partial",
|
||||
"detail": "The fresh transport batch passes25 files437/437, types/lint/build, twenty native Guild/Junction/World/Secret GameTable SSR journeys, two controlled Truthtrance cases and sixteen existing-profile paths. Genuine85/82/36/74-action sources earn original grants; permission and actual-discount responses remain separate, and Secret spends Nexus once at commitment. Review repairs the owned Truthtrance shadow and price consistency; smoke/type feedback is repaired. Earlier results below retain their original scope. The new Fremen Ambassador batch passes17 files358/358, types/lint/build, six native declaration/response/replacement GameTable SSR journeys and eight existing-profile paths. Genuine81-action sources earn a Nexus alliance and actual allied grant; both directions and responses retain trigger/ordinary allowances. One zero/absence assertion is repaired. The following earlier frozen results retain their original scope. The frozen batch passes22 files390/390, types/lint/build, fifteen native ordinary/Guild declaration/response/GameTable SSR journeys, two controlled late-stance cases and twenty-four existing-profile paths. Public35/37-action ordinary and45/48-action Guild sources earn actual shipment/Intrusion; a second genuine grant preserves the held group. Four older matching/projection expectations are updated. Controlled legacy responses are format compatibility, not saved captures. Wider Homeworld/Tech/Nexus/No-Field combinations, other matching producers, full modes and final assurance remain open.",
|
||||
"detail": "Resident gather proof passes15 files274/274, types/lint/build, seven native group-response/replacement GameTable SSR journeys and eight existing-profile paths. Genuine78/73-action sources earn Planetologist, original Karama, paid origins and real funding/Intrusion. Partial allowance is physically inert; second prevention retains the whole movement, and normal/gather denials cannot be bypassed. Fixture type feedback is repaired. Earlier results retain their original scope. The fresh transport batch passes25 files437/437, types/lint/build, twenty native Guild/Junction/World/Secret GameTable SSR journeys, two controlled Truthtrance cases and sixteen existing-profile paths. Genuine85/82/36/74-action sources earn original grants; permission and actual-discount responses remain separate, and Secret spends Nexus once at commitment. Review repairs the owned Truthtrance shadow and price consistency; smoke/type feedback is repaired. Earlier results below retain their original scope. The new Fremen Ambassador batch passes17 files358/358, types/lint/build, six native declaration/response/replacement GameTable SSR journeys and eight existing-profile paths. Genuine81-action sources earn a Nexus alliance and actual allied grant; both directions and responses retain trigger/ordinary allowances. One zero/absence assertion is repaired. The following earlier frozen results retain their original scope. The frozen batch passes22 files390/390, types/lint/build, fifteen native ordinary/Guild declaration/response/GameTable SSR journeys, two controlled late-stance cases and twenty-four existing-profile paths. Public35/37-action ordinary and45/48-action Guild sources earn actual shipment/Intrusion; a second genuine grant preserves the held group. Four older matching/projection expectations are updated. Controlled legacy responses are format compatibility, not saved captures. Wider Homeworld/Tech/Nexus/No-Field combinations, other matching producers, full modes and final assurance remain open.",
|
||||
"evidence": [
|
||||
"game/advisor-gather-matching.ts",
|
||||
"components/planetologist-movement.tsx",
|
||||
"tests/advisor-gather-matching.test.ts",
|
||||
"tests/advisor-gather-matching-engine.test.ts",
|
||||
"tests/fixture-advisor-gather-matching.ts",
|
||||
"game/advisor-transport-matching.ts",
|
||||
"game/transport-quote.ts",
|
||||
"game/junction-transport-options.ts",
|
||||
|
||||
Reference in new issue
Block a user