Resume suspended movement after Sapho recovery

This commit is contained in:
Quintine committed 2026-10-28 19:34:56 +11:00
1 parent 26be6fd8ee
commit ea910e9c82
11 files changed
+267 -17

No files matched your search

+2 -1
View File
@@ -8049,7 +8049,8 @@ function treacheryDiscardIntegrity(g: Game) {
typeof c.afterCurrent === 'string' && c.afterCurrent === c.previousActive &&
c.afterCurrent !== c.player && g.movementRemaining[0] === c.afterCurrent &&
saphoMovementStarted(g, getPlayer(g,c.afterCurrent)) &&
g.movementRemaining.filter(id=>id!==c.afterCurrent).every(id=>!saphoMovementStarted(g,getPlayer(g,id)))) &&
g.movementRemaining.filter(id=>id!==c.afterCurrent).every(id=>
!saphoMovementStarted(g,getPlayer(g,id)) || c.mode === 'first' && id === c.player)) &&
(c.mode === 'first'
? g.movementRemaining[c.afterCurrent === undefined ? 0 : 1] === c.player &&
c.previousActive !== c.player && !g.saphoMovementLast
+1 -1
View File
@@ -504,7 +504,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Discarding Sapho frees a hand slot. A full-hand holder can join a still-open lot through this order change. Once Around completed opportunities cannot repeat. A cyclic pass stays spent until a genuine raise reopens it; a finished lot cannot reopen.',
'The movement controls change complete shipment and movement turns at a boundary before the current player begins. First moves you ahead of the remaining unstarted turns, including after earlier players have fully finished. In Advanced, provisional first ordering retains an unspent Guild’s native next-boundary take/wait choice; Guild may still act ahead. An already granted current Guild turn cannot be displaced, and a Karama-locked privilege stays locked. Last stays after the Guild even if the Guild later chooses to wait.',
'At a clean boundary after another player has begun shipment and movement, an unstarted holder may provisionally reorder only later unstarted turns. First means first after that current actor finishes; last remains after Guild. In Basic or Advanced, the holder may also provisionally pause its own started turn and resume last, retaining already spent shipment and movement. Pending card, arrival, payment and shared movement preparations must finish first. Advanced Guild retains its next-boundary timing choice; Guild may voluntarily pause its own granted turn.',
'If you recover the physical Sapho after choosing Last while your turn remains unstarted, you may use it again to choose First. This provisionally replaces your own Last protection; completed turns and the current actor remain unchanged. Another holder cannot replace your protected Last, and replay after suspending your own started turn remains unsupported.',
'If you recover the physical Sapho after choosing Last, you may use it again to choose First, including after suspending your own started turn. This provisionally replaces your own Last protection without restoring spent actions. Completed turns stay complete and a different started current actor finishes first. Another holder cannot replace your protected Last.',
'Between completed battles, choose first or last among remaining battle choosers. The priority lasts for the rest of the Battle phase. You still need a real unresolved battle. Others may choose battles against you before your own turn; last is not immunity. This scope changes the chooser, not each battle’s aggressor or tie advantage.',
'In a fresh Richese battle, use unreserved Sapho at a clean boundary before both combatants finish final battle actions, including after sealed plans and revealed declarations. Habbanya still precedes ordinary aggressor tie priority. Participants, plans, promises, weapon choices and votes do not swap or replay. Older battles retain their pre-plan opportunity.',
'When two or more players declared Truthtrance in a supported clean parent, each queued holder may continue or use their held Sapho to move their entire pending question group first or last. Physical storm order and each holder’s card order stay unchanged. Supported Basic Semuta claim/pass restores the same question queue. Clean ordinary auction lots also preserve their standing bid and remaining clock. Richese cache Once Around and Silent lots preserve their original bidding and funding; Black Market, other special auctions, battle and pending-parent combinations remain unfinished.',
+13 -6
View File
@@ -32,10 +32,12 @@ function validate(context: SaphoMovementOrderContext): OrderedOpportunity {
context.started.every(player => queue.eligible.includes(player)),
'Started movement turns must name unique participants in this phase.');
const started = queue.remaining.filter(player => context.started.includes(player));
requireMovement(queue.currentStarted
? started.length === 1 && started[0] === queue.current
: started.length === 0,
'Only the current combined turn may have started among the remaining participants.');
// A previous self-Last can suspend one started actor at the protected tail.
// Its already-spent movement and shipment remain in the engine's counters.
// No other noncurrent started participant is admitted.
requireMovement(started.includes(queue.current) === queue.currentStarted &&
started.every(player => player === queue.current || player === queue.protectedLast),
'Only the current or protected suspended combined turn may have started among the remaining participants.');
requireMovement(context.guild === null || context.guild && typeof context.guild === 'object' &&
identifier(context.guild.player) && queue.eligible.includes(context.guild.player) &&
typeof context.guild.granted === 'boolean' && typeof context.guild.locked === 'boolean',
@@ -81,6 +83,8 @@ export function reorderSaphoMovementOrder(context: SaphoMovementOrderContext, re
requireMovement(original.protectedLast === request.player && request.mode === 'first',
'Only the protected player may relinquish their earlier last position.');
}
const suspendedReplay = original.protectedLast === request.player &&
original.current !== request.player && context.started.includes(request.player);
const queue = original.protectedLast ? { ...original, protectedLast: null } : original;
if (context.started.includes(request.player) && request.player === queue.current &&
context.allowStartedLast === true) {
@@ -96,15 +100,18 @@ export function reorderSaphoMovementOrder(context: SaphoMovementOrderContext, re
validateOrderedOpportunity(next);
return { event, afterCurrent: null, suspended: request.player, next };
}
requireMovement(!context.started.includes(request.player),
requireMovement(!context.started.includes(request.player) || suspendedReplay,
'Juice of Sapho cannot split a combined turn that has already started.');
const grantedGuild = context.guild && context.guild.player === queue.current &&
context.guild.granted && !context.guild.locked;
requireMovement(queue.currentStarted || request.mode !== 'first' || !grantedGuild,
'The Guild has already been granted the next combined turn.');
const next = request.mode === 'last'
const reordered = request.mode === 'last'
? protectOrderedOpportunityLast(queue, { event: queue.event, player: request.player })
: reorderOrderedOpportunity(queue, { event: queue.event, player: request.player, position: 'first' });
const next = suspendedReplay && reordered.current === request.player
? { ...reordered, currentStarted: true } : reordered;
validateOrderedOpportunity(next);
requireMovement(next.remaining.some((player, index) => player !== queue.remaining[index]),
'This participant is already in that remaining movement position.');
return { event, afterCurrent: queue.currentStarted ? queue.current : null, next };