dsh-metaphysics 0.21.0 → 0.34.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/liuyao.js CHANGED
@@ -1,237 +1,702 @@
1
- // 六爻(纳甲)引擎:摇卦、装卦、六亲、六神、世应、变卦
2
- import { STEMS, BRANCHES, toJDN, ganzhiName, dayGanzhiIndex, sexagenaryIndex, xunKongOf } from './calendar.js';
3
-
4
- // 八卦三位(自下而上,1 = 阳)
5
- const TRIGRAM_BITS = {
6
- 乾: [1, 1, 1], 兑: [1, 1, 0], 离: [1, 0, 1], 震: [1, 0, 0],
7
- 巽: [0, 1, 1], 坎: [0, 1, 0], 艮: [0, 0, 1], 坤: [0, 0, 0],
8
- };
9
- // 位值 = b0 + 2b1 + 4b2
10
- const TRIGRAM_BY_VALUE = ['坤', '震', '坎', '兑', '艮', '离', '巽', '乾'];
11
-
12
- const bitsValue = (bits) => bits[0] + 2 * bits[1] + 4 * bits[2];
13
-
14
- // 京房八宫:每宫八卦次序 = 本宫、一世…五世、游魂、归魂
15
- const PALACE_NAMES = {
16
- 乾: ['乾为天', '天风姤', '天山遁', '天地否', '风地观', '山地剥', '火地晋', '火天大有'],
17
- 兑: ['兑为泽', '泽水困', '泽地萃', '泽山咸', '水山蹇', '地山谦', '雷山小过', '雷泽归妹'],
18
- 离: ['离为火', '火山旅', '火风鼎', '火水未济', '山水蒙', '风水涣', '天水讼', '天火同人'],
19
- 震: ['震为雷', '雷地豫', '雷水解', '雷风恒', '地风升', '水风井', '泽风大过', '泽雷随'],
20
- 巽: ['巽为风', '风天小畜', '风火家人', '风雷益', '天雷无妄', '火雷噬嗑', '山雷颐', '山风蛊'],
21
- 坎: ['坎为水', '水泽节', '水雷屯', '水火既济', '泽火革', '雷火丰', '地火明夷', '地水师'],
22
- 艮: ['艮为山', '山火贲', '山天大畜', '山泽损', '火泽睽', '天泽履', '风泽中孚', '风山渐'],
23
- 坤: ['坤为地', '地雷复', '地泽临', '地天泰', '雷天大壮', '泽天夬', '水天需', '水地比'],
24
- };
25
-
26
- const PALACE_ELEMENT = {
27
- 乾: '金', 兑: '金', 离: '火', 震: '木', 巽: '木', 坎: '水', 艮: '土', 坤: '土',
28
- };
29
-
30
- // 八宫卦序:世爻位置(1 = 初爻)
31
- const SHI_POSITION = [6, 1, 2, 3, 4, 5, 4, 3];
32
-
33
- // 京房纳甲:内卦 / 外卦 的天干与三爻地支
34
- const NAJIA = {
35
- 乾: { gan: ['甲', '壬'], inner: ['子', '寅', '辰'], outer: ['午', '申', '戌'] },
36
- 坤: { gan: ['乙', '癸'], inner: ['未', '巳', '卯'], outer: ['丑', '亥', '酉'] },
37
- 震: { gan: ['庚', '庚'], inner: ['子', '寅', '辰'], outer: ['午', '申', '戌'] },
38
- 巽: { gan: ['辛', '辛'], inner: ['丑', '亥', '酉'], outer: ['未', '巳', '卯'] },
39
- 坎: { gan: ['戊', '戊'], inner: ['寅', '辰', '午'], outer: ['申', '戌', '子'] },
40
- 离: { gan: ['己', '己'], inner: ['卯', '丑', '亥'], outer: ['酉', '未', '巳'] },
41
- 艮: { gan: ['丙', '丙'], inner: ['辰', '午', '申'], outer: ['戌', '子', '寅'] },
42
- 兑: { gan: ['丁', '丁'], inner: ['巳', '卯', '丑'], outer: ['亥', '酉', '未'] },
43
- };
44
-
45
- const BRANCH_ELEMENT = {
46
- 子: '水', 丑: '土', 寅: '木', 卯: '木', 辰: '土', 巳: '火',
47
- 午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水',
48
- };
49
-
50
- const GENERATES = { 木: '火', 火: '土', 土: '金', 金: '水', 水: '木' };
51
- const OVERCOMES = { 木: '土', 土: '水', 水: '火', 火: '金', 金: '木' };
52
-
53
- const LIUSHEN = ['青龙', '朱雀', '勾陈', '螣蛇', '白虎', '玄武'];
54
- // 日干 -> 初爻所起六神
55
- const LIUSHEN_START = { 甲: 0, 乙: 0, 丙: 1, 丁: 1, 戊: 2, 己: 3, 庚: 4, 辛: 4, 壬: 5, 癸: 5 };
56
-
57
- function liuqin(palaceElement, branchElement) {
58
- if (palaceElement === branchElement) return '兄弟';
59
- if (GENERATES[branchElement] === palaceElement) return '父母';
60
- if (GENERATES[palaceElement] === branchElement) return '子孙';
61
- if (OVERCOMES[branchElement] === palaceElement) return '官鬼';
62
- if (OVERCOMES[palaceElement] === branchElement) return '妻财';
63
- return '?';
64
- }
65
-
66
- // 构建 (上卦, 下卦) -> 卦 的索引
67
- const HEXAGRAM_INDEX = new Map();
68
-
69
- function flip(bits, positions) {
70
- const out = bits.slice();
71
- for (const p of positions) out[p] = out[p] ? 0 : 1;
72
- return out;
73
- }
74
-
75
- for (const [palace, names] of Object.entries(PALACE_NAMES)) {
76
- const t = TRIGRAM_BITS[palace];
77
- const variants = [
78
- { lower: t, upper: t },
79
- { lower: flip(t, [0]), upper: t },
80
- { lower: flip(t, [0, 1]), upper: t },
81
- { lower: flip(t, [0, 1, 2]), upper: t },
82
- { lower: flip(t, [0, 1, 2]), upper: flip(t, [0]) },
83
- { lower: flip(t, [0, 1, 2]), upper: flip(t, [0, 1]) },
84
- { lower: flip(t, [0, 1, 2]), upper: flip(t, [1]) },
85
- { lower: t, upper: flip(t, [1]) },
86
- ];
87
- variants.forEach((v, order) => {
88
- const upper = TRIGRAM_BY_VALUE[bitsValue(v.upper)];
89
- const lower = TRIGRAM_BY_VALUE[bitsValue(v.lower)];
90
- HEXAGRAM_INDEX.set(`${upper}|${lower}`, {
91
- name: names[order],
92
- palace,
93
- palaceElement: PALACE_ELEMENT[palace],
94
- order,
95
- upper,
96
- lower,
97
- lines: [...v.lower, ...v.upper], // b0 初爻
98
- shi: SHI_POSITION[order],
99
- ying: SHI_POSITION[order] > 3 ? SHI_POSITION[order] - 3 : SHI_POSITION[order] + 3,
100
- });
101
- });
102
- }
103
-
104
- export const HEXAGRAM_COUNT = HEXAGRAM_INDEX.size;
105
-
106
- /** 由六爻(自下而上,1 = 阳)求卦 */
107
- export function hexagramFromBits(bits) {
108
- const upper = TRIGRAM_BY_VALUE[bitsValue(bits.slice(3))];
109
- const lower = TRIGRAM_BY_VALUE[bitsValue(bits.slice(0, 3))];
110
- return HEXAGRAM_INDEX.get(`${upper}|${lower}`);
111
- }
112
-
113
- /** 随机摇卦:三枚铜钱,背为阳(3),字为阴(2),合计 6/7/8/9 */
114
- export function tossLines(random = Math.random) {
115
- const lines = [];
116
- for (let i = 0; i < 6; i++) {
117
- let sum = 0;
118
- for (let c = 0; c < 3; c++) sum += random() < 0.5 ? 3 : 2;
119
- lines.push(sum); // 6 老阴 7 少阳 8 少阴 9 老阳
120
- }
121
- return lines;
122
- }
123
-
124
- function lineToBit(v) {
125
- return v === 7 || v === 9 ? 1 : 0;
126
- }
127
-
128
- function isMoving(v) {
129
- return v === 6 || v === 9;
130
- }
131
-
132
- /**
133
- * 装卦
134
- * @param {object} input { lines?: number[] (自下而上, 6/7/8/9), dayGanZhi?: {stem, branch}, question?: string }
135
- */
136
- export function buildLiuyao(input = {}) {
137
- const rawLines = Array.isArray(input.lines) && input.lines.length === 6
138
- ? input.lines.slice()
139
- : tossLines(input.random);
140
-
141
- const invalid = rawLines.filter((v) => ![6, 7, 8, 9].includes(v));
142
- if (invalid.length) throw new Error(`爻值只能是 6/7/8/9,收到 ${JSON.stringify(rawLines)}`);
143
-
144
- const bits = rawLines.map(lineToBit);
145
- const ben = hexagramFromBits(bits);
146
- const moving = rawLines.map((v, i) => (isMoving(v) ? i : -1)).filter((i) => i >= 0);
147
- const bianBits = moving.length ? flip(bits, moving) : null;
148
- const bian = bianBits ? hexagramFromBits(bianBits) : null;
149
-
150
- const dayGanZhi = input.dayGanZhi ?? (() => {
151
- const now = new Date();
152
- const jdn = toJDN(now.getFullYear(), now.getMonth() + 1, now.getDate());
153
- const g = ganzhiName(dayGanzhiIndex(jdn));
154
- return { stem: g.stem, branch: g.branch, name: g.name };
155
- })();
156
-
157
- const liushenStart = LIUSHEN_START[dayGanZhi.stem] ?? 0;
158
-
159
- // 旬空:由日柱所在的旬推两位空亡(甲子旬→戌亥、甲戌旬→申酉、甲申旬→午未、
160
- // 甲午旬→辰巳、甲辰旬→寅卯、甲寅旬→子丑)。六爻断卦离不开它,之前没有输出。
161
- // 公式统一放在 calendar.xunKongOf,三体系共用一份,避免各自演化后互相漂移。
162
- const dayIdx = sexagenaryIndex(STEMS.indexOf(dayGanZhi.stem), BRANCHES.indexOf(dayGanZhi.branch));
163
- const xun = dayIdx >= 0 ? Math.floor(dayIdx / 10) : -1;
164
- const xunKong = xunKongOf(`${dayGanZhi.stem}${dayGanZhi.branch}`);
165
- const xunShou = xun >= 0 ? ganzhiName(xun * 10).name : '';
166
-
167
- function decorate(hex, movingSet) {
168
- const rows = [];
169
- for (let i = 0; i < 6; i++) {
170
- const isInner = i < 3;
171
- const trigram = isInner ? hex.lower : hex.upper;
172
- const nj = NAJIA[trigram];
173
- const branch = isInner ? nj.inner[i] : nj.outer[i - 3];
174
- const gan = isInner ? nj.gan[0] : nj.gan[1];
175
- const element = BRANCH_ELEMENT[branch];
176
- const value = rawLines[i];
177
- rows.push({
178
- position: i + 1,
179
- name: ['初爻', '二爻', '三爻', '四爻', '五爻', '上爻'][i],
180
- yinYang: bits[i] ? '阳' : '阴',
181
- moving: movingSet ? movingSet.has(i) : isMoving(value),
182
- symbol: bits[i] ? '━━━━━' : '━━ ━━',
183
- ganzhi: gan + branch,
184
- stem: gan,
185
- branch,
186
- element,
187
- liuqin: liuqin(hex.palaceElement, element),
188
- liushen: LIUSHEN[(liushenStart + i) % 6],
189
- });
190
- }
191
- return rows;
192
- }
193
-
194
- const benRows = decorate(ben, new Set(moving));
195
- const movingSet = new Set(moving);
196
- const bianRows = bian ? decorate(bian, new Set()) : null;
197
- if (bianRows) {
198
- bianRows.forEach((r, i) => {
199
- r.changedFrom = movingSet.has(i) ? benRows[i].symbol : null;
200
- });
201
- }
202
-
203
- return {
204
- system: 'liuyao',
205
- question: input.question ?? null,
206
- cast: {
207
- lines: rawLines,
208
- notation: rawLines.map((v) => ({ 6: '老阴 ✕', 7: '少阳', 8: '少阴', 9: '老阳 ○' }[v])),
209
- movingPositions: moving.map((i) => i + 1),
210
- },
211
- dayGanZhi: { name: dayGanZhi.name, stem: dayGanZhi.stem, branch: dayGanZhi.branch },
212
- xunShou,
213
- xunKong,
214
- ben: {
215
- name: ben.name,
216
- upper: ben.upper,
217
- lower: ben.lower,
218
- palace: `${ben.palace}宫`,
219
- palaceElement: ben.palaceElement,
220
- shi: ben.shi,
221
- ying: ben.ying,
222
- lines: benRows,
223
- },
224
- bian: bian
225
- ? {
226
- name: bian.name,
227
- upper: bian.upper,
228
- lower: bian.lower,
229
- palace: `${bian.palace}宫`,
230
- palaceElement: bian.palaceElement,
231
- shi: bian.shi,
232
- ying: bian.ying,
233
- lines: bianRows,
234
- }
235
- : null,
236
- };
237
- }
1
+ // 六爻(纳甲)引擎:摇卦、装卦、六亲、六神、世应、变卦
2
+ import { STEMS, BRANCHES, toJDN, ganzhiName, dayGanzhiIndex, sexagenaryIndex, xunKongOf } from './calendar.js';
3
+
4
+ // 八卦三位(自下而上,1 = 阳)
5
+ const TRIGRAM_BITS = {
6
+ 乾: [1, 1, 1], 兑: [1, 1, 0], 离: [1, 0, 1], 震: [1, 0, 0],
7
+ 巽: [0, 1, 1], 坎: [0, 1, 0], 艮: [0, 0, 1], 坤: [0, 0, 0],
8
+ };
9
+ // 位值 = b0 + 2b1 + 4b2
10
+ const TRIGRAM_BY_VALUE = ['坤', '震', '坎', '兑', '艮', '离', '巽', '乾'];
11
+
12
+ const bitsValue = (bits) => bits[0] + 2 * bits[1] + 4 * bits[2];
13
+
14
+ // 京房八宫:每宫八卦次序 = 本宫、一世…五世、游魂、归魂
15
+ const PALACE_NAMES = {
16
+ 乾: ['乾为天', '天风姤', '天山遁', '天地否', '风地观', '山地剥', '火地晋', '火天大有'],
17
+ 兑: ['兑为泽', '泽水困', '泽地萃', '泽山咸', '水山蹇', '地山谦', '雷山小过', '雷泽归妹'],
18
+ 离: ['离为火', '火山旅', '火风鼎', '火水未济', '山水蒙', '风水涣', '天水讼', '天火同人'],
19
+ 震: ['震为雷', '雷地豫', '雷水解', '雷风恒', '地风升', '水风井', '泽风大过', '泽雷随'],
20
+ 巽: ['巽为风', '风天小畜', '风火家人', '风雷益', '天雷无妄', '火雷噬嗑', '山雷颐', '山风蛊'],
21
+ 坎: ['坎为水', '水泽节', '水雷屯', '水火既济', '泽火革', '雷火丰', '地火明夷', '地水师'],
22
+ 艮: ['艮为山', '山火贲', '山天大畜', '山泽损', '火泽睽', '天泽履', '风泽中孚', '风山渐'],
23
+ 坤: ['坤为地', '地雷复', '地泽临', '地天泰', '雷天大壮', '泽天夬', '水天需', '水地比'],
24
+ };
25
+
26
+ const PALACE_ELEMENT = {
27
+ 乾: '金', 兑: '金', 离: '火', 震: '木', 巽: '木', 坎: '水', 艮: '土', 坤: '土',
28
+ };
29
+
30
+ // 八宫卦序:世爻位置(1 = 初爻)
31
+ const SHI_POSITION = [6, 1, 2, 3, 4, 5, 4, 3];
32
+
33
+ // 京房纳甲:内卦 / 外卦 的天干与三爻地支
34
+ const NAJIA = {
35
+ 乾: { gan: ['甲', '壬'], inner: ['子', '寅', '辰'], outer: ['午', '申', '戌'] },
36
+ 坤: { gan: ['乙', '癸'], inner: ['未', '巳', '卯'], outer: ['丑', '亥', '酉'] },
37
+ 震: { gan: ['庚', '庚'], inner: ['子', '寅', '辰'], outer: ['午', '申', '戌'] },
38
+ 巽: { gan: ['辛', '辛'], inner: ['丑', '亥', '酉'], outer: ['未', '巳', '卯'] },
39
+ 坎: { gan: ['戊', '戊'], inner: ['寅', '辰', '午'], outer: ['申', '戌', '子'] },
40
+ 离: { gan: ['己', '己'], inner: ['卯', '丑', '亥'], outer: ['酉', '未', '巳'] },
41
+ 艮: { gan: ['丙', '丙'], inner: ['辰', '午', '申'], outer: ['戌', '子', '寅'] },
42
+ 兑: { gan: ['丁', '丁'], inner: ['巳', '卯', '丑'], outer: ['亥', '酉', '未'] },
43
+ };
44
+
45
+ const BRANCH_ELEMENT = {
46
+ 子: '水', 丑: '土', 寅: '木', 卯: '木', 辰: '土', 巳: '火',
47
+ 午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水',
48
+ };
49
+
50
+ const GENERATES = { 木: '火', 火: '土', 土: '金', 金: '水', 水: '木' };
51
+ const OVERCOMES = { 木: '土', 土: '水', 水: '火', 火: '金', 金: '木' };
52
+
53
+ const LIUSHEN = ['青龙', '朱雀', '勾陈', '螣蛇', '白虎', '玄武'];
54
+ const LIUQIN_NAMES = ['父母', '兄弟', '子孙', '妻财', '官鬼'];
55
+ // 日干 -> 初爻所起六神
56
+ const LIUSHEN_START = { 甲: 0, 乙: 0, 丙: 1, 丁: 1, 戊: 2, 己: 3, 庚: 4, 辛: 4, 壬: 5, 癸: 5 };
57
+
58
+ // ── 卦体关系:六冲卦 / 六合卦 / 伏吟 / 反吟 ───────────────────────────────
59
+ // 口径来源:《增删卜易》「六冲六合」「反吟伏吟」诸章,并以
60
+ // test/fixtures/classics.json 里 23 条带断语的卦例逐条对拍(test/liuyao-body.test.mjs)。
61
+ //
62
+ // 判六冲卦/六合卦不能看「相邻两爻」,要看六个地支能不能配成三组:
63
+ // 六冲 子午·丑未·寅申·卯酉·辰戌·巳亥 → 八纯卦全中(乾为天 子寅辰午申戌 = 子午/寅申/辰戌)
64
+ // 六合 子丑·寅亥·卯戌·辰酉·巳申·午未 → 地雷复 子寅辰丑亥酉 = 子丑/寅亥/辰酉
65
+ // 八卦对冲(乾巽·坎离·艮坤·震兑)是「卦反」,纳甲地支并不相冲,只能按卦象判。
66
+ const ZHI_CHONG_PAIRS = [['子', '午'], ['丑', '未'], ['寅', '申'], ['卯', '酉'], ['辰', '戌'], ['巳', '亥']];
67
+ const ZHI_HE_PAIRS = [['子', '丑'], ['寅', '亥'], ['卯', '戌'], ['辰', '酉'], ['巳', '申'], ['午', '未']];
68
+ const CHONG_OF = Object.fromEntries(ZHI_CHONG_PAIRS.flatMap(([a, b]) => [[a, b], [b, a]]));
69
+ const TRIGRAM_OPPOSITE = { 乾: '巽', 巽: '乾', 坎: '离', 离: '坎', 艮: '坤', 坤: '艮', 震: '兑', 兑: '震' };
70
+
71
+ // ── 化进神 / 化退神 ────────────────────────────────────────────────
72
+ // 动爻变出的地支与本支同五行、且在同五行的「顺次」上相邻,即化进神;反向为化退神。
73
+ // 木 寅→卯 火 巳→午 金 申→酉 水 亥→子 土 丑→辰→未→戌→丑
74
+ // 口径见《增删卜易》进神退神章;用 test/fixtures/classics.json 里 12 条带「化进/化退」
75
+ // 断语的卦例对拍(test/liuyao-jintui.test.mjs)。同五行但不相邻(如 寅→辰)不算。
76
+ const JIN_TUI_PAIRS = [['寅', '卯'], ['巳', '午'], ['申', '酉'], ['亥', '子'], ['丑', '辰'], ['辰', '未'], ['未', '戌'], ['戌', '丑']];
77
+ const JIN_OF = new Map(JIN_TUI_PAIRS);
78
+ const TUI_OF = new Map(JIN_TUI_PAIRS.map(([a, b]) => [b, a]));
79
+
80
+ /** 本支 → 变支:'进' / '退' / null */
81
+ export function jinTuiOf(from, to) {
82
+ if (!from || !to || from === to) return null;
83
+ if (JIN_OF.get(from) === to) return '进';
84
+ if (TUI_OF.get(from) === to) return '退';
85
+ return null;
86
+ }
87
+
88
+ /** 六个地支能否正好配成三对;配不上(含重支)返回 null */
89
+ function pairThirds(branches, table) {
90
+ const cnt = new Map();
91
+ for (const b of branches) cnt.set(b, (cnt.get(b) || 0) + 1);
92
+ const hits = [];
93
+ for (const [a, b] of table) {
94
+ const na = cnt.get(a) || 0;
95
+ const nb = cnt.get(b) || 0;
96
+ if (!na && !nb) continue;
97
+ if (na !== 1 || nb !== 1) return null;
98
+ cnt.set(a, 0);
99
+ cnt.set(b, 0);
100
+ hits.push([a, b]);
101
+ }
102
+ return hits.length * 2 === branches.length ? hits : null;
103
+ }
104
+
105
+ /** 卦的纳甲地支(内卦 = 1~3 爻,外卦 = 4~6 爻) */
106
+ function hexBranches(hex) {
107
+ const inner = NAJIA[hex.lower].inner.slice();
108
+ const outer = NAJIA[hex.upper].outer.slice();
109
+ return { inner, outer, all: [...inner, ...outer] };
110
+ }
111
+
112
+ /** 单个卦的卦体属性:是否六冲卦 / 六合卦,及配对明细 */
113
+ export function hexBody(hex) {
114
+ const { inner, outer, all } = hexBranches(hex);
115
+ const chongPairs = pairThirds(all, ZHI_CHONG_PAIRS);
116
+ const hePairs = pairThirds(all, ZHI_HE_PAIRS);
117
+ return {
118
+ name: hex.name,
119
+ upper: hex.upper,
120
+ lower: hex.lower,
121
+ inner,
122
+ outer,
123
+ chong: Boolean(chongPairs),
124
+ he: Boolean(hePairs),
125
+ chongPairs: chongPairs || [],
126
+ hePairs: hePairs || [],
127
+ };
128
+ }
129
+
130
+ /**
131
+ * 本卦 → 变卦 的卦体关系。
132
+ * @param {{name:string,upper:string,lower:string}} ben 本卦
133
+ * @param {object|null} bian 变卦(无动爻传 null)
134
+ * @param {number[]} [moving] 动爻位置(1~6)
135
+ */
136
+ export function bodyRelations(ben, bian = null, moving = []) {
137
+ const b = hexBody(ben);
138
+ const out = {
139
+ ben: { name: b.name, chong: b.chong, he: b.he, chongPairs: b.chongPairs, hePairs: b.hePairs, inner: b.inner, outer: b.outer },
140
+ bian: null,
141
+ fuYin: { inner: false, outer: false },
142
+ fanYin: { inner: null, outer: null }, // null 或 { by: ['chong'|'pair'] }
143
+ yaoFuYin: [],
144
+ yaoFanYin: [],
145
+ jinTui: [], // [{ position, from, to, type: '进'|'退' }]
146
+ };
147
+ if (!bian) return out;
148
+
149
+ const v = hexBody(bian);
150
+ out.bian = { name: v.name, chong: v.chong, he: v.he, chongPairs: v.chongPairs, hePairs: v.hePairs, inner: v.inner, outer: v.outer };
151
+
152
+ for (const side of ['inner', 'outer']) {
153
+ const x = b[side];
154
+ const y = v[side];
155
+ const tri = side === 'inner' ? [ben.lower, bian.lower] : [ben.upper, bian.upper];
156
+ const changed = tri[0] !== tri[1];
157
+ // 伏吟必须「卦变了而地支没变」。卦没变时地支当然相同,那不是伏吟。
158
+ // 纳甲上乾与震共用一套地支(子寅辰/午申戌),所以卦伏吟只可能出自乾↔震互换。
159
+ if (changed && x.every((z, i) => z === y[i])) {
160
+ out.fuYin[side] = true;
161
+ continue;
162
+ }
163
+ const by = [];
164
+ if (changed && x.every((z, i) => CHONG_OF[z] === y[i])) by.push('chong');
165
+ if (changed && TRIGRAM_OPPOSITE[tri[0]] === tri[1]) by.push('pair');
166
+ if (by.length) out.fanYin[side] = { by };
167
+ }
168
+
169
+ for (const p of moving) {
170
+ const i = p - 1;
171
+ if (!(i >= 0 && i < 6)) continue;
172
+ const x = i < 3 ? b.inner[i] : b.outer[i - 3];
173
+ const y = i < 3 ? v.inner[i] : v.outer[i - 3];
174
+ if (x === y) out.yaoFuYin.push(p);
175
+ else if (CHONG_OF[x] === y) out.yaoFanYin.push(p);
176
+ const jt = jinTuiOf(x, y);
177
+ if (jt) out.jinTui.push({ position: p, from: x, to: y, type: jt });
178
+ }
179
+ return out;
180
+ }
181
+
182
+ /** 卦体关系的短标签(面板 chip / 文本摘要共用一份措辞) */
183
+ export function bodyLabels(body) {
184
+ if (!body || !body.ben) return [];
185
+ const fuYin = body.fuYin || {};
186
+ const fanYin = body.fanYin || {};
187
+ const yaoFuYin = body.yaoFuYin || [];
188
+ const yaoFanYin = body.yaoFanYin || [];
189
+ const out = [];
190
+ if (body.ben.chong) out.push('六冲卦');
191
+ if (body.ben.he) out.push('六合卦');
192
+ if (body.bian) {
193
+ if (body.bian.chong) out.push('变六冲');
194
+ else if (body.bian.he) out.push('变六合');
195
+ }
196
+ const sides = [['inner', '内卦'], ['outer', '外卦']];
197
+ for (const [k, label] of sides) if (fuYin[k]) out.push(`${label}伏吟`);
198
+ for (const [k, label] of sides) if (fanYin[k]) out.push(`${label}反吟`);
199
+ if (yaoFuYin.length) out.push(`爻伏吟 ${yaoFuYin.join('、')}`);
200
+ if (yaoFanYin.length) out.push(`爻反吟 ${yaoFanYin.join('、')}`);
201
+ const jin = (body.jinTui || []).filter((x) => x.type === '进').map((x) => x.position);
202
+ const tui = (body.jinTui || []).filter((x) => x.type === '退').map((x) => x.position);
203
+ if (jin.length) out.push(`化进神 ${jin.join('、')}`);
204
+ if (tui.length) out.push(`化退神 ${tui.join('、')}`);
205
+ return out;
206
+ }
207
+
208
+ function liuqin(palaceElement, branchElement) {
209
+ if (palaceElement === branchElement) return '兄弟';
210
+ if (GENERATES[branchElement] === palaceElement) return '父母';
211
+ if (GENERATES[palaceElement] === branchElement) return '子孙';
212
+ if (OVERCOMES[branchElement] === palaceElement) return '官鬼';
213
+ if (OVERCOMES[palaceElement] === branchElement) return '妻财';
214
+ return '?';
215
+ }
216
+
217
+ // 构建 (上卦, 下卦) -> 卦 的索引
218
+ const HEXAGRAM_INDEX = new Map();
219
+
220
+ function flip(bits, positions) {
221
+ const out = bits.slice();
222
+ for (const p of positions) out[p] = out[p] ? 0 : 1;
223
+ return out;
224
+ }
225
+
226
+ for (const [palace, names] of Object.entries(PALACE_NAMES)) {
227
+ const t = TRIGRAM_BITS[palace];
228
+ const variants = [
229
+ { lower: t, upper: t },
230
+ { lower: flip(t, [0]), upper: t },
231
+ { lower: flip(t, [0, 1]), upper: t },
232
+ { lower: flip(t, [0, 1, 2]), upper: t },
233
+ { lower: flip(t, [0, 1, 2]), upper: flip(t, [0]) },
234
+ { lower: flip(t, [0, 1, 2]), upper: flip(t, [0, 1]) },
235
+ { lower: flip(t, [0, 1, 2]), upper: flip(t, [1]) },
236
+ { lower: t, upper: flip(t, [1]) },
237
+ ];
238
+ variants.forEach((v, order) => {
239
+ const upper = TRIGRAM_BY_VALUE[bitsValue(v.upper)];
240
+ const lower = TRIGRAM_BY_VALUE[bitsValue(v.lower)];
241
+ HEXAGRAM_INDEX.set(`${upper}|${lower}`, {
242
+ name: names[order],
243
+ palace,
244
+ palaceElement: PALACE_ELEMENT[palace],
245
+ order,
246
+ upper,
247
+ lower,
248
+ lines: [...v.lower, ...v.upper], // b0 初爻
249
+ shi: SHI_POSITION[order],
250
+ ying: SHI_POSITION[order] > 3 ? SHI_POSITION[order] - 3 : SHI_POSITION[order] + 3,
251
+ });
252
+ });
253
+ }
254
+
255
+ export const HEXAGRAM_COUNT = HEXAGRAM_INDEX.size;
256
+
257
+ /** 由六爻(自下而上,1 = 阳)求卦 */
258
+ export function hexagramFromBits(bits) {
259
+ const upper = TRIGRAM_BY_VALUE[bitsValue(bits.slice(3))];
260
+ const lower = TRIGRAM_BY_VALUE[bitsValue(bits.slice(0, 3))];
261
+ return HEXAGRAM_INDEX.get(`${upper}|${lower}`);
262
+ }
263
+
264
+ // 八宫首卦(每宫的纯卦):伏神要「于本宫首卦内寻之」,所以先备一张 宫 → 纯卦 的表
265
+ const PURE_BY_PALACE = new Map();
266
+ for (const h of HEXAGRAM_INDEX.values()) if (h.order === 0) PURE_BY_PALACE.set(h.palace, h);
267
+
268
+ /**
269
+ * 取用神:问什么,就以哪一类六亲为用。
270
+ * 感情分男女(男看妻财、女看官鬼),出行以世爻为用——这两条不接受单一六亲,
271
+ * 所以表里 liuqin 留 null,由 yongshenOf 按 gender / 世爻决定。
272
+ */
273
+ export const YONGSHEN_RULES = Object.freeze({
274
+ wealth: { liuqin: '妻财', ask: '求财、收入、订单' },
275
+ career: { liuqin: '官鬼', ask: '工作、职位、考核' },
276
+ health: { liuqin: '官鬼', ask: '病与压力(子孙为医药)' },
277
+ study: { liuqin: '父母', ask: '考试、证书、文书合同' },
278
+ love: { liuqin: null, ask: '感情:男看妻财、女看官鬼' },
279
+ travel: { liuqin: null, ask: '出行以世爻为用,父母为舟车' },
280
+ general: { liuqin: null, ask: '未指明所问之事' },
281
+ });
282
+
283
+ const WUXING_SAY = {
284
+ 飞生伏: '飞神生伏神,伏神借得上力',
285
+ 伏生飞: '伏神生飞神,力气往外泄',
286
+ 飞克伏: '飞神克伏神,伏神受压难出',
287
+ 伏克飞: '伏神克飞神,伏神能自出',
288
+ 比和: '飞伏比和,同气相求',
289
+ };
290
+
291
+ /** 飞伏生克在标签里的短说法(比 WUXING_SAY 更短,给 chip 行用) */
292
+ const RELATION_LABEL = {
293
+ 飞生伏: '伏神得飞神生',
294
+ 伏生飞: '伏神泄气于飞神',
295
+ 飞克伏: '伏神受飞神克',
296
+ 伏克飞: '伏神克飞神可出',
297
+ 比和: '飞伏比和',
298
+ };
299
+
300
+ function wuxingBetween(min, max) {
301
+ // min 是下面那个(伏神),max 是上面那个(飞神);返回「谁生谁 / 谁克谁」
302
+ if (GENERATES[max] === min) return '飞生伏';
303
+ if (GENERATES[min] === max) return '伏生飞';
304
+ if (OVERCOMES[max] === min) return '飞克伏';
305
+ if (OVERCOMES[min] === max) return '伏克飞';
306
+ return '比和';
307
+ }
308
+
309
+ /** 用神所在爻位的内外归属(1~3 内卦,4~6 外卦) */
310
+ const sideOf = (position) => (position <= 3 ? 'inner' : 'outer');
311
+
312
+ /**
313
+ * 取用神并把用神与卦体、伏神、旬空、动变串起来。
314
+ *
315
+ * 三步:① 按所问之事定六亲(感情分男女、出行以世为用);
316
+ * ② 在本卦里找这个六亲——找得到就用神在爻上,出现两次记「两现」;
317
+ * ③ 找不到(用神不上卦)就回到本宫首卦同六亲的爻位取伏神,并标飞伏生克。
318
+ *
319
+ * @param {object} result buildLiuyao 的返回
320
+ * @param {{direction?:string, gender?:string}} [opts]
321
+ */
322
+ export function yongshenOf(result, opts = {}) {
323
+ const r = result && typeof result === 'object' ? result : {};
324
+ const ben = r.ben && typeof r.ben === 'object' ? r.ben : {};
325
+ const lines = Array.isArray(ben.lines) ? ben.lines : [];
326
+ const direction = YONGSHEN_RULES[opts.direction] ? opts.direction : 'general';
327
+ const rule = YONGSHEN_RULES[direction];
328
+ const gender = opts.gender === 'female' ? 'female' : 'male';
329
+ const body = r.body && typeof r.body === 'object' ? r.body : {};
330
+ // xunKong 在结果里是数组(如 ['戌','亥']),兼容旧数据里拼成字符串的写法
331
+ const kongArr = Array.isArray(r.xunKong)
332
+ ? r.xunKong
333
+ : (typeof r.xunKong === 'string' ? r.xunKong.split('') : []);
334
+
335
+ // ① 定六亲 / 定「以世为用」
336
+ // 方向只是一层便利:调用方也可以直接给六亲(opts.liuqin),用于别的问法或对拍
337
+ const direct = LIUQIN_NAMES.includes(opts.liuqin) ? opts.liuqin : null;
338
+ const byShi = !direct && direction === 'travel';
339
+ const liuqinName = direct || (byShi ? '世' : (direction === 'love' ? (gender === 'female' ? '官鬼' : '妻财') : rule.liuqin));
340
+ const base = {
341
+ direction,
342
+ gender,
343
+ ask: rule.ask,
344
+ byShi,
345
+ liuqin: liuqinName,
346
+ found: [],
347
+ missing: false,
348
+ fushen: [],
349
+ labels: [],
350
+ };
351
+ if (!liuqinName) { base.labels = ['用神未取(未指明所问之事)']; return base; }
352
+
353
+ // ② 用神在爻上
354
+ const found = byShi
355
+ ? lines.filter((l) => l.position === ben.shi)
356
+ : lines.filter((l) => l.liuqin === liuqinName);
357
+ base.found = found.map((l) => ({
358
+ position: l.position,
359
+ name: l.name,
360
+ liuqin: l.liuqin,
361
+ branch: l.branch,
362
+ element: l.element,
363
+ ganzhi: l.ganzhi,
364
+ moving: Boolean(l.moving),
365
+ liushen: l.liushen,
366
+ shi: l.position === ben.shi,
367
+ ying: l.position === ben.ying,
368
+ kong: kongArr.includes(l.branch),
369
+ }));
370
+
371
+ const labels = [];
372
+ const label = byShi ? '用神 世爻' : `用神 ${liuqinName}`;
373
+ labels.push(label);
374
+ const jin = (body.jinTui || []).filter((x) => x.type === '进').map((x) => x.position);
375
+ const tui = (body.jinTui || []).filter((x) => x.type === '退').map((x) => x.position);
376
+ const yaoFu = body.yaoFuYin || [];
377
+ const yaoFan = body.yaoFanYin || [];
378
+
379
+ if (found.length) {
380
+ labels.push(`${label}在 ${found.map((l) => `${l.position}爻`).join('、')}`);
381
+ if (found.length > 1) labels.push(`用神两现 ${found.map((l) => l.position).join('、')}`);
382
+ if (found.some((l) => l.position === ben.shi)) labels.push('用神持世');
383
+ if (found.some((l) => l.position === ben.ying)) labels.push('用神临应');
384
+ if (found.some((l) => l.moving)) labels.push('用神发动');
385
+ if (found.some((l) => kongArr.includes(l.branch))) labels.push('用神空亡');
386
+ if (found.some((l) => jin.includes(l.position))) labels.push('用神化进神');
387
+ if (found.some((l) => tui.includes(l.position))) labels.push('用神化退神');
388
+ if (found.some((l) => yaoFu.includes(l.position))) labels.push('用神爻伏吟');
389
+ if (found.some((l) => yaoFan.includes(l.position))) labels.push('用神爻反吟');
390
+ } else {
391
+ // ③ 用神不上卦 → 本宫首卦同位取伏神
392
+ base.missing = true;
393
+ const palace = String(ben.palace || '').replace(/宫$/, '');
394
+ const pure = PURE_BY_PALACE.get(palace) || null;
395
+ if (pure) {
396
+ for (let i = 0; i < 6; i++) {
397
+ const isInner = i < 3;
398
+ const nj = NAJIA[isInner ? pure.lower : pure.upper];
399
+ const branch = isInner ? nj.inner[i] : nj.outer[i - 3];
400
+ const gan = isInner ? nj.gan[0] : nj.gan[1];
401
+ const element = BRANCH_ELEMENT[branch];
402
+ if (liuqin(pure.palaceElement, element) !== liuqinName) continue;
403
+ const fei = lines[i] || null;
404
+ const relation = fei ? wuxingBetween(element, fei.element) : null;
405
+ base.fushen.push({
406
+ position: i + 1,
407
+ liuqin: liuqinName,
408
+ branch,
409
+ element,
410
+ ganzhi: gan + branch,
411
+ relation,
412
+ fei: fei ? { position: fei.position, liuqin: fei.liuqin, branch: fei.branch, element: fei.element, ganzhi: fei.ganzhi } : null,
413
+ });
414
+ }
415
+ }
416
+ if (base.fushen.length) {
417
+ labels.push(`用神不上卦·伏于 ${base.fushen.map((f) => `${f.position}爻`).join('、')}`);
418
+ if (base.fushen.length > 1) labels.push(`伏神两现 ${base.fushen.map((f) => f.position).join('、')}`);
419
+ const rels = [...new Set(base.fushen.map((f) => f.relation).filter(Boolean))];
420
+ for (const rel of rels) labels.push(RELATION_LABEL[rel] || `伏神${rel}`);
421
+ } else {
422
+ labels.push('用神不上卦');
423
+ }
424
+ }
425
+
426
+ // ④ 与卦体联动:卦体只说动势,落到用神身上才有指向
427
+ const sides = found.length ? [...new Set(found.map((l) => sideOf(l.position)))] : [];
428
+ if (body.ben) {
429
+ if (body.ben.chong) labels.push('用神在六冲卦');
430
+ if (body.ben.he) labels.push('用神在六合卦');
431
+ if (body.bian && body.bian.chong) labels.push('用神逢变六冲');
432
+ if (body.bian && body.bian.he) labels.push('用神逢变六合');
433
+ }
434
+ for (const side of sides) {
435
+ if (body.fuYin && body.fuYin[side]) labels.push(`用神在${side === 'inner' ? '内' : '外'}卦伏吟`);
436
+ if (body.fanYin && body.fanYin[side]) {
437
+ const by = (body.fanYin[side].by || []).includes('chong') ? '地支全冲' : '八卦正对';
438
+ labels.push(`用神在${side === 'inner' ? '内' : '外'}卦反吟`);
439
+ labels.push(`反吟来源:${by}`);
440
+ }
441
+ }
442
+
443
+ base.labels = labels;
444
+ base.relationSay = base.fushen.map((f) => WUXING_SAY[f.relation]).filter(Boolean);
445
+ base.side = sides.length === 1 ? (sides[0] === 'inner' ? '内卦' : '外卦') : null;
446
+ return base;
447
+ }
448
+
449
+ /** 随机摇卦:三枚铜钱,背为阳(3),字为阴(2),合计 6/7/8/9 */
450
+ export function tossLines(random = Math.random) {
451
+ const lines = [];
452
+ for (let i = 0; i < 6; i++) {
453
+ let sum = 0;
454
+ for (let c = 0; c < 3; c++) sum += random() < 0.5 ? 3 : 2;
455
+ lines.push(sum); // 6 老阴 7 少阳 8 少阴 9 老阳
456
+ }
457
+ return lines;
458
+ }
459
+
460
+ function lineToBit(v) {
461
+ return v === 7 || v === 9 ? 1 : 0;
462
+ }
463
+
464
+ function isMoving(v) {
465
+ return v === 6 || v === 9;
466
+ }
467
+
468
+ /**
469
+ * 装卦
470
+ * @param {object} input { lines?: number[] (自下而上, 6/7/8/9), dayGanZhi?: {stem, branch}, question?: string }
471
+ */
472
+ export function buildLiuyao(input = {}) {
473
+ const rawLines = Array.isArray(input.lines) && input.lines.length === 6
474
+ ? input.lines.slice()
475
+ : tossLines(input.random);
476
+
477
+ const invalid = rawLines.filter((v) => ![6, 7, 8, 9].includes(v));
478
+ if (invalid.length) throw new Error(`爻值只能是 6/7/8/9,收到 ${JSON.stringify(rawLines)}`);
479
+
480
+ const bits = rawLines.map(lineToBit);
481
+ const ben = hexagramFromBits(bits);
482
+ const moving = rawLines.map((v, i) => (isMoving(v) ? i : -1)).filter((i) => i >= 0);
483
+ const bianBits = moving.length ? flip(bits, moving) : null;
484
+ const bian = bianBits ? hexagramFromBits(bianBits) : null;
485
+
486
+ const dayGanZhi = input.dayGanZhi ?? (() => {
487
+ const now = new Date();
488
+ const jdn = toJDN(now.getFullYear(), now.getMonth() + 1, now.getDate());
489
+ const g = ganzhiName(dayGanzhiIndex(jdn));
490
+ return { stem: g.stem, branch: g.branch, name: g.name };
491
+ })();
492
+
493
+ const liushenStart = LIUSHEN_START[dayGanZhi.stem] ?? 0;
494
+
495
+ // 旬空:由日柱所在的旬推两位空亡(甲子旬→戌亥、甲戌旬→申酉、甲申旬→午未、
496
+ // 甲午旬→辰巳、甲辰旬→寅卯、甲寅旬→子丑)。六爻断卦离不开它,之前没有输出。
497
+ // 公式统一放在 calendar.xunKongOf,三体系共用一份,避免各自演化后互相漂移。
498
+ const dayIdx = sexagenaryIndex(STEMS.indexOf(dayGanZhi.stem), BRANCHES.indexOf(dayGanZhi.branch));
499
+ const xun = dayIdx >= 0 ? Math.floor(dayIdx / 10) : -1;
500
+ const xunKong = xunKongOf(`${dayGanZhi.stem}${dayGanZhi.branch}`);
501
+ const xunShou = xun >= 0 ? ganzhiName(xun * 10).name : '';
502
+
503
+ function decorate(hex, movingSet) {
504
+ const rows = [];
505
+ for (let i = 0; i < 6; i++) {
506
+ const isInner = i < 3;
507
+ const trigram = isInner ? hex.lower : hex.upper;
508
+ const nj = NAJIA[trigram];
509
+ const branch = isInner ? nj.inner[i] : nj.outer[i - 3];
510
+ const gan = isInner ? nj.gan[0] : nj.gan[1];
511
+ const element = BRANCH_ELEMENT[branch];
512
+ const value = rawLines[i];
513
+ rows.push({
514
+ position: i + 1,
515
+ name: ['初爻', '二爻', '三爻', '四爻', '五爻', '上爻'][i],
516
+ yinYang: bits[i] ? '阳' : '阴',
517
+ moving: movingSet ? movingSet.has(i) : isMoving(value),
518
+ symbol: bits[i] ? '━━━━━' : '━━ ━━',
519
+ ganzhi: gan + branch,
520
+ stem: gan,
521
+ branch,
522
+ element,
523
+ liuqin: liuqin(hex.palaceElement, element),
524
+ liushen: LIUSHEN[(liushenStart + i) % 6],
525
+ });
526
+ }
527
+ return rows;
528
+ }
529
+
530
+ const benRows = decorate(ben, new Set(moving));
531
+ const movingSet = new Set(moving);
532
+ const bianRows = bian ? decorate(bian, new Set()) : null;
533
+ if (bianRows) {
534
+ bianRows.forEach((r, i) => {
535
+ r.changedFrom = movingSet.has(i) ? benRows[i].symbol : null;
536
+ });
537
+ }
538
+
539
+ return {
540
+ system: 'liuyao',
541
+ question: input.question ?? null,
542
+ cast: {
543
+ lines: rawLines,
544
+ notation: rawLines.map((v) => ({ 6: '老阴 ✕', 7: '少阳', 8: '少阴', 9: '老阳 ○' }[v])),
545
+ movingPositions: moving.map((i) => i + 1),
546
+ },
547
+ dayGanZhi: { name: dayGanZhi.name, stem: dayGanZhi.stem, branch: dayGanZhi.branch },
548
+ xunShou,
549
+ xunKong,
550
+ ben: {
551
+ name: ben.name,
552
+ upper: ben.upper,
553
+ lower: ben.lower,
554
+ palace: `${ben.palace}宫`,
555
+ palaceElement: ben.palaceElement,
556
+ shi: ben.shi,
557
+ ying: ben.ying,
558
+ lines: benRows,
559
+ },
560
+ bian: bian
561
+ ? {
562
+ name: bian.name,
563
+ upper: bian.upper,
564
+ lower: bian.lower,
565
+ palace: `${bian.palace}宫`,
566
+ palaceElement: bian.palaceElement,
567
+ shi: bian.shi,
568
+ ying: bian.ying,
569
+ lines: bianRows,
570
+ }
571
+ : null,
572
+ body: (() => {
573
+ const body = bodyRelations(ben, bian, moving.map((i) => i + 1));
574
+ // 标签只算一次挂到结果上:面板、CLI 摘要、解析层都读这一份,避免三处措辞走散
575
+ body.labels = bodyLabels(body);
576
+ return body;
577
+ })(),
578
+ };
579
+ }
580
+
581
+ // ── 用神旺衰:月建主旺衰、日辰司生克 ────────────────────────────────
582
+ // 这一层只吃「五行生克 + 地支六冲」,不引入任何需要背的断语,
583
+ // 所以每一格都能被独立复算出来——这是它敢写进结果的前提。
584
+ const ZHI_ELEMENT = {
585
+ 子: '水', 丑: '土', 寅: '木', 卯: '木', 辰: '土', 巳: '火',
586
+ 午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水',
587
+ };
588
+ const ELEMENT_SHENG = { 木: '火', 火: '土', 土: '金', 金: '水', 水: '木' };
589
+ const ELEMENT_KE = { 木: '土', 土: '水', 水: '火', 火: '金', 金: '木' };
590
+ const MONTH_STATE_ORDER = ['旺', '相', '休', '囚', '死'];
591
+ const MONTH_STATE_SAY = {
592
+ 旺: '与月令同气,当令得时',
593
+ 相: '受月令之生,气有余',
594
+ 休: '生月令而气泄,力减',
595
+ 囚: '克月令而力困',
596
+ 死: '被月令所克,气受制',
597
+ };
598
+ const DAY_RELATION_ORDER = ['日生', '日克', '比和', '泄', '耗'];
599
+ const DAY_RELATION_SAY = {
600
+ 日生: '日辰生扶,当天有人托着',
601
+ 日克: '日辰克制,当天被压一头',
602
+ 比和: '与日辰同气,当天得到同类之助',
603
+ 泄: '生扶日辰,气往外走',
604
+ 耗: '克制日辰,力有所耗',
605
+ };
606
+
607
+ export function zhiElementOf(branch) {
608
+ return ZHI_ELEMENT[branch] || null;
609
+ }
610
+
611
+ // 月令五态:同气旺、月令所生相、生月令休、克月令囚、被月令克死
612
+ export function monthStateOf(element, monthElement) {
613
+ if (!element || !monthElement) return 'unknown';
614
+ if (element === monthElement) return '旺';
615
+ if (ELEMENT_SHENG[monthElement] === element) return '相';
616
+ if (ELEMENT_SHENG[element] === monthElement) return '休';
617
+ if (ELEMENT_KE[element] === monthElement) return '囚';
618
+ if (ELEMENT_KE[monthElement] === element) return '死';
619
+ return 'unknown';
620
+ }
621
+
622
+ // 日辰对爻:站在「爻」这一边说,日辰是生我、克我,还是我泄它、耗它
623
+ export function dayRelationOf(element, dayElement) {
624
+ if (!element || !dayElement) return 'unknown';
625
+ if (element === dayElement) return '比和';
626
+ if (ELEMENT_SHENG[dayElement] === element) return '日生';
627
+ if (ELEMENT_KE[dayElement] === element) return '日克';
628
+ if (ELEMENT_SHENG[element] === dayElement) return '泄';
629
+ if (ELEMENT_KE[element] === dayElement) return '耗';
630
+ return 'unknown';
631
+ }
632
+
633
+ /**
634
+ * 用神旺衰:把月建、日辰两股外部力量落到用神那几爻上。
635
+ * 月建按月支、日辰按日支,各出自己的五行,再与本爻五行论生克;
636
+ * 地支相冲另标——月建冲爻叫月破,日辰冲静爻叫暗动、冲旬空叫冲空。
637
+ * @param {object} result buildLiuyao 的结果
638
+ * @param {object} useGod yongshenOf 的结果
639
+ * @param {{monthBranch?: string, dayBranch?: string}} opts
640
+ */
641
+ export function yongshenStrength(result, useGod, opts = {}) {
642
+ if (!useGod || !Array.isArray(useGod.found) || !useGod.found.length) return null;
643
+ const monthBranch = opts.monthBranch || null;
644
+ const dayBranch = opts.dayBranch || (result && result.dayGanZhi ? result.dayGanZhi.branch : null);
645
+ if (!monthBranch && !dayBranch) return null;
646
+ const monthElement = monthBranch ? zhiElementOf(monthBranch) : null;
647
+ const dayElement = dayBranch ? zhiElementOf(dayBranch) : null;
648
+ const kong = Array.isArray(result && result.xunKong)
649
+ ? result.xunKong
650
+ : (result && typeof result.xunKong === 'string' ? result.xunKong.split('') : []);
651
+
652
+ const lines = useGod.found.map((l) => {
653
+ const element = l.element || zhiElementOf(l.branch);
654
+ const monthChong = !!(monthBranch && CHONG_OF[l.branch] === monthBranch);
655
+ const dayChong = !!(dayBranch && CHONG_OF[l.branch] === dayBranch);
656
+ const monthState = monthStateOf(element, monthElement);
657
+ const dayRelation = dayRelationOf(element, dayElement);
658
+ const isEmpty = kong.includes(l.branch);
659
+ return {
660
+ position: l.position,
661
+ branch: l.branch,
662
+ element,
663
+ monthState,
664
+ monthSay: MONTH_STATE_SAY[monthState] || '',
665
+ monthChong,
666
+ dayRelation,
667
+ daySay: DAY_RELATION_SAY[dayRelation] || '',
668
+ dayChong,
669
+ kong: isEmpty,
670
+ moving: !!l.moving,
671
+ // 冲的三种情形混在一起说不清,这里分开定名
672
+ chongTag: monthChong ? '月破'
673
+ : dayChong ? (isEmpty ? '冲空' : (l.moving ? '逢日辰冲' : '暗动')) : '',
674
+ };
675
+ });
676
+
677
+ const labels = [];
678
+ const states = [...new Set(lines.map((l) => l.monthState))].filter((x) => x !== 'unknown');
679
+ states.sort((a, b) => MONTH_STATE_ORDER.indexOf(a) - MONTH_STATE_ORDER.indexOf(b));
680
+ for (const st of states) labels.push(`用神月建${st}`);
681
+ if (lines.some((l) => l.monthChong)) labels.push('用神月破');
682
+ const rels = [...new Set(lines.map((l) => l.dayRelation))].filter((x) => x !== 'unknown');
683
+ rels.sort((a, b) => DAY_RELATION_ORDER.indexOf(a) - DAY_RELATION_ORDER.indexOf(b));
684
+ for (const rel of rels) {
685
+ labels.push(rel === '日生' ? '用神得日辰生'
686
+ : rel === '日克' ? '用神受日辰克'
687
+ : rel === '比和' ? '用神与日辰比和'
688
+ : rel === '泄' ? '用神泄于日辰' : '用神耗日辰');
689
+ }
690
+ for (const tag of [...new Set(lines.map((l) => l.chongTag))].filter(Boolean)) {
691
+ labels.push(tag === '月破' ? '' : `用神${tag}`);
692
+ }
693
+ const clean = labels.filter(Boolean).filter((x, i, arr) => arr.indexOf(x) === i);
694
+
695
+ return {
696
+ month: monthBranch ? { branch: monthBranch, element: monthElement } : null,
697
+ day: dayBranch ? { branch: dayBranch, element: dayElement } : null,
698
+ lines,
699
+ labels: clean,
700
+ note: '月建主这一段的旺衰,日辰看当天是扶是克;两处都只给状态标签,不折成分数,也不单独断吉凶。',
701
+ };
702
+ }