433 lines
15 KiB
Python
433 lines
15 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
from functools import lru_cache
|
|
from typing import Any
|
|
|
|
from backend.features.heaven.engine import (
|
|
BRANCH_ELEMENT,
|
|
ELEMENT_CONTROLS,
|
|
ELEMENT_GENERATES,
|
|
LINE_POSITIONS,
|
|
Solar,
|
|
)
|
|
|
|
|
|
SHANGHAI = timezone(timedelta(hours=8), "Asia/Shanghai")
|
|
|
|
TRIGRAM_BITS = {
|
|
"乾": (1, 1, 1),
|
|
"兑": (1, 1, 0),
|
|
"离": (1, 0, 1),
|
|
"震": (1, 0, 0),
|
|
"巽": (0, 1, 1),
|
|
"坎": (0, 1, 0),
|
|
"艮": (0, 0, 1),
|
|
"坤": (0, 0, 0),
|
|
}
|
|
BITS_TRIGRAM = {bits: name for name, bits in TRIGRAM_BITS.items()}
|
|
PALACE_ELEMENT = {
|
|
"乾": "金",
|
|
"兑": "金",
|
|
"离": "火",
|
|
"震": "木",
|
|
"巽": "木",
|
|
"坎": "水",
|
|
"艮": "土",
|
|
"坤": "土",
|
|
}
|
|
|
|
# 京房纳甲通行表。每组均按初爻至三爻、四爻至上爻排列。
|
|
NAJIA = {
|
|
"乾": {
|
|
"inner": (("甲", "子"), ("甲", "寅"), ("甲", "辰")),
|
|
"outer": (("壬", "午"), ("壬", "申"), ("壬", "戌")),
|
|
},
|
|
"坤": {
|
|
"inner": (("乙", "未"), ("乙", "巳"), ("乙", "卯")),
|
|
"outer": (("癸", "丑"), ("癸", "亥"), ("癸", "酉")),
|
|
},
|
|
"震": {
|
|
"inner": (("庚", "子"), ("庚", "寅"), ("庚", "辰")),
|
|
"outer": (("庚", "午"), ("庚", "申"), ("庚", "戌")),
|
|
},
|
|
"巽": {
|
|
"inner": (("辛", "丑"), ("辛", "亥"), ("辛", "酉")),
|
|
"outer": (("辛", "未"), ("辛", "巳"), ("辛", "卯")),
|
|
},
|
|
"坎": {
|
|
"inner": (("戊", "寅"), ("戊", "辰"), ("戊", "午")),
|
|
"outer": (("戊", "申"), ("戊", "戌"), ("戊", "子")),
|
|
},
|
|
"离": {
|
|
"inner": (("己", "卯"), ("己", "丑"), ("己", "亥")),
|
|
"outer": (("己", "酉"), ("己", "未"), ("己", "巳")),
|
|
},
|
|
"艮": {
|
|
"inner": (("丙", "辰"), ("丙", "午"), ("丙", "申")),
|
|
"outer": (("丙", "戌"), ("丙", "子"), ("丙", "寅")),
|
|
},
|
|
"兑": {
|
|
"inner": (("丁", "巳"), ("丁", "卯"), ("丁", "丑")),
|
|
"outer": (("丁", "亥"), ("丁", "酉"), ("丁", "未")),
|
|
},
|
|
}
|
|
|
|
PALACE_STAGES = (
|
|
("本宫", (), 6),
|
|
("一世", (0,), 1),
|
|
("二世", (0, 1), 2),
|
|
("三世", (0, 1, 2), 3),
|
|
("四世", (0, 1, 2, 3), 4),
|
|
("五世", (0, 1, 2, 3, 4), 5),
|
|
("游魂", (0, 1, 2, 4), 4),
|
|
("归魂", (4,), 3),
|
|
)
|
|
|
|
SIX_SPIRITS = ("青龙", "朱雀", "勾陈", "螣蛇", "白虎", "玄武")
|
|
SPIRIT_START = {
|
|
"甲": 0,
|
|
"乙": 0,
|
|
"丙": 1,
|
|
"丁": 1,
|
|
"戊": 2,
|
|
"己": 3,
|
|
"庚": 4,
|
|
"辛": 4,
|
|
"壬": 5,
|
|
"癸": 5,
|
|
}
|
|
|
|
BRANCH_CLASH = {
|
|
"子": "午", "午": "子", "丑": "未", "未": "丑",
|
|
"寅": "申", "申": "寅", "卯": "酉", "酉": "卯",
|
|
"辰": "戌", "戌": "辰", "巳": "亥", "亥": "巳",
|
|
}
|
|
BRANCH_COMBINE = {
|
|
"子": "丑", "丑": "子", "寅": "亥", "亥": "寅",
|
|
"卯": "戌", "戌": "卯", "辰": "酉", "酉": "辰",
|
|
"巳": "申", "申": "巳", "午": "未", "未": "午",
|
|
}
|
|
BRANCH_HARM = {
|
|
"子": "未", "未": "子", "丑": "午", "午": "丑",
|
|
"寅": "巳", "巳": "寅", "卯": "辰", "辰": "卯",
|
|
"申": "亥", "亥": "申", "酉": "戌", "戌": "酉",
|
|
}
|
|
THREE_PUNISHMENTS = (frozenset("寅巳申"), frozenset("丑未戌"), frozenset("子卯"))
|
|
SELF_PUNISHMENT = set("辰午酉亥")
|
|
|
|
ADVANCE_PAIRS = {
|
|
("亥", "子"), ("寅", "卯"), ("巳", "午"), ("申", "酉"),
|
|
("丑", "辰"), ("辰", "未"), ("未", "戌"), ("戌", "丑"),
|
|
}
|
|
RETREAT_PAIRS = {(target, source) for source, target in ADVANCE_PAIRS}
|
|
|
|
|
|
def build_six_yao_chart(values: list[int], cast_at: str = "") -> dict[str, Any]:
|
|
"""Return a deterministic Jing Fang Na Jia chart for a six-coin result."""
|
|
if len(values) != 6 or any(value not in {6, 7, 8, 9} for value in values):
|
|
raise ValueError("六爻必须由六、七、八、九组成,且从初爻到上爻排列。")
|
|
observed_at = _parse_cast_at(cast_at)
|
|
solar = Solar.fromYmdHms(
|
|
observed_at.year,
|
|
observed_at.month,
|
|
observed_at.day,
|
|
observed_at.hour,
|
|
observed_at.minute,
|
|
observed_at.second,
|
|
)
|
|
lunar = solar.getLunar()
|
|
month_gz = lunar.getMonthInGanZhiExact()
|
|
day_gz = lunar.getDayInGanZhiExact2()
|
|
time_gz = lunar.getTimeInGanZhi()
|
|
void_branches = tuple(lunar.getDayXunKongExact2())
|
|
month_branch = month_gz[1]
|
|
day_stem, day_branch = day_gz[0], day_gz[1]
|
|
|
|
bits = tuple(1 if value % 2 else 0 for value in values)
|
|
transformed_values = tuple(7 if value == 6 else 8 if value == 9 else value for value in values)
|
|
transformed_bits = tuple(1 if value % 2 else 0 for value in transformed_values)
|
|
palace = _palace_map()[bits]
|
|
palace_element = PALACE_ELEMENT[palace["trigram"]]
|
|
self_position = int(palace["self_position"])
|
|
response_position = self_position + 3 if self_position <= 3 else self_position - 3
|
|
najia = _najia_for_bits(bits)
|
|
transformed_najia = _najia_for_bits(transformed_bits)
|
|
spirits = tuple(
|
|
SIX_SPIRITS[(SPIRIT_START[day_stem] + index) % 6] for index in range(6)
|
|
)
|
|
|
|
lines: list[dict[str, Any]] = []
|
|
for index, ((stem, branch), value) in enumerate(zip(najia, values)):
|
|
position = index + 1
|
|
element = BRANCH_ELEMENT[branch]
|
|
transformed_stem, transformed_branch = transformed_najia[index]
|
|
transformed_element = BRANCH_ELEMENT[transformed_branch]
|
|
line = {
|
|
"position": position,
|
|
"position_name": LINE_POSITIONS[index],
|
|
"value": value,
|
|
"yin_yang": "阳" if value % 2 else "阴",
|
|
"moving": value in {6, 9},
|
|
"stem": stem,
|
|
"branch": branch,
|
|
"element": element,
|
|
"relative": _six_relative(palace_element, element),
|
|
"spirit": spirits[index],
|
|
"role": "世" if position == self_position else "应" if position == response_position else "",
|
|
"void": branch in void_branches,
|
|
"month": _calendar_relation("月", month_branch, branch),
|
|
"day": _calendar_relation("日", day_branch, branch),
|
|
}
|
|
if line["moving"]:
|
|
line["transformation"] = {
|
|
"value": transformed_values[index],
|
|
"yin_yang": "阳" if transformed_values[index] % 2 else "阴",
|
|
"stem": transformed_stem,
|
|
"branch": transformed_branch,
|
|
"element": transformed_element,
|
|
"relative": _six_relative(palace_element, transformed_element),
|
|
"relation_to_origin": _transformation_relation(
|
|
branch,
|
|
element,
|
|
transformed_branch,
|
|
transformed_element,
|
|
),
|
|
}
|
|
lines.append(line)
|
|
|
|
hidden = _hidden_spirits(palace["trigram"], palace_element, lines)
|
|
for item in hidden:
|
|
lines[item["position"] - 1].setdefault("hidden_spirits", []).append(item)
|
|
|
|
return {
|
|
"method": "京房纳甲·八宫世应",
|
|
"method_version": "xiaobai-six-yao-v1",
|
|
"sources": ["jingfang", "huozhulin", "zengshan"],
|
|
"cast_at": observed_at.isoformat(timespec="seconds"),
|
|
"timezone": "Asia/Shanghai",
|
|
"day_boundary": "晚子时仍按民用当日排日柱",
|
|
"calendar": {
|
|
"month": month_gz,
|
|
"month_branch": month_branch,
|
|
"day": day_gz,
|
|
"day_branch": day_branch,
|
|
"time": time_gz,
|
|
"day_void": "".join(void_branches),
|
|
},
|
|
"palace": {
|
|
"name": f"{palace['trigram']}宫",
|
|
"trigram": palace["trigram"],
|
|
"element": palace_element,
|
|
"stage": palace["stage"],
|
|
"self_position": self_position,
|
|
"response_position": response_position,
|
|
},
|
|
"lines": lines,
|
|
"hidden_spirits": hidden,
|
|
"branch_pattern": _hexagram_branch_pattern(lines),
|
|
"relationships": _significant_line_relationships(lines),
|
|
}
|
|
|
|
|
|
def _parse_cast_at(raw: str) -> datetime:
|
|
value = str(raw or "").strip()
|
|
if not value:
|
|
return datetime.now(SHANGHAI)
|
|
try:
|
|
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
except ValueError as exc:
|
|
raise ValueError("起卦时间格式不正确。") from exc
|
|
if parsed.tzinfo is None:
|
|
parsed = parsed.replace(tzinfo=SHANGHAI)
|
|
return parsed.astimezone(SHANGHAI)
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def _palace_map() -> dict[tuple[int, ...], dict[str, Any]]:
|
|
result: dict[tuple[int, ...], dict[str, Any]] = {}
|
|
for trigram, trigram_bits in TRIGRAM_BITS.items():
|
|
pure = trigram_bits + trigram_bits
|
|
for stage, flipped, self_position in PALACE_STAGES:
|
|
bits = list(pure)
|
|
for index in flipped:
|
|
bits[index] = 1 - bits[index]
|
|
key = tuple(bits)
|
|
if key in result:
|
|
raise RuntimeError("八宫映射出现重复卦象。")
|
|
result[key] = {
|
|
"trigram": trigram,
|
|
"stage": stage,
|
|
"self_position": self_position,
|
|
}
|
|
if len(result) != 64:
|
|
raise RuntimeError("八宫映射未覆盖六十四卦。")
|
|
return result
|
|
|
|
|
|
def _najia_for_bits(bits: tuple[int, ...]) -> tuple[tuple[str, str], ...]:
|
|
inner = BITS_TRIGRAM[bits[:3]]
|
|
outer = BITS_TRIGRAM[bits[3:]]
|
|
return tuple(NAJIA[inner]["inner"] + NAJIA[outer]["outer"])
|
|
|
|
|
|
def _six_relative(palace_element: str, line_element: str) -> str:
|
|
if line_element == palace_element:
|
|
return "兄弟"
|
|
if ELEMENT_GENERATES[line_element] == palace_element:
|
|
return "父母"
|
|
if ELEMENT_GENERATES[palace_element] == line_element:
|
|
return "子孙"
|
|
if ELEMENT_CONTROLS[palace_element] == line_element:
|
|
return "妻财"
|
|
return "官鬼"
|
|
|
|
|
|
def _calendar_relation(prefix: str, actor_branch: str, line_branch: str) -> dict[str, Any]:
|
|
actor_element = BRANCH_ELEMENT[actor_branch]
|
|
line_element = BRANCH_ELEMENT[line_branch]
|
|
labels = []
|
|
if actor_branch == line_branch:
|
|
labels.append(f"临{prefix}{'建' if prefix == '月' else '辰'}")
|
|
if BRANCH_CLASH[actor_branch] == line_branch:
|
|
labels.append("月破" if prefix == "月" else "日冲")
|
|
if BRANCH_COMBINE[actor_branch] == line_branch:
|
|
labels.append(f"{prefix}合")
|
|
if BRANCH_HARM[actor_branch] == line_branch:
|
|
labels.append(f"{prefix}害")
|
|
element_relation = _actor_element_relation(actor_element, line_element, prefix)
|
|
return {
|
|
"branch": actor_branch,
|
|
"element": actor_element,
|
|
"branch_relations": labels,
|
|
"element_relation": element_relation,
|
|
}
|
|
|
|
|
|
def _actor_element_relation(actor: str, target: str, prefix: str) -> str:
|
|
if actor == target:
|
|
return f"{prefix}与爻同气"
|
|
if ELEMENT_GENERATES[actor] == target:
|
|
return f"{prefix}生爻"
|
|
if ELEMENT_CONTROLS[actor] == target:
|
|
return f"{prefix}克爻"
|
|
if ELEMENT_GENERATES[target] == actor:
|
|
return f"爻生{prefix}"
|
|
return f"爻克{prefix}"
|
|
|
|
|
|
def _transformation_relation(
|
|
origin_branch: str,
|
|
origin_element: str,
|
|
target_branch: str,
|
|
target_element: str,
|
|
) -> list[str]:
|
|
labels = []
|
|
if (origin_branch, target_branch) in ADVANCE_PAIRS:
|
|
labels.append("化进神")
|
|
elif (origin_branch, target_branch) in RETREAT_PAIRS:
|
|
labels.append("化退神")
|
|
if BRANCH_COMBINE[origin_branch] == target_branch:
|
|
labels.append("化合")
|
|
if BRANCH_CLASH[origin_branch] == target_branch:
|
|
labels.append("化冲")
|
|
if target_element == origin_element:
|
|
labels.append("变爻同气")
|
|
elif ELEMENT_GENERATES[target_element] == origin_element:
|
|
labels.append("回头生")
|
|
elif ELEMENT_CONTROLS[target_element] == origin_element:
|
|
labels.append("回头克")
|
|
elif ELEMENT_GENERATES[origin_element] == target_element:
|
|
labels.append("原爻生变")
|
|
else:
|
|
labels.append("原爻克变")
|
|
return labels
|
|
|
|
|
|
def _hidden_spirits(
|
|
palace_trigram: str,
|
|
palace_element: str,
|
|
lines: list[dict[str, Any]],
|
|
) -> list[dict[str, Any]]:
|
|
present = {str(line["relative"]) for line in lines}
|
|
missing = {"父母", "兄弟", "子孙", "妻财", "官鬼"} - present
|
|
if not missing:
|
|
return []
|
|
pure_bits = TRIGRAM_BITS[palace_trigram] + TRIGRAM_BITS[palace_trigram]
|
|
result = []
|
|
for index, (stem, branch) in enumerate(_najia_for_bits(pure_bits)):
|
|
element = BRANCH_ELEMENT[branch]
|
|
relative = _six_relative(palace_element, element)
|
|
if relative not in missing:
|
|
continue
|
|
result.append(
|
|
{
|
|
"position": index + 1,
|
|
"position_name": LINE_POSITIONS[index],
|
|
"stem": stem,
|
|
"branch": branch,
|
|
"element": element,
|
|
"relative": relative,
|
|
"flying_relative": lines[index]["relative"],
|
|
}
|
|
)
|
|
return result
|
|
|
|
|
|
def _hexagram_branch_pattern(lines: list[dict[str, Any]]) -> str:
|
|
pairs = ((0, 3), (1, 4), (2, 5))
|
|
if all(BRANCH_CLASH[lines[left]["branch"]] == lines[right]["branch"] for left, right in pairs):
|
|
return "六冲"
|
|
if all(BRANCH_COMBINE[lines[left]["branch"]] == lines[right]["branch"] for left, right in pairs):
|
|
return "六合"
|
|
return ""
|
|
|
|
|
|
def _significant_line_relationships(lines: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
result = []
|
|
for left_index in range(6):
|
|
for right_index in range(left_index + 1, 6):
|
|
left = lines[left_index]
|
|
right = lines[right_index]
|
|
if not (left["moving"] or right["moving"] or left["role"] or right["role"]):
|
|
continue
|
|
labels = _branch_pair_relations(left["branch"], right["branch"])
|
|
element_relation = _pair_element_relation(left["element"], right["element"])
|
|
if not labels and element_relation == "同气":
|
|
continue
|
|
result.append(
|
|
{
|
|
"positions": [left["position"], right["position"]],
|
|
"lines": [left["position_name"], right["position_name"]],
|
|
"branch_relations": labels,
|
|
"element_relation": element_relation,
|
|
}
|
|
)
|
|
return result
|
|
|
|
|
|
def _branch_pair_relations(left: str, right: str) -> list[str]:
|
|
labels = []
|
|
if BRANCH_COMBINE[left] == right:
|
|
labels.append("六合")
|
|
if BRANCH_CLASH[left] == right:
|
|
labels.append("六冲")
|
|
if BRANCH_HARM[left] == right:
|
|
labels.append("六害")
|
|
pair = frozenset((left, right))
|
|
if pair in THREE_PUNISHMENTS or (left == right and left in SELF_PUNISHMENT):
|
|
labels.append("相刑")
|
|
return labels
|
|
|
|
|
|
def _pair_element_relation(left: str, right: str) -> str:
|
|
if left == right:
|
|
return "同气"
|
|
if ELEMENT_GENERATES[left] == right:
|
|
return "前者生后者"
|
|
if ELEMENT_GENERATES[right] == left:
|
|
return "后者生前者"
|
|
if ELEMENT_CONTROLS[left] == right:
|
|
return "前者克后者"
|
|
return "后者克前者"
|