import { isWindowScanDisplayLayer, WINDOW_SCAN_DISPLAY_LAYER_ORDER, WINDOW_SCAN_SCORING_LAYER_ORDER, type WindowScanLayer, type WindowScanTransition, } from "./rectification-agentic/v9/refinement-packet"; import { workingRectificationHouseTable, type RectificationCandidateResult, type RectificationHouseTable, } from "./rectification-candidate-result"; export const RECTIFICATION_BOARD_SPLIT_MIN_PX = 832; export type GroupedWindowMinute = Readonly<{ at: string; scoringLayers: readonly WindowScanLayer[]; displayLayers: readonly WindowScanLayer[]; }>; export type RectificationBoardDiff = Readonly<{ houseNumbers: readonly number[]; lagnaChanged: boolean; timeChanged: boolean; transitionMinutes: readonly string[]; liveMessage: string; }>; const SCORING_RANK = new Map( WINDOW_SCAN_SCORING_LAYER_ORDER.map((layer, index) => [layer, index]), ); const DISPLAY_RANK = new Map( WINDOW_SCAN_DISPLAY_LAYER_ORDER.map((layer, index) => [layer, index]), ); const EMPTY_DIFF: RectificationBoardDiff = { houseNumbers: [], lagnaChanged: false, timeChanged: false, transitionMinutes: [], liveMessage: "", }; function sortLayers( layers: readonly WindowScanLayer[], rank: ReadonlyMap, ): readonly WindowScanLayer[] { return [...layers].sort((left, right) => (rank.get(left) ?? 99) - (rank.get(right) ?? 99)); } export function groupWindowTransitions( transitions: readonly WindowScanTransition[], ): readonly GroupedWindowMinute[] { const byMinute = new Map; display: Set }>(); for (const item of transitions) { const bucket = byMinute.get(item.at) ?? { scoring: new Set(), display: new Set() }; if (isWindowScanDisplayLayer(item.layer)) bucket.display.add(item.layer); else bucket.scoring.add(item.layer); byMinute.set(item.at, bucket); } return [...byMinute.entries()] .sort(([left], [right]) => left.localeCompare(right)) .map(([at, bucket]) => ({ at, scoringLayers: sortLayers([...bucket.scoring], SCORING_RANK), displayLayers: sortLayers([...bucket.display], DISPLAY_RANK), })); } export function changedHouseNumbers( previous: RectificationHouseTable | null, next: RectificationHouseTable | null, ): readonly number[] { if (!previous || !next) return []; const changed: number[] = []; for (let index = 0; index < 12; index += 1) { const before = previous.houses[index]; const after = next.houses[index]; if (!before || !after) continue; if (before.sign !== after.sign || before.occupants.join("\0") !== after.occupants.join("\0")) { changed.push(after.house); } } return changed; } function groupedMinuteKey(row: GroupedWindowMinute): string { return `${row.at}|${row.scoringLayers.join(",")}|${row.displayLayers.join(",")}`; } export function changedTransitionMinutes( previous: readonly WindowScanTransition[], next: readonly WindowScanTransition[], ): readonly string[] { const before = new Map(groupWindowTransitions(previous).map((row) => [row.at, groupedMinuteKey(row)])); const changed: string[] = []; for (const row of groupWindowTransitions(next)) { if (before.get(row.at) !== groupedMinuteKey(row)) changed.push(row.at); } return changed; } function joinHouses(houses: readonly number[]): string { return houses.map((house) => `第${house}宫`).join("、"); } export function rectificationBoardLiveMessage(input: Readonly<{ previous: RectificationCandidateResult | null; next: RectificationCandidateResult | null; houseNumbers: readonly number[]; lagnaChanged: boolean; timeChanged: boolean; transitionMinutes: readonly string[]; }>): string { const nextTable = input.next ? workingRectificationHouseTable(input.next) : null; if (!nextTable) return ""; if (!input.previous) { return `当前本命宫位已按 ${nextTable.time} 排出,上升${nextTable.lagna}。`; } const parts: string[] = []; if (input.timeChanged || input.lagnaChanged) { parts.push(`本命宫位已按 ${nextTable.time} 重算(上升 ${nextTable.lagna})。`); } if (input.houseNumbers.length > 0 && !(input.timeChanged && input.houseNumbers.length === 12)) { parts.push(`${joinHouses(input.houseNumbers)}已更新。`); } if (input.transitionMinutes.length > 0) { parts.push("换升时刻已更新。"); } return parts.join(""); } export function diffRectificationBoard( previous: RectificationCandidateResult | null, next: RectificationCandidateResult | null, ): RectificationBoardDiff { if (!next) return EMPTY_DIFF; const previousTable = previous ? workingRectificationHouseTable(previous) : null; const nextTable = workingRectificationHouseTable(next); const houseNumbers = changedHouseNumbers(previousTable, nextTable); const lagnaChanged = Boolean(previousTable && nextTable && previousTable.lagna !== nextTable.lagna); const timeChanged = Boolean(previousTable && nextTable && previousTable.time !== nextTable.time); const transitionMinutes = previous ? changedTransitionMinutes(previous.windowTransitions, next.windowTransitions) : []; return { houseNumbers, lagnaChanged, timeChanged, transitionMinutes, liveMessage: rectificationBoardLiveMessage({ previous, next, houseNumbers, lagnaChanged, timeChanged, transitionMinutes, }), }; } export function rectificationBoardPeekCopy(result: RectificationCandidateResult | null): Readonly<{ title: string; detail: string | null; }> { const table = result ? workingRectificationHouseTable(result) : null; if (!table) { return { title: "当前盘面", detail: "补充经历后会在这里更新" }; } return { title: "当前盘面", detail: `${table.time} · 上升${table.lagna}` }; } export function workingRectificationTime(result: RectificationCandidateResult | null): string | null { if (!result) return null; return result.selectedTime ?? result.representativeTime ?? workingRectificationHouseTable(result)?.time ?? null; }