Event scoring was measured at chance level on the 77 Rodden-AA holdout cases (BUG-1091). The new flow finds the dividing line nearest the reported time, describes how the flipped divisional charts read on either side, asks up to five self-assessment questions, and saves the user's own choice (provenance user_choice) with the domains to read in both versions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DbBXozbkyFNqbu8pVEtZ7W
261 lines
12 KiB
Python
261 lines
12 KiB
Python
"""Plain-language differences between the two sides of a dividing line.
|
||
|
||
Every sentence is derived from chart facts (sign of the divisional ascendant,
|
||
occupants of the domain houses, where the domain-house lords sit). The copy
|
||
states classical tendencies in modest words; it never claims that a life event
|
||
proves either side (BUG-1091).
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Any, Final
|
||
|
||
_SCRIPTS = Path(__file__).resolve().parents[1]
|
||
if str(_SCRIPTS) not in sys.path:
|
||
sys.path.insert(0, str(_SCRIPTS))
|
||
|
||
import varga # noqa: E402
|
||
|
||
SIGN_CN: Final = ("白羊", "金牛", "双子", "巨蟹", "狮子", "处女", "天秤", "天蝎", "射手", "摩羯", "水瓶", "双鱼")
|
||
SIGN_LORD: Final = ("Mars", "Venus", "Mercury", "Moon", "Sun", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Saturn", "Jupiter")
|
||
PLANETS: Final = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu")
|
||
PLANET_CN: Final = {
|
||
"Sun": "太阳", "Moon": "月亮", "Mars": "火星", "Mercury": "水星", "Jupiter": "木星",
|
||
"Venus": "金星", "Saturn": "土星", "Rahu": "罗睺", "Ketu": "计都",
|
||
}
|
||
|
||
# Domain per tracked chart, in the order questions are asked when more than five flip.
|
||
DOMAIN_ORDER: Final = ("D1", "D10", "D9", "D4", "D2", "D12", "D24")
|
||
DOMAIN: Final = {
|
||
"D1": {"key": "self", "label": "整体性格", "chart": "主盘 D1"},
|
||
"D2": {"key": "money", "label": "钱财", "chart": "钱财盘 D2"},
|
||
"D4": {"key": "home", "label": "住处", "chart": "住处盘 D4"},
|
||
"D9": {"key": "relationship", "label": "婚恋", "chart": "婚恋盘 D9"},
|
||
"D10": {"key": "career", "label": "事业", "chart": "事业盘 D10"},
|
||
"D12": {"key": "parents", "label": "父母", "chart": "父母盘 D12"},
|
||
"D24": {"key": "study", "label": "学业", "chart": "学业盘 D24"},
|
||
}
|
||
|
||
# What a planet brings to whichever domain house it occupies.
|
||
PLANET_TONE: Final = {
|
||
"Sun": "权威和管理,想出头、要面子",
|
||
"Moon": "和人打交道,变化多、投入感情",
|
||
"Mars": "竞争和技术,行动快、敢拼",
|
||
"Mercury": "沟通、商业、文字和数据",
|
||
"Jupiter": "大平台和贵人,重学识、有分寸",
|
||
"Venus": "审美、设计和人缘,讲究舒适",
|
||
"Saturn": "慢热、担责任、能坚持,早年辛苦、后劲足",
|
||
"Rahu": "野心大、不走寻常路,和外来或新兴的东西有缘",
|
||
"Ketu": "专精、抽离,不太在乎外界认可",
|
||
}
|
||
|
||
# Where the key lord of a domain sits, read for that domain. Index = house 1..12.
|
||
LORD_IN_HOUSE: Final = {
|
||
"career": (
|
||
"事业和自我绑在一起,靠个人特色和自己做主",
|
||
"事业和钱、家族或口才有关,重视收入稳定",
|
||
"路多半是自己闯出来的,靠主动、表达、写说做内容",
|
||
"工作和家、住处分不太开,适合和生活方式有关的行业",
|
||
"靠专长、兴趣和脑力吃饭,偏技术、研究、创作或教育",
|
||
"在竞争和解决问题里成长,适合服务、专业技术或要拼的岗位",
|
||
"事业靠合作、客户和对外打交道推动",
|
||
"事业有转折和起伏,适合幕后、研究或需要深挖的工作",
|
||
"遇到导师、去远方、读更高的学历会推动事业",
|
||
"事业心强,容易站到显眼的位置上",
|
||
"机会多来自人脉和圈子,收入导向明显",
|
||
"事业和外地、外企或幕后有关,常要离开熟悉的环境",
|
||
),
|
||
"home": (
|
||
"对住的地方有主见,住处跟着自己的选择走",
|
||
"住处和家里的钱、家族资源有关",
|
||
"搬动多,但多在同一城市或熟悉的范围里",
|
||
"住处安稳,和老家、家人的牵连深",
|
||
"住的地方和兴趣、学习或孩子有关",
|
||
"住处容易有琐碎的麻烦,要花心思打理",
|
||
"住处的变化常因为伴侣或合作的人",
|
||
"住处偶尔会有突然的变化",
|
||
"住处和远方、求学有关,可能为了深造搬家",
|
||
"人跟着工作走,为了事业定居",
|
||
"住处和朋友圈、收益有关,住得热闹",
|
||
"长期在出生地以外生活,心里有离乡感",
|
||
),
|
||
"money": (), # D2 uses the hora rule, not house lords
|
||
"relationship": (
|
||
"感情里很看重自我,伴侣和自己性格相近",
|
||
"伴侣和家庭、钱财观念紧密相关",
|
||
"感情从交流开始,伴侣可能是同学、同事或邻近的人",
|
||
"感情和家庭绑在一起,重视安稳",
|
||
"感情从兴趣和浪漫开始,重视心动的感觉",
|
||
"感情里有磨合和要处理的问题",
|
||
"重视伴侣本身,关系是生活的重心",
|
||
"感情有深度也有起伏,容易有转折",
|
||
"伴侣可能来自远方,或带着学识、信念上的影响",
|
||
"伴侣和事业、社会地位相关",
|
||
"感情从朋友圈里来,重视共同的圈子",
|
||
"感情和远方或私密的一面有关,可能异地",
|
||
),
|
||
"parents": (
|
||
"和这位家长性格相近,影响直接",
|
||
"这位家长和家里的钱、家族传承关系大",
|
||
"这位家长的支持是鼓励你自己去闯",
|
||
"这位家长和家庭、住处的牵连深",
|
||
"这位家长很看重你的学习和兴趣",
|
||
"和这位家长之间有要磨合的地方",
|
||
"这位家长的影响通过合作或对外关系体现",
|
||
"和这位家长的关系有深度也有变化",
|
||
"这位家长像导师,带给你信念和见识",
|
||
"这位家长在事业或社会上有分量",
|
||
"这位家长对你帮助大,带来实际的支持",
|
||
"和这位家长之间有距离,可能分开生活",
|
||
),
|
||
"study": (
|
||
"学习靠自己,主动性强",
|
||
"学的东西和挣钱、实用技能有关",
|
||
"学习靠多交流、多动手,读写表达好",
|
||
"学习需要安稳的环境,和家里的支持有关",
|
||
"学习从兴趣出发,专长明显",
|
||
"学业靠考试和拼劲,在竞争里出成绩",
|
||
"学习和合作、老师或同伴关系大",
|
||
"学业有停顿或转向,偏研究和深层的东西",
|
||
"深造运好,容易去远方读书",
|
||
"学的东西后来直接用在工作上,偏实用",
|
||
"学习带来实际收获和人脉",
|
||
"深造多半在外地,或者来得晚",
|
||
),
|
||
"self": (
|
||
"性格以自我为中心,有主见",
|
||
"看重钱和家里,说话有分量",
|
||
"主动、爱表达,靠自己闯",
|
||
"恋家,内心需要安全感",
|
||
"重兴趣和创造,脑子活",
|
||
"能吃苦,在磨炼里成长",
|
||
"以人为镜,重视伴侣和合作",
|
||
"经历转折多,喜欢探究深层的东西",
|
||
"有信念,容易遇到贵人",
|
||
"事业心重,看重地位",
|
||
"重视圈子和收获",
|
||
"向往远方,有离乡或内省的一面",
|
||
),
|
||
}
|
||
|
||
# (domain houses whose occupants matter, house whose lord is read, label of that lord)
|
||
DOMAIN_FOCUS: Final = {
|
||
"career": ((10, 1), 10, "管事业的星"),
|
||
"home": ((4, 12), 4, "管住处的星"),
|
||
"relationship": ((7, 1), 7, "管伴侣的星"),
|
||
"parents": ((4, 9), 4, "管母亲的星"),
|
||
"study": ((4, 5, 9), 9, "管深造的星"),
|
||
"self": ((1,), 1, "命主星"),
|
||
}
|
||
HOUSE_NAME: Final = {
|
||
1: "命位", 4: "家的位置", 5: "专长的位置", 7: "伴侣的位置", 9: "远方和贵人的位置",
|
||
10: "事业位", 12: "离乡的位置",
|
||
}
|
||
# Second lord read for parents (father) and study (the learner itself).
|
||
EXTRA_LORD: Final = {
|
||
"parents": (9, "管父亲的星"),
|
||
"study": (1, "代表学习者自己的星"),
|
||
"career": (1, "代表你自己的星"),
|
||
"home": (1, "代表你自己的星"),
|
||
"relationship": (1, "代表你自己的星"),
|
||
}
|
||
# Which person a parents sentence talks about, by the house whose lord is read.
|
||
PARENT_BY_HOUSE: Final = {4: "母亲", 9: "父亲"}
|
||
|
||
HORA_TEXT: Final = {
|
||
4: ("太阳这一半", "钱更多来自自己出头、拿职位、担责任,在体系里往上走,来得慢但稳"),
|
||
3: ("月亮这一半", "钱更多来自和人打交道、做买卖、做内容或和审美有关的东西,来得灵活、零散"),
|
||
}
|
||
|
||
|
||
def varga_chart(planet_longitudes: dict[str, float], ascendant_longitude: float, division: int) -> dict[str, int]:
|
||
"""Sign index of the ascendant and each planet in one divisional chart."""
|
||
if division == 1:
|
||
out = {"Ascendant": int(ascendant_longitude // 30) % 12}
|
||
out.update({p: int(planet_longitudes[p] // 30) % 12 for p in PLANETS if p in planet_longitudes})
|
||
return out
|
||
out = {"Ascendant": int(varga.calc_varga(ascendant_longitude, division)["sign_idx"])}
|
||
for p in PLANETS:
|
||
if p in planet_longitudes:
|
||
out[p] = int(varga.calc_varga(planet_longitudes[p], division)["sign_idx"])
|
||
return out
|
||
|
||
|
||
def _house(chart: dict[str, int], sign: int) -> int:
|
||
return (sign - chart["Ascendant"]) % 12 + 1
|
||
|
||
|
||
def _occupants(chart: dict[str, int], house: int) -> list[str]:
|
||
return [p for p in PLANETS if p in chart and _house(chart, chart[p]) == house]
|
||
|
||
|
||
def _lord_of(chart: dict[str, int], house: int) -> str:
|
||
return SIGN_LORD[(chart["Ascendant"] + house - 1) % 12]
|
||
|
||
|
||
def describe(chart_key: str, chart: dict[str, int]) -> dict[str, Any]:
|
||
"""Facts plus plain sentences for one side of one divisional chart."""
|
||
meta = DOMAIN[chart_key]
|
||
domain = meta["key"]
|
||
asc = chart["Ascendant"]
|
||
points: list[dict[str, str]] = []
|
||
if chart_key == "D2":
|
||
half, text = HORA_TEXT.get(asc, ("", ""))
|
||
own = [PLANET_CN[p] for p in PLANETS if chart.get(p) == asc]
|
||
detail = f"{'、'.join(own)}在你这一半。" if own else ""
|
||
points.append({"kind": "hora", "fact": f"上升落在{half}。{detail}", "meaning": text})
|
||
return {"chart": chart_key, "domain": domain, "label": meta["label"], "chart_label": meta["chart"],
|
||
"ascendant": SIGN_CN[asc], "points": points}
|
||
|
||
houses, lord_house, lord_label = DOMAIN_FOCUS[domain]
|
||
for house in houses:
|
||
for planet in _occupants(chart, house):
|
||
points.append({"kind": "occupant", "fact": f"{PLANET_CN[planet]}在{HOUSE_NAME.get(house, f'第 {house} 宫')}",
|
||
"meaning": PLANET_TONE[planet]})
|
||
lord_reads = [(lord_house, lord_label)]
|
||
if domain in EXTRA_LORD:
|
||
lord_reads.append(EXTRA_LORD[domain])
|
||
seen: set[str] = set()
|
||
for house, label in lord_reads:
|
||
lord = _lord_of(chart, house)
|
||
if lord in seen or lord not in chart:
|
||
continue
|
||
seen.add(lord)
|
||
where = _house(chart, chart[lord])
|
||
table = LORD_IN_HOUSE["self"] if (domain == "career" and house == 1) else LORD_IN_HOUSE[domain]
|
||
meaning = table[where - 1]
|
||
if domain == "parents":
|
||
meaning = meaning.replace("这位家长", PARENT_BY_HOUSE[house])
|
||
points.append({"kind": "lord", "fact": f"{label}({PLANET_CN[lord]})落在第 {where} 宫", "meaning": meaning})
|
||
return {"chart": chart_key, "domain": domain, "label": meta["label"], "chart_label": meta["chart"],
|
||
"ascendant": SIGN_CN[asc], "points": points}
|
||
|
||
|
||
def _option_text(own: dict[str, Any], other: dict[str, Any]) -> str:
|
||
"""Up to two lord readings that differ from the other side, else the first differing occupant tone."""
|
||
other_meanings = {p["meaning"] for p in other["points"]}
|
||
lords = [p["meaning"] for p in own["points"] if p["kind"] == "lord" and p["meaning"] not in other_meanings]
|
||
if lords:
|
||
return ";".join(lords[:2])
|
||
tones = [p for p in own["points"] if p["kind"] in ("occupant", "hora") and p["meaning"] not in other_meanings]
|
||
return tones[0]["meaning"] if tones else ""
|
||
|
||
|
||
def build_question(before: dict[str, Any], after: dict[str, Any]) -> dict[str, Any] | None:
|
||
"""One self-assessment question for a flipped chart, or None when both sides read alike."""
|
||
a, b = _option_text(before, after), _option_text(after, before)
|
||
if not a or not b or a == b:
|
||
return None
|
||
return {
|
||
"domain": before["domain"],
|
||
"chart": before["chart"],
|
||
"label": before["label"],
|
||
"prompt": f"你的{before['label']},更像哪一种?",
|
||
"options": [
|
||
{"value": "before", "text": a},
|
||
{"value": "after", "text": b},
|
||
{"value": "unsure", "text": "说不清"},
|
||
],
|
||
}
|