-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathtarot_draw.py
More file actions
303 lines (264 loc) · 13.6 KB
/
Copy pathtarot_draw.py
File metadata and controls
303 lines (264 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
"""Tarot card drawing with multiple spreads.
Subcommands:
one — single card
three — past / present / future
celtic — 10-card Celtic Cross (凯尔特十字)
relationship — 5-card relationship spread (关系阵)
daily — daily card
Args:
--seed N (optional, reproducibility)
--question "..." (optional, included in output)
Card data loaded from ``assets/tarot78.json`` when available; falls back to an
embedded summary table covering 22 大阿尔克那 + 56 小阿尔克那 by suit/number.
"""
from __future__ import annotations
import argparse
import json
import os
import random
import sys
import entropy
from utils import json_print, warn
# --------------------------------------------------------------------------- #
# Fallback minimal tarot deck (78 cards)
# --------------------------------------------------------------------------- #
MAJOR_ARCANA: list[dict] = [
{"number": 0, "en": "The Fool", "zh": "愚者", "keywords_up": "新开始, 冒险, 自由", "keywords_rev": "鲁莽, 犹豫, 风险"},
{"number": 1, "en": "The Magician", "zh": "魔术师", "keywords_up": "创造力, 意志, 行动", "keywords_rev": "欺骗, 三心二意"},
{"number": 2, "en": "The High Priestess","zh": "女祭司", "keywords_up": "直觉, 神秘, 内省", "keywords_rev": "压抑, 表象"},
{"number": 3, "en": "The Empress", "zh": "皇后", "keywords_up": "丰盛, 母性, 创造", "keywords_rev": "依赖, 占有"},
{"number": 4, "en": "The Emperor", "zh": "皇帝", "keywords_up": "权威, 秩序, 父性", "keywords_rev": "专断, 僵硬"},
{"number": 5, "en": "The Hierophant", "zh": "教皇", "keywords_up": "传统, 教化, 信仰", "keywords_rev": "反叛, 教条"},
{"number": 6, "en": "The Lovers", "zh": "恋人", "keywords_up": "结合, 抉择, 爱", "keywords_rev": "失衡, 第三者"},
{"number": 7, "en": "The Chariot", "zh": "战车", "keywords_up": "意志胜利, 前进", "keywords_rev": "失控, 受阻"},
{"number": 8, "en": "Strength", "zh": "力量", "keywords_up": "勇气, 内力, 慈悲", "keywords_rev": "自疑, 软弱"},
{"number": 9, "en": "The Hermit", "zh": "隐者", "keywords_up": "内省, 寻索, 智慧", "keywords_rev": "孤立, 退缩"},
{"number": 10, "en": "Wheel of Fortune", "zh": "命运之轮", "keywords_up": "转机, 命运转动", "keywords_rev": "厄运, 循环"},
{"number": 11, "en": "Justice", "zh": "正义", "keywords_up": "公正, 平衡, 因果", "keywords_rev": "偏私, 不公"},
{"number": 12, "en": "The Hanged Man", "zh": "倒吊人", "keywords_up": "牺牲, 暂停, 视角转换", "keywords_rev": "徒劳, 抗拒"},
{"number": 13, "en": "Death", "zh": "死神", "keywords_up": "结束, 转化, 新生", "keywords_rev": "停滞, 拒绝改变"},
{"number": 14, "en": "Temperance", "zh": "节制", "keywords_up": "中庸, 调和, 平衡", "keywords_rev": "极端, 失调"},
{"number": 15, "en": "The Devil", "zh": "恶魔", "keywords_up": "诱惑, 束缚, 物欲", "keywords_rev": "解脱, 觉察"},
{"number": 16, "en": "The Tower", "zh": "塔", "keywords_up": "突变, 崩塌, 启示", "keywords_rev": "避免灾难, 拖延"},
{"number": 17, "en": "The Star", "zh": "星星", "keywords_up": "希望, 灵感, 治愈", "keywords_rev": "失望, 自疑"},
{"number": 18, "en": "The Moon", "zh": "月亮", "keywords_up": "迷茫, 潜意识, 幻象", "keywords_rev": "澄清, 释放恐惧"},
{"number": 19, "en": "The Sun", "zh": "太阳", "keywords_up": "成功, 喜悦, 显化", "keywords_rev": "暂时阴霾, 过度"},
{"number": 20, "en": "Judgement", "zh": "审判", "keywords_up": "觉醒, 召唤, 重生", "keywords_rev": "犹豫, 自责"},
{"number": 21, "en": "The World", "zh": "世界", "keywords_up": "完成, 圆满, 整合", "keywords_rev": "未竟, 拖延"},
]
SUITS = [
{"en": "Wands", "zh": "权杖", "element": "火", "domain": "热情/事业/行动"},
{"en": "Cups", "zh": "圣杯", "element": "水", "domain": "情感/关系/直觉"},
{"en": "Swords", "zh": "宝剑", "element": "风", "domain": "思想/冲突/沟通"},
{"en": "Pentacles","zh": "钱币", "element": "土", "domain": "物质/工作/财富"},
]
COURT_RANKS = [
{"rank": "Page", "zh": "侍从", "number": 11},
{"rank": "Knight", "zh": "骑士", "number": 12},
{"rank": "Queen", "zh": "皇后", "number": 13},
{"rank": "King", "zh": "国王", "number": 14},
]
def build_minor_arcana() -> list[dict]:
out: list[dict] = []
for suit in SUITS:
for n in range(1, 11):
out.append({
"suit": suit["zh"], "suit_en": suit["en"],
"element": suit["element"], "domain": suit["domain"],
"number": n,
"en": f"{['Ace','Two','Three','Four','Five','Six','Seven','Eight','Nine','Ten'][n-1]} of {suit['en']}",
"zh": f"{suit['zh']}{['一','二','三','四','五','六','七','八','九','十'][n-1]}",
"keywords_up": f"{suit['domain']} 第{n}阶: 见详细解读",
"keywords_rev": f"{suit['domain']} 受阻或倒置",
})
for court in COURT_RANKS:
out.append({
"suit": suit["zh"], "suit_en": suit["en"],
"element": suit["element"], "domain": suit["domain"],
"number": court["number"],
"en": f"{court['rank']} of {suit['en']}",
"zh": f"{suit['zh']}{court['zh']}",
"keywords_up": f"{suit['domain']} 的{court['zh']}形象, 代表相应人物或品质",
"keywords_rev": f"{suit['domain']} 该角色的负面表现",
})
return out
def build_full_deck() -> list[dict]:
deck: list[dict] = []
for m in MAJOR_ARCANA:
deck.append({
"arcana": "major",
"number": m["number"],
"en": m["en"], "zh": m["zh"],
"suit": None, "element": None,
"keywords_up": m["keywords_up"],
"keywords_rev": m["keywords_rev"],
})
minor = build_minor_arcana()
for c in minor:
deck.append({
"arcana": "minor",
"number": c["number"],
"en": c["en"], "zh": c["zh"],
"suit": c["suit"], "suit_en": c["suit_en"],
"element": c["element"], "domain": c["domain"],
"keywords_up": c["keywords_up"],
"keywords_rev": c["keywords_rev"],
})
return deck
# Suit metadata for flattening the {major_arcana, minor_arcana} asset shape.
_ASSET_SUITS: dict[str, dict[str, str]] = {
"wands": {"zh": "权杖", "en": "Wands", "element": "火", "domain": "事业/行动"},
"cups": {"zh": "圣杯", "en": "Cups", "element": "水", "domain": "情感/关系"},
"swords": {"zh": "宝剑", "en": "Swords", "element": "风", "domain": "思想/冲突"},
"pentacles": {"zh": "星币", "en": "Pentacles", "element": "土", "domain": "物质/财务"},
}
_RANK_NUM: dict[str, int] = {
"Ace": 1, "Page": 11, "Knight": 12, "Queen": 13, "King": 14,
}
def _flatten_asset_deck(data: dict) -> list[dict]:
"""Convert the {major_arcana:[...], minor_arcana:{suit:[...]}} asset into
the flat card schema used by draw_cards. Returns [] on shape mismatch so
the caller can fall back to the embedded deck."""
major = data.get("major_arcana")
minor = data.get("minor_arcana")
if not isinstance(major, list) or not isinstance(minor, dict):
return []
deck: list[dict] = []
for m in major:
deck.append({
"arcana": "major",
"number": m.get("number"),
"en": m.get("name_en"), "zh": m.get("name_zh"),
"suit": None, "element": m.get("element"),
"keywords_up": "、".join(m.get("keywords_upright", [])) or m.get("upright_meaning", ""),
"keywords_rev": "、".join(m.get("keywords_reversed", [])) or m.get("reversed_meaning", ""),
})
for suit_key, cards in minor.items():
meta = _ASSET_SUITS.get(suit_key, {"zh": suit_key, "en": suit_key,
"element": None, "domain": ""})
for c in cards:
rank = str(c.get("rank", ""))
deck.append({
"arcana": "minor",
"number": _RANK_NUM.get(rank, int(rank) if rank.isdigit() else 0),
"en": f"{rank} of {meta['en']}",
"zh": c.get("name_zh"),
"suit": meta["zh"], "suit_en": meta["en"],
"element": meta["element"], "domain": meta["domain"],
"keywords_up": c.get("upright", ""),
"keywords_rev": c.get("reversed", ""),
})
return deck
def load_deck() -> list[dict]:
here = os.path.dirname(os.path.abspath(__file__))
candidates = [
os.path.join(here, "..", "assets", "tarot78.json"),
os.path.join(here, "assets", "tarot78.json"),
]
asset = next((p for p in candidates if os.path.exists(p)), None)
if asset:
try:
with open(asset, "r", encoding="utf-8") as f:
raw = json.load(f)
# Asset ships as {major_arcana, minor_arcana}; flatten it.
if isinstance(raw, dict):
deck = _flatten_asset_deck(raw)
elif isinstance(raw, list):
deck = raw
else:
deck = []
if len(deck) >= 78:
return deck
warn(f"tarot78.json yielded {len(deck)} cards (<78); using embedded deck")
except Exception as e:
warn(f"failed to load tarot78.json: {e}")
return build_full_deck()
# --------------------------------------------------------------------------- #
# Spreads
# --------------------------------------------------------------------------- #
SPREAD_DEFS: dict[str, list[str]] = {
"one": ["当下指引"],
"three": ["过去", "现在", "未来"],
"celtic": [
"现状 (核心)", "挑战 (对照)", "潜意识/根基", "近期过去",
"可能的未来", "近期未来", "你的态度", "外在影响", "希望与恐惧",
"最终结果",
],
"relationship": [
"你的状态", "对方状态", "双方共同的能量", "需要面对的挑战", "建议与方向",
],
"daily": ["今日提示"],
}
def draw_cards(rng: random.Random, n: int, deck: list[dict]) -> list[dict]:
indices = list(range(len(deck)))
rng.shuffle(indices)
drawn = []
for i in indices[:n]:
card = dict(deck[i])
orientation = "upright" if rng.random() < 0.5 else "reversed"
card["orientation"] = orientation
card["meaning_brief"] = (
card.get("keywords_up") if orientation == "upright"
else card.get("keywords_rev")
)
drawn.append(card)
return drawn
def position_summary(positions: list[str], cards: list[dict]) -> str:
parts = []
for pos, c in zip(positions, cards, strict=False):
orient_cn = "正位" if c["orientation"] == "upright" else "逆位"
parts.append(f"【{pos}】{c['zh']}({orient_cn})")
return " / ".join(parts)
# --------------------------------------------------------------------------- #
# CLI
# --------------------------------------------------------------------------- #
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(description="塔罗抽牌 (one/three/celtic/relationship/daily)")
sub = p.add_subparsers(dest="spread", required=True)
for s in ["one", "three", "celtic", "relationship", "daily"]:
ps = sub.add_parser(s, help=f"{s} 牌阵")
ps.add_argument("--seed", type=int, default=None)
ps.add_argument("--question", type=str, default=None)
ps.add_argument("--entropy", choices=["system", "quantum"], default="system",
help="熵源: system=OS CSPRNG (默认), quantum=ANU 量子真空真随机")
return p
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
deck = load_deck()
positions = SPREAD_DEFS.get(args.spread)
if positions is None:
json_print({"error": "unknown_spread", "valid": list(SPREAD_DEFS.keys())})
return 2
rng = entropy.get_rng(args.seed, args.entropy)
cards = draw_cards(rng, len(positions), deck)
out_cards = []
for pos, c in zip(positions, cards, strict=False):
out_cards.append({
"position_name": pos,
"card_name_zh": c.get("zh"),
"card_name_en": c.get("en"),
"number": c.get("number"),
"arcana": c.get("arcana"),
"suit": c.get("suit"),
"element": c.get("element"),
"orientation": c["orientation"],
"orientation_cn": "正位" if c["orientation"] == "upright" else "逆位",
"meaning_brief": c.get("meaning_brief"),
})
out = {
"spread": args.spread,
"spread_name_cn": {
"one": "单牌指引", "three": "三牌阵 (过去/现在/未来)",
"celtic": "凯尔特十字 (10 牌)",
"relationship": "关系阵 (5 牌)", "daily": "每日一牌",
}.get(args.spread),
"question": args.question,
"seed": args.seed,
"entropy": entropy.describe(rng, args.entropy, args.seed),
"cards": out_cards,
"summary": position_summary(positions, cards),
}
json_print(out)
return 0
if __name__ == "__main__":
sys.exit(main())