dsh-metaphysics 0.50.5 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/engine/geju.js CHANGED
@@ -1,475 +1,475 @@
1
- // 八字格局、用神与行运评分
2
- //
3
- // 定位:在 bazi.js 算出四柱、五行、旺衰之后,给出
4
- // 1) 格局(月令格 + 特殊格 + 常见组合);
5
- // 2) 用神 / 喜忌(调候优先 + 扶抑校验;气候平和时仍以扶抑为主);
6
- // 3) 大运、流年的吉凶倾向评分(含调候修正项)。
7
- // 取舍:格局派别差异极大,这里只给「规则可复核」的判定,evidence 写明依据;
8
- // 调候由 tiaohou.js 独立判定,与扶抑冲突时显式裁决并写明理由,不静默取用。
9
-
10
- import { judgeClimate, arbitrate } from './tiaohou.js';
11
- import { qtOf } from './qiongtong.js';
12
- import { sanheGroupOf } from './sanhe.js';
13
-
14
- const STEMS = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
15
- const BRANCHES = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
16
-
17
- const STEM_ELEMENT = {
18
- 甲: '木', 乙: '木', 丙: '火', 丁: '火', 戊: '土',
19
- 己: '土', 庚: '金', 辛: '金', 壬: '水', 癸: '水',
20
- };
21
- const BRANCH_ELEMENT = {
22
- 子: '水', 丑: '土', 寅: '木', 卯: '木', 辰: '土', 巳: '火',
23
- 午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水',
24
- };
25
- const GENERATES = { 木: '火', 火: '土', 土: '金', 金: '水', 水: '木' };
26
- const OVERCOMES = { 木: '土', 土: '水', 水: '火', 火: '金', 金: '木' };
27
- const HIDDEN = {
28
- 子: ['癸'], 丑: ['己', '癸', '辛'], 寅: ['甲', '丙', '戊'], 卯: ['乙'],
29
- 辰: ['戊', '乙', '癸'], 巳: ['丙', '戊', '庚'], 午: ['丁', '己'], 未: ['己', '丁', '乙'],
30
- 申: ['庚', '壬', '戊'], 酉: ['辛'], 戌: ['戊', '辛', '丁'], 亥: ['壬', '甲'],
31
- };
32
- const HIDDEN_W = [1, 0.5, 0.3];
33
-
34
- const LUSHEN_BRANCH = { 甲: '寅', 乙: '卯', 丙: '巳', 丁: '午', 戊: '巳', 己: '午', 庚: '申', 辛: '酉', 壬: '亥', 癸: '子' };
35
- const YANGREN_BRANCH = { 甲: '卯', 乙: '辰', 丙: '午', 丁: '未', 戊: '午', 己: '未', 庚: '酉', 辛: '戌', 壬: '子', 癸: '丑' };
36
- const STEM_HE = { 甲己: '土', 乙庚: '金', 丙辛: '水', 丁壬: '木', 戊癸: '火' };
37
-
38
- const isYangStem = (s) => STEMS.indexOf(s) % 2 === 0;
39
- const ELEMENTS = ['木', '火', '土', '金', '水'];
40
-
41
- /** 十神:以日干为我(与 bazi.js 同规则) */
42
- export function tenGod(dayStem, otherStem) {
43
- const me = STEM_ELEMENT[dayStem];
44
- const it = STEM_ELEMENT[otherStem];
45
- const same = isYangStem(dayStem) === isYangStem(otherStem);
46
- if (me === it) return same ? '比肩' : '劫财';
47
- if (GENERATES[me] === it) return same ? '食神' : '伤官';
48
- if (OVERCOMES[me] === it) return same ? '偏财' : '正财';
49
- if (OVERCOMES[it] === me) return same ? '七杀' : '正官';
50
- if (GENERATES[it] === me) return same ? '偏印' : '正印';
51
- return '?';
52
- }
53
-
54
- const godGroups = {
55
- 比劫: ['比肩', '劫财'],
56
- 食伤: ['食神', '伤官'],
57
- 财: ['正财', '偏财'],
58
- 官杀: ['正官', '七杀'],
59
- 印: ['正印', '偏印'],
60
- };
61
-
62
- /** 全局十神权重:四柱天干 1.0,地支藏干按本气 1 / 中气 0.5 / 余气 0.3 */
63
- function godStats(pillars, dayStem) {
64
- const total = { 比劫: 0, 食伤: 0, 财: 0, 官杀: 0, 印: 0 };
65
- const byGod = { 比肩: 0, 劫财: 0, 食神: 0, 伤官: 0, 正财: 0, 偏财: 0, 正官: 0, 七杀: 0, 正印: 0, 偏印: 0 };
66
- const add = (stem, w) => {
67
- const god = tenGod(dayStem, stem);
68
- const g = Object.keys(godGroups).find((k) => godGroups[k].includes(god));
69
- if (g) total[g] += w;
70
- if (byGod[god] !== undefined) byGod[god] += w;
71
- };
72
- for (const key of ['year', 'month', 'day', 'hour']) {
73
- add(pillars[key].stem, key === 'day' ? 0.6 : 1);
74
- (HIDDEN[pillars[key].branch] || []).forEach((h, i) => add(h, HIDDEN_W[i] ?? 0.1));
75
- }
76
- for (const k of Object.keys(total)) total[k] = Math.round(total[k] * 10) / 10;
77
- for (const k of Object.keys(byGod)) byGod[k] = Math.round(byGod[k] * 10) / 10;
78
- return { groups: total, byGod };
79
- }
80
-
81
- /** 用神所属五行:生我 / 同我 / 我生 / 我克 / 克我 */
82
- export function elementRoles(dayStem) {
83
- const me = STEM_ELEMENT[dayStem];
84
- const inMe = ELEMENTS.find((e) => GENERATES[e] === me);
85
- const guan = ELEMENTS.find((e) => OVERCOMES[e] === me);
86
- return { me, inMe, shi: GENERATES[me], cai: OVERCOMES[me], guan };
87
- }
88
-
89
- // ── 月令格 ───────────────────────────────────────────────────
90
-
91
- // 四墓(杂气)月:辰戌丑未所藏多、用神不一,取用先看透干,再看会支。
92
- // 会支只认三合局——子平真诠十六「逢申与子会局」的例子即三合,三会方与兼透兼用仍不纳入。
93
- const ZA_MONTHS = ['辰', '未', '戌', '丑'];
94
-
95
- function huiCandidate(pillars, monthBranch, cands) {
96
- if (!ZA_MONTHS.includes(monthBranch)) return null;
97
- const group = sanheGroupOf(monthBranch);
98
- if (!group) return null;
99
- const others = ['year', 'day', 'hour'];
100
- const need = group.branches.filter((b) => b !== monthBranch);
101
- if (!need.every((b) => others.some((k) => pillars[k].branch === b))) return null;
102
- const cand = cands.find((c) => STEM_ELEMENT[c.stem] === group.element);
103
- if (!cand) return null;
104
- const where = others
105
- .filter((k) => need.includes(pillars[k].branch))
106
- .map((k) => `${({ year: '年', day: '日', hour: '时' })[k]}支${pillars[k].branch}`);
107
- return { ...cand, group, where };
108
- }
109
-
110
- function monthPattern(pillars, dayStem, monthBranch) {
111
- const hidden = HIDDEN[monthBranch] || [];
112
- const cands = hidden.map((stem, i) => ({
113
- stem,
114
- god: tenGod(dayStem, stem),
115
- pos: ['本气', '中气', '余气'][i],
116
- w: HIDDEN_W[i] ?? 0.1,
117
- }));
118
- const others = ['year', 'month', 'hour'];
119
- const exposedKey = others.find((k) => cands.some((c) => c.stem === pillars[k].stem));
120
- const base = cands[0];
121
- const hui = exposedKey ? null : huiCandidate(pillars, monthBranch, cands);
122
- const use = exposedKey ? cands.find((c) => c.stem === pillars[exposedKey].stem) : (hui || base);
123
- const usedBy = exposedKey ? '透干' : (hui ? '会支' : '本气');
124
- const exposedPos = exposedKey ? { year: '年', month: '月', hour: '时' }[exposedKey] : null;
125
-
126
- // 建禄只认日干的禄位;戊土辰月、己土未月这类「本气比肩但不在禄位」的杂气月不再冒称建禄
127
- if (base.god === '比肩' && LUSHEN_BRANCH[dayStem] === monthBranch) {
128
- return {
129
- name: '建禄格',
130
- kind: '月令格',
131
- evidence: `月令${monthBranch}为日干${dayStem}之禄(本气${base.stem}比肩),身坐禄地`,
132
- };
133
- }
134
- if (base.god === '劫财') {
135
- if (YANGREN_BRANCH[dayStem] === monthBranch) {
136
- return {
137
- name: isYangStem(dayStem) ? '阳刃格' : '月刃格',
138
- kind: '月令格',
139
- evidence: `月令${monthBranch}为日干${dayStem}之刃(本气${base.stem}劫财)${isYangStem(dayStem) ? '' : ',阴干统称月刃'}`,
140
- };
141
- }
142
- return { name: '月劫格', kind: '月令格', evidence: `月令${monthBranch}本气${base.stem}为劫财` };
143
- }
144
- if (usedBy === '本气' && base.god === '比肩') {
145
- return {
146
- name: '比肩格',
147
- kind: '月令格',
148
- evidence: `月令${monthBranch}藏${hidden.join('')},本气${base.stem}为比肩,但${dayStem}禄在${LUSHEN_BRANCH[dayStem]}、不在${monthBranch},属杂气月比肩当权`,
149
- };
150
- }
151
- // 子平十六:「透乙则用月劫」——杂气月由透干或会支取到劫财时,仍作月劫论
152
- if (usedBy !== '本气' && use.god === '劫财') {
153
- const via = exposedKey ? `透于${exposedPos}干` : `${hui.where.join('、')}会成${hui.group.element}局`;
154
- return {
155
- name: '月劫格',
156
- kind: '月令格',
157
- evidence: `月令${monthBranch}藏${hidden.join('')},${use.pos}${use.stem}(劫财)${via},劫财为用,作月劫论`,
158
- };
159
- }
160
- const name = `${use.god}格`;
161
- let ex;
162
- if (exposedKey) {
163
- ex = `月令${monthBranch}藏${hidden.join('')},${use.pos}${use.stem}(${use.god})透于${exposedPos}干`;
164
- } else if (hui) {
165
- ex = `月令${monthBranch}藏${hidden.join('')},本气${base.stem}(${base.god})未透干,而${hui.where.join('、')}会成${hui.group.element}局,取${use.pos}${use.stem}(${use.god})为用`;
166
- } else {
167
- ex = `月令${monthBranch}藏${hidden.join('')},本气${base.stem}(${base.god})未透干,以月令本气取格`;
168
- }
169
- return { name, kind: '月令格', evidence: ex };
170
- }
171
-
172
- // ── 特殊格 / 组合 ────────────────────────────────────────────
173
-
174
- const ZHUANWANG = { 木: '曲直格', 火: '炎上格', 土: '稼穑格', 金: '从革格', 水: '润下格' };
175
- const CONG = { 财: '从财格', 官杀: '从杀格', 食伤: '从儿格', 印: '从强格', 比劫: '从强格' };
176
-
177
- function specialPatterns(pillars, dayStem, monthBranch, strength, wuxing, gods) {
178
- const out = [];
179
- const roles = elementRoles(dayStem);
180
- const me = roles.me;
181
- const mine = wuxing[me] ?? 0;
182
- const yin = wuxing[roles.inMe] ?? 0;
183
- const cai = wuxing[roles.cai] ?? 0;
184
- const guan = wuxing[roles.guan] ?? 0;
185
- const shi = wuxing[roles.shi] ?? 0;
186
-
187
- // 化气格:日干与年/月/时干五合,且月令助化神
188
- for (const key of ['year', 'month', 'hour']) {
189
- const other = pillars[key].stem;
190
- const target = STEM_HE[dayStem + other] || STEM_HE[other + dayStem];
191
- if (!target) continue;
192
- const monthEl = BRANCH_ELEMENT[monthBranch];
193
- const supports = monthEl === target || GENERATES[monthEl] === target;
194
- if (!supports) continue;
195
- out.push({
196
- name: `${target === '土' ? '化土格' : `化${target}格`}`,
197
- kind: '化气格',
198
- evidence: `日干${dayStem}与${['年', '月', '时'][['year', 'month', 'hour'].indexOf(key)]}干${other}合化${target},月令${monthBranch}${monthEl}${monthEl === target ? '同化神' : '生助化神'};化气格须全局无争合妒合才算真,此处仅列条件` ,
199
- });
200
- break;
201
- }
202
-
203
- // 专旺:日主之气独旺,官杀、财星皆弱
204
- if ((strength.level === '身强' || strength.level === '偏强') && mine + yin >= 5 && guan <= 1.2 && cai <= 1.8) {
205
- out.push({
206
- name: ZHUANWANG[me],
207
- kind: '专旺格',
208
- evidence: `日主${me}得${mine + yin}分(同类+印),官杀${guan}、财${cai}皆弱,成${ZHUANWANG[me]}之势`,
209
- });
210
- }
211
-
212
- // 从格:日主无根而被他神主导
213
- const hasRoot = ['year', 'month', 'day', 'hour'].some((key) =>
214
- (HIDDEN[pillars[key].branch] || []).some((h) => STEM_ELEMENT[h] === me || GENERATES[STEM_ELEMENT[h]] === me));
215
- if (!hasRoot && (strength.level === '身弱' || strength.level === '偏弱')) {
216
- const main = Object.entries({ 财: cai, 官杀: guan, 食伤: shi, 印: yin })
217
- .sort((a, b) => b[1] - a[1])[0];
218
- if (main[1] >= 2.5) {
219
- out.push({
220
- name: CONG[main[0]],
221
- kind: '从格',
222
- evidence: `日主无根(四支藏干无同类与印),${main[0]}占 ${main[1]} 分最旺,从${main[0]}之势`,
223
- });
224
- }
225
- }
226
-
227
- // 常见组合(只报事实并列,不作吉凶断语)
228
- const byGod = gods.byGod || {};
229
- const g = gods.groups || gods;
230
- const hasGod = (name) => (byGod[name] ?? 0) >= 0.5;
231
- const yinGods = (byGod.正印 ?? 0) + (byGod.偏印 ?? 0);
232
- const combos = [
233
- ['官印相生', hasGod('正官') && yinGods >= 0.5, `正官与印并见,官生印而护身`],
234
- ['杀印相生', hasGod('七杀') && yinGods >= 0.5 && (byGod.七杀 ?? 0) > (byGod.正官 ?? 0), `七杀与印并见,杀印相生`],
235
- ['食伤生财', (g.食伤 ?? 0) >= 0.6 && (g.财 ?? 0) >= 0.6, `食伤${g.食伤}+财${g.财},食伤生财`],
236
- ['财官相生', (g.财 ?? 0) >= 0.6 && (g.官杀 ?? 0) >= 0.6, `财${g.财}+官杀${g.官杀},财生官`],
237
- ['伤官配印', hasGod('伤官') && yinGods >= 0.5 && (g.财 ?? 0) <= 1, `伤官与印并见,伤官配印`],
238
- ['伤官见官', hasGod('伤官') && hasGod('正官'), `伤官与正官并见(伤官见官,为祸百端之说)`],
239
- ['羊刃驾杀', (g.官杀 ?? 0) >= 1 && YANGREN_BRANCH[dayStem] === monthBranch, `月令为刃,又有七杀${g.官杀},刃杀相制`],
240
- ];
241
- for (const [name, cond, evidence] of combos) if (cond) out.push({ name, kind: '组合', evidence });
242
- return out;
243
- }
244
-
245
- // ── 通关 ────────────────────────────────────────────────────
246
- // 滴天髓「通关」只给取象(织女、牛郎),这里把结构说清:
247
- // 用神与忌神两行在局中都真有分量、且相克时,五行里恰有一行能接上——克方所生、且能生被克方。
248
- // 只报这一行在局中的分量,不判「关通了就吉」。
249
- const TONGGUAN_MIN = 1.5;
250
- export function tongguanOf(favor = [], avoid = [], wuxing = {}) {
251
- const w = (el) => Math.round((Number(wuxing[el]) || 0) * 10) / 10;
252
- for (const use of favor) {
253
- if (w(use) < TONGGUAN_MIN) continue;
254
- for (const foe of avoid) {
255
- if (OVERCOMES[foe] !== use || w(foe) < TONGGUAN_MIN) continue;
256
- const bridge = GENERATES[foe];
257
- if (!bridge || GENERATES[bridge] !== use) continue;
258
- const weight = w(bridge);
259
- const bridgeIsAvoid = avoid.includes(bridge) && !favor.includes(bridge);
260
- const level = bridgeIsAvoid ? '为忌' : weight >= TONGGUAN_MIN ? '可通' : weight > 0 ? '关弱' : '未通';
261
- return {
262
- use, foe, bridge,
263
- weight,
264
- present: weight > 0,
265
- bridgeIsAvoid,
266
- level,
267
- usable: level === '可通',
268
- note: '相战=用神与忌神两行各在局中 ≥1.5 分且相克;通关一行=克方所生、又能生用神的那一行(五行里唯一),只报局中分量,不据此断吉凶',
269
- };
270
- }
271
- }
272
- return null;
273
- }
274
-
275
- // ── 用神 / 喜忌 ──────────────────────────────────────────────
276
-
277
-
278
- function yongshenOf(dayStem, monthBranch, strength, wuxing, branches = []) {
279
- const roles = elementRoles(dayStem);
280
- const me = roles.me;
281
- const weak = strength.level === '身弱' || strength.level === '偏弱';
282
- const strong = strength.level === '身强' || strength.level === '偏强';
283
- const roleName = (el) => (el === me ? '比劫' : el === roles.inMe ? '印'
284
- : el === roles.shi ? '食伤' : el === roles.cai ? '财' : '官杀');
285
- const ROLE_VERB = { 比劫: '帮身', 印: '生身', 食伤: '泄秀', 财: '耗', 官杀: '制' };
286
-
287
- const baseFavor = weak ? [roles.inMe, me] : strong ? [roles.shi, roles.cai, roles.guan] : [roles.inMe, me, roles.shi];
288
- const baseAvoid = weak ? [roles.shi, roles.cai, roles.guan] : strong ? [me, roles.inMe] : [roles.guan, roles.cai];
289
- let favor = baseFavor.slice();
290
- let avoid = baseAvoid.slice();
291
- // 调候所忌若正好是扶抑所喜(弱土遇燥的火、寒湿盘的强土遇水),两边不能同时列。
292
- // 本表保留在喜用、不列入忌,并单列「有争议」;否则摘要会出现同一行既喜又忌。
293
- const disputed = [];
294
-
295
- // 先判气候,再决定要不要让调候压过扶抑
296
- const climate = judgeClimate({ monthBranch, dayStem, wuxing, branches });
297
- // 《穷通宝鉴》120 格:同一「日主 + 月令」的取用顺序。只作展示与加权,不覆盖气候调候
298
- const qt = qtOf(dayStem, monthBranch);
299
- const arb = arbitrate(climate, baseFavor, baseAvoid);
300
- if (climate.need && climate.priority === '调候优先') {
301
- const needEl = climate.need.element;
302
- if (!favor.includes(needEl)) favor.unshift(needEl);
303
- avoid = avoid.filter((e) => e !== needEl);
304
- const badEl = climate.avoidElement;
305
- if (badEl && favor.includes(badEl)) {
306
- disputed.push({
307
- element: badEl,
308
- favorRole: roleName(badEl),
309
- favorVerb: ROLE_VERB[roleName(badEl)],
310
- climate: climate.label,
311
- note: `调候忌${badEl}(${climate.label}),但它在扶抑里为${roleName(badEl)};本表保留在喜用,不列入忌。`,
312
- });
313
- } else if (badEl && !avoid.includes(badEl)) {
314
- avoid.unshift(badEl);
315
- }
316
- }
317
-
318
- const expand = {
319
- 比劫: ['比肩', '劫财'], 印: ['正印', '偏印'], 食伤: ['食神', '伤官'],
320
- 财: ['正财', '偏财'], 官杀: ['正官', '七杀'],
321
- };
322
- const favorTenGods = Array.from(new Set(favor.flatMap((el) => expand[roleName(el)] || [])));
323
- const avoidTenGods = Array.from(new Set(avoid.flatMap((el) => expand[roleName(el)] || [])));
324
- const disputedTenGods = Array.from(new Set(disputed.flatMap((d) => expand[roleName(d.element)] || [])));
325
- const tongguan = tongguanOf(favor, avoid, wuxing);
326
-
327
- const describe = (el) => `${el}(${roleName(el)}·${ROLE_VERB[roleName(el)]})`;
328
- const disputedText = disputed
329
- .map((d) => `${d.element}(扶抑为${d.favorRole}·${d.favorVerb};${d.climate}盘调候忌${d.element},本表保留在喜用)`)
330
- .join('、');
331
- const detail = [
332
- `日主${dayStem}${me},${strength.level}(${strength.score} 分):`,
333
- `喜${favor.map(describe).join('、') || '—'};忌${avoid.map(describe).join('、') || '—'}。`,
334
- disputed.length ? `有争议:${disputedText}。` : '',
335
- !weak && !strong ? '身中和,先取调候与通关,喜忌随大运流年动态变化。' : '',
336
- `调候:${climate.label}(${(climate.evidence || []).join('、')})${climate.need ? `,取${climate.need.stem}${climate.need.element},忌${climate.avoidElement}${disputed.some((d) => d.element === climate.avoidElement) ? '(有争议)' : ''}` : ',气候平和'}。`,
337
- qt ? `穷通宝鉴:${qt.month}${dayStem}${me},用神${qt.yong.join('、')}(首取${qt.primary})。` : '',
338
- tongguan
339
- ? `通关:忌${tongguan.foe}克用${tongguan.use},五行以${tongguan.bridge}为关;局中${tongguan.level === '可通'
340
- ? `${tongguan.bridge} ${tongguan.weight} 分,关可通`
341
- : tongguan.level === '关弱'
342
- ? `仅 ${tongguan.bridge} ${tongguan.weight} 分,关弱`
343
- : tongguan.level === '为忌'
344
- ? `${tongguan.bridge} ${tongguan.weight} 分但此行为忌,通关力打折`
345
- : `未见${tongguan.bridge},关未通`}(只作结构提示,不据此断吉凶)。`
346
- : '',
347
- arb.text,
348
- arb.clash ? '取舍:本盘按「调候优先、扶抑次之」取用;若你更认同扶抑派,可把调候当环境偏好,强弱判断不受影响。' : '',
349
- ].filter(Boolean).join('');
350
-
351
- return {
352
- method: climate.priority === '调候优先' ? '调候优先 + 扶抑校验' : '扶抑为主 + 调候校验',
353
- level: strength.level,
354
- favor,
355
- avoid,
356
- disputed,
357
- favorRoles: favor.map(roleName),
358
- avoidRoles: avoid.map(roleName),
359
- favorTenGods,
360
- avoidTenGods,
361
- disputedTenGods,
362
- tiaohou: { ...climate, ...arb, element: climate.need ? climate.need.element : null },
363
- climate,
364
- qt,
365
- tongguan,
366
- detail,
367
- };
368
- }
369
-
370
- // ── 行运评分 ────────────────────────────────────────────────
371
-
372
- const TIERS = [
373
- [1.3, '大吉'],
374
- [0.6, '吉'],
375
- [-0.6, '平'],
376
- [-1.3, '忌'],
377
- [-Infinity, '大忌'],
378
- ];
379
-
380
- export function scoreLuck(ganzhi, frame) {
381
- const ys = frame.yongshen;
382
- const stem = ganzhi[0];
383
- const branch = ganzhi[1];
384
- const stemEl = STEM_ELEMENT[stem];
385
- const branchEl = BRANCH_ELEMENT[branch];
386
- const th = ys.tiaohou || ys.climate || null;
387
- const scoreEl = (el) => {
388
- let s = 0;
389
- if (ys.favor.includes(el)) s += 1;
390
- if (ys.avoid.includes(el)) s -= 1;
391
- return s;
392
- };
393
- let score = scoreEl(stemEl) * 1.0 + scoreEl(branchEl) * 0.8;
394
- for (const h of (HIDDEN[branch] || []).slice(1)) {
395
- score += scoreEl(STEM_ELEMENT[h]) * 0.2;
396
- }
397
-
398
- const reasons = [];
399
- if (ys.favor.includes(stemEl)) reasons.push(`天干${stem}${stemEl}为用神`);
400
- else if (ys.avoid.includes(stemEl)) reasons.push(`天干${stem}${stemEl}为忌神`);
401
- else reasons.push(`天干${stem}${stemEl}不喜不忌`);
402
- if (ys.favor.includes(branchEl)) reasons.push(`地支${branch}${branchEl}助用`);
403
- else if (ys.avoid.includes(branchEl)) reasons.push(`地支${branch}${branchEl}助忌`);
404
-
405
- // 调候修正:不是"有没有这个五行",而是"这一年是给命局添了一把火,还是泼了一盆水"
406
- if (th && th.need && th.priority === '调候优先') {
407
- const needEl = th.need.element;
408
- const oppEl = th.avoidElement;
409
- const cold = String(th.kind).includes('寒');
410
- if (stemEl === needEl) {
411
- score += 1.1;
412
- reasons.push(`${th.label}之局得${stem}${stemEl},${cold ? '解冻' : '得润'}`);
413
- } else if (stemEl === oppEl) {
414
- score -= 0.9;
415
- reasons.push(`${th.label}之局再逢${stem}${stemEl},${cold ? '冻木难生' : '燥上加燥'}`);
416
- }
417
- if (branchEl === needEl) {
418
- score += 0.9;
419
- reasons.push(`地支${branch}${branchEl}合调候`);
420
- } else if (branchEl === oppEl) {
421
- score -= 1.0;
422
- reasons.push(`地支${branch}${branchEl}加重${th.label}`);
423
- }
424
- if (cold && (stemEl === '水' || branchEl === '水')) score -= 0.3;
425
- } else if (th && th.need && (stemEl === th.need.element || branchEl === th.need.element)) {
426
- reasons.push(`合调候(${th.need.element})`);
427
- }
428
-
429
- const godStem = tenGod(frame.dayStem, stem);
430
- const bonus = ys.favorTenGods.includes(godStem) ? 0.3 : 0;
431
- // 穷通宝鉴:天干正好是这一格的首取/次取时,再给一点「按书取用」的分
432
- // 只在该干不属于忌神时加分——书里的取用与气候调候不同向时,以调候为先
433
- const qt = ys.qt || null;
434
- if (qt && qt.primary && !ys.avoid.includes(STEM_ELEMENT[qt.primary])) {
435
- if (stem === qt.primary) {
436
- score += 0.3;
437
- reasons.push(`天干${stem}正是${qt.branch}月${qt.stem}日的首取(穷通宝鉴)`);
438
- } else if (qt.yong.includes(stem) && !ys.avoid.includes(STEM_ELEMENT[stem])) {
439
- score += 0.15;
440
- reasons.push(`天干${stem}为穷通宝鉴次取`);
441
- }
442
- }
443
- let total = Math.round((score + bonus) * 10) / 10;
444
- total = Math.max(-4, Math.min(4, total));
445
-
446
- const tier = TIERS.find(([min]) => total >= min)[1];
447
- return { ganzhi, score: total, tier, stemTenGod: godStem, reasons, note: `${godStem}运,${tier}(${total})` };
448
- }
449
-
450
- // ── 对外入口 ────────────────────────────────────────────────
451
-
452
- /**
453
- * @param {object} p
454
- * @param {object} p.pillars 四柱
455
- * @param {string} p.dayStem 日干
456
- * @param {string} p.monthBranch 月支
457
- * @param {object} p.strength bazi.js 的旺衰结果
458
- * @param {object} p.wuxing 五行分值
459
- */
460
- export function analyzeBaziFrame({ pillars, dayStem, monthBranch, strength, wuxing }) {
461
- const gods = godStats(pillars, dayStem);
462
- const month = monthPattern(pillars, dayStem, monthBranch);
463
- const special = specialPatterns(pillars, dayStem, monthBranch, strength, wuxing, gods);
464
- const geju = [month, ...special];
465
- const yongshen = yongshenOf(dayStem, monthBranch, strength, wuxing, Object.values(pillars).map((p) => p.branch));
466
- return {
467
- geju,
468
- primary: month,
469
- primaryName: month.name,
470
- godStats: gods,
471
- yongshen,
472
- dayStem,
473
- monthBranch,
474
- };
475
- }
1
+ // 八字格局、用神与行运评分
2
+ //
3
+ // 定位:在 bazi.js 算出四柱、五行、旺衰之后,给出
4
+ // 1) 格局(月令格 + 特殊格 + 常见组合);
5
+ // 2) 用神 / 喜忌(调候优先 + 扶抑校验;气候平和时仍以扶抑为主);
6
+ // 3) 大运、流年的吉凶倾向评分(含调候修正项)。
7
+ // 取舍:格局派别差异极大,这里只给「规则可复核」的判定,evidence 写明依据;
8
+ // 调候由 tiaohou.js 独立判定,与扶抑冲突时显式裁决并写明理由,不静默取用。
9
+
10
+ import { judgeClimate, arbitrate } from './tiaohou.js';
11
+ import { qtOf } from './qiongtong.js';
12
+ import { sanheGroupOf } from './sanhe.js';
13
+
14
+ const STEMS = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
15
+ const BRANCHES = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
16
+
17
+ const STEM_ELEMENT = {
18
+ 甲: '木', 乙: '木', 丙: '火', 丁: '火', 戊: '土',
19
+ 己: '土', 庚: '金', 辛: '金', 壬: '水', 癸: '水',
20
+ };
21
+ const BRANCH_ELEMENT = {
22
+ 子: '水', 丑: '土', 寅: '木', 卯: '木', 辰: '土', 巳: '火',
23
+ 午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水',
24
+ };
25
+ const GENERATES = { 木: '火', 火: '土', 土: '金', 金: '水', 水: '木' };
26
+ const OVERCOMES = { 木: '土', 土: '水', 水: '火', 火: '金', 金: '木' };
27
+ const HIDDEN = {
28
+ 子: ['癸'], 丑: ['己', '癸', '辛'], 寅: ['甲', '丙', '戊'], 卯: ['乙'],
29
+ 辰: ['戊', '乙', '癸'], 巳: ['丙', '戊', '庚'], 午: ['丁', '己'], 未: ['己', '丁', '乙'],
30
+ 申: ['庚', '壬', '戊'], 酉: ['辛'], 戌: ['戊', '辛', '丁'], 亥: ['壬', '甲'],
31
+ };
32
+ const HIDDEN_W = [1, 0.5, 0.3];
33
+
34
+ const LUSHEN_BRANCH = { 甲: '寅', 乙: '卯', 丙: '巳', 丁: '午', 戊: '巳', 己: '午', 庚: '申', 辛: '酉', 壬: '亥', 癸: '子' };
35
+ const YANGREN_BRANCH = { 甲: '卯', 乙: '辰', 丙: '午', 丁: '未', 戊: '午', 己: '未', 庚: '酉', 辛: '戌', 壬: '子', 癸: '丑' };
36
+ const STEM_HE = { 甲己: '土', 乙庚: '金', 丙辛: '水', 丁壬: '木', 戊癸: '火' };
37
+
38
+ const isYangStem = (s) => STEMS.indexOf(s) % 2 === 0;
39
+ const ELEMENTS = ['木', '火', '土', '金', '水'];
40
+
41
+ /** 十神:以日干为我(与 bazi.js 同规则) */
42
+ export function tenGod(dayStem, otherStem) {
43
+ const me = STEM_ELEMENT[dayStem];
44
+ const it = STEM_ELEMENT[otherStem];
45
+ const same = isYangStem(dayStem) === isYangStem(otherStem);
46
+ if (me === it) return same ? '比肩' : '劫财';
47
+ if (GENERATES[me] === it) return same ? '食神' : '伤官';
48
+ if (OVERCOMES[me] === it) return same ? '偏财' : '正财';
49
+ if (OVERCOMES[it] === me) return same ? '七杀' : '正官';
50
+ if (GENERATES[it] === me) return same ? '偏印' : '正印';
51
+ return '?';
52
+ }
53
+
54
+ const godGroups = {
55
+ 比劫: ['比肩', '劫财'],
56
+ 食伤: ['食神', '伤官'],
57
+ 财: ['正财', '偏财'],
58
+ 官杀: ['正官', '七杀'],
59
+ 印: ['正印', '偏印'],
60
+ };
61
+
62
+ /** 全局十神权重:四柱天干 1.0,地支藏干按本气 1 / 中气 0.5 / 余气 0.3 */
63
+ function godStats(pillars, dayStem) {
64
+ const total = { 比劫: 0, 食伤: 0, 财: 0, 官杀: 0, 印: 0 };
65
+ const byGod = { 比肩: 0, 劫财: 0, 食神: 0, 伤官: 0, 正财: 0, 偏财: 0, 正官: 0, 七杀: 0, 正印: 0, 偏印: 0 };
66
+ const add = (stem, w) => {
67
+ const god = tenGod(dayStem, stem);
68
+ const g = Object.keys(godGroups).find((k) => godGroups[k].includes(god));
69
+ if (g) total[g] += w;
70
+ if (byGod[god] !== undefined) byGod[god] += w;
71
+ };
72
+ for (const key of ['year', 'month', 'day', 'hour']) {
73
+ add(pillars[key].stem, key === 'day' ? 0.6 : 1);
74
+ (HIDDEN[pillars[key].branch] || []).forEach((h, i) => add(h, HIDDEN_W[i] ?? 0.1));
75
+ }
76
+ for (const k of Object.keys(total)) total[k] = Math.round(total[k] * 10) / 10;
77
+ for (const k of Object.keys(byGod)) byGod[k] = Math.round(byGod[k] * 10) / 10;
78
+ return { groups: total, byGod };
79
+ }
80
+
81
+ /** 用神所属五行:生我 / 同我 / 我生 / 我克 / 克我 */
82
+ export function elementRoles(dayStem) {
83
+ const me = STEM_ELEMENT[dayStem];
84
+ const inMe = ELEMENTS.find((e) => GENERATES[e] === me);
85
+ const guan = ELEMENTS.find((e) => OVERCOMES[e] === me);
86
+ return { me, inMe, shi: GENERATES[me], cai: OVERCOMES[me], guan };
87
+ }
88
+
89
+ // ── 月令格 ───────────────────────────────────────────────────
90
+
91
+ // 四墓(杂气)月:辰戌丑未所藏多、用神不一,取用先看透干,再看会支。
92
+ // 会支只认三合局——子平真诠十六「逢申与子会局」的例子即三合,三会方与兼透兼用仍不纳入。
93
+ const ZA_MONTHS = ['辰', '未', '戌', '丑'];
94
+
95
+ function huiCandidate(pillars, monthBranch, cands) {
96
+ if (!ZA_MONTHS.includes(monthBranch)) return null;
97
+ const group = sanheGroupOf(monthBranch);
98
+ if (!group) return null;
99
+ const others = ['year', 'day', 'hour'];
100
+ const need = group.branches.filter((b) => b !== monthBranch);
101
+ if (!need.every((b) => others.some((k) => pillars[k].branch === b))) return null;
102
+ const cand = cands.find((c) => STEM_ELEMENT[c.stem] === group.element);
103
+ if (!cand) return null;
104
+ const where = others
105
+ .filter((k) => need.includes(pillars[k].branch))
106
+ .map((k) => `${({ year: '年', day: '日', hour: '时' })[k]}支${pillars[k].branch}`);
107
+ return { ...cand, group, where };
108
+ }
109
+
110
+ function monthPattern(pillars, dayStem, monthBranch) {
111
+ const hidden = HIDDEN[monthBranch] || [];
112
+ const cands = hidden.map((stem, i) => ({
113
+ stem,
114
+ god: tenGod(dayStem, stem),
115
+ pos: ['本气', '中气', '余气'][i],
116
+ w: HIDDEN_W[i] ?? 0.1,
117
+ }));
118
+ const others = ['year', 'month', 'hour'];
119
+ const exposedKey = others.find((k) => cands.some((c) => c.stem === pillars[k].stem));
120
+ const base = cands[0];
121
+ const hui = exposedKey ? null : huiCandidate(pillars, monthBranch, cands);
122
+ const use = exposedKey ? cands.find((c) => c.stem === pillars[exposedKey].stem) : (hui || base);
123
+ const usedBy = exposedKey ? '透干' : (hui ? '会支' : '本气');
124
+ const exposedPos = exposedKey ? { year: '年', month: '月', hour: '时' }[exposedKey] : null;
125
+
126
+ // 建禄只认日干的禄位;戊土辰月、己土未月这类「本气比肩但不在禄位」的杂气月不再冒称建禄
127
+ if (base.god === '比肩' && LUSHEN_BRANCH[dayStem] === monthBranch) {
128
+ return {
129
+ name: '建禄格',
130
+ kind: '月令格',
131
+ evidence: `月令${monthBranch}为日干${dayStem}之禄(本气${base.stem}比肩),身坐禄地`,
132
+ };
133
+ }
134
+ if (base.god === '劫财') {
135
+ if (YANGREN_BRANCH[dayStem] === monthBranch) {
136
+ return {
137
+ name: isYangStem(dayStem) ? '阳刃格' : '月刃格',
138
+ kind: '月令格',
139
+ evidence: `月令${monthBranch}为日干${dayStem}之刃(本气${base.stem}劫财)${isYangStem(dayStem) ? '' : ',阴干统称月刃'}`,
140
+ };
141
+ }
142
+ return { name: '月劫格', kind: '月令格', evidence: `月令${monthBranch}本气${base.stem}为劫财` };
143
+ }
144
+ if (usedBy === '本气' && base.god === '比肩') {
145
+ return {
146
+ name: '比肩格',
147
+ kind: '月令格',
148
+ evidence: `月令${monthBranch}藏${hidden.join('')},本气${base.stem}为比肩,但${dayStem}禄在${LUSHEN_BRANCH[dayStem]}、不在${monthBranch},属杂气月比肩当权`,
149
+ };
150
+ }
151
+ // 子平十六:「透乙则用月劫」——杂气月由透干或会支取到劫财时,仍作月劫论
152
+ if (usedBy !== '本气' && use.god === '劫财') {
153
+ const via = exposedKey ? `透于${exposedPos}干` : `${hui.where.join('、')}会成${hui.group.element}局`;
154
+ return {
155
+ name: '月劫格',
156
+ kind: '月令格',
157
+ evidence: `月令${monthBranch}藏${hidden.join('')},${use.pos}${use.stem}(劫财)${via},劫财为用,作月劫论`,
158
+ };
159
+ }
160
+ const name = `${use.god}格`;
161
+ let ex;
162
+ if (exposedKey) {
163
+ ex = `月令${monthBranch}藏${hidden.join('')},${use.pos}${use.stem}(${use.god})透于${exposedPos}干`;
164
+ } else if (hui) {
165
+ ex = `月令${monthBranch}藏${hidden.join('')},本气${base.stem}(${base.god})未透干,而${hui.where.join('、')}会成${hui.group.element}局,取${use.pos}${use.stem}(${use.god})为用`;
166
+ } else {
167
+ ex = `月令${monthBranch}藏${hidden.join('')},本气${base.stem}(${base.god})未透干,以月令本气取格`;
168
+ }
169
+ return { name, kind: '月令格', evidence: ex };
170
+ }
171
+
172
+ // ── 特殊格 / 组合 ────────────────────────────────────────────
173
+
174
+ const ZHUANWANG = { 木: '曲直格', 火: '炎上格', 土: '稼穑格', 金: '从革格', 水: '润下格' };
175
+ const CONG = { 财: '从财格', 官杀: '从杀格', 食伤: '从儿格', 印: '从强格', 比劫: '从强格' };
176
+
177
+ function specialPatterns(pillars, dayStem, monthBranch, strength, wuxing, gods) {
178
+ const out = [];
179
+ const roles = elementRoles(dayStem);
180
+ const me = roles.me;
181
+ const mine = wuxing[me] ?? 0;
182
+ const yin = wuxing[roles.inMe] ?? 0;
183
+ const cai = wuxing[roles.cai] ?? 0;
184
+ const guan = wuxing[roles.guan] ?? 0;
185
+ const shi = wuxing[roles.shi] ?? 0;
186
+
187
+ // 化气格:日干与年/月/时干五合,且月令助化神
188
+ for (const key of ['year', 'month', 'hour']) {
189
+ const other = pillars[key].stem;
190
+ const target = STEM_HE[dayStem + other] || STEM_HE[other + dayStem];
191
+ if (!target) continue;
192
+ const monthEl = BRANCH_ELEMENT[monthBranch];
193
+ const supports = monthEl === target || GENERATES[monthEl] === target;
194
+ if (!supports) continue;
195
+ out.push({
196
+ name: `${target === '土' ? '化土格' : `化${target}格`}`,
197
+ kind: '化气格',
198
+ evidence: `日干${dayStem}与${['年', '月', '时'][['year', 'month', 'hour'].indexOf(key)]}干${other}合化${target},月令${monthBranch}${monthEl}${monthEl === target ? '同化神' : '生助化神'};化气格须全局无争合妒合才算真,此处仅列条件` ,
199
+ });
200
+ break;
201
+ }
202
+
203
+ // 专旺:日主之气独旺,官杀、财星皆弱
204
+ if ((strength.level === '身强' || strength.level === '偏强') && mine + yin >= 5 && guan <= 1.2 && cai <= 1.8) {
205
+ out.push({
206
+ name: ZHUANWANG[me],
207
+ kind: '专旺格',
208
+ evidence: `日主${me}得${mine + yin}分(同类+印),官杀${guan}、财${cai}皆弱,成${ZHUANWANG[me]}之势`,
209
+ });
210
+ }
211
+
212
+ // 从格:日主无根而被他神主导
213
+ const hasRoot = ['year', 'month', 'day', 'hour'].some((key) =>
214
+ (HIDDEN[pillars[key].branch] || []).some((h) => STEM_ELEMENT[h] === me || GENERATES[STEM_ELEMENT[h]] === me));
215
+ if (!hasRoot && (strength.level === '身弱' || strength.level === '偏弱')) {
216
+ const main = Object.entries({ 财: cai, 官杀: guan, 食伤: shi, 印: yin })
217
+ .sort((a, b) => b[1] - a[1])[0];
218
+ if (main[1] >= 2.5) {
219
+ out.push({
220
+ name: CONG[main[0]],
221
+ kind: '从格',
222
+ evidence: `日主无根(四支藏干无同类与印),${main[0]}占 ${main[1]} 分最旺,从${main[0]}之势`,
223
+ });
224
+ }
225
+ }
226
+
227
+ // 常见组合(只报事实并列,不作吉凶断语)
228
+ const byGod = gods.byGod || {};
229
+ const g = gods.groups || gods;
230
+ const hasGod = (name) => (byGod[name] ?? 0) >= 0.5;
231
+ const yinGods = (byGod.正印 ?? 0) + (byGod.偏印 ?? 0);
232
+ const combos = [
233
+ ['官印相生', hasGod('正官') && yinGods >= 0.5, `正官与印并见,官生印而护身`],
234
+ ['杀印相生', hasGod('七杀') && yinGods >= 0.5 && (byGod.七杀 ?? 0) > (byGod.正官 ?? 0), `七杀与印并见,杀印相生`],
235
+ ['食伤生财', (g.食伤 ?? 0) >= 0.6 && (g.财 ?? 0) >= 0.6, `食伤${g.食伤}+财${g.财},食伤生财`],
236
+ ['财官相生', (g.财 ?? 0) >= 0.6 && (g.官杀 ?? 0) >= 0.6, `财${g.财}+官杀${g.官杀},财生官`],
237
+ ['伤官配印', hasGod('伤官') && yinGods >= 0.5 && (g.财 ?? 0) <= 1, `伤官与印并见,伤官配印`],
238
+ ['伤官见官', hasGod('伤官') && hasGod('正官'), `伤官与正官并见(伤官见官,为祸百端之说)`],
239
+ ['羊刃驾杀', (g.官杀 ?? 0) >= 1 && YANGREN_BRANCH[dayStem] === monthBranch, `月令为刃,又有七杀${g.官杀},刃杀相制`],
240
+ ];
241
+ for (const [name, cond, evidence] of combos) if (cond) out.push({ name, kind: '组合', evidence });
242
+ return out;
243
+ }
244
+
245
+ // ── 通关 ────────────────────────────────────────────────────
246
+ // 滴天髓「通关」只给取象(织女、牛郎),这里把结构说清:
247
+ // 用神与忌神两行在局中都真有分量、且相克时,五行里恰有一行能接上——克方所生、且能生被克方。
248
+ // 只报这一行在局中的分量,不判「关通了就吉」。
249
+ const TONGGUAN_MIN = 1.5;
250
+ export function tongguanOf(favor = [], avoid = [], wuxing = {}) {
251
+ const w = (el) => Math.round((Number(wuxing[el]) || 0) * 10) / 10;
252
+ for (const use of favor) {
253
+ if (w(use) < TONGGUAN_MIN) continue;
254
+ for (const foe of avoid) {
255
+ if (OVERCOMES[foe] !== use || w(foe) < TONGGUAN_MIN) continue;
256
+ const bridge = GENERATES[foe];
257
+ if (!bridge || GENERATES[bridge] !== use) continue;
258
+ const weight = w(bridge);
259
+ const bridgeIsAvoid = avoid.includes(bridge) && !favor.includes(bridge);
260
+ const level = bridgeIsAvoid ? '为忌' : weight >= TONGGUAN_MIN ? '可通' : weight > 0 ? '关弱' : '未通';
261
+ return {
262
+ use, foe, bridge,
263
+ weight,
264
+ present: weight > 0,
265
+ bridgeIsAvoid,
266
+ level,
267
+ usable: level === '可通',
268
+ note: '相战=用神与忌神两行各在局中 ≥1.5 分且相克;通关一行=克方所生、又能生用神的那一行(五行里唯一),只报局中分量,不据此断吉凶',
269
+ };
270
+ }
271
+ }
272
+ return null;
273
+ }
274
+
275
+ // ── 用神 / 喜忌 ──────────────────────────────────────────────
276
+
277
+
278
+ function yongshenOf(dayStem, monthBranch, strength, wuxing, branches = []) {
279
+ const roles = elementRoles(dayStem);
280
+ const me = roles.me;
281
+ const weak = strength.level === '身弱' || strength.level === '偏弱';
282
+ const strong = strength.level === '身强' || strength.level === '偏强';
283
+ const roleName = (el) => (el === me ? '比劫' : el === roles.inMe ? '印'
284
+ : el === roles.shi ? '食伤' : el === roles.cai ? '财' : '官杀');
285
+ const ROLE_VERB = { 比劫: '帮身', 印: '生身', 食伤: '泄秀', 财: '耗', 官杀: '制' };
286
+
287
+ const baseFavor = weak ? [roles.inMe, me] : strong ? [roles.shi, roles.cai, roles.guan] : [roles.inMe, me, roles.shi];
288
+ const baseAvoid = weak ? [roles.shi, roles.cai, roles.guan] : strong ? [me, roles.inMe] : [roles.guan, roles.cai];
289
+ let favor = baseFavor.slice();
290
+ let avoid = baseAvoid.slice();
291
+ // 调候所忌若正好是扶抑所喜(弱土遇燥的火、寒湿盘的强土遇水),两边不能同时列。
292
+ // 本表保留在喜用、不列入忌,并单列「有争议」;否则摘要会出现同一行既喜又忌。
293
+ const disputed = [];
294
+
295
+ // 先判气候,再决定要不要让调候压过扶抑
296
+ const climate = judgeClimate({ monthBranch, dayStem, wuxing, branches });
297
+ // 《穷通宝鉴》120 格:同一「日主 + 月令」的取用顺序。只作展示与加权,不覆盖气候调候
298
+ const qt = qtOf(dayStem, monthBranch);
299
+ const arb = arbitrate(climate, baseFavor, baseAvoid);
300
+ if (climate.need && climate.priority === '调候优先') {
301
+ const needEl = climate.need.element;
302
+ if (!favor.includes(needEl)) favor.unshift(needEl);
303
+ avoid = avoid.filter((e) => e !== needEl);
304
+ const badEl = climate.avoidElement;
305
+ if (badEl && favor.includes(badEl)) {
306
+ disputed.push({
307
+ element: badEl,
308
+ favorRole: roleName(badEl),
309
+ favorVerb: ROLE_VERB[roleName(badEl)],
310
+ climate: climate.label,
311
+ note: `调候忌${badEl}(${climate.label}),但它在扶抑里为${roleName(badEl)};本表保留在喜用,不列入忌。`,
312
+ });
313
+ } else if (badEl && !avoid.includes(badEl)) {
314
+ avoid.unshift(badEl);
315
+ }
316
+ }
317
+
318
+ const expand = {
319
+ 比劫: ['比肩', '劫财'], 印: ['正印', '偏印'], 食伤: ['食神', '伤官'],
320
+ 财: ['正财', '偏财'], 官杀: ['正官', '七杀'],
321
+ };
322
+ const favorTenGods = Array.from(new Set(favor.flatMap((el) => expand[roleName(el)] || [])));
323
+ const avoidTenGods = Array.from(new Set(avoid.flatMap((el) => expand[roleName(el)] || [])));
324
+ const disputedTenGods = Array.from(new Set(disputed.flatMap((d) => expand[roleName(d.element)] || [])));
325
+ const tongguan = tongguanOf(favor, avoid, wuxing);
326
+
327
+ const describe = (el) => `${el}(${roleName(el)}·${ROLE_VERB[roleName(el)]})`;
328
+ const disputedText = disputed
329
+ .map((d) => `${d.element}(扶抑为${d.favorRole}·${d.favorVerb};${d.climate}盘调候忌${d.element},本表保留在喜用)`)
330
+ .join('、');
331
+ const detail = [
332
+ `日主${dayStem}${me},${strength.level}(${strength.score} 分):`,
333
+ `喜${favor.map(describe).join('、') || '—'};忌${avoid.map(describe).join('、') || '—'}。`,
334
+ disputed.length ? `有争议:${disputedText}。` : '',
335
+ !weak && !strong ? '身中和,先取调候与通关,喜忌随大运流年动态变化。' : '',
336
+ `调候:${climate.label}(${(climate.evidence || []).join('、')})${climate.need ? `,取${climate.need.stem}${climate.need.element},忌${climate.avoidElement}${disputed.some((d) => d.element === climate.avoidElement) ? '(有争议)' : ''}` : ',气候平和'}。`,
337
+ qt ? `穷通宝鉴:${qt.month}${dayStem}${me},用神${qt.yong.join('、')}(首取${qt.primary})。` : '',
338
+ tongguan
339
+ ? `通关:忌${tongguan.foe}克用${tongguan.use},五行以${tongguan.bridge}为关;局中${tongguan.level === '可通'
340
+ ? `${tongguan.bridge} ${tongguan.weight} 分,关可通`
341
+ : tongguan.level === '关弱'
342
+ ? `仅 ${tongguan.bridge} ${tongguan.weight} 分,关弱`
343
+ : tongguan.level === '为忌'
344
+ ? `${tongguan.bridge} ${tongguan.weight} 分但此行为忌,通关力打折`
345
+ : `未见${tongguan.bridge},关未通`}(只作结构提示,不据此断吉凶)。`
346
+ : '',
347
+ arb.text,
348
+ arb.clash ? '取舍:本盘按「调候优先、扶抑次之」取用;若你更认同扶抑派,可把调候当环境偏好,强弱判断不受影响。' : '',
349
+ ].filter(Boolean).join('');
350
+
351
+ return {
352
+ method: climate.priority === '调候优先' ? '调候优先 + 扶抑校验' : '扶抑为主 + 调候校验',
353
+ level: strength.level,
354
+ favor,
355
+ avoid,
356
+ disputed,
357
+ favorRoles: favor.map(roleName),
358
+ avoidRoles: avoid.map(roleName),
359
+ favorTenGods,
360
+ avoidTenGods,
361
+ disputedTenGods,
362
+ tiaohou: { ...climate, ...arb, element: climate.need ? climate.need.element : null },
363
+ climate,
364
+ qt,
365
+ tongguan,
366
+ detail,
367
+ };
368
+ }
369
+
370
+ // ── 行运评分 ────────────────────────────────────────────────
371
+
372
+ const TIERS = [
373
+ [1.3, '大吉'],
374
+ [0.6, '吉'],
375
+ [-0.6, '平'],
376
+ [-1.3, '忌'],
377
+ [-Infinity, '大忌'],
378
+ ];
379
+
380
+ export function scoreLuck(ganzhi, frame) {
381
+ const ys = frame.yongshen;
382
+ const stem = ganzhi[0];
383
+ const branch = ganzhi[1];
384
+ const stemEl = STEM_ELEMENT[stem];
385
+ const branchEl = BRANCH_ELEMENT[branch];
386
+ const th = ys.tiaohou || ys.climate || null;
387
+ const scoreEl = (el) => {
388
+ let s = 0;
389
+ if (ys.favor.includes(el)) s += 1;
390
+ if (ys.avoid.includes(el)) s -= 1;
391
+ return s;
392
+ };
393
+ let score = scoreEl(stemEl) * 1.0 + scoreEl(branchEl) * 0.8;
394
+ for (const h of (HIDDEN[branch] || []).slice(1)) {
395
+ score += scoreEl(STEM_ELEMENT[h]) * 0.2;
396
+ }
397
+
398
+ const reasons = [];
399
+ if (ys.favor.includes(stemEl)) reasons.push(`天干${stem}${stemEl}为用神`);
400
+ else if (ys.avoid.includes(stemEl)) reasons.push(`天干${stem}${stemEl}为忌神`);
401
+ else reasons.push(`天干${stem}${stemEl}不喜不忌`);
402
+ if (ys.favor.includes(branchEl)) reasons.push(`地支${branch}${branchEl}助用`);
403
+ else if (ys.avoid.includes(branchEl)) reasons.push(`地支${branch}${branchEl}助忌`);
404
+
405
+ // 调候修正:不是"有没有这个五行",而是"这一年是给命局添了一把火,还是泼了一盆水"
406
+ if (th && th.need && th.priority === '调候优先') {
407
+ const needEl = th.need.element;
408
+ const oppEl = th.avoidElement;
409
+ const cold = String(th.kind).includes('寒');
410
+ if (stemEl === needEl) {
411
+ score += 1.1;
412
+ reasons.push(`${th.label}之局得${stem}${stemEl},${cold ? '解冻' : '得润'}`);
413
+ } else if (stemEl === oppEl) {
414
+ score -= 0.9;
415
+ reasons.push(`${th.label}之局再逢${stem}${stemEl},${cold ? '冻木难生' : '燥上加燥'}`);
416
+ }
417
+ if (branchEl === needEl) {
418
+ score += 0.9;
419
+ reasons.push(`地支${branch}${branchEl}合调候`);
420
+ } else if (branchEl === oppEl) {
421
+ score -= 1.0;
422
+ reasons.push(`地支${branch}${branchEl}加重${th.label}`);
423
+ }
424
+ if (cold && (stemEl === '水' || branchEl === '水')) score -= 0.3;
425
+ } else if (th && th.need && (stemEl === th.need.element || branchEl === th.need.element)) {
426
+ reasons.push(`合调候(${th.need.element})`);
427
+ }
428
+
429
+ const godStem = tenGod(frame.dayStem, stem);
430
+ const bonus = ys.favorTenGods.includes(godStem) ? 0.3 : 0;
431
+ // 穷通宝鉴:天干正好是这一格的首取/次取时,再给一点「按书取用」的分
432
+ // 只在该干不属于忌神时加分——书里的取用与气候调候不同向时,以调候为先
433
+ const qt = ys.qt || null;
434
+ if (qt && qt.primary && !ys.avoid.includes(STEM_ELEMENT[qt.primary])) {
435
+ if (stem === qt.primary) {
436
+ score += 0.3;
437
+ reasons.push(`天干${stem}正是${qt.branch}月${qt.stem}日的首取(穷通宝鉴)`);
438
+ } else if (qt.yong.includes(stem) && !ys.avoid.includes(STEM_ELEMENT[stem])) {
439
+ score += 0.15;
440
+ reasons.push(`天干${stem}为穷通宝鉴次取`);
441
+ }
442
+ }
443
+ let total = Math.round((score + bonus) * 10) / 10;
444
+ total = Math.max(-4, Math.min(4, total));
445
+
446
+ const tier = TIERS.find(([min]) => total >= min)[1];
447
+ return { ganzhi, score: total, tier, stemTenGod: godStem, reasons, note: `${godStem}运,${tier}(${total})` };
448
+ }
449
+
450
+ // ── 对外入口 ────────────────────────────────────────────────
451
+
452
+ /**
453
+ * @param {object} p
454
+ * @param {object} p.pillars 四柱
455
+ * @param {string} p.dayStem 日干
456
+ * @param {string} p.monthBranch 月支
457
+ * @param {object} p.strength bazi.js 的旺衰结果
458
+ * @param {object} p.wuxing 五行分值
459
+ */
460
+ export function analyzeBaziFrame({ pillars, dayStem, monthBranch, strength, wuxing }) {
461
+ const gods = godStats(pillars, dayStem);
462
+ const month = monthPattern(pillars, dayStem, monthBranch);
463
+ const special = specialPatterns(pillars, dayStem, monthBranch, strength, wuxing, gods);
464
+ const geju = [month, ...special];
465
+ const yongshen = yongshenOf(dayStem, monthBranch, strength, wuxing, Object.values(pillars).map((p) => p.branch));
466
+ return {
467
+ geju,
468
+ primary: month,
469
+ primaryName: month.name,
470
+ godStats: gods,
471
+ yongshen,
472
+ dayStem,
473
+ monthBranch,
474
+ };
475
+ }