Spaces:
Sleeping
Sleeping
| """ | |
| Life Frontier: Partner's Concerto | |
| Single-app turn-based game with SQLite state management | |
| Upgraded for Gradio 5.49 | |
| """ | |
| import gradio as gr | |
| import sqlite3 | |
| import json | |
| import random | |
| import time | |
| from datetime import datetime, timedelta | |
| from typing import Dict, List, Optional, Tuple | |
| from dataclasses import dataclass, asdict | |
| from enum import Enum | |
| import threading | |
| # ===== CONSTANTS ===== | |
| INITIAL_TIME = 24 | |
| INITIAL_HP = 10 | |
| INITIAL_CP = 5 | |
| INITIAL_MONEY = 10 | |
| STRESS_THRESHOLD = 20 | |
| DB_PATH = "game.db" | |
| # ===== DATA MODELS ===== | |
| class CardType(str, Enum): | |
| WORK = "W" | |
| DOMESTIC = "D" | |
| HEALTH = "H" | |
| RELATIONSHIP = "R" | |
| SOCIAL = "S" | |
| QOL = "Q" | |
| class PlayerState: | |
| player_id: str | |
| name: str | |
| character_id: str | |
| time: int = INITIAL_TIME | |
| hp: int = INITIAL_HP | |
| cp: int = INITIAL_CP | |
| money: int = INITIAL_MONEY | |
| stress: int = 0 | |
| qol: int = 0 | |
| reputation: int = 0 | |
| hp_max: int = INITIAL_HP | |
| cp_max: int = INITIAL_CP | |
| is_eliminated: bool = False | |
| has_ended_turn: bool = False | |
| completed_tasks: str = "" # JSON array | |
| def to_dict(self): | |
| return asdict(self) | |
| def calculate_final_qol(self) -> int: | |
| if self.is_eliminated: | |
| return 0 | |
| hp_bonus = (self.hp_max - INITIAL_HP) * 2 | |
| cp_bonus = (self.cp_max - INITIAL_CP) * 3 | |
| return self.qol + hp_bonus + cp_bonus + self.reputation - self.stress | |
| class GameState: | |
| room_id: str | |
| current_round: int = 1 | |
| total_rounds: int = 8 | |
| current_turn: int = 0 # 0=player1, 1=player2 | |
| status: str = "waiting" # waiting, playing, finished | |
| available_tasks: str = "" # JSON array of task IDs | |
| active_negotiation: str = "" # JSON negotiation data | |
| created_at: str = "" | |
| # ===== DATABASE MANAGEMENT ===== | |
| class GameDatabase: | |
| def __init__(self, db_path: str = DB_PATH): | |
| self.db_path = db_path | |
| self.lock = threading.Lock() | |
| self.init_db() | |
| def get_connection(self): | |
| conn = sqlite3.connect(self.db_path, check_same_thread=False) | |
| conn.row_factory = sqlite3.Row | |
| return conn | |
| def init_db(self): | |
| with self.lock, self.get_connection() as conn: | |
| # Rooms table | |
| conn.execute(""" | |
| CREATE TABLE IF NOT EXISTS rooms ( | |
| room_id TEXT PRIMARY KEY, | |
| current_round INTEGER DEFAULT 1, | |
| total_rounds INTEGER DEFAULT 8, | |
| current_turn INTEGER DEFAULT 0, | |
| status TEXT DEFAULT 'waiting', | |
| available_tasks TEXT DEFAULT '[]', | |
| active_negotiation TEXT DEFAULT '', | |
| created_at TEXT, | |
| winner_id TEXT | |
| ) | |
| """) | |
| # Players table | |
| conn.execute(""" | |
| CREATE TABLE IF NOT EXISTS players ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| room_id TEXT, | |
| player_id TEXT, | |
| name TEXT, | |
| character_id TEXT, | |
| player_slot INTEGER, | |
| time INTEGER DEFAULT 24, | |
| hp INTEGER DEFAULT 10, | |
| cp INTEGER DEFAULT 5, | |
| money INTEGER DEFAULT 10, | |
| stress INTEGER DEFAULT 0, | |
| qol INTEGER DEFAULT 0, | |
| reputation INTEGER DEFAULT 0, | |
| hp_max INTEGER DEFAULT 10, | |
| cp_max INTEGER DEFAULT 5, | |
| is_eliminated INTEGER DEFAULT 0, | |
| has_ended_turn INTEGER DEFAULT 0, | |
| completed_tasks TEXT DEFAULT '[]', | |
| UNIQUE(room_id, player_slot) | |
| ) | |
| """) | |
| # Spectators table | |
| conn.execute(""" | |
| CREATE TABLE IF NOT EXISTS spectators ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| room_id TEXT, | |
| spectator_id TEXT, | |
| name TEXT, | |
| joined_at TEXT | |
| ) | |
| """) | |
| # Rankings table | |
| conn.execute(""" | |
| CREATE TABLE IF NOT EXISTS rankings ( | |
| player_name TEXT PRIMARY KEY, | |
| total_games INTEGER DEFAULT 0, | |
| total_wins INTEGER DEFAULT 0, | |
| highest_qol INTEGER DEFAULT 0, | |
| average_qol REAL DEFAULT 0.0, | |
| last_played TEXT | |
| ) | |
| """) | |
| # Game history | |
| conn.execute(""" | |
| CREATE TABLE IF NOT EXISTS game_history ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| room_id TEXT, | |
| player1_name TEXT, | |
| player2_name TEXT, | |
| winner_name TEXT, | |
| player1_qol INTEGER, | |
| player2_qol INTEGER, | |
| total_rounds INTEGER, | |
| completed_at TEXT | |
| ) | |
| """) | |
| conn.commit() | |
| def create_room(self, room_id: str, total_rounds: int = 8) -> bool: | |
| try: | |
| with self.lock, self.get_connection() as conn: | |
| # Cleanup old rooms before creating new one (inline to avoid nested locks) | |
| stale_cutoff = (datetime.now() - timedelta(hours=24)).isoformat() | |
| conn.execute("DELETE FROM rooms WHERE status = 'waiting' AND created_at < ?", (stale_cutoff,)) | |
| try: | |
| conn.execute(""" | |
| INSERT INTO rooms (room_id, total_rounds, created_at, status) | |
| VALUES (?, ?, ?, 'waiting') | |
| """, (room_id, total_rounds, datetime.now().isoformat())) | |
| conn.commit() | |
| print(f"✨ Created room {room_id}") | |
| return True | |
| except sqlite3.IntegrityError as e: | |
| print(f"IntegrityError creating room: {e}") | |
| return False | |
| except Exception as e: | |
| print(f"Exception in create_room: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| return False | |
| def join_room(self, room_id: str, player_id: str, name: str, | |
| character_id: str) -> Tuple[bool, Optional[int], str]: | |
| """Returns (success, player_slot, message)""" | |
| with self.lock, self.get_connection() as conn: | |
| # Check room exists | |
| room = conn.execute( | |
| "SELECT status FROM rooms WHERE room_id = ?", (room_id,) | |
| ).fetchone() | |
| if not room: | |
| return False, None, "Room not found" | |
| if room['status'] != 'waiting': | |
| # Game already started, join as spectator | |
| conn.execute(""" | |
| INSERT INTO spectators (room_id, spectator_id, name, joined_at) | |
| VALUES (?, ?, ?, ?) | |
| """, (room_id, player_id, name, datetime.now().isoformat())) | |
| conn.commit() | |
| return True, None, f"Joined as spectator. Game in progress." | |
| # Try to join as player | |
| players = conn.execute( | |
| "SELECT player_slot FROM players WHERE room_id = ?", (room_id,) | |
| ).fetchall() | |
| if len(players) >= 2: | |
| # Room full, join as spectator | |
| conn.execute(""" | |
| INSERT INTO spectators (room_id, spectator_id, name, joined_at) | |
| VALUES (?, ?, ?, ?) | |
| """, (room_id, player_id, name, datetime.now().isoformat())) | |
| conn.commit() | |
| return True, None, f"Room full. Joined as spectator." | |
| # Apply character starting bonuses | |
| character = next((c for c in CHARACTERS if c['id'] == character_id), {}) | |
| hp_max = INITIAL_HP + character.get('hp_max_bonus', 0) | |
| # Join as player | |
| slot = 0 if len(players) == 0 else 1 | |
| conn.execute(""" | |
| INSERT INTO players ( | |
| room_id, player_id, name, character_id, player_slot, hp_max | |
| ) VALUES (?, ?, ?, ?, ?, ?) | |
| """, (room_id, player_id, name, character_id, slot, hp_max)) | |
| # If 2 players, start game | |
| if slot == 1: | |
| self._start_game(room_id, conn) | |
| conn.commit() | |
| return True, slot, f"✅ Joined as Player {slot + 1}! Game starting..." | |
| conn.commit() | |
| return True, slot, f"Joined as Player {slot + 1}. Waiting for Player 2..." | |
| def _start_game(self, room_id: str, conn): | |
| """Internal: Start game when 2 players joined""" | |
| # Update room status | |
| conn.execute(""" | |
| UPDATE rooms SET status = 'playing' WHERE room_id = ? | |
| """, (room_id,)) | |
| # Draw initial tasks (3 random) | |
| task_ids = random.sample([t['id'] for t in TASKS], 3) | |
| conn.execute(""" | |
| UPDATE rooms SET available_tasks = ? WHERE room_id = ? | |
| """, (json.dumps(task_ids), room_id)) | |
| print(f"🎮 Game started in room {room_id}!") | |
| def cleanup_old_rooms(self): | |
| """Remove empty or stale rooms.""" | |
| try: | |
| with self.lock, self.get_connection() as conn: | |
| # Using ISO 8601 format for comparisons | |
| stale_cutoff = (datetime.now() - timedelta(hours=24)).isoformat() | |
| empty_cutoff = (datetime.now() - timedelta(hours=1)).isoformat() | |
| # Find rooms waiting for more than 24 hours | |
| stale_rooms_q = conn.execute( | |
| "SELECT room_id FROM rooms WHERE status = 'waiting' AND created_at < ?", | |
| (stale_cutoff,) | |
| ) | |
| stale_ids = {row['room_id'] for row in stale_rooms_q.fetchall()} | |
| # Find rooms waiting for more than 1 hour with no players | |
| empty_rooms_q = conn.execute( | |
| """ | |
| SELECT room_id FROM rooms | |
| WHERE status = 'waiting' | |
| AND created_at < ? | |
| AND room_id NOT IN (SELECT DISTINCT room_id FROM players WHERE room_id IS NOT NULL) | |
| """, | |
| (empty_cutoff,) | |
| ) | |
| empty_ids = {row['room_id'] for row in empty_rooms_q.fetchall()} | |
| rooms_to_delete = stale_ids.union(empty_ids) | |
| if not rooms_to_delete: | |
| return 0 | |
| for room_id in rooms_to_delete: | |
| conn.execute("DELETE FROM rooms WHERE room_id = ?", (room_id,)) | |
| conn.execute("DELETE FROM players WHERE room_id = ?", (room_id,)) | |
| conn.execute("DELETE FROM spectators WHERE room_id = ?", (room_id,)) | |
| print(f"🧹 Cleaned up stale/empty room: {room_id}") | |
| conn.commit() | |
| return len(rooms_to_delete) | |
| except Exception as e: | |
| print(f"Exception in cleanup_old_rooms: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| return 0 | |
| def get_room_state(self, room_id: str) -> Optional[Dict]: | |
| """Get complete room state including players""" | |
| with self.lock, self.get_connection() as conn: | |
| room = conn.execute( | |
| "SELECT * FROM rooms WHERE room_id = ?", (room_id,) | |
| ).fetchone() | |
| if not room: | |
| return None | |
| players = conn.execute(""" | |
| SELECT * FROM players WHERE room_id = ? ORDER BY player_slot | |
| """, (room_id,)).fetchall() | |
| spectators = conn.execute(""" | |
| SELECT name FROM spectators WHERE room_id = ? | |
| """, (room_id,)).fetchall() | |
| return { | |
| 'room': dict(room), | |
| 'players': [dict(p) for p in players], | |
| 'spectators': [s['name'] for s in spectators], | |
| 'task_ids': json.loads(room['available_tasks'] or '[]') | |
| } | |
| def update_player(self, room_id: str, player_slot: int, updates: Dict): | |
| """Update player state""" | |
| with self.lock, self.get_connection() as conn: | |
| set_clause = ", ".join([f"{k} = ?" for k in updates.keys()]) | |
| values = list(updates.values()) + [room_id, player_slot] | |
| conn.execute(f""" | |
| UPDATE players SET {set_clause} | |
| WHERE room_id = ? AND player_slot = ? | |
| """, values) | |
| conn.commit() | |
| def end_turn(self, room_id: str, player_slot: int): | |
| """End player's turn and check for round end""" | |
| with self.lock, self.get_connection() as conn: | |
| # Mark turn ended | |
| conn.execute(""" | |
| UPDATE players SET has_ended_turn = 1 | |
| WHERE room_id = ? AND player_slot = ? | |
| """, (room_id, player_slot)) | |
| # Check if both ended turn | |
| count = conn.execute(""" | |
| SELECT COUNT(*) as cnt FROM players | |
| WHERE room_id = ? AND has_ended_turn = 1 | |
| """, (room_id,)).fetchone()['cnt'] | |
| if count == 2: | |
| # Both ended, advance round | |
| self._advance_round(room_id, conn) | |
| else: | |
| # Switch turn | |
| room = conn.execute( | |
| "SELECT current_turn FROM rooms WHERE room_id = ?", (room_id,) | |
| ).fetchone() | |
| next_turn = 1 - room['current_turn'] | |
| conn.execute(""" | |
| UPDATE rooms SET current_turn = ? WHERE room_id = ? | |
| """, (next_turn, room_id)) | |
| conn.commit() | |
| def _advance_round(self, room_id: str, conn): | |
| """Internal: Advance to next round""" | |
| room = conn.execute( | |
| "SELECT current_round, total_rounds FROM rooms WHERE room_id = ?", | |
| (room_id,) | |
| ).fetchone() | |
| next_round = room['current_round'] + 1 | |
| if next_round > room['total_rounds']: | |
| # Game over | |
| self._end_game(room_id, conn) | |
| else: | |
| # Get players for character effects | |
| players = conn.execute(""" | |
| SELECT * FROM players WHERE room_id = ? | |
| """, (room_id,)).fetchall() | |
| # Reset for next round and apply turn-start character effects | |
| for player in players: | |
| character = next((c for c in CHARACTERS if c['id'] == player['character_id']), {}) | |
| new_stress = player['stress'] | |
| # Workaholic: +1 stress per turn | |
| if character.get('turn_stress'): | |
| new_stress += character['turn_stress'] | |
| # Neat Freak: Check if did any domestic task | |
| if character.get('no_domestic_stress'): | |
| completed = json.loads(player.get('completed_tasks', '[]')) | |
| did_domestic = any(t.startswith('D') for t in completed) | |
| if not did_domestic: | |
| new_stress += character['no_domestic_stress'] | |
| conn.execute(""" | |
| UPDATE players SET | |
| time = ?, | |
| has_ended_turn = 0, | |
| completed_tasks = '[]', | |
| stress = ? | |
| WHERE room_id = ? AND player_slot = ? | |
| """, (INITIAL_TIME, new_stress, room_id, player['player_slot'])) | |
| # Draw new tasks | |
| task_ids = random.sample([t['id'] for t in TASKS], 3) | |
| conn.execute(""" | |
| UPDATE rooms SET | |
| current_round = ?, | |
| current_turn = 0, | |
| available_tasks = ? | |
| WHERE room_id = ? | |
| """, (next_round, json.dumps(task_ids), room_id)) | |
| def _end_game(self, room_id: str, conn): | |
| """Internal: End game and calculate winner""" | |
| players = conn.execute(""" | |
| SELECT * FROM players WHERE room_id = ? ORDER BY player_slot | |
| """, (room_id,)).fetchall() | |
| # Calculate final QoL | |
| p1 = PlayerState(**{k: players[0][k] for k in players[0].keys() | |
| if k in PlayerState.__annotations__}) | |
| p2 = PlayerState(**{k: players[1][k] for k in players[1].keys() | |
| if k in PlayerState.__annotations__}) | |
| qol1 = p1.calculate_final_qol() | |
| qol2 = p2.calculate_final_qol() | |
| winner_id = p1.player_id if qol1 > qol2 else p2.player_id | |
| winner_name = p1.name if qol1 > qol2 else p2.name | |
| # Update room | |
| conn.execute(""" | |
| UPDATE rooms SET status = 'finished', winner_id = ? | |
| WHERE room_id = ? | |
| """, (winner_id, room_id)) | |
| # Save to history | |
| conn.execute(""" | |
| INSERT INTO game_history ( | |
| room_id, player1_name, player2_name, winner_name, | |
| player1_qol, player2_qol, total_rounds, completed_at | |
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) | |
| """, (room_id, p1.name, p2.name, winner_name, qol1, qol2, | |
| players[0]['completed_tasks'], datetime.now().isoformat())) | |
| # Update rankings | |
| self._update_rankings(p1.name, qol1, winner_name == p1.name, conn) | |
| self._update_rankings(p2.name, qol2, winner_name == p2.name, conn) | |
| def _update_rankings(self, player_name: str, qol: int, won: bool, conn): | |
| """Internal: Update player rankings""" | |
| existing = conn.execute(""" | |
| SELECT * FROM rankings WHERE player_name = ? | |
| """, (player_name,)).fetchone() | |
| if existing: | |
| new_games = existing['total_games'] + 1 | |
| new_wins = existing['total_wins'] + (1 if won else 0) | |
| new_avg = ((existing['average_qol'] * existing['total_games']) + qol) / new_games | |
| new_high = max(existing['highest_qol'], qol) | |
| conn.execute(""" | |
| UPDATE rankings SET | |
| total_games = ?, total_wins = ?, | |
| average_qol = ?, highest_qol = ?, | |
| last_played = ? | |
| WHERE player_name = ? | |
| """, (new_games, new_wins, new_avg, new_high, | |
| datetime.now().isoformat(), player_name)) | |
| else: | |
| conn.execute(""" | |
| INSERT INTO rankings ( | |
| player_name, total_games, total_wins, | |
| average_qol, highest_qol, last_played | |
| ) VALUES (?, 1, ?, ?, ?, ?) | |
| """, (player_name, 1 if won else 0, qol, qol, | |
| datetime.now().isoformat())) | |
| def get_rankings(self, limit: int = 10) -> List[Dict]: | |
| """Get top rankings""" | |
| with self.get_connection() as conn: | |
| rows = conn.execute(""" | |
| SELECT | |
| player_name, total_games, total_wins, | |
| ROUND(average_qol, 1) as avg_qol, | |
| highest_qol, | |
| ROUND(CAST(total_wins AS REAL) / total_games * 100, 1) as win_rate | |
| FROM rankings | |
| WHERE total_games > 0 | |
| ORDER BY average_qol DESC | |
| LIMIT ? | |
| """, (limit,)).fetchall() | |
| return [dict(r) for r in rows] | |
| # ===== GAME DATA ===== | |
| CHARACTERS = [ | |
| {"id": "P1", "name": "Workaholic", "desc": "Work +2 VP, but +1 stress/turn", | |
| "work_bonus": 2, "turn_stress": 1}, | |
| {"id": "P2", "name": "Freelancer", "desc": "Work time -2, but coop +1 CP cost", | |
| "work_time_reduction": 2, "coop_cp_penalty": 1}, | |
| {"id": "P3", "name": "Frugal", "desc": "QoL tasks -2 VP cost", | |
| "qol_cost_reduction": 2}, | |
| {"id": "P4", "name": "Hedonist", "desc": "QoL +1, but domestic +2 time", | |
| "qol_bonus": 1, "domestic_time_penalty": 2}, | |
| {"id": "H1", "name": "Neat Freak", "desc": "Domestic success +2 CP, no domestic +2 stress", | |
| "domestic_cp_bonus": 2, "no_domestic_stress": 2}, | |
| {"id": "H2", "name": "Night Owl", "desc": "Rest HP halved, late work -1 CP", | |
| "rest_halved": True}, | |
| {"id": "H3", "name": "Fitness Buff", "desc": "Rest +1 HP, HP max +2", | |
| "rest_hp_bonus": 1, "hp_max_bonus": 2}, | |
| {"id": "H4", "name": "Procrastinator", "desc": "Long tasks +1 time, failure +1 stress", | |
| "long_task_penalty": 1, "failure_stress": 1}, | |
| {"id": "R1", "name": "Anxious", "desc": "Coop min 3 CP, success -1 stress, failure +2 stress", | |
| "coop_min_cp": 3, "coop_success_stress": -1, "coop_failure_stress": 2}, | |
| {"id": "R2", "name": "Supporter", "desc": "Partner +1 HP/CP on coop success", | |
| "support_bonus": True}, | |
| {"id": "R3", "name": "Planner", "desc": "Can force round-up in negotiation", | |
| "force_roundup": True}, | |
| {"id": "R4", "name": "Social Butterfly", "desc": "Social -1 time, +1 reputation", | |
| "social_time_reduction": 1, "social_rep_bonus": 1}, | |
| ] | |
| TASKS = [ | |
| # Work Tasks (W1-W10) | |
| {"id": "W1", "name": "Core Project", "type": "W", "time": 8, | |
| "solo": {"CP": 7, "HP": 3}, "coop": {"CP": 10}, "reward": 12}, | |
| {"id": "W2", "name": "Annual Report", "type": "W", "time": 6, | |
| "solo": {"CP": 6}, "coop": {"CP": 8}, "reward": 8}, | |
| {"id": "W3", "name": "Client Dispute", "type": "W", "time": 5, | |
| "solo": {"CP": 5}, "coop": {"CP": 7}, "reward": 6}, | |
| {"id": "W4", "name": "Promotion Prep", "type": "W", "time": 7, | |
| "solo": {"CP": 5, "HP": 4}, "coop": {"CP": 9}, "reward": 10}, | |
| {"id": "W5", "name": "Side Gig", "type": "W", "time": 4, | |
| "solo": {"CP": 4}, "coop": {"CP": 6}, "reward": 5}, | |
| {"id": "W6", "name": "Learn New Skill", "type": "W", "time": 5, | |
| "solo": {"CP": 5}, "coop": {"CP": 7}, "reward": 3, "effect": "cp_max:+1"}, | |
| {"id": "W7", "name": "Emergency Emails", "type": "W", "time": 3, | |
| "solo": {"CP": 3}, "coop": {"CP": 4}, "reward": 4}, | |
| {"id": "W8", "name": "Industry Conference", "type": "W", "time": 6, | |
| "solo": {"CP": 5, "HP": 3}, "coop": {"CP": 8}, "reward": 7}, | |
| {"id": "W9", "name": "Business Plan", "type": "W", "time": 9, | |
| "solo": {"CP": 8}, "coop": {"CP": 11}, "reward": 15}, | |
| {"id": "W10", "name": "Expense Reports", "type": "W", "time": 2, | |
| "solo": {"CP": 2}, "coop": {"CP": 3}, "reward": 2}, | |
| # Domestic Tasks (D1-D8) | |
| {"id": "D1", "name": "Deep Clean Kitchen", "type": "D", "time": 5, | |
| "solo": {"HP": 5}, "coop": {"HP": 7}, "reward": 0, "effect": "stress:-2"}, | |
| {"id": "D2", "name": "Laundry & Organize", "type": "D", "time": 3, | |
| "solo": {"HP": 3}, "coop": {"HP": 4}, "reward": 0, "effect": "hp:+2"}, | |
| {"id": "D3", "name": "Pay Bills", "type": "D", "time": 2, | |
| "solo": {"CP": 2}, "coop": {"CP": 3}, "reward": 0, "effect": "cp:+1"}, | |
| {"id": "D4", "name": "Grocery Shopping", "type": "D", "time": 4, | |
| "solo": {"HP": 3}, "coop": {"HP": 5}, "reward": 0, "effect": "hp:+1"}, | |
| {"id": "D5", "name": "Take Out Trash", "type": "D", "time": 3, | |
| "solo": {"HP": 3}, "coop": {"HP": 4}, "reward": 0, "effect": "stress:-1"}, | |
| {"id": "D6", "name": "Fix Appliance", "type": "D", "time": 5, | |
| "solo": {"HP": 4}, "coop": {"HP": 6}, "reward": 1}, | |
| {"id": "D7", "name": "Tidy Common Area", "type": "D", "time": 4, | |
| "solo": {"HP": 4}, "coop": {"HP": 5}, "reward": 0, "effect": "stress:-1"}, | |
| {"id": "D8", "name": "Cook Healthy Meal", "type": "D", "time": 3, | |
| "solo": {"HP": 3}, "coop": {"HP": 4}, "reward": 0, "effect": "hp:+1"}, | |
| # Health Tasks (H1-H6) | |
| {"id": "H1", "name": "Fitness Training", "type": "H", "time": 5, | |
| "solo": {"HP": 6}, "coop": {"HP": 8}, "reward": 1, "effect": "hp_max:+1"}, | |
| {"id": "H2", "name": "Deep Meditation", "type": "H", "time": 4, | |
| "solo": {"CP": 4}, "coop": {"CP": 5}, "reward": 0, "effect": "cp:+3"}, | |
| {"id": "H3", "name": "Annual Checkup", "type": "H", "time": 6, | |
| "solo": {"HP": 5}, "coop": {"HP": 7}, "reward": 2, "effect": "stress:-2"}, | |
| {"id": "H4", "name": "Emotion Regulation Class", "type": "H", "time": 5, | |
| "solo": {"CP": 4}, "coop": {"CP": 6}, "reward": 1, "effect": "cp_max:+1"}, | |
| {"id": "H5", "name": "Sleep Schedule Reset", "type": "H", "time": 3, | |
| "solo": {"HP": 3}, "coop": {"HP": 4}, "reward": 0, "effect": "hp:+4"}, | |
| {"id": "H6", "name": "Personal Alone Time", "type": "H", "time": 2, | |
| "solo": {"CP": 2}, "coop": {"CP": 3}, "reward": 0, "effect": "stress:-2"}, | |
| # Relationship & Social Tasks (R1-R2, S1-S2) | |
| {"id": "R1", "name": "Romantic Date", "type": "R", "time": 5, | |
| "solo": {"CP": 5}, "coop": {"CP": 7}, "cost": 5, "qol": 3, "effect": "stress:-3"}, | |
| {"id": "R2", "name": "Future Planning", "type": "R", "time": 6, | |
| "solo": {"CP": 6}, "coop": {"CP": 8}, "cost": 3, "qol": 2, "effect": "cp:+1"}, | |
| {"id": "S1", "name": "Host Friends", "type": "S", "time": 4, | |
| "solo": {"CP": 4}, "coop": {"CP": 6}, "cost": 4, "qol": 1}, | |
| {"id": "S2", "name": "Community Event", "type": "S", "time": 3, | |
| "solo": {"HP": 3}, "coop": {"HP": 4}, "reward": 0}, | |
| # QoL Investment Tasks (Q1-Q4) | |
| {"id": "Q1", "name": "Buy Furniture", "type": "Q", "time": 2, | |
| "solo": {"HP": 2}, "coop": {"HP": 3}, "cost": 10, "qol": 5}, | |
| {"id": "Q2", "name": "Weekend Trip", "type": "Q", "time": 7, | |
| "solo": {"HP": 5, "CP": 4}, "coop": {"HP": 9}, "cost": 15, "qol": 7, "effect": "recover_all"}, | |
| {"id": "Q3", "name": "Home Upgrade", "type": "Q", "time": 4, | |
| "solo": {"HP": 3}, "coop": {"HP": 5}, "cost": 8, "qol": 4}, | |
| {"id": "Q4", "name": "Spa Day", "type": "Q", "time": 5, | |
| "solo": {"HP": 4}, "coop": {"HP": 6}, "cost": 12, "qol": 6, "effect": "stress:-3"}, | |
| ] | |
| # ===== GLOBAL STATE ===== | |
| db = GameDatabase() | |
| # ===== GAME LOGIC ===== | |
| def roll_dice() -> int: | |
| return random.randint(1, 6) | |
| def can_afford_task(player: Dict, task: Dict, is_solo: bool = True) -> Tuple[bool, str]: | |
| """Check if player can afford task""" | |
| req = task['solo'] if is_solo else task['coop'] | |
| if player['time'] < task['time']: | |
| return False, f"Not enough time (need {task['time']}h)" | |
| for res, amount in req.items(): | |
| if res == "HP" and player['hp'] < amount: | |
| return False, f"Not enough HP (need {amount})" | |
| if res == "CP" and player['cp'] < amount: | |
| return False, f"Not enough CP (need {amount})" | |
| if 'cost' in task and player['money'] < task['cost']: | |
| return False, f"Not enough money (need ${task['cost']})" | |
| return True, "OK" | |
| def execute_rest(player: Dict, rest_type: str) -> Tuple[bool, str, Dict]: | |
| """Execute rest action""" | |
| if rest_type == "quick_nap": | |
| if player['time'] < 3: | |
| return False, "Not enough time (need 3h)", {} | |
| dice = roll_dice() | |
| hp_recovered = 2 * dice | |
| new_hp = min(player['hp'] + hp_recovered, player['hp_max']) | |
| return True, f"Rolled {dice}! Recovered {hp_recovered} HP", { | |
| 'time': player['time'] - 3, | |
| 'hp': new_hp | |
| } | |
| elif rest_type == "full_sleep": | |
| if player['time'] < 9: | |
| return False, "Not enough time (need 9h)", {} | |
| return True, "Recovered to base levels and reduced stress", { | |
| 'time': player['time'] - 9, | |
| 'hp': min(8, player['hp_max']), | |
| 'cp': min(4, player['cp_max']), | |
| 'stress': max(0, player['stress'] - 1) | |
| } | |
| elif rest_type == "deep_rest": | |
| if player['time'] < 12: | |
| return False, "Not enough time (need 12h)", {} | |
| return True, "Full recovery!", { | |
| 'time': player['time'] - 12, | |
| 'hp': player['hp_max'], | |
| 'cp': player['cp_max'], | |
| 'stress': max(0, player['stress'] - 2) | |
| } | |
| return False, "Invalid rest type", {} | |
| def execute_solo_task(player: Dict, task: Dict) -> Tuple[bool, str, Dict]: | |
| """Execute task solo""" | |
| # Check affordability | |
| can_afford, msg = can_afford_task(player, task, is_solo=True) | |
| if not can_afford: | |
| return False, msg, {} | |
| solo_req = task['solo'] | |
| # Execute | |
| updates = { | |
| 'time': player['time'] - task['time'], | |
| 'stress': player['stress'] + 1 # Fatigue | |
| } | |
| # Spend resources | |
| if 'HP' in solo_req: | |
| updates['hp'] = player['hp'] - solo_req['HP'] | |
| if 'CP' in solo_req: | |
| updates['cp'] = player['cp'] - solo_req['CP'] | |
| if 'cost' in task: | |
| updates['money'] = player['money'] - task['cost'] | |
| # Apply rewards | |
| if 'reward' in task: | |
| updates['money'] = updates.get('money', player['money']) + task['reward'] | |
| if 'qol' in task: | |
| updates['qol'] = player['qol'] + task['qol'] | |
| # Apply effects | |
| if 'effect' in task: | |
| for effect in task['effect'].split(','): | |
| if ':' in effect: | |
| eff_type, value = effect.split(':') | |
| value = int(value) | |
| if eff_type == 'stress': | |
| updates['stress'] = max(0, updates.get('stress', player['stress']) + value) | |
| elif eff_type == 'hp': | |
| updates['hp'] = min(updates.get('hp', player['hp']) + value, player['hp_max']) | |
| elif eff_type == 'cp': | |
| updates['cp'] = min(updates.get('cp', player['cp']) + value, player['cp_max']) | |
| elif eff_type == 'hp_max': | |
| updates['hp_max'] = player['hp_max'] + value | |
| elif eff_type == 'cp_max': | |
| updates['cp_max'] = player['cp_max'] + value | |
| elif effect == 'recover_all': | |
| updates['hp'] = player['hp_max'] | |
| updates['cp'] = player['cp_max'] | |
| # Check elimination | |
| if updates.get('stress', player['stress']) >= STRESS_THRESHOLD: | |
| updates['is_eliminated'] = 1 | |
| return True, f"⚠️ Task completed but ELIMINATED (stress reached {updates['stress']})", updates | |
| # Build success message | |
| msg_parts = [f"✓ {task['name']} completed!"] | |
| if 'reward' in task and task['reward'] > 0: | |
| msg_parts.append(f"+${task['reward']}") | |
| if 'qol' in task and task['qol'] > 0: | |
| msg_parts.append(f"+{task['qol']} QoL") | |
| if 'effect' in task and 'stress:-' in task['effect']: | |
| msg_parts.append(f"Stress reduced!") | |
| return True, " ".join(msg_parts), updates | |
| def initiate_negotiation(initiator: Dict, partner: Dict, task: Dict, | |
| initiator_contrib: int) -> Tuple[bool, str, Dict]: | |
| """Initiate cooperation negotiation""" | |
| # Check initiator can afford their contribution | |
| if initiator['cp'] < initiator_contrib: | |
| return False, "Not enough CP to contribute", {} | |
| if initiator['time'] < task['time']: | |
| return False, f"Not enough time (need {task['time']}h)", {} | |
| # Check task has coop requirements | |
| if 'coop' not in task: | |
| return False, "This task doesn't support cooperation", {} | |
| # Calculate target for partner | |
| coop_req = task['coop'] | |
| total_needed = list(coop_req.values())[0] # Get first requirement value | |
| partner_target = total_needed - initiator_contrib | |
| if partner_target <= 0: | |
| return False, "You're already contributing enough! Just do it solo.", {} | |
| # Save negotiation state | |
| negotiation_data = { | |
| 'task_id': task['id'], | |
| 'initiator_contrib': initiator_contrib, | |
| 'partner_target': partner_target, | |
| 'total_needed': total_needed, | |
| 'pending': True | |
| } | |
| return True, f"Negotiation started! Partner needs to contribute {partner_target} points.", negotiation_data | |
| def respond_negotiation(partner: Dict, negotiation: Dict, | |
| cp_invest: int, hp_invest: int) -> Tuple[bool, str, int]: | |
| """Partner responds to negotiation""" | |
| if cp_invest < 1: | |
| return False, "Must invest at least 1 CP", 0 | |
| if partner['cp'] < cp_invest: | |
| return False, f"Not enough CP (need {cp_invest})", 0 | |
| if partner['hp'] < hp_invest: | |
| return False, f"Not enough HP (need {hp_invest})", 0 | |
| # Calculate result using formula | |
| target = negotiation['partner_target'] | |
| investment = cp_invest + hp_invest | |
| # Simple formula: (target × investment) / investment = target | |
| # But if investment < target, get proportional result | |
| actual_contrib = min(target, investment) | |
| # Round up if close | |
| if actual_contrib >= target * 0.9: | |
| actual_contrib = target | |
| return True, f"Contributed {actual_contrib} points", actual_contrib | |
| # ===== GRADIO UI ===== | |
| def create_ui(): | |
| with gr.Blocks(title="Life Frontier: Partner's Concerto", theme=gr.themes.Soft()) as app: | |
| gr.Markdown(""" | |
| # 🏠 Life Frontier: Partner's Concerto | |
| *A turn-based strategy game for couples* | |
| """) | |
| # Session state - Gradio 5 style | |
| session_state = gr.State({ | |
| 'room_id': None, | |
| 'player_id': None, | |
| 'player_slot': None, | |
| 'is_spectator': False | |
| }) | |
| with gr.Tab("🎮 Join/Create Game"): | |
| with gr.Row(): | |
| room_id_input = gr.Textbox(label="Room ID", placeholder="Leave empty to create new") | |
| your_name_input = gr.Textbox(label="Your Name", value="Player") | |
| character_select = gr.Dropdown( | |
| choices=[(c['name'], c['id']) for c in CHARACTERS], | |
| label="Select Character", | |
| value=CHARACTERS[0]['id'], | |
| interactive=True | |
| ) | |
| with gr.Row(): | |
| create_btn = gr.Button("🆕 Create Room", variant="primary") | |
| join_btn = gr.Button("🚪 Join Room", variant="secondary") | |
| join_result = gr.Markdown(value="") | |
| with gr.Tab("🎯 Game Board"): | |
| gr.Markdown("### Game Status") | |
| status_display = gr.Markdown(value="*No game joined*") | |
| with gr.Row(): | |
| # Player 1 Panel | |
| with gr.Column(): | |
| gr.Markdown("### Player 1") | |
| p1_name = gr.Textbox(label="Name", interactive=False, value="") | |
| p1_character = gr.Textbox(label="Character", interactive=False, value="") | |
| p1_display = gr.JSON(label="Status", value={}) | |
| with gr.Row(): | |
| p1_time = gr.Number(label="⏰ Time", interactive=False, value=0) | |
| p1_money = gr.Number(label="💰 Money", interactive=False, value=0) | |
| with gr.Row(): | |
| p1_hp = gr.Number(label="❤️ HP", interactive=False, value=0) | |
| p1_cp = gr.Number(label="🧠 CP", interactive=False, value=0) | |
| with gr.Row(): | |
| p1_stress = gr.Number(label="😰 Stress", interactive=False, value=0) | |
| p1_qol = gr.Number(label="✨ QoL", interactive=False, value=0) | |
| # Center - Actions | |
| with gr.Column(): | |
| gr.Markdown("### Your Actions") | |
| your_turn_msg = gr.Markdown(value="*Waiting for your turn...*") | |
| gr.Markdown("**Rest Actions**") | |
| with gr.Row(): | |
| quick_nap_btn = gr.Button("🛌 Quick Nap (3h)", size="sm") | |
| full_sleep_btn = gr.Button("😴 Full Sleep (9h)", size="sm") | |
| deep_rest_btn = gr.Button("💤 Deep Rest (12h)", size="sm") | |
| gr.Markdown("**Available Tasks**") | |
| tasks_display = gr.Dataframe( | |
| headers=["ID", "Name", "Type", "Time", "Req (Solo)", "Req (Coop)", "Reward"], | |
| label="Public Tasks", | |
| wrap=True, | |
| value=[] | |
| ) | |
| task_select = gr.Dropdown(label="Select Task", choices=[], value=None) | |
| with gr.Row(): | |
| solo_task_btn = gr.Button("▶️ Execute Solo", variant="primary") | |
| coop_task_btn = gr.Button("🤝 Propose Cooperation", variant="secondary") | |
| # Cooperation panel (initially hidden) | |
| with gr.Group(visible=False) as coop_panel: | |
| gr.Markdown("### 🤝 Cooperation Proposal") | |
| coop_contrib_slider = gr.Slider(1, 10, value=3, step=1, | |
| label="Your CP Contribution") | |
| send_proposal_btn = gr.Button("📤 Send Proposal to Partner") | |
| # Negotiation response (for partner) | |
| with gr.Group(visible=False) as negotiation_panel: | |
| gr.Markdown("### 🤝 Partner's Cooperation Request") | |
| negotiation_info = gr.Markdown(value="") | |
| with gr.Row(): | |
| response_cp = gr.Slider(1, 10, value=1, step=1, label="Your CP Investment") | |
| response_hp = gr.Slider(0, 5, value=0, step=1, label="Your HP Investment") | |
| with gr.Row(): | |
| accept_coop_btn = gr.Button("✅ Accept", variant="primary") | |
| reject_coop_btn = gr.Button("❌ Reject", variant="stop") | |
| end_turn_btn = gr.Button("⏭️ End Turn", variant="secondary", size="lg") | |
| action_result = gr.Markdown(value="") | |
| # Player 2 Panel | |
| with gr.Column(): | |
| gr.Markdown("### Player 2") | |
| p2_name = gr.Textbox(label="Name", interactive=False, value="") | |
| p2_character = gr.Textbox(label="Character", interactive=False, value="") | |
| p2_display = gr.JSON(label="Status", value={}) | |
| with gr.Row(): | |
| p2_time = gr.Number(label="⏰ Time", interactive=False, value=0) | |
| p2_money = gr.Number(label="💰 Money", interactive=False, value=0) | |
| with gr.Row(): | |
| p2_hp = gr.Number(label="❤️ HP", interactive=False, value=0) | |
| p2_cp = gr.Number(label="🧠 CP", interactive=False, value=0) | |
| with gr.Row(): | |
| p2_stress = gr.Number(label="😰 Stress", interactive=False, value=0) | |
| p2_qol = gr.Number(label="✨ QoL", interactive=False, value=0) | |
| gr.Markdown("### Spectators") | |
| spectators_display = gr.Markdown(value="*No spectators*") | |
| refresh_btn = gr.Button("🔄 Refresh Game State", variant="secondary") | |
| with gr.Tab("🏆 Rankings"): | |
| rankings_table = gr.Dataframe( | |
| headers=["Rank", "Player", "Avg QoL", "Games", "Wins", "Win %"], | |
| label="Top Players", | |
| value=[] | |
| ) | |
| refresh_rankings_btn = gr.Button("🔄 Refresh Rankings") | |
| # ===== EVENT HANDLERS ===== | |
| def create_room_handler(your_name, character_id, session): | |
| try: | |
| import uuid | |
| print(f"DEBUG: create_room_handler called with name={your_name}, char={character_id}") | |
| print(f"DEBUG: Current session: {session}") | |
| # Validate inputs | |
| if not your_name or not your_name.strip(): | |
| print("DEBUG: Name validation failed") | |
| return session, "❌ Please enter your name" | |
| if not character_id: | |
| print("DEBUG: Character validation failed") | |
| return session, "❌ Please select a character" | |
| room_id = str(uuid.uuid4())[:8].upper() | |
| player_id = str(uuid.uuid4())[:8] | |
| print(f"DEBUG: Creating room {room_id} for player {player_id}") | |
| create_result = db.create_room(room_id) | |
| print(f"DEBUG: create_room result: {create_result}") | |
| if not create_result: | |
| print("DEBUG: Failed to create room in DB") | |
| return session, "❌ Failed to create room. Please try again." | |
| print(f"DEBUG: About to join room with name={your_name}, char={character_id}") | |
| success, slot, msg = db.join_room(room_id, player_id, your_name, character_id) | |
| print(f"DEBUG: Join result - success={success}, slot={slot}, msg={msg}") | |
| if success: | |
| new_session = { | |
| 'room_id': room_id, | |
| 'player_id': player_id, | |
| 'player_slot': slot, | |
| 'is_spectator': slot is None | |
| } | |
| result_msg = f"✅ Room created: **{room_id}**\n\n{msg}\n\nShare this Room ID with your partner!" | |
| print(f"DEBUG: About to return - session={new_session}") | |
| print(f"DEBUG: About to return - msg={result_msg}") | |
| return new_session, result_msg | |
| # If join fails, clean up the created room | |
| print(f"DEBUG: Join failed, cleaning up room {room_id}") | |
| with db.get_connection() as conn: | |
| conn.execute("DELETE FROM rooms WHERE room_id = ?", (room_id,)) | |
| conn.commit() | |
| return session, f"❌ Failed to join room after creation: {msg}" | |
| except Exception as e: | |
| print(f"ERROR in create_room_handler: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| return session, f"❌ Error: {str(e)}" | |
| def join_room_handler(room_id, your_name, character_id, session): | |
| try: | |
| import uuid | |
| print(f"DEBUG: join_room_handler called - room={room_id}, name={your_name}, char={character_id}") | |
| # Validate inputs | |
| if not room_id or not room_id.strip(): | |
| return session, "❌ Please enter a Room ID" | |
| if not your_name or not your_name.strip(): | |
| return session, "❌ Please enter your name" | |
| if not character_id: | |
| return session, "❌ Please select a character" | |
| player_id = str(uuid.uuid4())[:8] | |
| success, slot, msg = db.join_room(room_id.strip().upper(), player_id, your_name, character_id) | |
| print(f"DEBUG: Join result - success={success}, slot={slot}, msg={msg}") | |
| if success: | |
| new_session = { | |
| 'room_id': room_id.strip().upper(), | |
| 'player_id': player_id, | |
| 'player_slot': slot, | |
| 'is_spectator': slot is None | |
| } | |
| return new_session, f"✅ {msg}" | |
| return session, f"❌ {msg}" | |
| except Exception as e: | |
| print(f"ERROR in join_room_handler: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| return session, f"❌ Error: {str(e)}" | |
| def refresh_game_state(session): | |
| empty_return = ("*No game joined*", "", "", {}, 0, 0, 0, 0, 0, 0, "", "", {}, 0, 0, 0, 0, 0, 0, [], [], "*No spectators*", "") | |
| if not session or not session.get('room_id'): | |
| return empty_return | |
| state = db.get_room_state(session['room_id']) | |
| if not state: | |
| return empty_return | |
| room = state['room'] | |
| players = state['players'] | |
| # Status | |
| status_md = f""" | |
| **Room:** {session['room_id']} | **Round:** {room['current_round']}/{room['total_rounds']} | |
| **Status:** {room['status'].upper()} | **Current Turn:** Player {room['current_turn'] + 1} | |
| """ | |
| if room['status'] == 'finished': | |
| winner_name = next((p['name'] for p in players if p['player_id'] == room['winner_id']), "Unknown") | |
| status_md += f"\n\n🏆 **GAME OVER!** Winner: **{winner_name}**" | |
| # Player data with defaults | |
| if len(players) > 0: | |
| p1_data = players[0] | |
| p1_name_val = p1_data.get('name', '') | |
| p1_char = p1_data.get('character_id', '') | |
| p1_time_val = p1_data.get('time', 0) | |
| p1_money_val = p1_data.get('money', 0) | |
| p1_hp_val = p1_data.get('hp', 0) | |
| p1_cp_val = p1_data.get('cp', 0) | |
| p1_stress_val = p1_data.get('stress', 0) | |
| p1_qol_val = p1_data.get('qol', 0) | |
| else: | |
| p1_data = {} | |
| p1_name_val = "" | |
| p1_char = "" | |
| p1_time_val = 0 | |
| p1_money_val = 0 | |
| p1_hp_val = 0 | |
| p1_cp_val = 0 | |
| p1_stress_val = 0 | |
| p1_qol_val = 0 | |
| if len(players) > 1: | |
| p2_data = players[1] | |
| p2_name_val = p2_data.get('name', '') | |
| p2_char = p2_data.get('character_id', '') | |
| p2_time_val = p2_data.get('time', 0) | |
| p2_money_val = p2_data.get('money', 0) | |
| p2_hp_val = p2_data.get('hp', 0) | |
| p2_cp_val = p2_data.get('cp', 0) | |
| p2_stress_val = p2_data.get('stress', 0) | |
| p2_qol_val = p2_data.get('qol', 0) | |
| else: | |
| p2_data = {} | |
| p2_name_val = "" | |
| p2_char = "" | |
| p2_time_val = 0 | |
| p2_money_val = 0 | |
| p2_hp_val = 0 | |
| p2_cp_val = 0 | |
| p2_stress_val = 0 | |
| p2_qol_val = 0 | |
| # Tasks with better formatting | |
| task_rows = [] | |
| task_options = [] | |
| for task_id in state['task_ids']: | |
| task = next((t for t in TASKS if t['id'] == task_id), None) | |
| if task: | |
| solo_req = ', '.join([f"{k}:{v}" for k, v in task['solo'].items()]) | |
| coop_req = ', '.join([f"{k}:{v}" for k, v in task['coop'].items()]) if 'coop' in task else "N/A" | |
| if 'reward' in task and task['reward'] > 0: | |
| reward_str = f"${task['reward']}" | |
| elif 'qol' in task: | |
| reward_str = f"{task['qol']} QoL" | |
| else: | |
| reward_str = "Utility" | |
| task_rows.append([ | |
| task['id'], task['name'], task['type'], | |
| f"{task['time']}h", solo_req, coop_req, reward_str | |
| ]) | |
| task_options.append((f"{task['id']} - {task['name']}", task['id'])) | |
| # Spectators | |
| spec_md = f"👥 {len(state['spectators'])} watching: {', '.join(state['spectators'])}" if state['spectators'] else "*No spectators*" | |
| # Turn indicator | |
| if session.get('is_spectator'): | |
| turn_msg = "👁️ **You are spectating** - Refresh to see updates" | |
| elif room['status'] == 'waiting': | |
| turn_msg = "⏳ **Waiting for Player 2 to join...**" | |
| elif room['status'] == 'finished': | |
| turn_msg = "🏁 **Game Finished!** Check Rankings tab." | |
| elif session.get('player_slot') == room['current_turn']: | |
| turn_msg = "✅ **YOUR TURN!** Take an action below." | |
| else: | |
| turn_msg = "⏳ **Partner's turn...** Click Refresh to see updates." | |
| return (status_md, | |
| p1_name_val, p1_char, p1_data, p1_time_val, p1_money_val, p1_hp_val, p1_cp_val, p1_stress_val, p1_qol_val, | |
| p2_name_val, p2_char, p2_data, p2_time_val, p2_money_val, p2_hp_val, p2_cp_val, p2_stress_val, p2_qol_val, | |
| task_rows, task_options, spec_md, turn_msg) | |
| def execute_rest_handler(session, rest_type): | |
| if not session.get('room_id') or session.get('is_spectator'): | |
| return "❌ Not in game as player" | |
| state = db.get_room_state(session['room_id']) | |
| if not state: | |
| return "❌ Room not found" | |
| room = state['room'] | |
| if room['current_turn'] != session['player_slot']: | |
| return "❌ Not your turn!" | |
| player = state['players'][session['player_slot']] | |
| success, msg, updates = execute_rest(player, rest_type) | |
| if success: | |
| db.update_player(session['room_id'], session['player_slot'], updates) | |
| return f"✅ {msg}" | |
| return f"❌ {msg}" | |
| def execute_solo_task_handler(session, task_id): | |
| if not session.get('room_id') or session.get('is_spectator'): | |
| return "❌ Not in game as player" | |
| if not task_id: | |
| return "❌ Select a task first" | |
| state = db.get_room_state(session['room_id']) | |
| if not state: | |
| return "❌ Room not found" | |
| room = state['room'] | |
| if room['current_turn'] != session['player_slot']: | |
| return "❌ Not your turn!" | |
| player = state['players'][session['player_slot']] | |
| task = next((t for t in TASKS if t['id'] == task_id), None) | |
| if not task: | |
| return "❌ Task not found" | |
| success, msg, updates = execute_solo_task(player, task) | |
| if success: | |
| # Add task to completed list | |
| completed = json.loads(player.get('completed_tasks', '[]')) | |
| completed.append(task_id) | |
| updates['completed_tasks'] = json.dumps(completed) | |
| db.update_player(session['room_id'], session['player_slot'], updates) | |
| return f"✅ {msg}" | |
| return f"❌ {msg}" | |
| def end_turn_handler(session): | |
| if not session.get('room_id') or session.get('is_spectator'): | |
| return "❌ Not in game as player" | |
| state = db.get_room_state(session['room_id']) | |
| if not state: | |
| return "❌ Room not found" | |
| room = state['room'] | |
| if room['current_turn'] != session['player_slot']: | |
| return "❌ Not your turn!" | |
| db.end_turn(session['room_id'], session['player_slot']) | |
| return "✅ Turn ended. Waiting for partner..." | |
| def load_rankings(): | |
| rankings = db.get_rankings(20) | |
| rows = [] | |
| for i, r in enumerate(rankings, 1): | |
| rows.append([ | |
| i, | |
| r['player_name'], | |
| f"{r['avg_qol']:.1f}", | |
| r['total_games'], | |
| r['total_wins'], | |
| f"{r['win_rate']:.1f}%" | |
| ]) | |
| return rows | |
| # Wire up events - Gradio 5 style with proper outputs | |
| create_click = create_btn.click( | |
| fn=create_room_handler, | |
| inputs=[your_name_input, character_select, session_state], | |
| outputs=[session_state, join_result], | |
| show_progress="full" | |
| ).then( | |
| fn=refresh_game_state, | |
| inputs=[session_state], | |
| outputs=[status_display, | |
| p1_name, p1_character, p1_display, p1_time, p1_money, p1_hp, p1_cp, p1_stress, p1_qol, | |
| p2_name, p2_character, p2_display, p2_time, p2_money, p2_hp, p2_cp, p2_stress, p2_qol, | |
| tasks_display, task_select, spectators_display, your_turn_msg] | |
| ) | |
| join_click = join_btn.click( | |
| fn=join_room_handler, | |
| inputs=[room_id_input, your_name_input, character_select, session_state], | |
| outputs=[session_state, join_result], | |
| show_progress="full" | |
| ).then( | |
| fn=refresh_game_state, | |
| inputs=[session_state], | |
| outputs=[status_display, | |
| p1_name, p1_character, p1_display, p1_time, p1_money, p1_hp, p1_cp, p1_stress, p1_qol, | |
| p2_name, p2_character, p2_display, p2_time, p2_money, p2_hp, p2_cp, p2_stress, p2_qol, | |
| tasks_display, task_select, spectators_display, your_turn_msg] | |
| ) | |
| refresh_btn.click( | |
| fn=refresh_game_state, | |
| inputs=[session_state], | |
| outputs=[status_display, | |
| p1_name, p1_character, p1_display, p1_time, p1_money, p1_hp, p1_cp, p1_stress, p1_qol, | |
| p2_name, p2_character, p2_display, p2_time, p2_money, p2_hp, p2_cp, p2_stress, p2_qol, | |
| tasks_display, task_select, spectators_display, your_turn_msg] | |
| ) | |
| quick_nap_btn.click( | |
| fn=lambda s: execute_rest_handler(s, "quick_nap"), | |
| inputs=[session_state], | |
| outputs=[action_result] | |
| ).then( | |
| fn=refresh_game_state, | |
| inputs=[session_state], | |
| outputs=[status_display, | |
| p1_name, p1_character, p1_display, p1_time, p1_money, p1_hp, p1_cp, p1_stress, p1_qol, | |
| p2_name, p2_character, p2_display, p2_time, p2_money, p2_hp, p2_cp, p2_stress, p2_qol, | |
| tasks_display, task_select, spectators_display, your_turn_msg] | |
| ) | |
| full_sleep_btn.click( | |
| fn=lambda s: execute_rest_handler(s, "full_sleep"), | |
| inputs=[session_state], | |
| outputs=[action_result] | |
| ).then( | |
| fn=refresh_game_state, | |
| inputs=[session_state], | |
| outputs=[status_display, | |
| p1_name, p1_character, p1_display, p1_time, p1_money, p1_hp, p1_cp, p1_stress, p1_qol, | |
| p2_name, p2_character, p2_display, p2_time, p2_money, p2_hp, p2_cp, p2_stress, p2_qol, | |
| tasks_display, task_select, spectators_display, your_turn_msg] | |
| ) | |
| deep_rest_btn.click( | |
| fn=lambda s: execute_rest_handler(s, "deep_rest"), | |
| inputs=[session_state], | |
| outputs=[action_result] | |
| ).then( | |
| fn=refresh_game_state, | |
| inputs=[session_state], | |
| outputs=[status_display, | |
| p1_name, p1_character, p1_display, p1_time, p1_money, p1_hp, p1_cp, p1_stress, p1_qol, | |
| p2_name, p2_character, p2_display, p2_time, p2_money, p2_hp, p2_cp, p2_stress, p2_qol, | |
| tasks_display, task_select, spectators_display, your_turn_msg] | |
| ) | |
| solo_task_btn.click( | |
| fn=execute_solo_task_handler, | |
| inputs=[session_state, task_select], | |
| outputs=[action_result] | |
| ).then( | |
| fn=refresh_game_state, | |
| inputs=[session_state], | |
| outputs=[status_display, | |
| p1_name, p1_character, p1_display, p1_time, p1_money, p1_hp, p1_cp, p1_stress, p1_qol, | |
| p2_name, p2_character, p2_display, p2_time, p2_money, p2_hp, p2_cp, p2_stress, p2_qol, | |
| tasks_display, task_select, spectators_display, your_turn_msg] | |
| ) | |
| end_turn_btn.click( | |
| fn=end_turn_handler, | |
| inputs=[session_state], | |
| outputs=[action_result] | |
| ).then( | |
| fn=refresh_game_state, | |
| inputs=[session_state], | |
| outputs=[status_display, | |
| p1_name, p1_character, p1_display, p1_time, p1_money, p1_hp, p1_cp, p1_stress, p1_qol, | |
| p2_name, p2_character, p2_display, p2_time, p2_money, p2_hp, p2_cp, p2_stress, p2_qol, | |
| tasks_display, task_select, spectators_display, your_turn_msg] | |
| ) | |
| refresh_rankings_btn.click( | |
| fn=load_rankings, | |
| outputs=[rankings_table] | |
| ) | |
| # Auto-refresh on tab switch | |
| app.load(fn=load_rankings, outputs=[rankings_table]) | |
| return app | |
| # ===== MAIN ===== | |
| if __name__ == "__main__": | |
| app = create_ui() | |
| app.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False | |
| ) |