Files
xiaobai-review/backend/features/heaven/six_yao.py
T

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 "后者克前者"