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