近期在读任俊和应小萍的《乐商》,从了解习得性无助,到乐商(OD)是一种可以培养的能力,很多观点给了我启发,书中主要讲的是塞利格曼的积极心理学,讨论了对事物的控制感对于人心理的影响,习得性无助可以通过增加成功体验、增加控制感来预防,既然能够预防,那乐观也同样可以习得,我对书中的一个问卷比较感兴趣,叫做“解释风格”问卷,我将他写成了一个简易的Python程序。

import re
import sys
from dataclasses import dataclass


QUESTION_TEXT = """
1. 你所负责的那项计划非常成功
A:我监督手下很严
B:每一个人都花了很多心血在上面
PsG:A1分,B0分
2. 你和配偶(男/女朋友)在吵完架后讲和了
A:我原谅了他/她
B:我一般来说是很宽宏大量,不记仇的
PmG:A0分,B1分
3. 你开车去朋友家,中途迷路了
A:我错过了一个路口没转弯
B:我朋友给我的指引讲得不清不楚
PsB:A1分,B0分
4. 你的配偶(男/女朋友)出乎意料地买了一件礼物给你
A:他/她加薪了
B:我昨晚请他/她出去吃了豪华大餐
PsG:A0分,B1分
5. 你忘记你的配偶(男/女朋友)的生日
A:我对记生日是很差劲的
B:我太忙了
PmB:A1分,B0分
6. 神秘的爱慕者送了你一束花
A:我对他/她很有吸引力
B:我的人缘很好
PvG:A0分,B1分
7. 你当选了社区的公职(民意代表)
A:我花了很多时间和精力去竞选
B:我做任何事都全力以赴
PvG:A0分,B1分
8. 你忘记了一个很重要的约会
A:我的记忆有时真是很糟糕
B:我有时会忘记去看记事本上的约会记录
PvB:A1分,B0分
9. 你竞选民意代表,结果你落选了
A:我的竞选宣传不够
B:我的对手人缘比较好
PsB:A1分,B0分
10. 你成功地主持了一个宴会
A:我那晚真是风度翩翩
B:我是一个好主人
PmG:A0分,B1分
11. 你及时报警阻止了一件犯罪
A:我听到奇怪的声音,觉得不对劲
B:我那天很警觉
PsG:A0分,B1分
12. 你这一年都很健康
A:我周围的人几乎都不生病,所以我没被传染
B:我很注意我的饮食,而且每天休息都足够
PsG:A0分,B1分
13. 你因为借书逾期未还而被图书馆罚款
A:当我全神贯注在阅读时,我常忘记借阅到期了
B:我全心在写报告上,忘记去还那本书了
PmB:A1分,B0分
14. 你买卖股票赚了不少钱
A:我的经纪人决定去试一个新的投资
B:我的经纪人是一流的
PmG:A0分,B1分
15. 你赢了运动会上的竞赛
A:我觉得我是东方不败
B:我很努力训练自己
PmG:A0分,B1分
16. 你在大考中失败了
A:我没有其他考生那么聪明
B:我准备得不够
PvB:A1分,B0分
17. 你特意为你的朋友做了一道菜,而他连尝都不尝
A:我做得不好
B:我的食谱也许不太合口味
PvB:A1分,B0分
18. 你花很长的时间练习某项运动,但在比赛时失败了
A:我不是一个好的运动员
B:我对那项运动不在行
PvB:A1分,B0分
19. 你的车子在深夜的大街上没了汽油
A:我没有事先检查一下油箱还有多少油
B:油表的指针坏了
PsB:A1分,B0分
20. 你对朋友发了一顿脾气
A:他/她总是烦我
B:他/她今天很不友善
PmB:A1分,B0分
21. 你因为没有申报个人所得税而受罚
A:我总是拖着不愿去办有关所得税的事
B:我今年很懒散,不想报税
PmB:A1分,B0分
22. 你约一个人出去玩,但他/她拒绝了你
A:我那一天什么事都做不成,心情恶劣
B:我去约他/她时紧张得说不出话来
PvB:A1分,B0分
23. 一个现场节目的主持人从众多的观众中,单挑了你上台去参加节目
A:我坐的位置比较好
B:我表现得最热心
PsG:A0分,B1分
24. 你在舞会上很热门,常有人请你跳舞
A:我在舞会上很活跃
B:那一晚我所有表现都十全十美
PmG:A1分,B0分
25. 你替你的配偶(男/女朋友)买了一件礼物,而他/她并不喜欢
A:我没有好好用心思去想应该买什么
B:他/她是个很挑剔的人
PsB:A1分,B0分
26. 你在应聘时的面试上表现得很好
A:我在面试时觉得非常自信
B:我很会面试
PmG:A0分,B1分
27. 你说了一个笑话,每个人都捧腹大笑
A:这个笑话很好笑
B:我说笑话说得很好,时间拿捏得很准
PsG:A0分,B1分
28. 你的老板只给你一点点时间去完成一个计划,但是你还是如期达成了
A:我对我的工作很内行
B:我是一个很有效率的人
PvG:A0分,B1分
29. 你最近觉得很疲倦
A:我从来都没有机会放松一下
B:我这个礼拜特别忙
PmB:A1分,B0分
30. 你邀请某个人跳舞,他/她拒绝了
A:我不是一个好的舞者
B:他/她不喜欢跳舞
PsB:A1分,B0分
31. 你救了一个人使他没有噎死
A:我知道如何急救哽噎的人,我会这个技术
B:我知道在紧急的情况如何处理
PvG:A0分,B1分
32. 你的热恋情侶想要冷静疏远一阵子
A:我太自我中心了
B:我花在他/她身上的时间不够
PvB:A1分,B0分
33. 一个朋友说了一些使你伤心的话
A:他/她说话每次不经过大脑就冲口而出
B:他/她心情不好,把气出在我身上
PmB:A1分,B0分
34. 你的老板来找你,要你给他忠告
A:我是这个领域的专家
B:我给的忠告一向都切实可行
PvG:A0分,B1分
35. 一个朋友谢谢你帮助他/她走过一段困难时期
A:我很乐意协助朋友度过困难期
B:我关心朋友
PvG:A0分,B1分
36. 你在宴会上玩得很痛快
A:这里的每一个人都很友善
B:我很友善
PsG:A0分,B1分
37. 你的医生说你的身体健康情况极佳
A:我坚持经常运动
B:我对健康很小心也很注意
PvG:A0分,B1分
38. 你的配偶(男/女朋友)带你去度一个罗曼蒂克的周末
A:他/她需要远离城市几天
B:他/她喜欢去看看新的、没去过的地方
PmG:A0分,B1分
39. 你的医生说你吃太多甜的东西
A:我对饮食不大注意
B:我避免不了糖分,到处都是甜品,每样东西里都有糖
PsB:A1分,B0分
40. 老板指派你去做一个重要计划的主持人
A:我才刚刚成功地做完一个类似的计划
B:我是好的计划主持人,监督严谨,沟通良好
PmG:A0分,B1分
41. 你和你的配偶(男/女朋友)最近一直吵架
A:我最近压力很大,心情不好
B:他/她最近心情恶劣
PsB:A1分,B0分
42. 你滑雪时总是摔跤
A:滑雪很困难
B:滑雪道结冰了很溜滑
PmB:A1分,B0分
43. 你赢得了一个很好的大奖
A:我解决了一个重大的难题
B:我是最好的员工
PvG:A0分,B1分
44. 你买的股票现在跌入谷底
A:我那个时候对商业投资不是很懂
B:我买错了股票
PvB:A1分,B0分
45. 我中了奖券(大乐透)
A:真是运气
B:我选对了号码
PsG:A0分,B1分
46. 你在放假时胖了起来,现在瘦不回去
A:就长远来说,节食是没有用的
B:我这次用的这个减肥法没效
PmB:A0分,B1分
47. 你生病住院,但是没什么人来看你
A:我在生病的时候脾气不好
B:我的朋友常会疏忽像探病这种事
PsB:A1分,B0分
48. 商店拒收你的信用卡
A:我有时高估了自己的信用额度
B:我有时候忘记去付信用卡账单
PvB:A1分,B0分
"""


@dataclass(frozen=True)
class Question:
    number: int
    scale: str
    prompt: str
    option_a: str
    option_b: str
    score_a: int
    score_b: int


@dataclass(frozen=True)
class Band:
    minimum: int | None = None
    maximum: int | None = None
    label: str = ""
    brief: str = ""

    def matches(self, score: int) -> bool:
        if self.minimum is not None and score < self.minimum:
            return False
        if self.maximum is not None and score > self.maximum:
            return False
        return True


SCALE_LABELS = {
    "PmB": "PmB 坏事件永久性",
    "PvB": "PvB 坏事件普遍性",
    "PsB": "PsB 坏事件个人化",
    "PmG": "PmG 好事件永久性",
    "PvG": "PvG 好事件普遍性",
    "PsG": "PsG 好事件个人化",
}


SCALE_EXPLANATIONS = {
    "PmB": "Permanence of Bad,表示你是否把坏事看成长期、难改变的状态。",
    "PvB": "Pervasiveness of Bad,表示你是否把一次挫折泛化到很多生活领域。",
    "PsB": "Personalization of Bad,表示你是否倾向把坏结果归因为自己的缺陷。",
    "PmG": "Permanence of Good,表示你是否相信好结果能够持续和延续。",
    "PvG": "Pervasiveness of Good,表示你是否能把好事扩展到更多生活领域。",
    "PsG": "Personalization of Good,表示你是否愿意把成功归因为自己的能力和努力。",
}


SCALE_BANDS = {
    "PmB": [
        Band(0, 1, "非常乐观", "高乐商"),
        Band(2, 3, "中等乐观", "良好乐商"),
        Band(4, 4, "一般", "中等乐商"),
        Band(5, 6, "中度悲观", "轻度低乐商"),
        Band(7, 8, "重度悲观", "重度低乐商"),
    ],
    "PvB": [
        Band(0, 1, "非常乐观", "高乐商"),
        Band(2, 3, "中等乐观", "良好乐商"),
        Band(4, 4, "一般", "一般乐商"),
        Band(5, 6, "中度悲观", "轻度低乐商"),
        Band(7, 8, "非常悲观", "重度低乐商"),
    ],
    "PsB": [
        Band(0, 1, "自视很高", "高乐商"),
        Band(2, 3, "中等自傲", "良好乐商"),
        Band(4, 4, "自视一般", "一般乐商"),
        Band(5, 6, "中度自卑", "轻度低乐商"),
        Band(7, 8, "极度自卑", "重度低乐商"),
    ],
    "PmG": [
        Band(7, 8, "非常乐观", "高乐商"),
        Band(6, 6, "中度乐观", "良好乐商"),
        Band(4, 5, "一般", "一般乐商"),
        Band(3, 3, "中度悲观", "轻度低乐商"),
        Band(0, 2, "重度悲观", "重度低乐商"),
    ],
    "PvG": [
        Band(7, 8, "非常乐观", "较高乐商"),
        Band(6, 6, "中等乐观", "良好乐商"),
        Band(4, 5, "一般", "中等乐商"),
        Band(3, 3, "中等悲观", "轻度低乐商"),
        Band(0, 2, "非常悲观", "重度低乐商"),
    ],
    "PsG": [
        Band(7, 8, "非常乐观", "高乐商"),
        Band(6, 6, "中度乐观", "中等乐商"),
        Band(4, 5, "一般", "一般乐商"),
        Band(3, 3, "中度悲观", "轻度低乐商"),
        Band(0, 2, "极度悲观", "重度低乐商"),
    ],
}


GB_BANDS = [
    Band(9, None, "很乐观", "高乐商"),
    Band(6, 8, "中等程度乐观", "较高乐商"),
    Band(3, 5, "一般性乐观", "一般乐商"),
    Band(1, 2, "中等悲观", "轻度低乐商"),
    Band(None, 0, "极端悲观", "重度低乐商"),
]


def load_questions(text: str) -> list[Question]:
    lines = [line.strip() for line in text.splitlines() if line.strip()]
    questions: list[Question] = []
    i = 0

    while i < len(lines):
        match = re.match(r"^(\d+)\.\s*(.+)$", lines[i])
        if not match:
            i += 1
            continue

        number = int(match.group(1))
        prompt = match.group(2).strip()
        a_line = lines[i + 1]
        b_line = lines[i + 2]
        score_line = lines[i + 3]

        a_match = re.match(r"^A[::]\s*(.+)$", a_line)
        b_match = re.match(r"^B[::]\s*(.+)$", b_line)
        score_match = re.match(
            r"^(PmB|PvB|PsB|PmG|PvG|PsG)[::]\s*A\s*(\d)\s*分[,,]\s*B\s*(\d)\s*分$",
            score_line,
        )
        if not a_match or not b_match or not score_match:
            raise ValueError(f"第 {number} 题格式错误。")

        questions.append(
            Question(
                number=number,
                scale=score_match.group(1),
                prompt=prompt,
                option_a=a_match.group(1).strip(),
                option_b=b_match.group(1).strip(),
                score_a=int(score_match.group(2)),
                score_b=int(score_match.group(3)),
            )
        )
        i += 4

    if len(questions) != 48:
        raise ValueError(f"题目数量错误:{len(questions)}")
    return questions


def ask_choice(question: Question, total: int) -> str:
    print(f"\n第 {question.number}/{total} 题")
    print(question.prompt)
    print(f"A. {question.option_a}")
    print(f"B. {question.option_b}")
    while True:
        choice = input("> ").strip().upper()
        if choice in {"A", "B"}:
            return choice


def get_band(scale: str, score: int) -> Band:
    for band in SCALE_BANDS[scale]:
        if band.matches(score):
            return band
    raise ValueError(f"未找到 {scale} 的分档:{score}")


def get_gb_band(score: int) -> Band:
    for band in GB_BANDS:
        if band.matches(score):
            return band
    raise ValueError(f"未找到 G-B 的分档:{score}")


def interpret_scale(scale: str, score: int) -> list[str]:
    band = get_band(scale, score)
    return [
        f"{SCALE_LABELS[scale]}:{score}/8",
        f"分档:{band.label},对应 {band.brief}。",
        f"{SCALE_EXPLANATIONS[scale]}",
    ]


def interpret_overall(scores: dict[str, int]) -> list[str]:
    bad_total = scores["PmB"] + scores["PvB"] + scores["PsB"]
    good_total = scores["PmG"] + scores["PvG"] + scores["PsG"]
    gb = good_total - bad_total
    band = get_gb_band(gb)

    if gb >= 6:
        summary = "整体上更倾向于把好事内化并推广,把坏事看得较不持久。"
    elif gb >= 1:
        summary = "整体解释风格略偏积极,但在压力情境下仍可能出现悲观归因。"
    else:
        summary = "整体解释风格偏悲观,更容易把挫折看得持久、普遍,或更多归到自己身上。"

    return [
        f"G-B:{gb}",
        f"总体分档:{band.label},对应 {band.brief}。",
        f"坏事件总分:{bad_total}/24,好事件总分:{good_total}/24。",
        summary
    ]


def main() -> None:
    if hasattr(sys.stdin, "reconfigure"):
        sys.stdin.reconfigure(encoding="utf-8")
    if hasattr(sys.stdout, "reconfigure"):
        sys.stdout.reconfigure(encoding="utf-8")

    questions = load_questions(QUESTION_TEXT)
    scores = {key: 0 for key in SCALE_LABELS}

    for question in questions:
        choice = ask_choice(question, len(questions))
        scores[question.scale] += question.score_a if choice == "A" else question.score_b

    print("\n结果")
    for scale in ["PmB", "PvB", "PsB", "PmG", "PvG", "PsG"]:
        for line in interpret_scale(scale, scores[scale]):
            print(line)
        print()

    for line in interpret_overall(scores):
        print(line)


if __name__ == "__main__":
    main()