"""神煞求值单元测试(chart/shensha.py)。 对拍基准:rules/94_神煞细节.md 各条目查法 + 《三命通会》卷三诸神煞篇。 整盘向量取「丁亥/乙巳/庚子/辛巳(女)」,与人工按 94 篇逐条手推的结果一致。 """ import sys import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) from chart.shensha import evaluate_natal, evaluate_target # noqa: E402 def natal(year, month, day, hour, gender="男"): return evaluate_natal( {"year": year, "month": month, "day": day, "hour": hour}, gender) def stars(result, key): return {s for s, _ in result[key]} class WholeChartVectorTest(unittest.TestCase): """丁亥/乙巳/庚子/辛巳(女)整盘对拍。""" @classmethod def setUpClass(cls): cls.r = natal("丁亥", "乙巳", "庚子", "辛巳", gender="女") def test_year(self): self.assertEqual(stars(self.r, "year"), {"文昌", "太极贵人", "亡神"}) def test_month(self): self.assertEqual(stars(self.r, "month"), {"月德合", "驿马", "劫煞", "地网"}) def test_day(self): self.assertEqual(stars(self.r, "day"), {"月德", "桃花", "将星"}) def test_hour(self): self.assertEqual(stars(self.r, "hour"), {"天德", "驿马", "劫煞", "地网", "童子煞"}) def test_combo_empty(self): self.assertEqual(self.r["combo"], []) class GanLookupTest(unittest.TestCase): """日干查支诸星。""" def test_tianyi(self): r = natal("甲子", "丙寅", "甲午", "辛未") # 甲日 → 丑未 self.assertIn("天乙贵人", stars(r, "hour")) self.assertNotIn("天乙贵人", stars(r, "year")) def test_lu_ren_jinyu(self): r = natal("丙子", "庚寅", "甲申", "己巳") # 甲禄在寅 self.assertIn("禄神", stars(r, "month")) all_stars = stars(r, "month") | stars(r, "day") | stars(r, "year") | stars(r, "hour") self.assertNotIn("羊刃", all_stars) # 盘无卯,甲日亦无刃可取 r2 = natal("丙子", "庚寅", "甲辰", "己巳") # 甲日金舆在辰 self.assertIn("金舆", stars(r2, "day")) def test_yangren_yang_gan(self): r = natal("丙子", "辛卯", "甲戌", "己巳") # 甲刃在卯 self.assertIn("羊刃", stars(r, "month")) def test_yangren_yin_gan_none(self): """阴干无刃:乙日见卯只取禄,不取刃。""" r = natal("丙子", "庚寅", "乙卯", "己巳") self.assertNotIn("羊刃", stars(r, "day")) self.assertIn("禄神", stars(r, "day")) def test_taiji_fuxing_hongyan(self): r = natal("丙子", "庚寅", "庚午", "丁亥") # 庚日:太极寅亥、福星午、红艳戌 self.assertIn("太极贵人", stars(r, "hour")) self.assertIn("福星贵人", stars(r, "day")) self.assertNotIn("红艳", stars(r, "day") | stars(r, "hour")) def test_wenchang(self): r = natal("丙子", "庚寅", "乙巳", "丁午") # 乙日文昌在午 self.assertIn("文昌", stars(r, "hour")) class MonthLingTest(unittest.TestCase): """月令查天德、月德及其合(可应天干或地支)。""" def test_tiande_stem_and_branch(self): r = natal("辛亥", "庚寅", "丁子", "辛巳") # 寅月天德丁(日干)、天德合壬 self.assertIn("天德", stars(r, "day")) r2 = natal("辛亥", "庚寅", "壬子", "辛巳") self.assertIn("天德合", stars(r2, "day")) def test_tiande_branch_hit(self): r = natal("甲子", "庚午", "丙子", "己亥") # 午月天德亥(时支)、月德丙(日干) self.assertIn("天德", stars(r, "hour")) self.assertIn("月德", stars(r, "day")) def test_yuede(self): r = natal("戊申", "壬子", "甲子", "丙寅") # 子月(申子辰)月德壬、月德合丁 self.assertIn("月德", stars(r, "month")) r2 = natal("戊申", "丁丑", "甲子", "丙寅") # 丑月(巳酉丑)月德庚 self.assertNotIn("月德", stars(r2, "month")) class SanheLookupTest(unittest.TestCase): """三合局诸煞:年支、日支并查。""" def test_yima_double_anchor(self): # 年支巳(巳酉丑→亥)、日支午(寅午戌→申) r = natal("辛巳", "庚寅", "戊午", "壬申") self.assertIn("驿马", stars(r, "hour")) # 以日支午查 r2 = natal("辛巳", "庚寅", "戊午", "乙亥") self.assertIn("驿马", stars(r2, "hour")) # 以年支巳查 def test_taohua_huagai_jiangxing(self): # 日支辰(申子辰):桃花酉、华盖辰、将星子 r = natal("甲子", "辛酉", "丙辰", "戊子") self.assertIn("桃花", stars(r, "month")) self.assertIn("华盖", stars(r, "day")) self.assertIn("将星", stars(r, "year")) self.assertIn("将星", stars(r, "hour")) def test_jiesha_wangshen_zaisha(self): # 年支卯(亥卯未):劫煞申、亡神寅、灾煞酉 r = natal("丁卯", "壬寅", "甲午", "壬申") self.assertIn("亡神", stars(r, "month")) self.assertIn("劫煞", stars(r, "hour")) r2 = natal("丁卯", "己酉", "甲午", "壬申") self.assertIn("灾煞", stars(r2, "month")) class YearBranchAndGenderTest(unittest.TestCase): """孤辰寡宿、元辰、勾绞——与阴阳男女有关。""" def test_guchen_guasu(self): r = natal("戊寅", "乙巳", "甲午", "戊辰") # 年支寅(寅卯辰):孤辰巳、寡宿丑 self.assertIn("孤辰", stars(r, "month")) self.assertNotIn("寡宿", stars(r, "month")) def test_yuanchen_shun(self): """阳男:冲前一位。子年冲午,午前一位 = 未。""" r = natal("甲子", "辛未", "丙午", "戊子", gender="男") self.assertIn("元辰", stars(r, "month")) def test_yuanchen_ni(self): """阴女同阳男顺查;阴男阳女逆查。子年阴男:冲后一位 = 巳。""" r_male = natal("乙丑", "辛巳", "丙午", "戊子", gender="男") # 乙丑年支丑,阴男:冲后一位。丑冲未,未后一位 = 午 self.assertIn("元辰", stars(r_male, "day") | stars(r_male, "hour")) r_female = natal("乙丑", "辛巳", "丙午", "戊子", gender="女") # 阴女顺查:冲前一位 = 申,盘无申 → 无元辰 self.assertNotIn("元辰", stars(r_female, "month") | stars(r_female, "day") | stars(r_female, "year") | stars(r_female, "hour")) def test_goujiao(self): """甲子阳男:命前三辰卯为勾、后三辰酉为绞。""" r = natal("甲子", "丁卯", "辛酉", "戊子", gender="男") self.assertIn("勾煞", stars(r, "month")) self.assertIn("绞煞", stars(r, "day")) r2 = natal("甲子", "丁卯", "辛酉", "戊子", gender="女") self.assertIn("绞煞", stars(r2, "month")) self.assertIn("勾煞", stars(r2, "day")) class NayinTest(unittest.TestCase): """年柱纳音查:天罗地网、童子煞。""" def test_tianluo_fire(self): r = natal("丙寅", "壬辰", "甲戌", "乙亥") # 炉中火 → 天罗戌亥 self.assertIn("天罗", stars(r, "day")) self.assertIn("天罗", stars(r, "hour")) def test_diwang_water_earth(self): r = natal("丙子", "壬辰", "甲辰", "己巳") # 涧下水 → 地网辰巳 self.assertIn("地网", stars(r, "month")) self.assertIn("地网", stars(r, "day")) self.assertIn("地网", stars(r, "hour")) def test_luowang_metal_wood_none(self): r = natal("甲子", "壬辰", "甲戌", "乙亥") # 海中金 → 无罗网 for k in ("year", "month", "day", "hour"): self.assertNotIn("天罗", stars(r, k)) self.assertNotIn("地网", stars(r, k)) def test_tongzi_season(self): r = natal("甲子", "丙寅", "戊寅", "甲寅") # 春生见寅(日支、时支皆中) self.assertIn("童子煞", stars(r, "day")) self.assertIn("童子煞", stars(r, "hour")) def test_tongzi_nayin(self): # 庚午路旁土 → 辰巳;秋月生(寅子),日时支午卯两不涉 → 须无 r = natal("庚午", "甲申", "甲午", "丁卯") self.assertNotIn("童子煞", stars(r, "day") | stars(r, "hour")) # 甲子海中金 → 午卯;秋月生不涉季节组,纯验纳音组 r2 = natal("甲子", "壬申", "甲午", "丁卯") self.assertIn("童子煞", stars(r2, "day")) self.assertIn("童子煞", stars(r2, "hour")) class FixedDayGroupTest(unittest.TestCase): """日柱干支固定组。""" def test_yincha_yangcuo(self): r = natal("甲子", "丙寅", "丙子", "戊子") self.assertIn("阴差阳错", stars(r, "day")) r2 = natal("甲子", "丙寅", "丙戌", "戊子") self.assertNotIn("阴差阳错", stars(r2, "day")) def test_shiling_kuigang(self): r = natal("甲子", "丙寅", "庚戌", "戊子") self.assertIn("十灵日", stars(r, "day")) self.assertIn("魁罡", stars(r, "day")) def test_shie_dabai_jichou(self): """己丑为十恶大败(甲申旬空午未、己禄在午);epub 作乙丑系讹字。""" r = natal("甲子", "丙寅", "己丑", "戊子") self.assertIn("十恶大败", stars(r, "day")) r2 = natal("甲子", "丙寅", "乙丑", "戊子") self.assertNotIn("十恶大败", stars(r2, "day")) def test_tianshe(self): r = natal("甲子", "丙寅", "戊寅", "戊午") # 春月戊寅日 self.assertIn("天赦", stars(r, "day")) r2 = natal("甲子", "壬申", "戊寅", "戊午") # 秋月戊寅日 → 非 self.assertNotIn("天赦", stars(r2, "day")) class SanqiTest(unittest.TestCase): def test_shunbu(self): r = natal("乙巳", "丙戌", "丁亥", "辛丑") self.assertEqual(len(r["combo"]), 1) star, note = r["combo"][0] self.assertEqual(star, "三奇贵人") self.assertIn("顺布", note) def test_nibu(self): r = natal("丁巳", "丙戌", "乙亥", "辛丑") self.assertEqual(len(r["combo"]), 1) self.assertIn("逆布", r["combo"][0][1]) def test_not_adjacent_no_sanqi(self): r = natal("乙巳", "甲戌", "丙子", "丁丑") # 乙丙丁不相连 self.assertEqual(r["combo"], []) class TargetQueryTest(unittest.TestCase): """岁运查询(evaluate_target)。""" P = {"year": "丁亥", "month": "乙巳", "day": "庚子", "hour": "辛巳"} def test_anchored(self): """丙午 对 丁亥/乙巳/庚子/辛巳(女):福星(庚见午)、天德合(丙)、 灾煞(日支子见午)、元辰(阴女顺查,亥冲巳前一位为午);丙午自身属阴差阳错。""" r = evaluate_target(self.P, "女", "丙午") names = {s for s, _ in r["anchored"]} self.assertEqual(names, {"福星贵人", "天德合", "灾煞", "元辰"}) own = {s for s, _ in r["own"]} self.assertEqual(own, {"阴差阳错"}) def test_anchored_yisi(self): r = evaluate_target(self.P, "女", "乙巳") names = {s for s, _ in r["anchored"]} self.assertEqual(names, {"月德合", "驿马", "劫煞", "地网", "童子煞"}) own = {s for s, _ in r["own"]} self.assertEqual(own, {"十恶大败"}) if __name__ == "__main__": unittest.main()