dsh-metaphysics 0.58.0 → 0.59.1

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,547 +1,548 @@
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 { SANHE_GROUPS, 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
- const POS_LABEL = { year: '年', month: '月', day: '日', hour: '时' };
177
-
178
- // 日主根气:地支藏干里的同类(比劫)与生我(印),以及天干(日干除外)透出的比劫/印。
179
- // 只用于真从/假从:无根才是真从,有中气/余气微根或虚透只能算假从;
180
- // 见一个本气根(≥1 分)就不作从格——日主已能站住,再称从会误判。
181
- function rootProfile(pillars, dayStem) {
182
- const me = STEM_ELEMENT[dayStem];
183
- const out = { bi: 0, yin: 0, total: 0, details: [], helpers: [] };
184
- for (const key of ['year', 'month', 'day', 'hour']) {
185
- const branch = pillars[key].branch;
186
- (HIDDEN[branch] || []).forEach((stem, i) => {
187
- const el = STEM_ELEMENT[stem];
188
- const w = HIDDEN_W[i] ?? 0.1;
189
- if (el === me) {
190
- out.bi += w;
191
- out.details.push(`${POS_LABEL[key]}支${branch}藏${stem}(比劫 ${w})`);
192
- } else if (GENERATES[el] === me) {
193
- out.yin += w;
194
- out.details.push(`${POS_LABEL[key]}支${branch}藏${stem}(印 ${w})`);
195
- }
196
- });
197
- }
198
- for (const key of ['year', 'month', 'hour']) {
199
- const stem = pillars[key].stem;
200
- const el = STEM_ELEMENT[stem];
201
- if (el === me || GENERATES[el] === me) out.helpers.push(`${POS_LABEL[key]}干${stem}(${el === me ? '比劫' : '印'})`);
202
- }
203
- out.bi = Math.round(out.bi * 10) / 10;
204
- out.yin = Math.round(out.yin * 10) / 10;
205
- out.total = Math.round((out.bi + out.yin) * 10) / 10;
206
- return out;
207
- }
208
-
209
- function specialPatterns(pillars, dayStem, monthBranch, strength, wuxing, gods) {
210
- const out = [];
211
- const roles = elementRoles(dayStem);
212
- const me = roles.me;
213
- const mine = wuxing[me] ?? 0;
214
- const yin = wuxing[roles.inMe] ?? 0;
215
- const cai = wuxing[roles.cai] ?? 0;
216
- const guan = wuxing[roles.guan] ?? 0;
217
- const shi = wuxing[roles.shi] ?? 0;
218
-
219
- // 化气格:日干与年/月/时干五合,且月令助化神
220
- for (const key of ['year', 'month', 'hour']) {
221
- const other = pillars[key].stem;
222
- const target = STEM_HE[dayStem + other] || STEM_HE[other + dayStem];
223
- if (!target) continue;
224
- const monthEl = BRANCH_ELEMENT[monthBranch];
225
- const supports = monthEl === target || GENERATES[monthEl] === target;
226
- if (!supports) continue;
227
- out.push({
228
- name: `${target === '土' ? '化土格' : `化${target}格`}`,
229
- kind: '化气格',
230
- evidence: `日干${dayStem}与${['年', '月', '时'][['year', 'month', 'hour'].indexOf(key)]}干${other}合化${target},月令${monthBranch}${monthEl}${monthEl === target ? '同化神' : '生助化神'};化气格须全局无争合妒合才算真,此处仅列条件` ,
231
- });
232
- break;
233
- }
234
-
235
- // 专旺:日主之气独旺,官杀、财星皆弱。
236
- // 成格=旺神得月令(同类或印)且不见破;待定=气势已成但月令不助;破格=官杀逆势或财星耗旺。
237
- if ((strength.level === '身强' || strength.level === '偏强') && mine + yin >= 5) {
238
- const breaks = [];
239
- if (guan > 1.2) breaks.push(`官杀 ${guan} 分,逆旺势`);
240
- if (cai > 1.8) breaks.push(`财 ${cai} 分,耗旺气`);
241
- const monthEl = BRANCH_ELEMENT[monthBranch];
242
- const monthSupport = monthEl === me || monthEl === roles.inMe;
243
- const group = SANHE_GROUPS.find((g) => g.element === me);
244
- const branches = ['year', 'month', 'day', 'hour'].map((k) => pillars[k].branch);
245
- const ju = group && group.branches.every((b) => branches.includes(b)) ? group.branches.join('') : null;
246
- const status = breaks.length ? '破格' : monthSupport ? '成格' : '待定';
247
- out.push({
248
- name: ZHUANWANG[me],
249
- kind: '专旺格',
250
- status,
251
- breaks,
252
- ju,
253
- evidence: `日主${me}得${mine + yin}分(同类+印),官杀${guan}、财${cai}${breaks.length ? '' : '皆弱'};`
254
- + `${monthSupport ? `月令${monthBranch}${monthEl}助旺` : `月令${monthBranch}${monthEl}不助旺`}`
255
- + `${ju ? `,地支${ju}成局` : ''},${status === '成格' ? '成格' : status === '破格' ? '见破格条件' : '成势而条件未全'}`,
256
- breakRule: breaks.length ? '专旺以官杀逆旺势、财星耗旺气为破;见其一即不纯' : undefined,
257
- });
258
- }
259
-
260
- // 从格:日主无根而被他神主导。真从=完全无根、无比劫印绶透出;
261
- // 假从=有微根或虚透,气势仍从,但行运助身则不再从。
262
- const root = rootProfile(pillars, dayStem);
263
- if (root.total < 1 && (strength.level === '身弱' || strength.level === '偏弱')) {
264
- const main = Object.entries({ 财: cai, 官杀: guan, 食伤: shi, 印: yin })
265
- .sort((a, b) => b[1] - a[1])[0];
266
- if (main[1] >= 2.5) {
267
- const fake = root.total > 0 || root.helpers.length > 0;
268
- const reasons = [...root.details, ...root.helpers.map((h) => `天干${h}虚透`)];
269
- out.push({
270
- name: CONG[main[0]],
271
- kind: '从格',
272
- status: fake ? '假从' : '真从',
273
- breaks: fake ? reasons : [],
274
- rootWeight: root.total,
275
- evidence: fake
276
- ? `日主有微根或虚透(${reasons.join(';')}),${main[0]}占 ${main[1]} 分最旺,假从${main[0]};行运助身则不从`
277
- : `日主无根(四支藏干无同类与印、天干无比劫印绶),${main[0]}占 ${main[1]} 分最旺,真从${main[0]}`,
278
- breakRule: fake ? '从格以日主有根、比劫印绶透出为破;有根而从者称假从,运助身则不从' : undefined,
279
- });
280
- }
281
- }
282
-
283
- // 常见组合(只报事实并列,不作吉凶断语)
284
- const byGod = gods.byGod || {};
285
- const g = gods.groups || gods;
286
- const hasGod = (name) => (byGod[name] ?? 0) >= 0.5;
287
- const yinGods = (byGod.正印 ?? 0) + (byGod.偏印 ?? 0);
288
- const combos = [
289
- ['官印相生', hasGod('正官') && yinGods >= 0.5, `正官与印并见,官生印而护身`],
290
- ['杀印相生', hasGod('七杀') && yinGods >= 0.5 && (byGod.七杀 ?? 0) > (byGod.正官 ?? 0), `七杀与印并见,杀印相生`],
291
- ['食伤生财', (g.食伤 ?? 0) >= 0.6 && (g.财 ?? 0) >= 0.6, `食伤${g.食伤}+财${g.财},食伤生财`],
292
- ['财官相生', (g.财 ?? 0) >= 0.6 && (g.官杀 ?? 0) >= 0.6, `财${g.财}+官杀${g.官杀},财生官`],
293
- ['伤官配印', hasGod('伤官') && yinGods >= 0.5 && (g.财 ?? 0) <= 1, `伤官与印并见,伤官配印`],
294
- ['伤官见官', hasGod('伤官') && hasGod('正官'), `伤官与正官并见(伤官见官,为祸百端之说)`],
295
- ['羊刃驾杀', (g.官杀 ?? 0) >= 1 && YANGREN_BRANCH[dayStem] === monthBranch, `月令为刃,又有七杀${g.官杀},刃杀相制`],
296
- ];
297
- for (const [name, cond, evidence] of combos) if (cond) out.push({ name, kind: '组合', evidence });
298
- return out;
299
- }
300
-
301
- // ── 通关 ────────────────────────────────────────────────────
302
- // 滴天髓「通关」只给取象(织女、牛郎),这里把结构说清:
303
- // 用神与忌神两行在局中都真有分量、且相克时,五行里恰有一行能接上——克方所生、且能生被克方。
304
- // 只报这一行在局中的分量,不判「关通了就吉」。
305
- const TONGGUAN_MIN = 1.5;
306
- /** 忌神在全局不足这个分量时,展示层降为「中性偏忌」(仍留在 avoid 里参与行运评分) */
307
- const NEUTRAL_MAX = 0.5;
308
- export function tongguanOf(favor = [], avoid = [], wuxing = {}) {
309
- const w = (el) => Math.round((Number(wuxing[el]) || 0) * 10) / 10;
310
- for (const use of favor) {
311
- if (w(use) < TONGGUAN_MIN) continue;
312
- for (const foe of avoid) {
313
- if (OVERCOMES[foe] !== use || w(foe) < TONGGUAN_MIN) continue;
314
- const bridge = GENERATES[foe];
315
- if (!bridge || GENERATES[bridge] !== use) continue;
316
- const weight = w(bridge);
317
- const bridgeIsAvoid = avoid.includes(bridge) && !favor.includes(bridge);
318
- const level = bridgeIsAvoid ? '为忌' : weight >= TONGGUAN_MIN ? '可通' : weight > 0 ? '关弱' : '未通';
319
- return {
320
- use, foe, bridge,
321
- weight,
322
- present: weight > 0,
323
- bridgeIsAvoid,
324
- level,
325
- usable: level === '可通',
326
- note: '相战=用神与忌神两行各在局中 ≥1.5 分且相克;通关一行=克方所生、又能生用神的那一行(五行里唯一),只报局中分量,不据此断吉凶',
327
- };
328
- }
329
- }
330
- return null;
331
- }
332
-
333
- // ── 用神 / 喜忌 ──────────────────────────────────────────────
334
-
335
-
336
- function yongshenOf(dayStem, monthBranch, strength, wuxing, branches = []) {
337
- const roles = elementRoles(dayStem);
338
- const me = roles.me;
339
- const weak = strength.level === '身弱' || strength.level === '偏弱';
340
- const strong = strength.level === '身强' || strength.level === '偏强';
341
- const roleName = (el) => (el === me ? '比劫' : el === roles.inMe ? '印'
342
- : el === roles.shi ? '食伤' : el === roles.cai ? '财' : '官杀');
343
- const ROLE_VERB = { 比劫: '帮身', 印: '生身', 食伤: '泄秀', 财: '耗', 官杀: '制' };
344
-
345
- const baseFavor = weak ? [roles.inMe, me] : strong ? [roles.shi, roles.cai, roles.guan] : [roles.inMe, me, roles.shi];
346
- const baseAvoid = weak ? [roles.shi, roles.cai, roles.guan] : strong ? [me, roles.inMe] : [roles.guan, roles.cai];
347
- let favor = baseFavor.slice();
348
- let avoid = baseAvoid.slice();
349
- // 调候所忌若正好是扶抑所喜(弱土遇燥的火、寒湿盘的强土遇水),两边不能同时列。
350
- // 本表保留在喜用、不列入忌,并单列「有争议」;否则摘要会出现同一行既喜又忌。
351
- const disputed = [];
352
-
353
- // 先判气候,再决定要不要让调候压过扶抑
354
- const climate = judgeClimate({ monthBranch, dayStem, wuxing, branches });
355
- // 《穷通宝鉴》120 格:同一「日主 + 月令」的取用顺序。只作展示与加权,不覆盖气候调候
356
- const qt = qtOf(dayStem, monthBranch);
357
- const arb = arbitrate(climate, baseFavor, baseAvoid);
358
- if (climate.need && climate.priority === '调候优先') {
359
- const needEl = climate.need.element;
360
- if (!favor.includes(needEl)) favor.unshift(needEl);
361
- avoid = avoid.filter((e) => e !== needEl);
362
- const badEl = climate.avoidElement;
363
- if (badEl && favor.includes(badEl)) {
364
- disputed.push({
365
- element: badEl,
366
- favorRole: roleName(badEl),
367
- favorVerb: ROLE_VERB[roleName(badEl)],
368
- climate: climate.label,
369
- note: `调候忌${badEl}(${climate.label}),但它在扶抑里为${roleName(badEl)};本表保留在喜用,不列入忌。`,
370
- });
371
- } else if (badEl && !avoid.includes(badEl)) {
372
- avoid.unshift(badEl);
373
- }
374
- }
375
-
376
- const expand = {
377
- 比劫: ['比肩', '劫财'], 印: ['正印', '偏印'], 食伤: ['食神', '伤官'],
378
- 财: ['正财', '偏财'], 官杀: ['正官', '七杀'],
379
- };
380
- const favorTenGods = Array.from(new Set(favor.flatMap((el) => expand[roleName(el)] || [])));
381
- const avoidTenGods = Array.from(new Set(avoid.flatMap((el) => expand[roleName(el)] || [])));
382
- const disputedTenGods = Array.from(new Set(disputed.flatMap((d) => expand[roleName(d.element)] || [])));
383
- // 中性偏忌:忌神里全局分量极轻(不足 NEUTRAL_MAX 分)的那些。
384
- // 不从 avoid 里删——行运评分要让「这一行走运到位」仍按忌计分:局中没有,不等于运里不来。
385
- // 只在展示层把它们从「实忌」降为「中性偏忌」,避免把一张几乎无该行的盘说成三行皆忌。
386
- const weightOf = (el) => Math.round((Number(wuxing[el]) || 0) * 10) / 10;
387
- const neutral = avoid
388
- .filter((el) => weightOf(el) < NEUTRAL_MAX)
389
- .map((el) => ({ element: el, role: roleName(el), verb: ROLE_VERB[roleName(el)], weight: weightOf(el) }));
390
- const neutralTenGods = Array.from(new Set(neutral.flatMap((n) => expand[n.role] || [])));
391
- const tongguan = tongguanOf(favor, avoid, wuxing);
392
-
393
- const describe = (el) => `${el}(${roleName(el)}·${ROLE_VERB[roleName(el)]})`;
394
- const disputedText = disputed
395
- .map((d) => `${d.element}(扶抑为${d.favorRole}·${d.favorVerb};${d.climate}盘调候忌${d.element},本表保留在喜用)`)
396
- .join('、');
397
- const detail = [
398
- `日主${dayStem}${me},${strength.level}(${strength.score} 分):`,
399
- `喜${favor.map(describe).join('、') || '—'};忌${avoid.map(describe).join('、') || '—'}。`,
400
- neutral.length
401
- ? `中性偏忌:${neutral.map((n) => `${n.element}(局中 ${n.weight} 分)`).join('、')}——分量近零,不作实忌论,但不从此盘喜忌中删除(行运遇之仍按忌计)。`
402
- : '',
403
- disputed.length ? `有争议:${disputedText}。` : '',
404
- !weak && !strong ? '身中和,先取调候与通关,喜忌随大运流年动态变化。' : '',
405
- `调候:${climate.label}(${(climate.evidence || []).join('、')})${climate.need ? `,取${climate.need.stem}${climate.need.element},忌${climate.avoidElement}${disputed.some((d) => d.element === climate.avoidElement) ? '(有争议)' : ''}` : ',气候平和'}。`,
406
- qt ? `穷通宝鉴:${qt.month}${dayStem}${me},用神${qt.yong.join('、')}(首取${qt.primary})。` : '',
407
- tongguan
408
- ? `通关:忌${tongguan.foe}克用${tongguan.use},五行以${tongguan.bridge}为关;局中${tongguan.level === '可通'
409
- ? `${tongguan.bridge} ${tongguan.weight} 分,关可通`
410
- : tongguan.level === '关弱'
411
- ? `仅 ${tongguan.bridge} ${tongguan.weight} 分,关弱`
412
- : tongguan.level === '为忌'
413
- ? `${tongguan.bridge} ${tongguan.weight} 分但此行为忌,通关力打折`
414
- : `未见${tongguan.bridge},关未通`}(只作结构提示,不据此断吉凶)。`
415
- : '',
416
- arb.text,
417
- arb.clash ? '取舍:本盘按「调候优先、扶抑次之」取用;若你更认同扶抑派,可把调候当环境偏好,强弱判断不受影响。' : '',
418
- ].filter(Boolean).join('');
419
-
420
- return {
421
- method: climate.priority === '调候优先' ? '调候优先 + 扶抑校验' : '扶抑为主 + 调候校验',
422
- level: strength.level,
423
- favor,
424
- avoid,
425
- disputed,
426
- neutral,
427
- favorRoles: favor.map(roleName),
428
- avoidRoles: avoid.map(roleName),
429
- neutralRoles: neutral.map((n) => n.role),
430
- favorTenGods,
431
- avoidTenGods,
432
- disputedTenGods,
433
- neutralTenGods,
434
- tiaohou: { ...climate, ...arb, element: climate.need ? climate.need.element : null },
435
- climate,
436
- qt,
437
- tongguan,
438
- detail,
439
- };
440
- }
441
-
442
- // ── 行运评分 ────────────────────────────────────────────────
443
-
444
- const TIERS = [
445
- [1.3, '大吉'],
446
- [0.6, '吉'],
447
- [-0.6, '平'],
448
- [-1.3, '忌'],
449
- [-Infinity, '大忌'],
450
- ];
451
-
452
- export function scoreLuck(ganzhi, frame) {
453
- const ys = frame.yongshen;
454
- const stem = ganzhi[0];
455
- const branch = ganzhi[1];
456
- const stemEl = STEM_ELEMENT[stem];
457
- const branchEl = BRANCH_ELEMENT[branch];
458
- const th = ys.tiaohou || ys.climate || null;
459
- const scoreEl = (el) => {
460
- let s = 0;
461
- if (ys.favor.includes(el)) s += 1;
462
- if (ys.avoid.includes(el)) s -= 1;
463
- return s;
464
- };
465
- let score = scoreEl(stemEl) * 1.0 + scoreEl(branchEl) * 0.8;
466
- for (const h of (HIDDEN[branch] || []).slice(1)) {
467
- score += scoreEl(STEM_ELEMENT[h]) * 0.2;
468
- }
469
-
470
- const reasons = [];
471
- if (ys.favor.includes(stemEl)) reasons.push(`天干${stem}${stemEl}为用神`);
472
- else if (ys.avoid.includes(stemEl)) reasons.push(`天干${stem}${stemEl}为忌神`);
473
- else reasons.push(`天干${stem}${stemEl}不喜不忌`);
474
- if (ys.favor.includes(branchEl)) reasons.push(`地支${branch}${branchEl}助用`);
475
- else if (ys.avoid.includes(branchEl)) reasons.push(`地支${branch}${branchEl}助忌`);
476
-
477
- // 调候修正:不是"有没有这个五行",而是"这一年是给命局添了一把火,还是泼了一盆水"
478
- if (th && th.need && th.priority === '调候优先') {
479
- const needEl = th.need.element;
480
- const oppEl = th.avoidElement;
481
- const cold = String(th.kind).includes('寒');
482
- if (stemEl === needEl) {
483
- score += 1.1;
484
- reasons.push(`${th.label}之局得${stem}${stemEl},${cold ? '解冻' : '得润'}`);
485
- } else if (stemEl === oppEl) {
486
- score -= 0.9;
487
- reasons.push(`${th.label}之局再逢${stem}${stemEl},${cold ? '冻木难生' : '燥上加燥'}`);
488
- }
489
- if (branchEl === needEl) {
490
- score += 0.9;
491
- reasons.push(`地支${branch}${branchEl}合调候`);
492
- } else if (branchEl === oppEl) {
493
- score -= 1.0;
494
- reasons.push(`地支${branch}${branchEl}加重${th.label}`);
495
- }
496
- if (cold && (stemEl === '水' || branchEl === '水')) score -= 0.3;
497
- } else if (th && th.need && (stemEl === th.need.element || branchEl === th.need.element)) {
498
- reasons.push(`合调候(${th.need.element})`);
499
- }
500
-
501
- const godStem = tenGod(frame.dayStem, stem);
502
- const bonus = ys.favorTenGods.includes(godStem) ? 0.3 : 0;
503
- // 穷通宝鉴:天干正好是这一格的首取/次取时,再给一点「按书取用」的分
504
- // 只在该干不属于忌神时加分——书里的取用与气候调候不同向时,以调候为先
505
- const qt = ys.qt || null;
506
- if (qt && qt.primary && !ys.avoid.includes(STEM_ELEMENT[qt.primary])) {
507
- if (stem === qt.primary) {
508
- score += 0.3;
509
- reasons.push(`天干${stem}正是${qt.branch}月${qt.stem}日的首取(穷通宝鉴)`);
510
- } else if (qt.yong.includes(stem) && !ys.avoid.includes(STEM_ELEMENT[stem])) {
511
- score += 0.15;
512
- reasons.push(`天干${stem}为穷通宝鉴次取`);
513
- }
514
- }
515
- let total = Math.round((score + bonus) * 10) / 10;
516
- total = Math.max(-4, Math.min(4, total));
517
-
518
- const tier = TIERS.find(([min]) => total >= min)[1];
519
- return { ganzhi, score: total, tier, stemTenGod: godStem, reasons, note: `${godStem}运,${tier}(${total})` };
520
- }
521
-
522
- // ── 对外入口 ────────────────────────────────────────────────
523
-
524
- /**
525
- * @param {object} p
526
- * @param {object} p.pillars 四柱
527
- * @param {string} p.dayStem 日干
528
- * @param {string} p.monthBranch 月支
529
- * @param {object} p.strength bazi.js 的旺衰结果
530
- * @param {object} p.wuxing 五行分值
531
- */
532
- export function analyzeBaziFrame({ pillars, dayStem, monthBranch, strength, wuxing }) {
533
- const gods = godStats(pillars, dayStem);
534
- const month = monthPattern(pillars, dayStem, monthBranch);
535
- const special = specialPatterns(pillars, dayStem, monthBranch, strength, wuxing, gods);
536
- const geju = [month, ...special];
537
- const yongshen = yongshenOf(dayStem, monthBranch, strength, wuxing, Object.values(pillars).map((p) => p.branch));
538
- return {
539
- geju,
540
- primary: month,
541
- primaryName: month.name,
542
- godStats: gods,
543
- yongshen,
544
- dayStem,
545
- monthBranch,
546
- };
547
- }
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 { SANHE_GROUPS, 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
+ // 时辰未知时 pillars.hour 为 null:遍历跳过,不能对 null 取 .stem
73
+ for (const key of ['year', 'month', 'day', 'hour'].filter((k) => pillars[k])) {
74
+ add(pillars[key].stem, key === 'day' ? 0.6 : 1);
75
+ (HIDDEN[pillars[key].branch] || []).forEach((h, i) => add(h, HIDDEN_W[i] ?? 0.1));
76
+ }
77
+ for (const k of Object.keys(total)) total[k] = Math.round(total[k] * 10) / 10;
78
+ for (const k of Object.keys(byGod)) byGod[k] = Math.round(byGod[k] * 10) / 10;
79
+ return { groups: total, byGod };
80
+ }
81
+
82
+ /** 用神所属五行:生我 / 同我 / 我生 / 我克 / 克我 */
83
+ export function elementRoles(dayStem) {
84
+ const me = STEM_ELEMENT[dayStem];
85
+ const inMe = ELEMENTS.find((e) => GENERATES[e] === me);
86
+ const guan = ELEMENTS.find((e) => OVERCOMES[e] === me);
87
+ return { me, inMe, shi: GENERATES[me], cai: OVERCOMES[me], guan };
88
+ }
89
+
90
+ // ── 月令格 ───────────────────────────────────────────────────
91
+
92
+ // 四墓(杂气)月:辰戌丑未所藏多、用神不一,取用先看透干,再看会支。
93
+ // 会支只认三合局——子平真诠十六「逢申与子会局」的例子即三合,三会方与兼透兼用仍不纳入。
94
+ const ZA_MONTHS = ['辰', '未', '戌', '丑'];
95
+
96
+ function huiCandidate(pillars, monthBranch, cands) {
97
+ if (!ZA_MONTHS.includes(monthBranch)) return null;
98
+ const group = sanheGroupOf(monthBranch);
99
+ if (!group) return null;
100
+ const others = ['year', 'day', 'hour'];
101
+ const need = group.branches.filter((b) => b !== monthBranch);
102
+ if (!need.every((b) => others.some((k) => pillars[k].branch === b))) return null;
103
+ const cand = cands.find((c) => STEM_ELEMENT[c.stem] === group.element);
104
+ if (!cand) return null;
105
+ const where = others
106
+ .filter((k) => need.includes(pillars[k].branch))
107
+ .map((k) => `${({ year: '年', day: '日', hour: '时' })[k]}支${pillars[k].branch}`);
108
+ return { ...cand, group, where };
109
+ }
110
+
111
+ function monthPattern(pillars, dayStem, monthBranch) {
112
+ const hidden = HIDDEN[monthBranch] || [];
113
+ const cands = hidden.map((stem, i) => ({
114
+ stem,
115
+ god: tenGod(dayStem, stem),
116
+ pos: ['本气', '中气', '余气'][i],
117
+ w: HIDDEN_W[i] ?? 0.1,
118
+ }));
119
+ const others = ['year', 'month', 'hour'];
120
+ const exposedKey = others.find((k) => cands.some((c) => c.stem === pillars[k].stem));
121
+ const base = cands[0];
122
+ const hui = exposedKey ? null : huiCandidate(pillars, monthBranch, cands);
123
+ const use = exposedKey ? cands.find((c) => c.stem === pillars[exposedKey].stem) : (hui || base);
124
+ const usedBy = exposedKey ? '透干' : (hui ? '会支' : '本气');
125
+ const exposedPos = exposedKey ? { year: '年', month: '月', hour: '时' }[exposedKey] : null;
126
+
127
+ // 建禄只认日干的禄位;戊土辰月、己土未月这类「本气比肩但不在禄位」的杂气月不再冒称建禄
128
+ if (base.god === '比肩' && LUSHEN_BRANCH[dayStem] === monthBranch) {
129
+ return {
130
+ name: '建禄格',
131
+ kind: '月令格',
132
+ evidence: `月令${monthBranch}为日干${dayStem}之禄(本气${base.stem}比肩),身坐禄地`,
133
+ };
134
+ }
135
+ if (base.god === '劫财') {
136
+ if (YANGREN_BRANCH[dayStem] === monthBranch) {
137
+ return {
138
+ name: isYangStem(dayStem) ? '阳刃格' : '月刃格',
139
+ kind: '月令格',
140
+ evidence: `月令${monthBranch}为日干${dayStem}之刃(本气${base.stem}劫财)${isYangStem(dayStem) ? '' : ',阴干统称月刃'}`,
141
+ };
142
+ }
143
+ return { name: '月劫格', kind: '月令格', evidence: `月令${monthBranch}本气${base.stem}为劫财` };
144
+ }
145
+ if (usedBy === '本气' && base.god === '比肩') {
146
+ return {
147
+ name: '比肩格',
148
+ kind: '月令格',
149
+ evidence: `月令${monthBranch}藏${hidden.join('')},本气${base.stem}为比肩,但${dayStem}禄在${LUSHEN_BRANCH[dayStem]}、不在${monthBranch},属杂气月比肩当权`,
150
+ };
151
+ }
152
+ // 子平十六:「透乙则用月劫」——杂气月由透干或会支取到劫财时,仍作月劫论
153
+ if (usedBy !== '本气' && use.god === '劫财') {
154
+ const via = exposedKey ? `透于${exposedPos}干` : `${hui.where.join('、')}会成${hui.group.element}局`;
155
+ return {
156
+ name: '月劫格',
157
+ kind: '月令格',
158
+ evidence: `月令${monthBranch}藏${hidden.join('')},${use.pos}${use.stem}(劫财)${via},劫财为用,作月劫论`,
159
+ };
160
+ }
161
+ const name = `${use.god}格`;
162
+ let ex;
163
+ if (exposedKey) {
164
+ ex = `月令${monthBranch}藏${hidden.join('')},${use.pos}${use.stem}(${use.god})透于${exposedPos}干`;
165
+ } else if (hui) {
166
+ ex = `月令${monthBranch}藏${hidden.join('')},本气${base.stem}(${base.god})未透干,而${hui.where.join('、')}会成${hui.group.element}局,取${use.pos}${use.stem}(${use.god})为用`;
167
+ } else {
168
+ ex = `月令${monthBranch}藏${hidden.join('')},本气${base.stem}(${base.god})未透干,以月令本气取格`;
169
+ }
170
+ return { name, kind: '月令格', evidence: ex };
171
+ }
172
+
173
+ // ── 特殊格 / 组合 ────────────────────────────────────────────
174
+
175
+ const ZHUANWANG = { 木: '曲直格', 火: '炎上格', 土: '稼穑格', 金: '从革格', 水: '润下格' };
176
+ const CONG = { 财: '从财格', 官杀: '从杀格', 食伤: '从儿格', 印: '从强格', 比劫: '从强格' };
177
+ const POS_LABEL = { year: '年', month: '月', day: '日', hour: '时' };
178
+
179
+ // 日主根气:地支藏干里的同类(比劫)与生我(印),以及天干(日干除外)透出的比劫/印。
180
+ // 只用于真从/假从:无根才是真从,有中气/余气微根或虚透只能算假从;
181
+ // 见一个本气根(≥1 分)就不作从格——日主已能站住,再称从会误判。
182
+ function rootProfile(pillars, dayStem) {
183
+ const me = STEM_ELEMENT[dayStem];
184
+ const out = { bi: 0, yin: 0, total: 0, details: [], helpers: [] };
185
+ for (const key of ['year', 'month', 'day', 'hour'].filter((k) => pillars[k])) {
186
+ const branch = pillars[key].branch;
187
+ (HIDDEN[branch] || []).forEach((stem, i) => {
188
+ const el = STEM_ELEMENT[stem];
189
+ const w = HIDDEN_W[i] ?? 0.1;
190
+ if (el === me) {
191
+ out.bi += w;
192
+ out.details.push(`${POS_LABEL[key]}支${branch}藏${stem}(比劫 ${w})`);
193
+ } else if (GENERATES[el] === me) {
194
+ out.yin += w;
195
+ out.details.push(`${POS_LABEL[key]}支${branch}藏${stem}(印 ${w})`);
196
+ }
197
+ });
198
+ }
199
+ for (const key of ['year', 'month', 'hour']) {
200
+ const stem = pillars[key].stem;
201
+ const el = STEM_ELEMENT[stem];
202
+ if (el === me || GENERATES[el] === me) out.helpers.push(`${POS_LABEL[key]}干${stem}(${el === me ? '比劫' : '印'})`);
203
+ }
204
+ out.bi = Math.round(out.bi * 10) / 10;
205
+ out.yin = Math.round(out.yin * 10) / 10;
206
+ out.total = Math.round((out.bi + out.yin) * 10) / 10;
207
+ return out;
208
+ }
209
+
210
+ function specialPatterns(pillars, dayStem, monthBranch, strength, wuxing, gods) {
211
+ const out = [];
212
+ const roles = elementRoles(dayStem);
213
+ const me = roles.me;
214
+ const mine = wuxing[me] ?? 0;
215
+ const yin = wuxing[roles.inMe] ?? 0;
216
+ const cai = wuxing[roles.cai] ?? 0;
217
+ const guan = wuxing[roles.guan] ?? 0;
218
+ const shi = wuxing[roles.shi] ?? 0;
219
+
220
+ // 化气格:日干与年/月/时干五合,且月令助化神
221
+ for (const key of ['year', 'month', 'hour']) {
222
+ const other = pillars[key].stem;
223
+ const target = STEM_HE[dayStem + other] || STEM_HE[other + dayStem];
224
+ if (!target) continue;
225
+ const monthEl = BRANCH_ELEMENT[monthBranch];
226
+ const supports = monthEl === target || GENERATES[monthEl] === target;
227
+ if (!supports) continue;
228
+ out.push({
229
+ name: `${target === '土' ? '化土格' : `化${target}格`}`,
230
+ kind: '化气格',
231
+ evidence: `日干${dayStem}与${['年', '月', '时'][['year', 'month', 'hour'].indexOf(key)]}干${other}合化${target},月令${monthBranch}${monthEl}${monthEl === target ? '同化神' : '生助化神'};化气格须全局无争合妒合才算真,此处仅列条件` ,
232
+ });
233
+ break;
234
+ }
235
+
236
+ // 专旺:日主之气独旺,官杀、财星皆弱。
237
+ // 成格=旺神得月令(同类或印)且不见破;待定=气势已成但月令不助;破格=官杀逆势或财星耗旺。
238
+ if ((strength.level === '身强' || strength.level === '偏强') && mine + yin >= 5) {
239
+ const breaks = [];
240
+ if (guan > 1.2) breaks.push(`官杀 ${guan} 分,逆旺势`);
241
+ if (cai > 1.8) breaks.push(`财 ${cai} 分,耗旺气`);
242
+ const monthEl = BRANCH_ELEMENT[monthBranch];
243
+ const monthSupport = monthEl === me || monthEl === roles.inMe;
244
+ const group = SANHE_GROUPS.find((g) => g.element === me);
245
+ const branches = ['year', 'month', 'day', 'hour'].filter((k) => pillars[k]).map((k) => pillars[k].branch);
246
+ const ju = group && group.branches.every((b) => branches.includes(b)) ? group.branches.join('') : null;
247
+ const status = breaks.length ? '破格' : monthSupport ? '成格' : '待定';
248
+ out.push({
249
+ name: ZHUANWANG[me],
250
+ kind: '专旺格',
251
+ status,
252
+ breaks,
253
+ ju,
254
+ evidence: `日主${me}得${mine + yin}分(同类+印),官杀${guan}、财${cai}${breaks.length ? '' : '皆弱'};`
255
+ + `${monthSupport ? `月令${monthBranch}${monthEl}助旺` : `月令${monthBranch}${monthEl}不助旺`}`
256
+ + `${ju ? `,地支${ju}成局` : ''},${status === '成格' ? '成格' : status === '破格' ? '见破格条件' : '成势而条件未全'}`,
257
+ breakRule: breaks.length ? '专旺以官杀逆旺势、财星耗旺气为破;见其一即不纯' : undefined,
258
+ });
259
+ }
260
+
261
+ // 从格:日主无根而被他神主导。真从=完全无根、无比劫印绶透出;
262
+ // 假从=有微根或虚透,气势仍从,但行运助身则不再从。
263
+ const root = rootProfile(pillars, dayStem);
264
+ if (root.total < 1 && (strength.level === '身弱' || strength.level === '偏弱')) {
265
+ const main = Object.entries({ 财: cai, 官杀: guan, 食伤: shi, 印: yin })
266
+ .sort((a, b) => b[1] - a[1])[0];
267
+ if (main[1] >= 2.5) {
268
+ const fake = root.total > 0 || root.helpers.length > 0;
269
+ const reasons = [...root.details, ...root.helpers.map((h) => `天干${h}虚透`)];
270
+ out.push({
271
+ name: CONG[main[0]],
272
+ kind: '从格',
273
+ status: fake ? '假从' : '真从',
274
+ breaks: fake ? reasons : [],
275
+ rootWeight: root.total,
276
+ evidence: fake
277
+ ? `日主有微根或虚透(${reasons.join(';')}),${main[0]}占 ${main[1]} 分最旺,假从${main[0]};行运助身则不从`
278
+ : `日主无根(四支藏干无同类与印、天干无比劫印绶),${main[0]}占 ${main[1]} 分最旺,真从${main[0]}`,
279
+ breakRule: fake ? '从格以日主有根、比劫印绶透出为破;有根而从者称假从,运助身则不从' : undefined,
280
+ });
281
+ }
282
+ }
283
+
284
+ // 常见组合(只报事实并列,不作吉凶断语)
285
+ const byGod = gods.byGod || {};
286
+ const g = gods.groups || gods;
287
+ const hasGod = (name) => (byGod[name] ?? 0) >= 0.5;
288
+ const yinGods = (byGod.正印 ?? 0) + (byGod.偏印 ?? 0);
289
+ const combos = [
290
+ ['官印相生', hasGod('正官') && yinGods >= 0.5, `正官与印并见,官生印而护身`],
291
+ ['杀印相生', hasGod('七杀') && yinGods >= 0.5 && (byGod.七杀 ?? 0) > (byGod.正官 ?? 0), `七杀与印并见,杀印相生`],
292
+ ['食伤生财', (g.食伤 ?? 0) >= 0.6 && (g.财 ?? 0) >= 0.6, `食伤${g.食伤}+财${g.财},食伤生财`],
293
+ ['财官相生', (g.财 ?? 0) >= 0.6 && (g.官杀 ?? 0) >= 0.6, `财${g.财}+官杀${g.官杀},财生官`],
294
+ ['伤官配印', hasGod('伤官') && yinGods >= 0.5 && (g.财 ?? 0) <= 1, `伤官与印并见,伤官配印`],
295
+ ['伤官见官', hasGod('伤官') && hasGod('正官'), `伤官与正官并见(伤官见官,为祸百端之说)`],
296
+ ['羊刃驾杀', (g.官杀 ?? 0) >= 1 && YANGREN_BRANCH[dayStem] === monthBranch, `月令为刃,又有七杀${g.官杀},刃杀相制`],
297
+ ];
298
+ for (const [name, cond, evidence] of combos) if (cond) out.push({ name, kind: '组合', evidence });
299
+ return out;
300
+ }
301
+
302
+ // ── 通关 ────────────────────────────────────────────────────
303
+ // 滴天髓「通关」只给取象(织女、牛郎),这里把结构说清:
304
+ // 用神与忌神两行在局中都真有分量、且相克时,五行里恰有一行能接上——克方所生、且能生被克方。
305
+ // 只报这一行在局中的分量,不判「关通了就吉」。
306
+ const TONGGUAN_MIN = 1.5;
307
+ /** 忌神在全局不足这个分量时,展示层降为「中性偏忌」(仍留在 avoid 里参与行运评分) */
308
+ const NEUTRAL_MAX = 0.5;
309
+ export function tongguanOf(favor = [], avoid = [], wuxing = {}) {
310
+ const w = (el) => Math.round((Number(wuxing[el]) || 0) * 10) / 10;
311
+ for (const use of favor) {
312
+ if (w(use) < TONGGUAN_MIN) continue;
313
+ for (const foe of avoid) {
314
+ if (OVERCOMES[foe] !== use || w(foe) < TONGGUAN_MIN) continue;
315
+ const bridge = GENERATES[foe];
316
+ if (!bridge || GENERATES[bridge] !== use) continue;
317
+ const weight = w(bridge);
318
+ const bridgeIsAvoid = avoid.includes(bridge) && !favor.includes(bridge);
319
+ const level = bridgeIsAvoid ? '为忌' : weight >= TONGGUAN_MIN ? '可通' : weight > 0 ? '关弱' : '未通';
320
+ return {
321
+ use, foe, bridge,
322
+ weight,
323
+ present: weight > 0,
324
+ bridgeIsAvoid,
325
+ level,
326
+ usable: level === '可通',
327
+ note: '相战=用神与忌神两行各在局中 ≥1.5 分且相克;通关一行=克方所生、又能生用神的那一行(五行里唯一),只报局中分量,不据此断吉凶',
328
+ };
329
+ }
330
+ }
331
+ return null;
332
+ }
333
+
334
+ // ── 用神 / 喜忌 ──────────────────────────────────────────────
335
+
336
+
337
+ function yongshenOf(dayStem, monthBranch, strength, wuxing, branches = []) {
338
+ const roles = elementRoles(dayStem);
339
+ const me = roles.me;
340
+ const weak = strength.level === '身弱' || strength.level === '偏弱';
341
+ const strong = strength.level === '身强' || strength.level === '偏强';
342
+ const roleName = (el) => (el === me ? '比劫' : el === roles.inMe ? '印'
343
+ : el === roles.shi ? '食伤' : el === roles.cai ? '财' : '官杀');
344
+ const ROLE_VERB = { 比劫: '帮身', 印: '生身', 食伤: '泄秀', 财: '耗', 官杀: '制' };
345
+
346
+ const baseFavor = weak ? [roles.inMe, me] : strong ? [roles.shi, roles.cai, roles.guan] : [roles.inMe, me, roles.shi];
347
+ const baseAvoid = weak ? [roles.shi, roles.cai, roles.guan] : strong ? [me, roles.inMe] : [roles.guan, roles.cai];
348
+ let favor = baseFavor.slice();
349
+ let avoid = baseAvoid.slice();
350
+ // 调候所忌若正好是扶抑所喜(弱土遇燥的火、寒湿盘的强土遇水),两边不能同时列。
351
+ // 本表保留在喜用、不列入忌,并单列「有争议」;否则摘要会出现同一行既喜又忌。
352
+ const disputed = [];
353
+
354
+ // 先判气候,再决定要不要让调候压过扶抑
355
+ const climate = judgeClimate({ monthBranch, dayStem, wuxing, branches });
356
+ // 《穷通宝鉴》120 格:同一「日主 + 月令」的取用顺序。只作展示与加权,不覆盖气候调候
357
+ const qt = qtOf(dayStem, monthBranch);
358
+ const arb = arbitrate(climate, baseFavor, baseAvoid);
359
+ if (climate.need && climate.priority === '调候优先') {
360
+ const needEl = climate.need.element;
361
+ if (!favor.includes(needEl)) favor.unshift(needEl);
362
+ avoid = avoid.filter((e) => e !== needEl);
363
+ const badEl = climate.avoidElement;
364
+ if (badEl && favor.includes(badEl)) {
365
+ disputed.push({
366
+ element: badEl,
367
+ favorRole: roleName(badEl),
368
+ favorVerb: ROLE_VERB[roleName(badEl)],
369
+ climate: climate.label,
370
+ note: `调候忌${badEl}(${climate.label}),但它在扶抑里为${roleName(badEl)};本表保留在喜用,不列入忌。`,
371
+ });
372
+ } else if (badEl && !avoid.includes(badEl)) {
373
+ avoid.unshift(badEl);
374
+ }
375
+ }
376
+
377
+ const expand = {
378
+ 比劫: ['比肩', '劫财'], 印: ['正印', '偏印'], 食伤: ['食神', '伤官'],
379
+ 财: ['正财', '偏财'], 官杀: ['正官', '七杀'],
380
+ };
381
+ const favorTenGods = Array.from(new Set(favor.flatMap((el) => expand[roleName(el)] || [])));
382
+ const avoidTenGods = Array.from(new Set(avoid.flatMap((el) => expand[roleName(el)] || [])));
383
+ const disputedTenGods = Array.from(new Set(disputed.flatMap((d) => expand[roleName(d.element)] || [])));
384
+ // 中性偏忌:忌神里全局分量极轻(不足 NEUTRAL_MAX 分)的那些。
385
+ // 不从 avoid 里删——行运评分要让「这一行走运到位」仍按忌计分:局中没有,不等于运里不来。
386
+ // 只在展示层把它们从「实忌」降为「中性偏忌」,避免把一张几乎无该行的盘说成三行皆忌。
387
+ const weightOf = (el) => Math.round((Number(wuxing[el]) || 0) * 10) / 10;
388
+ const neutral = avoid
389
+ .filter((el) => weightOf(el) < NEUTRAL_MAX)
390
+ .map((el) => ({ element: el, role: roleName(el), verb: ROLE_VERB[roleName(el)], weight: weightOf(el) }));
391
+ const neutralTenGods = Array.from(new Set(neutral.flatMap((n) => expand[n.role] || [])));
392
+ const tongguan = tongguanOf(favor, avoid, wuxing);
393
+
394
+ const describe = (el) => `${el}(${roleName(el)}·${ROLE_VERB[roleName(el)]})`;
395
+ const disputedText = disputed
396
+ .map((d) => `${d.element}(扶抑为${d.favorRole}·${d.favorVerb};${d.climate}盘调候忌${d.element},本表保留在喜用)`)
397
+ .join('、');
398
+ const detail = [
399
+ `日主${dayStem}${me},${strength.level}(${strength.score} 分):`,
400
+ `喜${favor.map(describe).join('、') || '—'};忌${avoid.map(describe).join('、') || '—'}。`,
401
+ neutral.length
402
+ ? `中性偏忌:${neutral.map((n) => `${n.element}(局中 ${n.weight} 分)`).join('、')}——分量近零,不作实忌论,但不从此盘喜忌中删除(行运遇之仍按忌计)。`
403
+ : '',
404
+ disputed.length ? `有争议:${disputedText}。` : '',
405
+ !weak && !strong ? '身中和,先取调候与通关,喜忌随大运流年动态变化。' : '',
406
+ `调候:${climate.label}(${(climate.evidence || []).join('、')})${climate.need ? `,取${climate.need.stem}${climate.need.element},忌${climate.avoidElement}${disputed.some((d) => d.element === climate.avoidElement) ? '(有争议)' : ''}` : ',气候平和'}。`,
407
+ qt ? `穷通宝鉴:${qt.month}${dayStem}${me},用神${qt.yong.join('、')}(首取${qt.primary})。` : '',
408
+ tongguan
409
+ ? `通关:忌${tongguan.foe}克用${tongguan.use},五行以${tongguan.bridge}为关;局中${tongguan.level === '可通'
410
+ ? `${tongguan.bridge} ${tongguan.weight} 分,关可通`
411
+ : tongguan.level === '关弱'
412
+ ? `仅 ${tongguan.bridge} ${tongguan.weight} 分,关弱`
413
+ : tongguan.level === '为忌'
414
+ ? `${tongguan.bridge} ${tongguan.weight} 分但此行为忌,通关力打折`
415
+ : `未见${tongguan.bridge},关未通`}(只作结构提示,不据此断吉凶)。`
416
+ : '',
417
+ arb.text,
418
+ arb.clash ? '取舍:本盘按「调候优先、扶抑次之」取用;若你更认同扶抑派,可把调候当环境偏好,强弱判断不受影响。' : '',
419
+ ].filter(Boolean).join('');
420
+
421
+ return {
422
+ method: climate.priority === '调候优先' ? '调候优先 + 扶抑校验' : '扶抑为主 + 调候校验',
423
+ level: strength.level,
424
+ favor,
425
+ avoid,
426
+ disputed,
427
+ neutral,
428
+ favorRoles: favor.map(roleName),
429
+ avoidRoles: avoid.map(roleName),
430
+ neutralRoles: neutral.map((n) => n.role),
431
+ favorTenGods,
432
+ avoidTenGods,
433
+ disputedTenGods,
434
+ neutralTenGods,
435
+ tiaohou: { ...climate, ...arb, element: climate.need ? climate.need.element : null },
436
+ climate,
437
+ qt,
438
+ tongguan,
439
+ detail,
440
+ };
441
+ }
442
+
443
+ // ── 行运评分 ────────────────────────────────────────────────
444
+
445
+ const TIERS = [
446
+ [1.3, '大吉'],
447
+ [0.6, '吉'],
448
+ [-0.6, '平'],
449
+ [-1.3, '忌'],
450
+ [-Infinity, '大忌'],
451
+ ];
452
+
453
+ export function scoreLuck(ganzhi, frame) {
454
+ const ys = frame.yongshen;
455
+ const stem = ganzhi[0];
456
+ const branch = ganzhi[1];
457
+ const stemEl = STEM_ELEMENT[stem];
458
+ const branchEl = BRANCH_ELEMENT[branch];
459
+ const th = ys.tiaohou || ys.climate || null;
460
+ const scoreEl = (el) => {
461
+ let s = 0;
462
+ if (ys.favor.includes(el)) s += 1;
463
+ if (ys.avoid.includes(el)) s -= 1;
464
+ return s;
465
+ };
466
+ let score = scoreEl(stemEl) * 1.0 + scoreEl(branchEl) * 0.8;
467
+ for (const h of (HIDDEN[branch] || []).slice(1)) {
468
+ score += scoreEl(STEM_ELEMENT[h]) * 0.2;
469
+ }
470
+
471
+ const reasons = [];
472
+ if (ys.favor.includes(stemEl)) reasons.push(`天干${stem}${stemEl}为用神`);
473
+ else if (ys.avoid.includes(stemEl)) reasons.push(`天干${stem}${stemEl}为忌神`);
474
+ else reasons.push(`天干${stem}${stemEl}不喜不忌`);
475
+ if (ys.favor.includes(branchEl)) reasons.push(`地支${branch}${branchEl}助用`);
476
+ else if (ys.avoid.includes(branchEl)) reasons.push(`地支${branch}${branchEl}助忌`);
477
+
478
+ // 调候修正:不是"有没有这个五行",而是"这一年是给命局添了一把火,还是泼了一盆水"
479
+ if (th && th.need && th.priority === '调候优先') {
480
+ const needEl = th.need.element;
481
+ const oppEl = th.avoidElement;
482
+ const cold = String(th.kind).includes('寒');
483
+ if (stemEl === needEl) {
484
+ score += 1.1;
485
+ reasons.push(`${th.label}之局得${stem}${stemEl},${cold ? '解冻' : '得润'}`);
486
+ } else if (stemEl === oppEl) {
487
+ score -= 0.9;
488
+ reasons.push(`${th.label}之局再逢${stem}${stemEl},${cold ? '冻木难生' : '燥上加燥'}`);
489
+ }
490
+ if (branchEl === needEl) {
491
+ score += 0.9;
492
+ reasons.push(`地支${branch}${branchEl}合调候`);
493
+ } else if (branchEl === oppEl) {
494
+ score -= 1.0;
495
+ reasons.push(`地支${branch}${branchEl}加重${th.label}`);
496
+ }
497
+ if (cold && (stemEl === '水' || branchEl === '水')) score -= 0.3;
498
+ } else if (th && th.need && (stemEl === th.need.element || branchEl === th.need.element)) {
499
+ reasons.push(`合调候(${th.need.element})`);
500
+ }
501
+
502
+ const godStem = tenGod(frame.dayStem, stem);
503
+ const bonus = ys.favorTenGods.includes(godStem) ? 0.3 : 0;
504
+ // 穷通宝鉴:天干正好是这一格的首取/次取时,再给一点「按书取用」的分
505
+ // 只在该干不属于忌神时加分——书里的取用与气候调候不同向时,以调候为先
506
+ const qt = ys.qt || null;
507
+ if (qt && qt.primary && !ys.avoid.includes(STEM_ELEMENT[qt.primary])) {
508
+ if (stem === qt.primary) {
509
+ score += 0.3;
510
+ reasons.push(`天干${stem}正是${qt.branch}月${qt.stem}日的首取(穷通宝鉴)`);
511
+ } else if (qt.yong.includes(stem) && !ys.avoid.includes(STEM_ELEMENT[stem])) {
512
+ score += 0.15;
513
+ reasons.push(`天干${stem}为穷通宝鉴次取`);
514
+ }
515
+ }
516
+ let total = Math.round((score + bonus) * 10) / 10;
517
+ total = Math.max(-4, Math.min(4, total));
518
+
519
+ const tier = TIERS.find(([min]) => total >= min)[1];
520
+ return { ganzhi, score: total, tier, stemTenGod: godStem, reasons, note: `${godStem}运,${tier}(${total})` };
521
+ }
522
+
523
+ // ── 对外入口 ────────────────────────────────────────────────
524
+
525
+ /**
526
+ * @param {object} p
527
+ * @param {object} p.pillars 四柱
528
+ * @param {string} p.dayStem 日干
529
+ * @param {string} p.monthBranch 月支
530
+ * @param {object} p.strength bazi.js 的旺衰结果
531
+ * @param {object} p.wuxing 五行分值
532
+ */
533
+ export function analyzeBaziFrame({ pillars, dayStem, monthBranch, strength, wuxing }) {
534
+ const gods = godStats(pillars, dayStem);
535
+ const month = monthPattern(pillars, dayStem, monthBranch);
536
+ const special = specialPatterns(pillars, dayStem, monthBranch, strength, wuxing, gods);
537
+ const geju = [month, ...special];
538
+ const yongshen = yongshenOf(dayStem, monthBranch, strength, wuxing, Object.values(pillars).map((p) => p.branch));
539
+ return {
540
+ geju,
541
+ primary: month,
542
+ primaryName: month.name,
543
+ godStats: gods,
544
+ yongshen,
545
+ dayStem,
546
+ monthBranch,
547
+ };
548
+ }