diff --git a/references/yoga_rules.json b/references/yoga_rules.json index 560dbae3..4c06750f 100644 --- a/references/yoga_rules.json +++ b/references/yoga_rules.json @@ -11444,13 +11444,11 @@ }, "effects": [ "母亲健康不佳", - "与母亲缘薄", - "母亲早逝" + "与母亲缘薄" ], "effects_en": [ "Mother's health is poor", - "Weak bond with mother", - "Early loss of mother" + "Weak bond with mother" ], "strength": "弱", "strength_en": "weak", diff --git a/scripts/curse_yoga_detector.py b/scripts/curse_yoga_detector.py index 4db8a8e5..7da4a5a2 100644 --- a/scripts/curse_yoga_detector.py +++ b/scripts/curse_yoga_detector.py @@ -11,6 +11,10 @@ curse_yoga_detector.py — 凶星合相命名与检测引擎 from typing import Dict, List, Optional, Tuple from dataclasses import dataclass, field +from yoga_engine import badhaka_house + +# houses_critical 里的占位符:按上升星座换成 Badhaka 宫(变动 11 / 固定 9 / 双体 7)。 +BADHAKA_HOUSE = "badhaka" # 凶星合相定义 CURSE_DEFINITIONS = { @@ -25,26 +29,26 @@ CURSE_DEFINITIONS = { "remedy": "需要上师保护,唱诵Guru Vandana,侍奉上师", "deity": "上师/Guru", "body_parts": ["血液", "骨骼", "神经系统"], - "family_effect": "家族血脉可能中断,无子女或子女早夭", + "family_effect": "家族责任与传承压力主题;不能据此断定子女、婚育或家人安危", }, "preta_yoga_saturn_rahu": { "name": "Preta Yoga", "name_cn": "亡灵瑜伽(土星+罗喉)", "planets": ["Saturn", "Rahu"], - "houses_critical": [8, 11, 6], # 8宫=死亡,11宫=Badhaka,6宫=疾病 + "houses_critical": [8, BADHAKA_HOUSE, 6], # 8宫=突变,Badhaka 宫按上升动/定/变取 11/9/7,6宫=疾病 "meaning": "长期压力、健康脆弱性或家族责任主题;不能据此断定寿命、死亡或疾病。", "activation": ["Saturn或Rahu的Dasha", "Badhaka宫被激活", "水瓶座相关事件"], "severity": 5, "remedy": "湿婆能量,唱诵Mahamrityunjaya Mantra,进行祖先祭祀", "deity": "湿婆/Shiva", "body_parts": ["慢性疾病部位", "骨骼", "神经系统退化"], - "family_effect": "家族血脉停止,女性不婚或无子女,男性只有女儿", + "family_effect": "家族传承与责任主题;不能据此断定婚育或子女", }, "preta_yoga_saturn_ketu": { "name": "Preta Yoga", "name_cn": "亡灵瑜伽(土星+计都)", "planets": ["Saturn", "Ketu"], - "houses_critical": [8, 11, 6], + "houses_critical": [8, BADHAKA_HOUSE, 6], "meaning": "长期压力、健康脆弱性或灵性困扰主题;不能据此断定寿命、死亡或疾病。", "activation": ["Saturn或Ketu的Dasha", "灵性敏感期"], "severity": 5, @@ -57,7 +61,7 @@ CURSE_DEFINITIONS = { "name": "Rakshasa Yoga", "name_cn": "恶魔瑜伽", "planets": ["Mars", "Rahu"], - "houses_critical": [6, 8], # 6宫=暴力/疾病,8宫=突变/死亡 + "houses_critical": [6, 8], # 6宫=冲突/疾病,8宫=突变 "meaning": "暴力事件,外部攻击,神经系统受损,可能被粗俗或微妙的恶魔力量攻击。", "activation": ["Mars或Rahu的Dasha", "6宫或8宫被Transit激活"], "severity": 4, @@ -70,7 +74,7 @@ CURSE_DEFINITIONS = { "name": "Pisacha Yoga", "name_cn": "鬼魅瑜伽", "planets": ["Mars", "Ketu"], - "houses_critical": [8, 1, 6], # 8宫=死亡,1宫=身体,6宫=疾病 + "houses_critical": [8, 1, 6], # 8宫=突变,1宫=身体,6宫=疾病 "meaning": "与有毒的人接触,被充满愤怒仇恨的鬼魂困扰,可能遭遇事故前感到被推动。", "activation": ["Mars或Ketu的Dasha", "天蝎座相关事件", "8宫被激活"], "severity": 4, @@ -90,7 +94,7 @@ HOUSE_PERSONIFICATION = { 5: "子女/创造力/投机", 6: "疾病/敌人/服务", 7: "配偶/合作/公开对手", - 8: "死亡/突变/遗产/灵性", + 8: "突变/遗产/隐秘/灵性", 9: "父亲/长途旅行/高等教育", 10: "事业/社会地位/母亲", 11: "收益/愿望/社交网络", @@ -191,14 +195,17 @@ class CurseYogaDetector: analysis = CurseAnalysis() # Step 1: 在D1中检测 - d1_curses = self._detect_in_chart(planets, "D1") + d1_curses = self._detect_in_chart(planets, "D1", asc_sign) analysis.curses_detected.extend(d1_curses) # Step 2: 在D9中检测(如果有) context = chart_data.get("context", {}) d9_planets = context.get("navamsa_planets", {}) if d9_planets: - d9_curses = self._detect_in_chart(d9_planets, "D9") + d9_asc = context.get("navamsa_ascendant") or context.get("navamsa_lagna") or "" + if isinstance(d9_asc, dict): + d9_asc = d9_asc.get("sign", "") + d9_curses = self._detect_in_chart(d9_planets, "D9", str(d9_asc or "")) for c in d9_curses: c.narrative = f"[Navamsa] {c.narrative}" analysis.curses_detected.extend(d9_curses) @@ -218,7 +225,8 @@ class CurseYogaDetector: return analysis - def _detect_in_chart(self, planets: Dict, chart_name: str) -> List[CurseDetection]: + def _detect_in_chart(self, planets: Dict, chart_name: str, + ascendant_sign: str = "") -> List[CurseDetection]: """在单个星盘中检测诅咒Yoga""" detected = [] @@ -250,7 +258,8 @@ class CurseYogaDetector: def_planets = definition["planets"] if set(pair) == set(def_planets): detection = self._create_detection( - curse_id, definition, pair, house, sign, chart_name + curse_id, definition, pair, house, sign, chart_name, + ascendant_sign, ) detected.append(detection) @@ -258,13 +267,14 @@ class CurseYogaDetector: def _create_detection(self, curse_id: str, definition: Dict, planets: Tuple[str, ...], house: int, - sign: str, chart_name: str) -> CurseDetection: + sign: str, chart_name: str, + ascendant_sign: str = "") -> CurseDetection: """创建单个检测结果""" severity = definition["severity"] severity_label = "critical" if severity >= 5 else "high" if severity >= 4 else "medium" # 如果在危险宫位,提高严重程度 - if house in definition.get("houses_critical", []): + if house in self._critical_houses(definition, ascendant_sign): severity = min(severity + 1, 5) severity_label = "critical" @@ -298,6 +308,19 @@ class CurseYogaDetector: narrative=f"[{chart_name}] 在{sign}第{house}宫发现 {definition['name']} ({definition['name_cn']})", ) + @staticmethod + def _critical_houses(definition: Dict, ascendant_sign: str) -> List[int]: + """展开 Badhaka 占位符;上升未知时 Badhaka 宫不参与加重。""" + houses: List[int] = [] + for item in definition.get("houses_critical", []): + if item == BADHAKA_HOUSE: + resolved = badhaka_house(ascendant_sign) + if resolved is not None: + houses.append(resolved) + else: + houses.append(item) + return houses + def _get_mantras(self, deity: str) -> List[str]: """获取对应神祇的咒语""" mantras = { diff --git a/scripts/yoga_engine.py b/scripts/yoga_engine.py index 7fe6e2fd..af1403c5 100644 --- a/scripts/yoga_engine.py +++ b/scripts/yoga_engine.py @@ -65,6 +65,17 @@ PLANET_INDEX = {planet: idx for idx, planet in enumerate(ALL_PLANETS)} RAHU_KETU_OWNED_SIGN_INDEX = {'Rahu': 10, 'Ketu': 7} # PyJHora const.house_strengths_of_planets, copied to make Yoga rule alignment deterministic. # Strength scale: 5 own/lord, 4 exalted, 3 friend, 2 neutral, 1 enemy, 0 debilitated. +def badhaka_house(ascendant_sign: str) -> Optional[int]: + """Badhaka 宫的唯一定义:变动 11、固定 9、双体 7;上升未知返回 None。""" + if ascendant_sign in MOVABLE_SIGNS: + return 11 + if ascendant_sign in FIXED_SIGNS: + return 9 + if ascendant_sign in DUAL_SIGNS: + return 7 + return None + + HOUSE_STRENGTHS = { 'Sun': [4, 1, 2, 2, 5, 2, 0, 3, 3, 1, 1, 3], 'Moon': [2, 4, 3, 5, 3, 3, 2, 0, 2, 2, 2, 2], @@ -506,13 +517,8 @@ class YogaContext: return house in [2, 7] def is_badhaka(self, house: int) -> bool: - """Badhaka 宫:基本盘7宫,固定盘9宫,变动盘11宫""" - asc = self.ascendant - if asc in ['Aries', 'Cancer', 'Libra', 'Capricorn']: - return house == 11 - if asc in ['Taurus', 'Leo', 'Scorpio', 'Aquarius']: - return house == 9 - return house == 7 + """Badhaka 宫:变动上升 11 宫,固定上升 9 宫,双体上升 7 宫。""" + return badhaka_house(self.ascendant) == house # --- 相对宫位关系 --- def house_offset(self, from_house: int, to_house: int) -> int: diff --git a/tests/test_curse_yoga_badhaka_house.py b/tests/test_curse_yoga_badhaka_house.py new file mode 100644 index 00000000..fcc5ed36 --- /dev/null +++ b/tests/test_curse_yoga_badhaka_house.py @@ -0,0 +1,41 @@ +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / 'scripts')) + +from curse_yoga_detector import CURSE_DEFINITIONS, CurseYogaDetector, detect_curse_yogas # noqa: E402 + +EXPECTED_BADHAKA = { + 'Aries': 11, 'Cancer': 11, 'Libra': 11, 'Capricorn': 11, + 'Taurus': 9, 'Leo': 9, 'Scorpio': 9, 'Aquarius': 9, + 'Gemini': 7, 'Virgo': 7, 'Sagittarius': 7, 'Pisces': 7, +} +DEATH_CLAIMS = re.compile(r'过早死亡|死亡风险|早夭|早逝|夭折|血脉(?:可能)?(?:中断|停止)|死亡之神') + + +def test_preta_critical_houses_use_lagna_badhaka_not_fixed_eleventh() -> None: + definition = CURSE_DEFINITIONS['preta_yoga_saturn_rahu'] + for ascendant, badhaka in EXPECTED_BADHAKA.items(): + houses = CurseYogaDetector._critical_houses(definition, ascendant) + assert houses == [8, badhaka, 6], ascendant + assert CurseYogaDetector._critical_houses(definition, '') == [8, 6] + + +def test_curse_yoga_outputs_and_definitions_carry_no_death_claims() -> None: + for name in ('curse_yoga_detector.py', 'rashi_tulya_navamsa.py'): + source = (ROOT / 'scripts' / name).read_text(encoding='utf-8') + assert not DEATH_CLAIMS.search(source), name + chart = { + 'ascendant': {'sign': 'Taurus'}, + 'planets': { + 'Mars': {'sign': 'Scorpio', 'house': 7}, + 'Saturn': {'sign': 'Scorpio', 'house': 7}, + 'Rahu': {'sign': 'Capricorn', 'house': 9}, + 'Ketu': {'sign': 'Cancer', 'house': 3}, + }, + } + result = detect_curse_yogas(chart) + assert result['curses_detected'] + assert not DEATH_CLAIMS.search(str(result))