Extend bounded Basic ground-movement reachability to held native CHOAM Kulon and owned paid Box recovery. Before payment, use only known current-discard identities. Preserve actual paid event, full inspection, one two-spice fee, original physical card replay and existing provisional full-hand exchange. Reject a Yes-breaking selection before the server shuffle without losing the paid search. Keep No-only searches on their normal decision before movement can end. Record movement-relevant current-discard certainty at actual disposal, paid search, mixed claim and deck rebuild boundaries without projecting raw metadata or erasing historical memory. Use deterministic preparation on private copies, include preparation state in search memoization, protect fee-funded routes from voluntary spending and keep native Kulon counter semantics. Persistent busy-safe guidance and existing legal profiles use the actual next paid action; no strategy tuning. Verify 17 files231/231, types/lint/build, three original-action control/table-rendering programmes and four existing profiles with40 accepted actions including neutral passes. Original public sources44/25 retain actual purchases1+1, shipment costs6/1, exhausted Hajr and native Kulon route4. Separate full-hand, legacy certainty, fee-loss and acquired-counter edges disclose conserved staging. Two prior-engine Auditor fixture failures are repaired by actual battle-result acknowledgements; test-only const lint is corrected. Other timing, preparations/counter mitigation, unknown private possibilities, optional/Advanced, full modes, comprehensive assurance and deployed acceptance remain open. No saved-room/reset/restart/deployment operation.
83 lines
3.6 KiB
TypeScript
83 lines
3.6 KiB
TypeScript
import type { Card } from './cards';
|
|
import type { FreshDiscardBatch } from './semuta-drug';
|
|
|
|
/** Remembered movement-relevant discard identities, never a public pile view. */
|
|
export type MovementDiscardKnowledge = Record<string, string[]>;
|
|
type Entries = FreshDiscardBatch['entries'];
|
|
type Knowledge = MovementDiscardKnowledge | undefined;
|
|
|
|
const relevant = (card: Card): boolean =>
|
|
(card.kind === 'special' && card.effect === 'hajr') ||
|
|
(card.kind === 'special' && card.id === 'richese-ornithopter' && card.effect === 'ornithopter') ||
|
|
(card.kind === 'worthless' && card.name === 'Kulon');
|
|
|
|
function viewers(seats: readonly string[]): string[] {
|
|
return [...new Set(seats)].sort();
|
|
}
|
|
function remembered(state: Knowledge, seats: readonly string[]) {
|
|
return new Map(viewers(seats).map(player => [player, new Set(state?.[player] ?? [])]));
|
|
}
|
|
function finish(knowledge: Map<string, Set<string>>): Knowledge {
|
|
const entries = [...knowledge].filter(([, ids]) => ids.size)
|
|
.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
.map(([player, ids]) => [player, [...ids].sort()] as const);
|
|
return entries.length ? Object.fromEntries(entries) : undefined;
|
|
}
|
|
|
|
/** Public disposal teaches every seat; private disposal teaches its actual
|
|
* former holder only. Unrelated cards create no new preparation knowledge. */
|
|
export function recordMovementDiscardKnowledge(
|
|
state: Knowledge, seats: readonly string[], entries: Entries,
|
|
): Knowledge {
|
|
const knowledge = remembered(state, seats);
|
|
for (const entry of entries) {
|
|
if (!relevant(entry.card)) continue;
|
|
for (const [player, known] of knowledge)
|
|
if (entry.publicFace || entry.discardedBy === player) known.add(entry.card.id);
|
|
}
|
|
return finish(knowledge);
|
|
}
|
|
|
|
/** Current remembered faces only. An unknown face cannot become a preparation
|
|
* witness merely because the authoritative pile happens to contain it. */
|
|
export function knownMovementDiscards(
|
|
state: Knowledge, player: string, discard: readonly Card[],
|
|
): Card[] {
|
|
const known = new Set(state?.[player] ?? []), selected = new Set<string>();
|
|
return structuredClone(discard.filter(card => {
|
|
if (!known.has(card.id) || !relevant(card) || selected.has(card.id)) return false;
|
|
selected.add(card.id); return true;
|
|
}));
|
|
}
|
|
|
|
/** A private Box choice may remove any remembered card. The paid search owner
|
|
* alone knows the remaining eligible faces after the search has settled. */
|
|
export function recordMovementBoxKnowledge(
|
|
_state: Knowledge, seats: readonly string[], owner: string, remainingDiscard: readonly Card[],
|
|
): Knowledge {
|
|
const knowledge = remembered(undefined, seats);
|
|
const own = knowledge.get(owner);
|
|
if (own) for (const card of remainingDiscard) if (relevant(card)) own.add(card.id);
|
|
return finish(knowledge);
|
|
}
|
|
|
|
/** Others cannot infer the privately selected member of a fresh mixed batch.
|
|
* The committed claimant has inspected that batch and knows its remainder. */
|
|
export function recordMovementClaimKnowledge(
|
|
state: Knowledge, seats: readonly string[], owner: string,
|
|
batchEntries: Entries, claimedId: string,
|
|
): Knowledge {
|
|
const knowledge = remembered(state, seats);
|
|
const possible = new Set(batchEntries.map(entry => entry.card.id));
|
|
for (const known of knowledge.values()) for (const id of possible) known.delete(id);
|
|
const own = knowledge.get(owner);
|
|
if (own) for (const { card } of batchEntries)
|
|
if (card.id !== claimedId && relevant(card)) own.add(card.id);
|
|
return finish(knowledge);
|
|
}
|
|
|
|
/** Rebuilding the draw deck removes all remembered discard preparation faces. */
|
|
export function clearMovementDiscardKnowledge(_state: Knowledge): Knowledge {
|
|
return undefined;
|
|
}
|