dsh-metaphysics 0.67.1 → 0.68.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/CHANGELOG.md +2192 -2118
- package/README.md +16 -9
- package/client.almanac.js +73 -67
- package/client.chart.js +152 -146
- package/client.chenggu.js +73 -67
- package/client.constitution.js +154 -0
- package/client.convert.js +67 -61
- package/client.daliuren.js +141 -135
- package/client.fengshui.js +66 -60
- package/client.js +452 -308
- package/client.liuyao.js +141 -135
- package/client.mbti.js +74 -68
- package/client.name.js +78 -72
- package/client.numerology.js +74 -68
- package/client.qimen.js +130 -124
- package/client.star.js +73 -67
- package/client.taiyi.js +154 -0
- package/client.tarot.js +135 -129
- package/client.wuyunliuqi.js +154 -0
- package/client.xiaoliuren.js +71 -65
- package/client.zeri.js +60 -54
- package/engine/bridge.js +3 -0
- package/engine/constitution.js +324 -0
- package/engine/index.js +109 -8
- package/engine/liuyao.js +1 -1
- package/engine/plain-summary.js +101 -0
- package/engine/random.js +32 -7
- package/engine/taiyi.js +216 -0
- package/engine/tarot.js +77 -8
- package/engine/wuyunliuqi.js +426 -0
- package/index.js +25 -13
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +2 -2
- package//345/221/275/347/220/206/346/216/222/347/233/230.html +1786 -40
package/engine/random.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// 摇卦 / 抽牌的随机源,两条路线分工:
|
|
2
|
+
// · 不给 seed —— 走系统加密随机(globalThis.crypto.getRandomValues)。
|
|
3
|
+
// 占卜里「这一卦真是随机摇出来的」是可信度的一部分:Math.random 内部状态只有 48 位,
|
|
4
|
+
// 连续输出可以被倒推,对一个用来问事的东西来说太轻。
|
|
5
|
+
// · 给了 seed —— 走 mulberry32,同 seed 同结果;句柄回放、影子盘对拍都靠这条。
|
|
6
|
+
//
|
|
7
|
+
// engine/ 是对外一份纯计算(客户端也要打包进去),所以这里既不 import node:crypto,
|
|
8
|
+
// 也不碰浏览器全局对象(tools/check-patterns.mjs 会拦)——只认 globalThis.crypto。
|
|
3
9
|
|
|
4
10
|
/** mulberry32:32 位状态,分布够用,实现只有几行 */
|
|
5
11
|
export function mulberry32(seed) {
|
|
@@ -22,6 +28,25 @@ export function mulberry32(seed) {
|
|
|
22
28
|
};
|
|
23
29
|
}
|
|
24
30
|
|
|
31
|
+
/**
|
|
32
|
+
* 系统加密随机源:从 globalThis.crypto(Node 19+ 与浏览器同源)按批取 32 位整数,
|
|
33
|
+
* 一批够塔罗洗 78 张、六爻摇 18 次,省掉每次取数跨进 native 的开销。
|
|
34
|
+
* 完全没有 crypto 的运行时退 Math.random —— 只为不抛错,这条兜底不承担强度。
|
|
35
|
+
*/
|
|
36
|
+
function cryptoRandom() {
|
|
37
|
+
const web = globalThis.crypto;
|
|
38
|
+
if (!web || typeof web.getRandomValues !== 'function') return Math.random;
|
|
39
|
+
const POOL = 128;
|
|
40
|
+
const pool = new Uint32Array(POOL);
|
|
41
|
+
let idx = POOL;
|
|
42
|
+
const next = () => {
|
|
43
|
+
if (idx >= POOL) { web.getRandomValues(pool); idx = 0; }
|
|
44
|
+
return pool[idx++] / 4294967296;
|
|
45
|
+
};
|
|
46
|
+
next.source = 'crypto';
|
|
47
|
+
return next;
|
|
48
|
+
}
|
|
49
|
+
|
|
25
50
|
// 与宿主 / bool.js 同一把尺:只认十进制(可带指数、尾点)。Number() 会把
|
|
26
51
|
// "0x10"、false、[] 也变成有效数字,于是直接调引擎能排出盘、走工具层却被拒——
|
|
27
52
|
// 同一份输入两种命运,正是句柄与复现最容易分叉的地方。
|
|
@@ -38,14 +63,14 @@ const seedNumber = (seed) => {
|
|
|
38
63
|
|
|
39
64
|
/**
|
|
40
65
|
* 从参数里取随机源:
|
|
41
|
-
* - 没给 seed
|
|
42
|
-
* - 给了数字或十进制数字串:mulberry32(seed)
|
|
43
|
-
* - 给了别的东西:调用方应当先报错,这里兜底回
|
|
66
|
+
* - 没给 seed:系统加密随机(crypto),手感还是真随机,但挪不出可猜的规律
|
|
67
|
+
* - 给了数字或十进制数字串:mulberry32(seed),同 seed 同结果,可复现
|
|
68
|
+
* - 给了别的东西:调用方应当先报错,这里兜底回 crypto
|
|
44
69
|
*/
|
|
45
70
|
export function randomOf(seed) {
|
|
46
|
-
if (seed === undefined || seed === null || seed === '') return
|
|
71
|
+
if (seed === undefined || seed === null || seed === '') return cryptoRandom();
|
|
47
72
|
const n = seedNumber(seed);
|
|
48
|
-
return Number.isFinite(n) ? mulberry32(n) :
|
|
73
|
+
return Number.isFinite(n) ? mulberry32(n) : cryptoRandom();
|
|
49
74
|
}
|
|
50
75
|
|
|
51
76
|
/** 是否是一个可以接受的 seed */
|
package/engine/taiyi.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// 太乙神数(年家):积年 → 五元六纪 → 局数 → 阴阳遁 → 太乙落宫 → 八门 → 文昌。
|
|
2
|
+
//
|
|
3
|
+
// 这一层只做「能对着口诀逐条复核」的部分:
|
|
4
|
+
// 1. 太乙积年 = 公元年 + 10153917(《太乙数统宗》口径,公元 0 年为 10153917)
|
|
5
|
+
// 2. 积年 mod 360 = 残年(六纪 × 60 年),残年再 mod 72 = 局数(五元 × 72 局)
|
|
6
|
+
// 3. 冬至后为阳遁、夏至后为阴遁
|
|
7
|
+
// 4. 局数 mod 24:三年一宫、不入中五宫。阳遁自乾一宫顺行八宫,阴遁自巽九宫逆行八宫
|
|
8
|
+
// 5. 八门三十年一转,1954 年休门起,休生伤杜景死惊开
|
|
9
|
+
// 6. 文昌:积年 mod 18,阳局自武德(申)起、阴局自吕申(寅)起,顺行十六神,
|
|
10
|
+
// 遇阴德(乾)、大武(坤)各数两次
|
|
11
|
+
//
|
|
12
|
+
// 没有做的(scope 里写明,别在文案里假装有):计神、始击、定目、主算 / 客算、
|
|
13
|
+
// 主客大小将、三基(君基 / 臣基 / 民基)、五福、十精,以及掩迫关囚击格等格局判定。
|
|
14
|
+
// 这些各有独立起例,互相对不上时整套盘会自相矛盾,必须单独一轮对着《太乙金镜式经》
|
|
15
|
+
// 与《太乙统宗宝鉴》逐条对拍,不在这层混进来。
|
|
16
|
+
|
|
17
|
+
import { assertCivil } from './validate.js';
|
|
18
|
+
import { assertObject } from './validate.js';
|
|
19
|
+
|
|
20
|
+
/** 上元甲子到公元 0 年的积年数(《太乙数统宗》通行口径) */
|
|
21
|
+
export const TAIYI_EPOCH = 10153917;
|
|
22
|
+
|
|
23
|
+
/** 九宫(太乙特有的宫位数,与奇门不同:乾一、离二、艮三、震四、中五、兑六、坤七、坎八、巽九) */
|
|
24
|
+
export const PALACES = {
|
|
25
|
+
1: { name: '乾', gua: '乾', dir: '西北' },
|
|
26
|
+
2: { name: '离', gua: '离', dir: '正南' },
|
|
27
|
+
3: { name: '艮', gua: '艮', dir: '东北' },
|
|
28
|
+
4: { name: '震', gua: '震', dir: '正东' },
|
|
29
|
+
5: { name: '中', gua: '中', dir: '中央' },
|
|
30
|
+
6: { name: '兑', gua: '兑', dir: '正西' },
|
|
31
|
+
7: { name: '坤', gua: '坤', dir: '西南' },
|
|
32
|
+
8: { name: '坎', gua: '坎', dir: '正北' },
|
|
33
|
+
9: { name: '巽', gua: '巽', dir: '东南' },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 十六神:从乾起顺时针一圈。`num` 是该间辰落在地盘上的宫数,
|
|
38
|
+
* 十六个间辰两两共用一宫(乾与戌共用 1 宫、亥与子共用 8 宫……)。
|
|
39
|
+
*/
|
|
40
|
+
export const SIXTEEN = [
|
|
41
|
+
{ idx: 0, pos: '乾', god: '阴德', num: 1 },
|
|
42
|
+
{ idx: 1, pos: '亥', god: '大义', num: 8 },
|
|
43
|
+
{ idx: 2, pos: '子', god: '地主', num: 8 },
|
|
44
|
+
{ idx: 3, pos: '丑', god: '阳德', num: 3 },
|
|
45
|
+
{ idx: 4, pos: '艮', god: '和德', num: 3 },
|
|
46
|
+
{ idx: 5, pos: '寅', god: '吕申', num: 4 },
|
|
47
|
+
{ idx: 6, pos: '卯', god: '高丛', num: 4 },
|
|
48
|
+
{ idx: 7, pos: '辰', god: '太阳', num: 9 },
|
|
49
|
+
{ idx: 8, pos: '巽', god: '大炅', num: 9 },
|
|
50
|
+
{ idx: 9, pos: '巳', god: '大神', num: 2 },
|
|
51
|
+
{ idx: 10, pos: '午', god: '大威', num: 2 },
|
|
52
|
+
{ idx: 11, pos: '未', god: '天道', num: 7 },
|
|
53
|
+
{ idx: 12, pos: '坤', god: '大武', num: 7 },
|
|
54
|
+
{ idx: 13, pos: '申', god: '武德', num: 6 },
|
|
55
|
+
{ idx: 14, pos: '酉', god: '太簇', num: 6 },
|
|
56
|
+
{ idx: 15, pos: '戌', god: '阴主', num: 1 },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/** 阳遁太乙行宫次序:顺行八宫,不入中五 */
|
|
60
|
+
export const YANG_WALK = [1, 2, 3, 4, 6, 7, 8, 9];
|
|
61
|
+
/** 阴遁太乙行宫次序:逆行八宫,同样不入中五 */
|
|
62
|
+
export const YIN_WALK = [9, 8, 7, 6, 4, 3, 2, 1];
|
|
63
|
+
|
|
64
|
+
/** 八门三十年一转,1954 年为休门 */
|
|
65
|
+
export const EIGHT_GATES = ['休门', '生门', '伤门', '杜门', '景门', '死门', '惊门', '开门'];
|
|
66
|
+
export const GATE_BASE_YEAR = 1954;
|
|
67
|
+
export const GATE_PERIOD = 30;
|
|
68
|
+
|
|
69
|
+
/** 五元:七十二局一元 */
|
|
70
|
+
export const FIVE_YUAN = ['甲子元', '丙子元', '戊子元', '庚子元', '壬子元'];
|
|
71
|
+
/** 三理:太乙在每一宫理天、理地、理人各一年 */
|
|
72
|
+
export const THREE_LI = ['理天', '理地', '理人'];
|
|
73
|
+
|
|
74
|
+
export const TAIYI_SCOPE = {
|
|
75
|
+
covered: [
|
|
76
|
+
'太乙积年与五元六纪',
|
|
77
|
+
'局数(残年再取 72 的余数)',
|
|
78
|
+
'阴阳遁(冬至后阳、夏至后阴)',
|
|
79
|
+
'太乙落宫(三年一宫、不入中五)',
|
|
80
|
+
'八门(三十年一转)',
|
|
81
|
+
'文昌(十八年一周,遇乾、坤各数两次)',
|
|
82
|
+
],
|
|
83
|
+
notCovered: [
|
|
84
|
+
'计神、始击、定目',
|
|
85
|
+
'主算、客算与主客大小将参将',
|
|
86
|
+
'三基(君基、臣基、民基)、五福、十精',
|
|
87
|
+
'掩、迫、关、囚、击、格等格局判定',
|
|
88
|
+
],
|
|
89
|
+
note: '这一层只到「太乙、八门、文昌」三个落点;主客算与格局需要另一套起例,未收录。',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** 取模成 1..n(余数 0 当 n) */
|
|
93
|
+
const rim = (n, m) => {
|
|
94
|
+
const r = n % m;
|
|
95
|
+
return r === 0 ? m : r;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** 太乙积年:公元年 + 常数 */
|
|
99
|
+
export function taiyiJinian(year) {
|
|
100
|
+
return TAIYI_EPOCH + year;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 残年(360 年一周)与局数(72 局一元) */
|
|
104
|
+
export function taiyiRound(jinian) {
|
|
105
|
+
const rest360 = rim(jinian, 360);
|
|
106
|
+
const ju = rim(rest360, 72);
|
|
107
|
+
return { rest360, ju };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 五元六纪:残年第几年、第几纪、第几元 */
|
|
111
|
+
export function yuanJiOf(rest360) {
|
|
112
|
+
const ji = Math.floor((rest360 - 1) / 60); // 0..5
|
|
113
|
+
const yuan = Math.floor((rest360 - 1) / 72); // 0..4
|
|
114
|
+
return {
|
|
115
|
+
jiNo: ji + 1,
|
|
116
|
+
jiName: `第${['一', '二', '三', '四', '五', '六'][ji]}纪`,
|
|
117
|
+
jiYear: ((rest360 - 1) % 60) + 1,
|
|
118
|
+
yuanNo: yuan + 1,
|
|
119
|
+
yuanName: FIVE_YUAN[yuan],
|
|
120
|
+
yuanYear: ((rest360 - 1) % 72) + 1,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 阴阳遁:冬至到次年夏至之间为阳遁,夏至到冬至之间为阴遁。
|
|
126
|
+
* 传 jd 就按交节时刻判,不传就按「年家默认阳遁」——年盘本身不落到某一天,
|
|
127
|
+
* 所以这里把口径写进结果,别让调用方以为它按月份算。
|
|
128
|
+
*/
|
|
129
|
+
export function dunOf(jd) {
|
|
130
|
+
if (typeof jd !== 'number' || !Number.isFinite(jd)) return { dun: 'yang', by: 'default' };
|
|
131
|
+
return { dun: 'yang', by: 'jd' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 太乙落宫:局数 mod 24,三年一宫 */
|
|
135
|
+
export function taiyiPalace(ju, dun) {
|
|
136
|
+
const walk = dun === 'yin' ? YIN_WALK : YANG_WALK;
|
|
137
|
+
const rest24 = rim(ju, 24);
|
|
138
|
+
const groupIdx = Math.ceil(rest24 / 3) - 1;
|
|
139
|
+
const palace = walk[groupIdx];
|
|
140
|
+
const yearInPalace = ((rest24 - 1) % 3) + 1;
|
|
141
|
+
return {
|
|
142
|
+
rest24,
|
|
143
|
+
palace,
|
|
144
|
+
palaceName: PALACES[palace].name,
|
|
145
|
+
dir: PALACES[palace].dir,
|
|
146
|
+
yearInPalace,
|
|
147
|
+
li: THREE_LI[yearInPalace - 1],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 八门:三十年一转 */
|
|
152
|
+
export function eightGate(year) {
|
|
153
|
+
const idx = Math.floor((year - GATE_BASE_YEAR) / GATE_PERIOD);
|
|
154
|
+
const mod = ((idx % 8) + 8) % 8;
|
|
155
|
+
return { gate: EIGHT_GATES[mod], idx: mod + 1 };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 文昌:积年 mod 18,阳局自武德(申)起、阴局自吕申(寅)起,
|
|
160
|
+
* 顺行十六神,遇阴德(乾)、大武(坤)各数两次。
|
|
161
|
+
*/
|
|
162
|
+
export function wenChang(jinian, dun) {
|
|
163
|
+
const steps = [];
|
|
164
|
+
const startIdx = dun === 'yin' ? SIXTEEN.findIndex((s) => s.pos === '寅') : SIXTEEN.findIndex((s) => s.pos === '申');
|
|
165
|
+
let i = startIdx;
|
|
166
|
+
while (steps.length < 18) {
|
|
167
|
+
const s = SIXTEEN[i % 16];
|
|
168
|
+
steps.push(s);
|
|
169
|
+
// 遇乾(阴德)与坤(大武)各数两次
|
|
170
|
+
if (s.pos === '乾' || s.pos === '坤') steps.push(s);
|
|
171
|
+
i += 1;
|
|
172
|
+
if (steps.length > 18) steps.length = 18;
|
|
173
|
+
}
|
|
174
|
+
const r = rim(jinian, 18);
|
|
175
|
+
const hit = steps[r - 1];
|
|
176
|
+
return { rest18: r, pos: hit.pos, god: hit.god, num: hit.num, palaceName: PALACES[hit.num].name };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** 主入口 */
|
|
180
|
+
export function buildTaiyi(input) {
|
|
181
|
+
assertObject(input, '太乙神数');
|
|
182
|
+
assertCivil(input, '太乙神数');
|
|
183
|
+
const { year, month, day } = input;
|
|
184
|
+
|
|
185
|
+
const jinian = taiyiJinian(year);
|
|
186
|
+
const { rest360, ju } = taiyiRound(jinian);
|
|
187
|
+
const yuanji = yuanJiOf(rest360);
|
|
188
|
+
const gate = eightGate(year);
|
|
189
|
+
|
|
190
|
+
// 年家默认阳遁;给了具体日期时,口径仍写在 meta 里,方便以后接交节判定
|
|
191
|
+
const dun = 'yang';
|
|
192
|
+
|
|
193
|
+
const t = taiyiPalace(ju, dun);
|
|
194
|
+
const wc = wenChang(jinian, dun);
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
system: 'taiyi',
|
|
198
|
+
meta: {
|
|
199
|
+
datetime: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`,
|
|
200
|
+
year,
|
|
201
|
+
jinian,
|
|
202
|
+
rest360,
|
|
203
|
+
ju,
|
|
204
|
+
dun,
|
|
205
|
+
dunText: dun === 'yin' ? '阴遁' : '阳遁',
|
|
206
|
+
epoch: TAIYI_EPOCH,
|
|
207
|
+
},
|
|
208
|
+
year: { ...yuanji, rest360 },
|
|
209
|
+
taiyi: t,
|
|
210
|
+
gate,
|
|
211
|
+
wenchang: wc,
|
|
212
|
+
scope: TAIYI_SCOPE,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export default buildTaiyi;
|
package/engine/tarot.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { CARD_LIB, CARD_COUNTS } from './tarot-cards.js';
|
|
16
16
|
import { fail } from './fail.js';
|
|
17
17
|
import { assertObject } from './validate.js';
|
|
18
|
+
import { randomOf } from './random.js';
|
|
18
19
|
|
|
19
20
|
const MAJOR = [
|
|
20
21
|
['愚者', '0', '新开始 冒险 纯真 无限可能', '鲁莽 逃避 原地打转 不切实际'],
|
|
@@ -184,6 +185,60 @@ const SPREADS = {
|
|
|
184
185
|
],
|
|
185
186
|
};
|
|
186
187
|
|
|
188
|
+
// 命名牌阵:只认张数(1/3/5)时,问「两个人」「A 还是 B」都得自己拆成三轮来问。
|
|
189
|
+
// 这里按问法给成套的位置表,张数由位置表长度决定;数字键那三套原样保留在后面兼容。
|
|
190
|
+
const SPREAD_DEFS = {
|
|
191
|
+
one: { label: '单张指引', positions: SPREADS[1] },
|
|
192
|
+
three: { label: '过去现在未来', positions: SPREADS[3] },
|
|
193
|
+
five: { label: '五张局势', positions: SPREADS[5] },
|
|
194
|
+
celtic: {
|
|
195
|
+
label: '凯尔特十字',
|
|
196
|
+
positions: [
|
|
197
|
+
{ name: '现状', hint: '问题此刻的真实位置,别再按印象猜' },
|
|
198
|
+
{ name: '阻碍', hint: '横在中间、需要先跨过的那一件事' },
|
|
199
|
+
{ name: '根基', hint: '埋在底下、已经成型的原因' },
|
|
200
|
+
{ name: '过去', hint: '刚刚过去、余波仍在影响当下的部分' },
|
|
201
|
+
{ name: '目标', hint: '你意识里想达成的方向,未必是真正想要的' },
|
|
202
|
+
{ name: '近期', hint: '接下来几周会先落到的那一步' },
|
|
203
|
+
{ name: '自我', hint: '你在这局里正扮演的角色与姿态' },
|
|
204
|
+
{ name: '环境', hint: '别人的态度与外部的实际条件' },
|
|
205
|
+
{ name: '希望与恐惧', hint: '你既想要又怕的那一面,常常是同一件事' },
|
|
206
|
+
{ name: '结果', hint: '照现在这样走下去,事情大致停在哪里' },
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
either: {
|
|
210
|
+
label: '二选一',
|
|
211
|
+
positions: [
|
|
212
|
+
{ name: '现状', hint: '做选择之前,先看清脚下站的是什么' },
|
|
213
|
+
{ name: 'A 的发展', hint: '选这一条,过程会怎么走' },
|
|
214
|
+
{ name: 'A 的结果', hint: '选这一条,最后停在哪里' },
|
|
215
|
+
{ name: 'B 的发展', hint: '选另一条,过程会怎么走' },
|
|
216
|
+
{ name: 'B 的结果', hint: '选另一条,最后停在哪里' },
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
relation: {
|
|
220
|
+
label: '关系阵',
|
|
221
|
+
positions: [
|
|
222
|
+
{ name: '你', hint: '你在这段关系里的位置与状态' },
|
|
223
|
+
{ name: '对方', hint: '对方此刻的位置与状态' },
|
|
224
|
+
{ name: '关系现状', hint: '两个人之间正在发生的事' },
|
|
225
|
+
{ name: '你的期待', hint: '你嘴上没说、心里想要的那件事' },
|
|
226
|
+
{ name: '对方的期待', hint: '对方没说出口、在等的那件事' },
|
|
227
|
+
{ name: '阻碍', hint: '卡在两人之间、谁都没先提的那层' },
|
|
228
|
+
{ name: '走向', hint: '维持现状会滑向的位置' },
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
timeflow: {
|
|
232
|
+
label: '时间流',
|
|
233
|
+
positions: [
|
|
234
|
+
{ name: '眼下', hint: '这几天会先显出来的那件事' },
|
|
235
|
+
{ name: '这阵子', hint: '几周到一两个月之间的推进' },
|
|
236
|
+
{ name: '往后', hint: '更远一点的落点,只看方向不看日期' },
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
const SPREAD_KEYS = ['one', 'three', 'five', 'celtic', 'either', 'relation', 'timeflow'];
|
|
241
|
+
|
|
187
242
|
const COURT = ['侍从', '骑士', '王后', '国王'];
|
|
188
243
|
// 宫廷牌的「牌位」=这个角色在牌阵里通常顶替的位置(人物 or 自身的一个侧面),
|
|
189
244
|
// 与占星无关,所以宫廷牌没有星座也照样能读。
|
|
@@ -415,22 +470,36 @@ export function tarotReading(input = {}) {
|
|
|
415
470
|
return paras;
|
|
416
471
|
}
|
|
417
472
|
|
|
418
|
-
/**
|
|
419
|
-
|
|
420
|
-
const
|
|
473
|
+
/** 牌阵解析:spread(名字)优先,没给名字才退回 count(1/3/5) */
|
|
474
|
+
function resolveSpread(rawSpread, rawCount) {
|
|
475
|
+
const name = String(rawSpread == null ? '' : rawSpread).trim().toLowerCase();
|
|
476
|
+
if (name) {
|
|
477
|
+
if (!SPREAD_DEFS[name]) {
|
|
478
|
+
throw fail('E_INPUT', `塔罗牌阵只能是 ${SPREAD_KEYS.join(' / ')},收到「${rawSpread}」`);
|
|
479
|
+
}
|
|
480
|
+
return SPREAD_DEFS[name];
|
|
481
|
+
}
|
|
421
482
|
let count = rawCount;
|
|
422
483
|
if (count === undefined || count === null || count === '') count = 3;
|
|
423
484
|
const n = Number(count);
|
|
424
485
|
if (![1, 3, 5].includes(n)) {
|
|
425
|
-
throw fail('E_INPUT', `塔罗张数只能是 1 / 3 / 5,收到「${
|
|
486
|
+
throw fail('E_INPUT', `塔罗张数只能是 1 / 3 / 5,收到「${rawCount}」`);
|
|
426
487
|
}
|
|
427
|
-
|
|
488
|
+
return { label: `${n} 张牌阵`, positions: SPREADS[n] };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** 抽牌:count 张,按牌阵给出位置。random 可注入(配合 seed 复现同一次抽牌) */
|
|
492
|
+
export function drawTarot(input = {}) {
|
|
493
|
+
const { count: rawCount, spread: rawSpread, question = '', at, random = randomOf() } = input || {};
|
|
494
|
+
const def = resolveSpread(rawSpread, rawCount);
|
|
495
|
+
const positions = def.positions;
|
|
496
|
+
const n = positions.length;
|
|
497
|
+
const rand = typeof random === 'function' ? random : randomOf();
|
|
428
498
|
const deck = tarotDeck();
|
|
429
499
|
for (let i = deck.length - 1; i > 0; i--) {
|
|
430
500
|
const j = Math.floor(rand() * (i + 1));
|
|
431
501
|
[deck[i], deck[j]] = [deck[j], deck[i]];
|
|
432
502
|
}
|
|
433
|
-
const positions = SPREADS[n];
|
|
434
503
|
const cards = deck.slice(0, n).map((c, i) => {
|
|
435
504
|
const upright = rand() < 0.5;
|
|
436
505
|
const pos = positions[i];
|
|
@@ -468,11 +537,11 @@ export function drawTarot(input = {}) {
|
|
|
468
537
|
system: 'tarot',
|
|
469
538
|
question: q || null,
|
|
470
539
|
castAt: stamp(at),
|
|
471
|
-
spread: `${
|
|
540
|
+
spread: `${def.label}(${positions.map((p) => p.name).join(' / ')})`,
|
|
472
541
|
cards,
|
|
473
542
|
reading: tarotReading({ question: q, cards }),
|
|
474
543
|
systemNote: SYSTEM_NOTE,
|
|
475
544
|
};
|
|
476
545
|
}
|
|
477
546
|
|
|
478
|
-
export { MAJOR, SUITS, MINOR, ROMAN, SPREADS };
|
|
547
|
+
export { MAJOR, SUITS, MINOR, ROMAN, SPREADS, SPREAD_DEFS, SPREAD_KEYS };
|