Files
Jyotisha/scripts/chart_choice/readings.py
T
ClaudeandClaude Opus 5.5 97afd1716a feat: chart choice replaces event-scored rectification page
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
2026-11-06 02:06:17 +08:00

261 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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": "说不清"},
],
}