feat: complete heaven readings and screener publication
This commit is contained in:
@@ -0,0 +1,432 @@
|
||||
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 "后者克前者"
|
||||
Reference in New Issue
Block a user