| """ |
| 遊戲化系統模組 |
| 包含成就、等級、積分系統 |
| """ |
|
|
| import json |
| import time |
| import threading |
| import hashlib |
| from datetime import datetime |
| from typing import Dict, List, Optional, Tuple |
| from enum import Enum |
|
|
|
|
| |
| |
| |
| DAILY_CHALLENGES = [ |
| { |
| "id": "low_risk_conversations", |
| "name": "低風險對話", |
| "description": "完成 {count} 次風險分數 < {threshold} 的對話", |
| "params": {"count": 3, "threshold": 30}, |
| "reward": {"points": 100, "badge": "🛡️ 風險控制者"} |
| }, |
| { |
| "id": "different_personas", |
| "name": "人格探索", |
| "description": "嘗試 {count} 種不同人格", |
| "params": {"count": 2}, |
| "reward": {"points": 75, "badge": "👥 人格大師"} |
| }, |
| { |
| "id": "low_risk_streak", |
| "name": "連續低風險", |
| "description": "達成 {count} 次連續低風險回應", |
| "params": {"count": 5}, |
| "reward": {"points": 150, "badge": "🔥 連擊王"} |
| }, |
| { |
| "id": "quick_responses", |
| "name": "快速回應", |
| "description": "完成 {count} 次在 {time} 秒內的對話", |
| "params": {"count": 2, "time": 30}, |
| "reward": {"points": 100, "badge": "⚡ 閃電俠"} |
| }, |
| { |
| "id": "scenario_explorer", |
| "name": "情境探索者", |
| "description": "嘗試 {count} 種不同情境", |
| "params": {"count": 3}, |
| "reward": {"points": 75, "badge": "🗺️ 探索家"} |
| }, |
| { |
| "id": "high_score_achiever", |
| "name": "高分達人", |
| "description": "獲得 {count} 次得分 > {score} 的對話", |
| "params": {"count": 2, "score": 80}, |
| "reward": {"points": 125, "badge": "🏆 分數王"} |
| } |
| ] |
|
|
|
|
| |
| |
| |
| ACHIEVEMENTS = { |
| |
| "first_success": { |
| "name": "🎯 首次成功", |
| "description": "完成第一次對話", |
| "points": 50, |
| "icon": "🎯", |
| "condition": lambda stats: stats["total_conversations"] >= 1, |
| "hidden": False |
| }, |
| |
| |
| "streak_3": { |
| "name": "🌟 連續3次低風險", |
| "description": "連續3次風險分數 < 30", |
| "points": 100, |
| "icon": "🌟", |
| "condition": lambda stats: stats.get("current_streak", 0) >= 3, |
| "hidden": False |
| }, |
| |
| "streak_5": { |
| "name": "🛡️ 防禦專家", |
| "description": "連續5次風險 < 20", |
| "points": 250, |
| "icon": "🛡️", |
| "condition": lambda stats: stats.get("current_streak", 0) >= 5, |
| "hidden": False |
| }, |
| |
| |
| "perfect_response": { |
| "name": "🏆 完美應對", |
| "description": "單次風險分數 < 10", |
| "points": 150, |
| "icon": "🏆", |
| "condition": lambda stats: stats.get("best_risk_score", 100) < 10, |
| "hidden": False |
| }, |
| |
| |
| "scenario_explorer": { |
| "name": "📚 情境探索者", |
| "description": "完成5種不同情境", |
| "points": 200, |
| "icon": "📚", |
| "condition": lambda stats: len(stats.get("completed_scenarios", [])) >= 5, |
| "hidden": False |
| }, |
| |
| "persona_master": { |
| "name": "👤 人格大師", |
| "description": "完成所有7種人格", |
| "points": 300, |
| "icon": "👤", |
| "condition": lambda stats: len(stats.get("completed_personas", [])) >= 7, |
| "hidden": False |
| }, |
| |
| "full_clear": { |
| "name": "🗺️ 全境通關", |
| "description": "完成所有49種組合", |
| "points": 500, |
| "icon": "🗺️", |
| "condition": lambda stats: stats.get("total_combinations", 0) >= 49, |
| "hidden": False |
| }, |
| |
| |
| "quick_response": { |
| "name": "⚡ 快速應對", |
| "description": "在30秒內完成對話", |
| "points": 100, |
| "icon": "⚡", |
| "condition": lambda stats: stats.get("fastest_time", 999) < 30, |
| "hidden": False |
| }, |
| |
| |
| "communication_master": { |
| "name": "🎖️ 溝通大師", |
| "description": "總分數 > 5000", |
| "points": 1000, |
| "icon": "🎖️", |
| "condition": lambda stats: stats.get("total_score", 0) > 5000, |
| "hidden": False |
| }, |
| |
| |
| "diversity": { |
| "name": "🌈 多樣化", |
| "description": "完成10種以上組合", |
| "points": 150, |
| "icon": "🌈", |
| "condition": lambda stats: stats.get("total_combinations", 0) >= 10, |
| "hidden": False |
| }, |
| |
| |
| "high_score_streak": { |
| "name": "🔥 高分連發", |
| "description": "連續3次得分 > 80", |
| "points": 200, |
| "icon": "🔥", |
| "condition": lambda stats: stats.get("high_score_streak", 0) >= 3, |
| "hidden": False |
| }, |
| |
| |
| "risk_master": { |
| "name": "🎯 風險控制大師", |
| "description": "平均風險分數 < 20", |
| "points": 300, |
| "icon": "🎯", |
| "condition": lambda stats: stats.get("average_risk", 100) < 20, |
| "hidden": False |
| }, |
| |
| |
| "scenario_master": { |
| "name": "📋 情境全通", |
| "description": "完成所有7種情境", |
| "points": 250, |
| "icon": "📋", |
| "condition": lambda stats: len(stats.get("completed_scenarios", [])) >= 7, |
| "hidden": False |
| }, |
| |
| |
| "challenge_complete": { |
| "name": "⚔️ 挑戰者", |
| "description": "完成一次挑戰模式", |
| "points": 100, |
| "icon": "⚔️", |
| "condition": lambda stats: stats.get("challenge_completed", 0) >= 1, |
| "hidden": False |
| }, |
| |
| |
| "daily_challenge": { |
| "name": "📅 每日挑戰", |
| "description": "完成今日挑戰", |
| "points": 75, |
| "icon": "📅", |
| "condition": lambda stats: stats.get("daily_challenge_completed", False), |
| "hidden": False |
| }, |
| } |
|
|
|
|
| |
| |
| |
| LEVELS = { |
| 1: {"name": "新手教師", "min_score": 0, "color": "#3B82F6", "icon": "🌱"}, |
| 2: {"name": "初級教師", "min_score": 500, "color": "#10B981", "icon": "🌿"}, |
| 3: {"name": "中級教師", "min_score": 1500, "color": "#F59E0B", "icon": "🌼"}, |
| 4: {"name": "資深教師", "min_score": 3000, "color": "#F97316", "icon": "🌳"}, |
| 5: {"name": "專家教師", "min_score": 5000, "color": "#8B5CF6", "icon": "🌟"}, |
| 6: {"name": "溝通大師", "min_score": 8000, "color": "#EF4444", "icon": "👑"}, |
| 7: {"name": "傳奇大師", "min_score": 12000, "color": "#FFD700", "icon": "🏆"}, |
| } |
|
|
|
|
| |
| |
| |
| SCENARIO_DIFFICULTY = { |
| "📉 成績退步質疑": 1.0, |
| "📚 作業量過多抱怨": 1.2, |
| "😢 孩子情緒問題歸咎老師": 1.5, |
| "😞 孩子被同學霸凌卻未即時處理": 2.0, |
| "✏️ 孩子不寫作業、家長被指責教養問題": 1.5, |
| "🧩 特殊生輔導方式爭議(例如ADHD)": 2.0, |
| "💻 線上學習或遠距教學設備問題投訴": 1.2, |
| } |
|
|
|
|
| |
| |
| |
| PERSONA_COMPLEXITY = { |
| "😰 焦慮型家長": 1.2, |
| "🤔 質疑型家長": 1.5, |
| "😠 高衝突型家長": 2.0, |
| "🛡️ 過度保護型家長": 2.0, |
| "⚖️ 理性協商型家長": 1.0, |
| "😴 疲憊工作型家長": 1.2, |
| "🌏 文化差異型家長": 1.5, |
| } |
|
|
|
|
| |
| |
| |
| class GameState: |
| def __init__(self, storage_path: str = "player_data.json"): |
| self.storage_path = storage_path |
| self.data = self.load_data() |
| self.session_start_time = time.time() |
| self.last_conversation_time = None |
| self.current_streak = 0 |
| self.high_score_streak = 0 |
| self.current_challenge_mode = None |
| self.current_persona = None |
| self.current_scenario = None |
| self.current_round = 0 |
| self.challenge_start_time = None |
| |
| def load_data(self) -> Dict: |
| """載入玩家數據""" |
| try: |
| with open(self.storage_path, 'r', encoding='utf-8') as f: |
| return json.load(f) |
| except (FileNotFoundError, json.JSONDecodeError): |
| return self.get_default_data() |
| |
| def save_data(self): |
| """儲存玩家數據""" |
| try: |
| with open(self.storage_path, 'w', encoding='utf-8') as f: |
| json.dump(self.data, f, ensure_ascii=False, indent=2) |
| except Exception as e: |
| print(f"儲存數據失敗: {e}") |
| |
| def get_default_data(self) -> Dict: |
| """取得預設數據""" |
| return { |
| "total_score": 0, |
| "level": 1, |
| "achievements": [], |
| "statistics": { |
| "total_conversations": 0, |
| "average_risk": 0, |
| "best_risk_score": 100, |
| "best_score": 0, |
| "completed_scenarios": [], |
| "completed_personas": [], |
| "total_combinations": 0, |
| "fastest_time": 999, |
| "challenge_completed": 0, |
| "daily_challenge_completed": False, |
| "last_daily_challenge_date": None, |
| "current_daily_challenge": None, |
| "daily_challenge_progress": 0, |
| "daily_challenge_badges": [], |
| }, |
| "last_updated": datetime.now().isoformat() |
| } |
| |
| def calculate_score(self, risk_score: int, persona: str, scenario: str, |
| conversation_time: float) -> int: |
| """計算本次對話得分""" |
| |
| base_score = 100 - risk_score |
| |
| |
| streak_bonus = self.current_streak * 10 |
| |
| |
| scenario_bonus = int(SCENARIO_DIFFICULTY.get(scenario, 1.0) * 20) |
| |
| |
| persona_bonus = int(PERSONA_COMPLEXITY.get(persona, 1.0) * 15) |
| |
| |
| time_bonus = 0 |
| if conversation_time < 30: |
| time_bonus = 20 |
| |
| total_score = base_score + streak_bonus + scenario_bonus + persona_bonus + time_bonus |
| |
| |
| return max(0, total_score) |
| |
| def update_stats(self, risk_score: int, score: int, scenario: str, |
| persona: str, conversation_time: float): |
| """更新統計數據""" |
| stats = self.data["statistics"] |
| |
| |
| stats["total_conversations"] += 1 |
| |
| |
| total_risk = stats["average_risk"] * (stats["total_conversations"] - 1) + risk_score |
| stats["average_risk"] = total_risk / stats["total_conversations"] |
| |
| |
| if risk_score < stats["best_risk_score"]: |
| stats["best_risk_score"] = risk_score |
| |
| |
| if score > stats["best_score"]: |
| stats["best_score"] = score |
| |
| |
| if scenario not in stats["completed_scenarios"]: |
| stats["completed_scenarios"].append(scenario) |
| |
| |
| if persona not in stats["completed_personas"]: |
| stats["completed_personas"].append(persona) |
| |
| |
| stats["total_combinations"] = len(stats["completed_scenarios"]) * len(stats["completed_personas"]) |
| |
| |
| if conversation_time < stats["fastest_time"]: |
| stats["fastest_time"] = conversation_time |
| |
| |
| self.data["total_score"] += score |
| |
| |
| if risk_score < 30: |
| self.current_streak += 1 |
| else: |
| self.current_streak = 0 |
| |
| |
| if score > 80: |
| self.high_score_streak += 1 |
| else: |
| self.high_score_streak = 0 |
| |
| |
| self.check_daily_challenge() |
| self.update_daily_challenge_progress(risk_score, score, scenario, persona, conversation_time) |
| |
| |
| self.update_level() |
| |
| |
| self.data["last_updated"] = datetime.now().isoformat() |
| |
| |
| self.save_data() |
| |
| def update_level(self): |
| """更新等級""" |
| total_score = self.data["total_score"] |
| |
| for level_num, level_data in LEVELS.items(): |
| if total_score >= level_data["min_score"]: |
| self.data["level"] = level_num |
| |
| def generate_daily_challenge(self, date_str: str) -> Dict: |
| """根據日期生成每日挑戰""" |
| |
| hash_obj = hashlib.md5(date_str.encode()) |
| hash_int = int(hash_obj.hexdigest(), 16) |
| challenge_index = hash_int % len(DAILY_CHALLENGES) |
| challenge = DAILY_CHALLENGES[challenge_index].copy() |
|
|
| |
| description = challenge["description"].format(**challenge["params"]) |
| challenge["description"] = description |
|
|
| return challenge |
|
|
| def check_daily_challenge(self): |
| """檢查每日挑戰""" |
| today = datetime.now().date().isoformat() |
| stats = self.data["statistics"] |
|
|
| if stats["last_daily_challenge_date"] != today: |
| |
| stats["daily_challenge_completed"] = False |
| stats["last_daily_challenge_date"] = today |
| stats["daily_challenge_progress"] = 0 |
|
|
| |
| challenge = self.generate_daily_challenge(today) |
| stats["current_daily_challenge"] = challenge |
| |
| def update_daily_challenge_progress(self, risk_score: int, score: int, scenario: str, persona: str, conversation_time: float): |
| """更新每日挑戰進度""" |
| stats = self.data["statistics"] |
| if not stats.get("current_daily_challenge") or stats.get("daily_challenge_completed"): |
| return |
|
|
| challenge = stats["current_daily_challenge"] |
| challenge_id = challenge["id"] |
| params = challenge["params"] |
|
|
| if challenge_id == "low_risk_conversations": |
| if risk_score < params["threshold"]: |
| stats["daily_challenge_progress"] += 1 |
| elif challenge_id == "different_personas": |
| if persona not in stats.get("daily_personas_used", []): |
| stats.setdefault("daily_personas_used", []).append(persona) |
| stats["daily_challenge_progress"] = len(stats["daily_personas_used"]) |
| elif challenge_id == "low_risk_streak": |
| if self.current_streak >= params["count"]: |
| stats["daily_challenge_progress"] = params["count"] |
| elif challenge_id == "quick_responses": |
| if conversation_time < params["time"]: |
| stats["daily_challenge_progress"] += 1 |
| elif challenge_id == "scenario_explorer": |
| if scenario not in stats.get("daily_scenarios_used", []): |
| stats.setdefault("daily_scenarios_used", []).append(scenario) |
| stats["daily_challenge_progress"] = len(stats["daily_scenarios_used"]) |
| elif challenge_id == "high_score_achiever": |
| if score > params["score"]: |
| stats["daily_challenge_progress"] += 1 |
|
|
| |
| if stats["daily_challenge_progress"] >= params["count"]: |
| self.complete_daily_challenge() |
|
|
| def complete_daily_challenge(self): |
| """完成每日挑戰""" |
| stats = self.data["statistics"] |
| if stats.get("daily_challenge_completed"): |
| return |
|
|
| stats["daily_challenge_completed"] = True |
| challenge = stats["current_daily_challenge"] |
| reward = challenge["reward"] |
|
|
| |
| self.data["total_score"] += reward["points"] |
|
|
| |
| if reward["badge"] not in stats.get("daily_challenge_badges", []): |
| stats.setdefault("daily_challenge_badges", []).append(reward["badge"]) |
|
|
| self.save_data() |
| |
| def check_achievements(self) -> List[Dict]: |
| """檢查並返回新解鎖的成就""" |
| new_achievements = [] |
| stats = self.data["statistics"] |
| |
| |
| session_stats = { |
| "current_streak": self.current_streak, |
| "high_score_streak": self.high_score_streak, |
| "total_score": self.data["total_score"], |
| } |
| all_stats = {**stats, **session_stats} |
| |
| for achievement_id, achievement in ACHIEVEMENTS.items(): |
| if achievement_id not in self.data["achievements"]: |
| try: |
| if achievement["condition"](all_stats): |
| self.data["achievements"].append(achievement_id) |
| new_achievements.append({ |
| "id": achievement_id, |
| "name": achievement["name"], |
| "points": achievement["points"], |
| "icon": achievement["icon"] |
| }) |
| except Exception as e: |
| print(f"檢查成就 {achievement_id} 時發生錯誤: {e}") |
| |
| if new_achievements: |
| self.save_data() |
| |
| return new_achievements |
| |
| def get_level_info(self) -> Dict: |
| """取得當前等級資訊""" |
| level_num = self.data["level"] |
| level_data = LEVELS[level_num] |
| |
| |
| if level_num < len(LEVELS): |
| next_level = LEVELS[level_num + 1] |
| current_score = self.data["total_score"] |
| min_score = level_data["min_score"] |
| next_min_score = next_level["min_score"] |
| |
| progress = (current_score - min_score) / (next_min_score - min_score) * 100 |
| progress = min(100, max(0, progress)) |
| else: |
| progress = 100 |
| |
| return { |
| "level": level_num, |
| "name": level_data["name"], |
| "color": level_data["color"], |
| "icon": level_data["icon"], |
| "score": self.data["total_score"], |
| "progress": progress, |
| "next_level": level_num + 1 if level_num < len(LEVELS) else None |
| } |
| |
| def get_achievements_summary(self) -> Dict: |
| """取得成就摘要""" |
| unlocked = len(self.data["achievements"]) |
| total = len(ACHIEVEMENTS) |
| |
| return { |
| "unlocked": unlocked, |
| "total": total, |
| "progress": (unlocked / total) * 100 if total > 0 else 0, |
| "recent": self.data["achievements"][-5:] if len(self.data["achievements"]) > 0 else [] |
| } |
| |
| def get_daily_challenge_info(self) -> Dict: |
| """取得每日挑戰資訊""" |
| stats = self.data["statistics"] |
| challenge = stats.get("current_daily_challenge") |
|
|
| if not challenge: |
| return {"active": False} |
|
|
| params = challenge["params"] |
| progress = stats.get("daily_challenge_progress", 0) |
| completed = stats.get("daily_challenge_completed", False) |
|
|
| return { |
| "active": True, |
| "name": challenge["name"], |
| "description": challenge["description"], |
| "progress": progress, |
| "target": params["count"], |
| "completed": completed, |
| "reward": challenge["reward"] |
| } |
|
|
| def get_statistics_summary(self) -> Dict: |
| """取得統計摘要""" |
| stats = self.data["statistics"] |
|
|
| return { |
| "total_conversations": stats["total_conversations"], |
| "average_risk": round(stats["average_risk"], 1), |
| "best_risk_score": stats["best_risk_score"], |
| "best_score": stats["best_score"], |
| "completed_scenarios": len(stats["completed_scenarios"]), |
| "completed_personas": len(stats["completed_personas"]), |
| "total_combinations": stats["total_combinations"], |
| "fastest_time": stats["fastest_time"] if stats["fastest_time"] < 999 else None, |
| } |
|
|
| def get_adaptive_selection(self) -> Dict[str, str]: |
| """根據玩家表現自適應選擇情境和人格""" |
| import random |
| stats = self.data["statistics"] |
| avg_risk = stats["average_risk"] |
| completed_scenarios = stats["completed_scenarios"] |
| completed_personas = stats["completed_personas"] |
|
|
| |
| scenario_weights = {s: 1.0 for s in SCENARIO_DIFFICULTY} |
| persona_weights = {p: 1.0 for p in PERSONA_COMPLEXITY} |
|
|
| |
| performance_factor = 1.0 |
| if avg_risk < 20: |
| performance_factor = 1.5 |
| elif avg_risk > 50: |
| performance_factor = 0.7 |
|
|
| |
| for scenario, difficulty in SCENARIO_DIFFICULTY.items(): |
| if scenario in completed_scenarios: |
| scenario_weights[scenario] *= 0.5 |
| scenario_weights[scenario] *= (difficulty ** performance_factor) |
|
|
| for persona, complexity in PERSONA_COMPLEXITY.items(): |
| if persona in completed_personas: |
| persona_weights[persona] *= 0.5 |
| persona_weights[persona] *= (complexity ** performance_factor) |
|
|
| |
| total_scenario = sum(scenario_weights.values()) |
| scenario_probs = {s: w / total_scenario for s, w in scenario_weights.items()} |
|
|
| total_persona = sum(persona_weights.values()) |
| persona_probs = {p: w / total_persona for p, w in persona_weights.items()} |
|
|
| |
| selected_scenario = random.choices( |
| list(scenario_probs.keys()), |
| weights=list(scenario_probs.values()) |
| )[0] |
|
|
| selected_persona = random.choices( |
| list(persona_probs.keys()), |
| weights=list(persona_probs.values()) |
| )[0] |
|
|
| return {"scenario": selected_scenario, "persona": selected_persona} |
| |
| def reset_session(self): |
| """重置 session 狀態""" |
| self.current_streak = 0 |
| self.high_score_streak = 0 |
| self.last_conversation_time = None |
| |
| stats = self.data["statistics"] |
| stats.pop("daily_personas_used", None) |
| stats.pop("daily_scenarios_used", None) |
| |
| def check_challenge_complete(self) -> bool: |
| """檢查挑戰是否完成""" |
| if not self.current_challenge_mode: |
| return False |
| if self.current_challenge_mode == "時間挑戰 (30秒)": |
| if self.challenge_start_time: |
| elapsed = time.time() - self.challenge_start_time |
| return elapsed >= 30 |
| return False |
| elif self.current_challenge_mode == "連續挑戰 (3回合)": |
| return self.current_round >= 3 |
| else: |
| return False |
|
|
| def add_conversation(self, score: int, risk_score: int, persona: str, scenario: str, time_taken: float): |
| """添加對話並更新統計""" |
| self.update_stats(risk_score, score, scenario, persona, time_taken) |
| if self.current_challenge_mode == "連續挑戰 (3回合)": |
| self.current_round += 1 |
|
|
|
|
| |
| |
| |
| game_state = GameState() |