import { createChart, parseGanZhi } from './chart'; import { BRANCH_ELEMENT, CONTROLS, ELEMENT_LABEL, HIDDEN_STEMS, STEM_INFO, } from './constants'; import { calculateLuckCycles } from './luck-cycle'; import { BRANCH_THREE_COMBINATION_RULES, BRANCH_TRIPLE_PUNISHMENT_RULES, SELF_PUNISHMENT_BRANCHES, branchPairKey, getBranchSixCombinationElement, getStemCombinationElement, isBranchClash, isBranchHarm, } from './relation-rules'; import { getTenGod } from './relations'; import { getTwelveGrowthStage } from './season-context'; import type { BaziInput, CalculateLuckCyclesOptions, EarthlyBranchName, HiddenStemOrder, LuckAnalysisBranchMember, LuckAnalysisBranchRelation, LuckAnalysisHiddenStem, LuckAnalysisPeriod, LuckAnalysisResult, LuckAnalysisStemMember, LuckAnalysisStemRelation, LuckCyclePeriod, Pillar, } from './types'; const PILLAR_LABEL = { year: '年', month: '月', day: '日', hour: '时', luck: '大运', } as const; function hiddenOrder(index: number): HiddenStemOrder { if (index === 0) return 'main'; if (index === 1) return 'middle'; return 'residual'; } function natalStemMember(pillar: Pillar): LuckAnalysisStemMember { return { source: 'natal', pillar: pillar.key, position: 'stem', value: pillar.stem.name, }; } function luckStemMember(value: LuckAnalysisStemMember['value']): LuckAnalysisStemMember { return { source: 'luck', pillar: 'luck', position: 'stem', value }; } function natalBranchMember(pillar: Pillar): LuckAnalysisBranchMember { return { source: 'natal', pillar: pillar.key, position: 'branch', value: pillar.branch.name, }; } function luckBranchMember(value: EarthlyBranchName): LuckAnalysisBranchMember { return { source: 'luck', pillar: 'luck', position: 'branch', value }; } function locationLabel( member: LuckAnalysisStemMember | LuckAnalysisBranchMember, ): string { return `${PILLAR_LABEL[member.pillar]}${member.position === 'stem' ? '干' : '支'}${member.value}`; } function cartesianMembers( groups: LuckAnalysisBranchMember[][], index = 0, current: LuckAnalysisBranchMember[] = [], ): LuckAnalysisBranchMember[][] { if (index === groups.length) return [current]; const group = groups[index] ?? []; return group.flatMap((member) => cartesianMembers(groups, index + 1, [...current, member]) ); } function detectStemRelations( luckStem: LuckAnalysisStemMember['value'], natalPillars: Pillar[], cycleIndex: number, ): LuckAnalysisStemRelation[] { const luck = luckStemMember(luckStem); const relations: LuckAnalysisStemRelation[] = []; let combinationIndex = 0; let controlIndex = 0; for (const pillar of natalPillars) { const natal = natalStemMember(pillar); const candidateElement = getStemCombinationElement(luck.value, natal.value); if (candidateElement) { combinationIndex += 1; relations.push({ id: `luck-${String(cycleIndex).padStart(2, '0')}-stem-combination-${String(combinationIndex).padStart(2, '0')}`, type: 'stem-combination', subtype: 'five-combination', members: [luck, natal], candidateElement, transformationStatus: 'candidate-only', description: `${locationLabel(luck)}与${locationLabel(natal)}构成天干五合,候选化${ELEMENT_LABEL[candidateElement]};luck-analysis-v1 不判断是否合化成功。`, }); } const luckElement = STEM_INFO[luck.value].element; const natalElement = STEM_INFO[natal.value].element; let controller: LuckAnalysisStemMember | null = null; let controlled: LuckAnalysisStemMember | null = null; if (CONTROLS[luckElement] === natalElement) { controller = luck; controlled = natal; } else if (CONTROLS[natalElement] === luckElement) { controller = natal; controlled = luck; } if (controller && controlled) { controlIndex += 1; relations.push({ id: `luck-${String(cycleIndex).padStart(2, '0')}-stem-control-${String(controlIndex).padStart(2, '0')}`, type: 'stem-control', members: [luck, natal], controller, controlled, description: `${locationLabel(controller)}${ELEMENT_LABEL[STEM_INFO[controller.value].element]}克${locationLabel(controlled)}${ELEMENT_LABEL[STEM_INFO[controlled.value].element]}。`, }); } } return relations; } function detectCompleteBranchRelations( luck: LuckAnalysisBranchMember, natal: LuckAnalysisBranchMember[], cycleIndex: number, ): LuckAnalysisBranchRelation[] { const relations: LuckAnalysisBranchRelation[] = []; let threeCombinationIndex = 0; let triplePunishmentIndex = 0; for (const definition of BRANCH_THREE_COMBINATION_RULES) { if (!definition.branches.includes(luck.value)) continue; const groups = definition.branches.map((branch) => branch === luck.value ? [luck] : natal.filter((member) => member.value === branch) ); if (groups.some((group) => group.length === 0)) continue; for (const members of cartesianMembers(groups)) { const [first, second, third] = members; if (!first || !second || !third) continue; threeCombinationIndex += 1; relations.push({ id: `luck-${String(cycleIndex).padStart(2, '0')}-branch-three-combination-${String(threeCombinationIndex).padStart(2, '0')}`, type: 'branch-three-combination', members: [first, second, third], candidateElement: definition.element, transformationStatus: 'candidate-only', description: `${members.map(locationLabel).join('、')}构成${definition.label},候选化${ELEMENT_LABEL[definition.element]};luck-analysis-v1 不判断是否合化成功。`, }); } } for (const definition of BRANCH_TRIPLE_PUNISHMENT_RULES) { if (!definition.branches.includes(luck.value)) continue; const groups = definition.branches.map((branch) => branch === luck.value ? [luck] : natal.filter((member) => member.value === branch) ); if (groups.some((group) => group.length === 0)) continue; for (const members of cartesianMembers(groups)) { triplePunishmentIndex += 1; relations.push({ id: `luck-${String(cycleIndex).padStart(2, '0')}-branch-triple-punishment-${String(triplePunishmentIndex).padStart(2, '0')}`, type: 'branch-punishment', subtype: 'triple', pattern: definition.pattern, members, description: `${members.map(locationLabel).join('、')}构成${definition.label}。`, }); } } return relations; } function detectBranchRelations( luckBranch: EarthlyBranchName, natalPillars: Pillar[], cycleIndex: number, ): LuckAnalysisBranchRelation[] { const luck = luckBranchMember(luckBranch); const natal = natalPillars.map(natalBranchMember); const relations: LuckAnalysisBranchRelation[] = []; let combinationIndex = 0; let clashIndex = 0; let harmIndex = 0; let punishmentIndex = 0; for (const member of natal) { const key = branchPairKey(luck.value, member.value); const candidateElement = getBranchSixCombinationElement(luck.value, member.value); if (candidateElement) { combinationIndex += 1; relations.push({ id: `luck-${String(cycleIndex).padStart(2, '0')}-branch-six-combination-${String(combinationIndex).padStart(2, '0')}`, type: 'branch-six-combination', members: [luck, member], candidateElement, transformationStatus: 'candidate-only', description: `${locationLabel(luck)}与${locationLabel(member)}构成六合,候选化${ELEMENT_LABEL[candidateElement]};luck-analysis-v1 不判断是否合化成功。`, }); } if (isBranchClash(luck.value, member.value)) { clashIndex += 1; relations.push({ id: `luck-${String(cycleIndex).padStart(2, '0')}-branch-clash-${String(clashIndex).padStart(2, '0')}`, type: 'branch-clash', members: [luck, member], description: `${locationLabel(luck)}与${locationLabel(member)}构成六冲(${key}冲)。`, }); } if (isBranchHarm(luck.value, member.value)) { harmIndex += 1; relations.push({ id: `luck-${String(cycleIndex).padStart(2, '0')}-branch-harm-${String(harmIndex).padStart(2, '0')}`, type: 'branch-harm', members: [luck, member], description: `${locationLabel(luck)}与${locationLabel(member)}构成六害(${key}害)。`, }); } if (key === '子卯') { punishmentIndex += 1; relations.push({ id: `luck-${String(cycleIndex).padStart(2, '0')}-branch-punishment-${String(punishmentIndex).padStart(2, '0')}`, type: 'branch-punishment', subtype: 'mutual', pattern: 'zi-mao', members: [luck, member], description: `${locationLabel(luck)}与${locationLabel(member)}构成子卯刑。`, }); } if (luck.value === member.value && SELF_PUNISHMENT_BRANCHES.includes(luck.value)) { punishmentIndex += 1; relations.push({ id: `luck-${String(cycleIndex).padStart(2, '0')}-branch-punishment-${String(punishmentIndex).padStart(2, '0')}`, type: 'branch-punishment', subtype: 'self', pattern: 'self', members: [luck, member], description: `${locationLabel(luck)}与${locationLabel(member)}构成${luck.value}${luck.value}自刑。`, }); } } relations.push(...detectCompleteBranchRelations(luck, natal, cycleIndex)); return relations; } function analyzePeriod( cycle: LuckCyclePeriod, chart: ReturnType, ): LuckAnalysisPeriod { const [stemName, branchName] = parseGanZhi(cycle.ganZhi, 'luckCycle.ganZhi'); const stemInfo = STEM_INFO[stemName]; const hiddenStems: LuckAnalysisHiddenStem[] = HIDDEN_STEMS[branchName].map( (name, index) => ({ name, element: STEM_INFO[name].element, yinYang: STEM_INFO[name].yinYang, tenGod: getTenGod(chart.dayMaster.stem, name), order: hiddenOrder(index), }), ); const roots = hiddenStems .filter((hiddenStem) => hiddenStem.element === chart.dayMaster.element) .map((hiddenStem) => ({ hiddenStem: hiddenStem.name, order: hiddenStem.order, source: `大运${branchName}支藏${hiddenStem.name}`, })); const natalPillars = Object.values(chart.pillars); return { cycle, stem: { name: stemName, element: stemInfo.element, yinYang: stemInfo.yinYang, tenGod: getTenGod(chart.dayMaster.stem, stemName), }, branch: { name: branchName, element: BRANCH_ELEMENT[branchName], hiddenStems, twelveGrowth: getTwelveGrowthStage(chart.dayMaster.stem, branchName), root: { hasRoot: roots.length > 0, roots }, }, relations: { stems: detectStemRelations(stemName, natalPillars, cycle.index), branches: detectBranchRelations(branchName, natalPillars, cycle.index), }, }; } export function analyzeLuckCycles( input: BaziInput, options: CalculateLuckCyclesOptions, ): LuckAnalysisResult { const luckCycles = calculateLuckCycles(input, options); const chart = createChart(luckCycles.sourcePillars); const cycles = luckCycles.cycles.map((cycle) => analyzePeriod(cycle, chart)); return { model: 'luck-analysis-v1', input: luckCycles.input, sourcePillars: luckCycles.sourcePillars, dayMaster: chart.dayMaster, direction: luckCycles.direction, directionReason: luckCycles.directionReason, startsAt: luckCycles.startsAt, cycles, appliedRules: { luckCycleModel: 'luck-cycle-v1', relationsModel: 'luck-to-natal-relations-v1', tenGod: 'day-master-relative', twelveGrowth: 'ten-stem-yang-forward-yin-reverse', root: 'same-element-hidden-stem', tripleRelations: 'complete-only', transformations: 'candidate-only', interpretation: 'none', }, trace: [ ...luckCycles.trace, { ruleId: 'luck-analysis.cycle-facts', description: '逐步计算大运干支相对日主的十神、十二长生、根气,以及与原局四柱构成的关系事实。', input: { sourcePillars: luckCycles.sourcePillars, cycles: luckCycles.cycles.map((cycle) => cycle.ganZhi), }, output: { cycles: cycles.map((period) => ({ index: period.cycle.index, ganZhi: period.cycle.ganZhi, stemTenGod: period.stem.tenGod, twelveGrowth: period.branch.twelveGrowth, hasRoot: period.branch.root.hasRoot, stemRelationIds: period.relations.stems.map((relation) => relation.id), branchRelationIds: period.relations.branches.map((relation) => relation.id), })), }, }, ], warnings: [ 'luck-analysis-v1 只输出大运与原局的结构化关系事实,不判断好运、坏运或具体事件。', '天干五合、地支六合和三合只输出候选化行,不判断合化成功,也不删除或改写原局干支。', '大运根气只表示大运地支藏干出现日主同五行,不直接改判原局旺衰。', ], }; }