npc0 commited on
Commit
3d34345
·
verified ·
1 Parent(s): c034337

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +902 -0
app.py ADDED
@@ -0,0 +1,902 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Life Frontier: Partner's Concerto
3
+ Single-app turn-based game with SQLite state management
4
+ """
5
+
6
+ import gradio as gr
7
+ import sqlite3
8
+ import json
9
+ import random
10
+ import time
11
+ from datetime import datetime
12
+ from typing import Dict, List, Optional, Tuple
13
+ from dataclasses import dataclass, asdict
14
+ from enum import Enum
15
+ import threading
16
+
17
+ # ===== CONSTANTS =====
18
+ INITIAL_TIME = 24
19
+ INITIAL_HP = 10
20
+ INITIAL_CP = 5
21
+ INITIAL_MONEY = 10
22
+ STRESS_THRESHOLD = 20
23
+ DB_PATH = "game.db"
24
+
25
+ # ===== DATA MODELS =====
26
+ class CardType(str, Enum):
27
+ WORK = "W"
28
+ DOMESTIC = "D"
29
+ HEALTH = "H"
30
+ RELATIONSHIP = "R"
31
+ SOCIAL = "S"
32
+ QOL = "Q"
33
+
34
+ @dataclass
35
+ class PlayerState:
36
+ player_id: str
37
+ name: str
38
+ character_id: str
39
+ time: int = INITIAL_TIME
40
+ hp: int = INITIAL_HP
41
+ cp: int = INITIAL_CP
42
+ money: int = INITIAL_MONEY
43
+ stress: int = 0
44
+ qol: int = 0
45
+ reputation: int = 0
46
+ hp_max: int = INITIAL_HP
47
+ cp_max: int = INITIAL_CP
48
+ is_eliminated: bool = False
49
+ has_ended_turn: bool = False
50
+ completed_tasks: str = "" # JSON array
51
+
52
+ def to_dict(self):
53
+ return asdict(self)
54
+
55
+ def calculate_final_qol(self) -> int:
56
+ if self.is_eliminated:
57
+ return 0
58
+ hp_bonus = (self.hp_max - INITIAL_HP) * 2
59
+ cp_bonus = (self.cp_max - INITIAL_CP) * 3
60
+ return self.qol + hp_bonus + cp_bonus + self.reputation - self.stress
61
+
62
+ @dataclass
63
+ class GameState:
64
+ room_id: str
65
+ current_round: int = 1
66
+ total_rounds: int = 8
67
+ current_turn: int = 0 # 0=player1, 1=player2
68
+ status: str = "waiting" # waiting, playing, finished
69
+ available_tasks: str = "" # JSON array of task IDs
70
+ active_negotiation: str = "" # JSON negotiation data
71
+ created_at: str = ""
72
+
73
+ # ===== DATABASE MANAGEMENT =====
74
+ class GameDatabase:
75
+ def __init__(self, db_path: str = DB_PATH):
76
+ self.db_path = db_path
77
+ self.lock = threading.Lock()
78
+ self.init_db()
79
+
80
+ def get_connection(self):
81
+ conn = sqlite3.connect(self.db_path, check_same_thread=False)
82
+ conn.row_factory = sqlite3.Row
83
+ return conn
84
+
85
+ def init_db(self):
86
+ with self.lock, self.get_connection() as conn:
87
+ # Rooms table
88
+ conn.execute("""
89
+ CREATE TABLE IF NOT EXISTS rooms (
90
+ room_id TEXT PRIMARY KEY,
91
+ current_round INTEGER DEFAULT 1,
92
+ total_rounds INTEGER DEFAULT 8,
93
+ current_turn INTEGER DEFAULT 0,
94
+ status TEXT DEFAULT 'waiting',
95
+ available_tasks TEXT DEFAULT '[]',
96
+ active_negotiation TEXT DEFAULT '',
97
+ created_at TEXT,
98
+ winner_id TEXT
99
+ )
100
+ """)
101
+
102
+ # Players table
103
+ conn.execute("""
104
+ CREATE TABLE IF NOT EXISTS players (
105
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
106
+ room_id TEXT,
107
+ player_id TEXT,
108
+ name TEXT,
109
+ character_id TEXT,
110
+ player_slot INTEGER,
111
+ time INTEGER DEFAULT 24,
112
+ hp INTEGER DEFAULT 10,
113
+ cp INTEGER DEFAULT 5,
114
+ money INTEGER DEFAULT 10,
115
+ stress INTEGER DEFAULT 0,
116
+ qol INTEGER DEFAULT 0,
117
+ reputation INTEGER DEFAULT 0,
118
+ hp_max INTEGER DEFAULT 10,
119
+ cp_max INTEGER DEFAULT 5,
120
+ is_eliminated INTEGER DEFAULT 0,
121
+ has_ended_turn INTEGER DEFAULT 0,
122
+ completed_tasks TEXT DEFAULT '[]',
123
+ UNIQUE(room_id, player_slot)
124
+ )
125
+ """)
126
+
127
+ # Spectators table
128
+ conn.execute("""
129
+ CREATE TABLE IF NOT EXISTS spectators (
130
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
131
+ room_id TEXT,
132
+ spectator_id TEXT,
133
+ name TEXT,
134
+ joined_at TEXT
135
+ )
136
+ """)
137
+
138
+ # Rankings table
139
+ conn.execute("""
140
+ CREATE TABLE IF NOT EXISTS rankings (
141
+ player_name TEXT PRIMARY KEY,
142
+ total_games INTEGER DEFAULT 0,
143
+ total_wins INTEGER DEFAULT 0,
144
+ highest_qol INTEGER DEFAULT 0,
145
+ average_qol REAL DEFAULT 0.0,
146
+ last_played TEXT
147
+ )
148
+ """)
149
+
150
+ # Game history
151
+ conn.execute("""
152
+ CREATE TABLE IF NOT EXISTS game_history (
153
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
154
+ room_id TEXT,
155
+ player1_name TEXT,
156
+ player2_name TEXT,
157
+ winner_name TEXT,
158
+ player1_qol INTEGER,
159
+ player2_qol INTEGER,
160
+ total_rounds INTEGER,
161
+ completed_at TEXT
162
+ )
163
+ """)
164
+
165
+ conn.commit()
166
+
167
+ def create_room(self, room_id: str, total_rounds: int = 8) -> bool:
168
+ with self.lock, self.get_connection() as conn:
169
+ try:
170
+ conn.execute("""
171
+ INSERT INTO rooms (room_id, total_rounds, created_at, status)
172
+ VALUES (?, ?, ?, 'waiting')
173
+ """, (room_id, total_rounds, datetime.now().isoformat()))
174
+ conn.commit()
175
+ return True
176
+ except sqlite3.IntegrityError:
177
+ return False
178
+
179
+ def join_room(self, room_id: str, player_id: str, name: str,
180
+ character_id: str) -> Tuple[bool, Optional[int], str]:
181
+ """Returns (success, player_slot, message)"""
182
+ with self.lock, self.get_connection() as conn:
183
+ # Check room exists
184
+ room = conn.execute(
185
+ "SELECT status FROM rooms WHERE room_id = ?", (room_id,)
186
+ ).fetchone()
187
+
188
+ if not room:
189
+ return False, None, "Room not found"
190
+
191
+ if room['status'] != 'waiting':
192
+ # Game already started, join as spectator
193
+ conn.execute("""
194
+ INSERT INTO spectators (room_id, spectator_id, name, joined_at)
195
+ VALUES (?, ?, ?, ?)
196
+ """, (room_id, player_id, name, datetime.now().isoformat()))
197
+ conn.commit()
198
+ return True, None, f"Joined as spectator. Game in progress."
199
+
200
+ # Try to join as player
201
+ players = conn.execute(
202
+ "SELECT player_slot FROM players WHERE room_id = ?", (room_id,)
203
+ ).fetchall()
204
+
205
+ if len(players) >= 2:
206
+ # Room full, join as spectator
207
+ conn.execute("""
208
+ INSERT INTO spectators (room_id, spectator_id, name, joined_at)
209
+ VALUES (?, ?, ?, ?)
210
+ """, (room_id, player_id, name, datetime.now().isoformat()))
211
+ conn.commit()
212
+ return True, None, f"Room full. Joined as spectator."
213
+
214
+ # Join as player
215
+ slot = 0 if len(players) == 0 else 1
216
+ conn.execute("""
217
+ INSERT INTO players (
218
+ room_id, player_id, name, character_id, player_slot
219
+ ) VALUES (?, ?, ?, ?, ?)
220
+ """, (room_id, player_id, name, character_id, slot))
221
+ conn.commit()
222
+
223
+ # If 2 players, start game
224
+ if slot == 1:
225
+ self._start_game(room_id, conn)
226
+
227
+ return True, slot, f"Joined as Player {slot + 1}"
228
+
229
+ def _start_game(self, room_id: str, conn):
230
+ """Internal: Start game when 2 players joined"""
231
+ # Update room status
232
+ conn.execute("""
233
+ UPDATE rooms SET status = 'playing' WHERE room_id = ?
234
+ """, (room_id,))
235
+
236
+ # Draw initial tasks (3 random)
237
+ task_ids = random.sample([t['id'] for t in TASKS], 3)
238
+ conn.execute("""
239
+ UPDATE rooms SET available_tasks = ? WHERE room_id = ?
240
+ """, (json.dumps(task_ids), room_id))
241
+
242
+ conn.commit()
243
+
244
+ def get_room_state(self, room_id: str) -> Optional[Dict]:
245
+ """Get complete room state including players"""
246
+ with self.lock, self.get_connection() as conn:
247
+ room = conn.execute(
248
+ "SELECT * FROM rooms WHERE room_id = ?", (room_id,)
249
+ ).fetchone()
250
+
251
+ if not room:
252
+ return None
253
+
254
+ players = conn.execute("""
255
+ SELECT * FROM players WHERE room_id = ? ORDER BY player_slot
256
+ """, (room_id,)).fetchall()
257
+
258
+ spectators = conn.execute("""
259
+ SELECT name FROM spectators WHERE room_id = ?
260
+ """, (room_id,)).fetchall()
261
+
262
+ return {
263
+ 'room': dict(room),
264
+ 'players': [dict(p) for p in players],
265
+ 'spectators': [s['name'] for s in spectators],
266
+ 'task_ids': json.loads(room['available_tasks'] or '[]')
267
+ }
268
+
269
+ def update_player(self, room_id: str, player_slot: int, updates: Dict):
270
+ """Update player state"""
271
+ with self.lock, self.get_connection() as conn:
272
+ set_clause = ", ".join([f"{k} = ?" for k in updates.keys()])
273
+ values = list(updates.values()) + [room_id, player_slot]
274
+
275
+ conn.execute(f"""
276
+ UPDATE players SET {set_clause}
277
+ WHERE room_id = ? AND player_slot = ?
278
+ """, values)
279
+ conn.commit()
280
+
281
+ def end_turn(self, room_id: str, player_slot: int):
282
+ """End player's turn and check for round end"""
283
+ with self.lock, self.get_connection() as conn:
284
+ # Mark turn ended
285
+ conn.execute("""
286
+ UPDATE players SET has_ended_turn = 1
287
+ WHERE room_id = ? AND player_slot = ?
288
+ """, (room_id, player_slot))
289
+
290
+ # Check if both ended turn
291
+ count = conn.execute("""
292
+ SELECT COUNT(*) as cnt FROM players
293
+ WHERE room_id = ? AND has_ended_turn = 1
294
+ """, (room_id,)).fetchone()['cnt']
295
+
296
+ if count == 2:
297
+ # Both ended, advance round
298
+ self._advance_round(room_id, conn)
299
+ else:
300
+ # Switch turn
301
+ room = conn.execute(
302
+ "SELECT current_turn FROM rooms WHERE room_id = ?", (room_id,)
303
+ ).fetchone()
304
+ next_turn = 1 - room['current_turn']
305
+ conn.execute("""
306
+ UPDATE rooms SET current_turn = ? WHERE room_id = ?
307
+ """, (next_turn, room_id))
308
+
309
+ conn.commit()
310
+
311
+ def _advance_round(self, room_id: str, conn):
312
+ """Internal: Advance to next round"""
313
+ room = conn.execute(
314
+ "SELECT current_round, total_rounds FROM rooms WHERE room_id = ?",
315
+ (room_id,)
316
+ ).fetchone()
317
+
318
+ next_round = room['current_round'] + 1
319
+
320
+ if next_round > room['total_rounds']:
321
+ # Game over
322
+ self._end_game(room_id, conn)
323
+ else:
324
+ # Reset for next round
325
+ conn.execute("""
326
+ UPDATE players SET
327
+ time = ?, has_ended_turn = 0, completed_tasks = '[]'
328
+ WHERE room_id = ?
329
+ """, (INITIAL_TIME, room_id))
330
+
331
+ # Draw new tasks
332
+ task_ids = random.sample([t['id'] for t in TASKS], 3)
333
+ conn.execute("""
334
+ UPDATE rooms SET
335
+ current_round = ?,
336
+ current_turn = 0,
337
+ available_tasks = ?
338
+ WHERE room_id = ?
339
+ """, (next_round, json.dumps(task_ids), room_id))
340
+
341
+ def _end_game(self, room_id: str, conn):
342
+ """Internal: End game and calculate winner"""
343
+ players = conn.execute("""
344
+ SELECT * FROM players WHERE room_id = ? ORDER BY player_slot
345
+ """, (room_id,)).fetchall()
346
+
347
+ # Calculate final QoL
348
+ p1 = PlayerState(**{k: players[0][k] for k in players[0].keys()
349
+ if k in PlayerState.__annotations__})
350
+ p2 = PlayerState(**{k: players[1][k] for k in players[1].keys()
351
+ if k in PlayerState.__annotations__})
352
+
353
+ qol1 = p1.calculate_final_qol()
354
+ qol2 = p2.calculate_final_qol()
355
+
356
+ winner_id = p1.player_id if qol1 > qol2 else p2.player_id
357
+ winner_name = p1.name if qol1 > qol2 else p2.name
358
+
359
+ # Update room
360
+ conn.execute("""
361
+ UPDATE rooms SET status = 'finished', winner_id = ?
362
+ WHERE room_id = ?
363
+ """, (winner_id, room_id))
364
+
365
+ # Save to history
366
+ conn.execute("""
367
+ INSERT INTO game_history (
368
+ room_id, player1_name, player2_name, winner_name,
369
+ player1_qol, player2_qol, total_rounds, completed_at
370
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
371
+ """, (room_id, p1.name, p2.name, winner_name, qol1, qol2,
372
+ players[0]['completed_tasks'], datetime.now().isoformat()))
373
+
374
+ # Update rankings
375
+ self._update_rankings(p1.name, qol1, winner_name == p1.name, conn)
376
+ self._update_rankings(p2.name, qol2, winner_name == p2.name, conn)
377
+
378
+ def _update_rankings(self, player_name: str, qol: int, won: bool, conn):
379
+ """Internal: Update player rankings"""
380
+ existing = conn.execute("""
381
+ SELECT * FROM rankings WHERE player_name = ?
382
+ """, (player_name,)).fetchone()
383
+
384
+ if existing:
385
+ new_games = existing['total_games'] + 1
386
+ new_wins = existing['total_wins'] + (1 if won else 0)
387
+ new_avg = ((existing['average_qol'] * existing['total_games']) + qol) / new_games
388
+ new_high = max(existing['highest_qol'], qol)
389
+
390
+ conn.execute("""
391
+ UPDATE rankings SET
392
+ total_games = ?, total_wins = ?,
393
+ average_qol = ?, highest_qol = ?,
394
+ last_played = ?
395
+ WHERE player_name = ?
396
+ """, (new_games, new_wins, new_avg, new_high,
397
+ datetime.now().isoformat(), player_name))
398
+ else:
399
+ conn.execute("""
400
+ INSERT INTO rankings (
401
+ player_name, total_games, total_wins,
402
+ average_qol, highest_qol, last_played
403
+ ) VALUES (?, 1, ?, ?, ?, ?)
404
+ """, (player_name, 1 if won else 0, qol, qol,
405
+ datetime.now().isoformat()))
406
+
407
+ def get_rankings(self, limit: int = 10) -> List[Dict]:
408
+ """Get top rankings"""
409
+ with self.get_connection() as conn:
410
+ rows = conn.execute("""
411
+ SELECT
412
+ player_name, total_games, total_wins,
413
+ ROUND(average_qol, 1) as avg_qol,
414
+ highest_qol,
415
+ ROUND(CAST(total_wins AS REAL) / total_games * 100, 1) as win_rate
416
+ FROM rankings
417
+ WHERE total_games > 0
418
+ ORDER BY average_qol DESC
419
+ LIMIT ?
420
+ """, (limit,)).fetchall()
421
+ return [dict(r) for r in rows]
422
+
423
+ # ===== GAME DATA =====
424
+ CHARACTERS = [
425
+ {"id": "P1", "name": "Workaholic", "desc": "Work +2 VP, but +1 stress/turn"},
426
+ {"id": "P4", "name": "Hedonist", "desc": "QoL +1, but domestic +2 time"},
427
+ {"id": "R3", "name": "Planner", "desc": "Can force round-up in negotiation"},
428
+ {"id": "H3", "name": "Fitness Buff", "desc": "Rest +1 HP, HP max +2"},
429
+ ]
430
+
431
+ TASKS = [
432
+ {"id": "W1", "name": "Core Project", "type": "W", "time": 8,
433
+ "solo": {"CP": 7, "HP": 3}, "coop": {"CP": 10}, "reward": 12},
434
+ {"id": "W2", "name": "Annual Report", "type": "W", "time": 6,
435
+ "solo": {"CP": 6}, "coop": {"CP": 8}, "reward": 8},
436
+ {"id": "D1", "name": "Deep Clean Kitchen", "type": "D", "time": 5,
437
+ "solo": {"HP": 5}, "coop": {"HP": 7}, "reward": 0, "effect": "stress:-2"},
438
+ {"id": "D2", "name": "Laundry & Organize", "type": "D", "time": 3,
439
+ "solo": {"HP": 3}, "coop": {"HP": 4}, "reward": 0, "effect": "hp:+2"},
440
+ {"id": "H1", "name": "Fitness Training", "type": "H", "time": 5,
441
+ "solo": {"HP": 6}, "coop": {"HP": 8}, "reward": 1, "effect": "hp_max:+1"},
442
+ {"id": "R1", "name": "Romantic Date", "type": "R", "time": 5,
443
+ "solo": {"CP": 5}, "coop": {"CP": 7}, "cost": 5, "qol": 3, "effect": "stress:-3"},
444
+ {"id": "Q2", "name": "Weekend Trip", "type": "Q", "time": 7,
445
+ "solo": {"HP": 5, "CP": 4}, "coop": {"HP": 9}, "cost": 15, "qol": 7, "effect": "recover_all"},
446
+ ]
447
+
448
+ # ===== GLOBAL STATE =====
449
+ db = GameDatabase()
450
+
451
+ # ===== GAME LOGIC =====
452
+ def roll_dice() -> int:
453
+ return random.randint(1, 6)
454
+
455
+ def execute_rest(player: Dict, rest_type: str) -> Tuple[bool, str, Dict]:
456
+ """Execute rest action"""
457
+ if rest_type == "quick_nap":
458
+ if player['time'] < 3:
459
+ return False, "Not enough time (need 3h)", {}
460
+ dice = roll_dice()
461
+ hp_recovered = 2 * dice
462
+ new_hp = min(player['hp'] + hp_recovered, player['hp_max'])
463
+ return True, f"Rolled {dice}! Recovered {hp_recovered} HP", {
464
+ 'time': player['time'] - 3,
465
+ 'hp': new_hp
466
+ }
467
+
468
+ elif rest_type == "full_sleep":
469
+ if player['time'] < 9:
470
+ return False, "Not enough time (need 9h)", {}
471
+ return True, "Recovered to base levels and reduced stress", {
472
+ 'time': player['time'] - 9,
473
+ 'hp': min(8, player['hp_max']),
474
+ 'cp': min(4, player['cp_max']),
475
+ 'stress': max(0, player['stress'] - 1)
476
+ }
477
+
478
+ elif rest_type == "deep_rest":
479
+ if player['time'] < 12:
480
+ return False, "Not enough time (need 12h)", {}
481
+ return True, "Full recovery!", {
482
+ 'time': player['time'] - 12,
483
+ 'hp': player['hp_max'],
484
+ 'cp': player['cp_max'],
485
+ 'stress': max(0, player['stress'] - 2)
486
+ }
487
+
488
+ return False, "Invalid rest type", {}
489
+
490
+ def execute_solo_task(player: Dict, task: Dict) -> Tuple[bool, str, Dict]:
491
+ """Execute task solo"""
492
+ # Check requirements
493
+ solo_req = task['solo']
494
+ if player['time'] < task['time']:
495
+ return False, f"Not enough time (need {task['time']}h)", {}
496
+
497
+ for res, amount in solo_req.items():
498
+ if res == "HP" and player['hp'] < amount:
499
+ return False, f"Not enough HP (need {amount})", {}
500
+ if res == "CP" and player['cp'] < amount:
501
+ return False, f"Not enough CP (need {amount})", {}
502
+
503
+ # Check cost
504
+ if 'cost' in task and player['money'] < task['cost']:
505
+ return False, f"Not enough money (need ${task['cost']})", {}
506
+
507
+ # Execute
508
+ updates = {
509
+ 'time': player['time'] - task['time'],
510
+ 'stress': player['stress'] + 1 # Fatigue
511
+ }
512
+
513
+ # Spend resources
514
+ if 'HP' in solo_req:
515
+ updates['hp'] = player['hp'] - solo_req['HP']
516
+ if 'CP' in solo_req:
517
+ updates['cp'] = player['cp'] - solo_req['CP']
518
+ if 'cost' in task:
519
+ updates['money'] = player['money'] - task['cost']
520
+
521
+ # Apply rewards
522
+ if 'reward' in task:
523
+ updates['money'] = updates.get('money', player['money']) + task['reward']
524
+ if 'qol' in task:
525
+ updates['qol'] = player['qol'] + task['qol']
526
+
527
+ # Apply effects
528
+ if 'effect' in task:
529
+ for effect in task['effect'].split(','):
530
+ if ':' in effect:
531
+ eff_type, value = effect.split(':')
532
+ value = int(value)
533
+ if eff_type == 'stress':
534
+ updates['stress'] = max(0, updates.get('stress', player['stress']) + value)
535
+ elif eff_type == 'hp':
536
+ updates['hp'] = min(updates.get('hp', player['hp']) + value, player['hp_max'])
537
+ elif eff_type == 'hp_max':
538
+ updates['hp_max'] = player['hp_max'] + value
539
+ elif effect == 'recover_all':
540
+ updates['hp'] = player['hp_max']
541
+ updates['cp'] = player['cp_max']
542
+
543
+ # Check elimination
544
+ if updates.get('stress', player['stress']) >= STRESS_THRESHOLD:
545
+ updates['is_eliminated'] = 1
546
+ return True, f"⚠️ Task completed but ELIMINATED (stress {updates['stress']})", updates
547
+
548
+ msg = f"✓ {task['name']} completed!"
549
+ if 'reward' in task:
550
+ msg += f" +${task['reward']}"
551
+ if 'qol' in task:
552
+ msg += f" +{task['qol']} QoL"
553
+
554
+ return True, msg, updates
555
+
556
+ # ===== GRADIO UI =====
557
+ def create_ui():
558
+ with gr.Blocks(title="Life Frontier: Partner's Concerto", theme=gr.themes.Soft()) as app:
559
+ gr.Markdown("""
560
+ # 🏠 Life Frontier: Partner's Concerto
561
+ *A turn-based strategy game for couples*
562
+ """)
563
+
564
+ # Session state
565
+ session_state = gr.State({
566
+ 'room_id': None,
567
+ 'player_id': None,
568
+ 'player_slot': None,
569
+ 'is_spectator': False
570
+ })
571
+
572
+ with gr.Tab("🎮 Join/Create Game"):
573
+ with gr.Row():
574
+ room_id_input = gr.Textbox(label="Room ID", placeholder="Leave empty to create new")
575
+ your_name_input = gr.Textbox(label="Your Name", value="Player")
576
+
577
+ character_select = gr.Dropdown(
578
+ choices=[(c['name'], c['id']) for c in CHARACTERS],
579
+ label="Select Character",
580
+ value=CHARACTERS[0]['id']
581
+ )
582
+
583
+ with gr.Row():
584
+ create_btn = gr.Button("🆕 Create Room", variant="primary")
585
+ join_btn = gr.Button("🚪 Join Room", variant="secondary")
586
+
587
+ join_result = gr.Markdown()
588
+
589
+ with gr.Tab("🎯 Game Board"):
590
+ gr.Markdown("### Game Status")
591
+ status_display = gr.Markdown("*No game joined*")
592
+
593
+ with gr.Row():
594
+ # Player 1 Panel
595
+ with gr.Column():
596
+ gr.Markdown("### Player 1")
597
+ p1_display = gr.JSON(label="Status")
598
+
599
+ # Center - Actions
600
+ with gr.Column():
601
+ gr.Markdown("### Your Actions")
602
+ your_turn_msg = gr.Markdown("*Waiting for your turn...*")
603
+
604
+ gr.Markdown("**Rest Actions**")
605
+ with gr.Row():
606
+ quick_nap_btn = gr.Button("Quick Nap (3h)")
607
+ full_sleep_btn = gr.Button("Full Sleep (9h)")
608
+ deep_rest_btn = gr.Button("Deep Rest (12h)")
609
+
610
+ gr.Markdown("**Available Tasks**")
611
+ tasks_display = gr.Dataframe(
612
+ headers=["ID", "Name", "Type", "Time", "Requirements", "Reward"],
613
+ label="Public Tasks"
614
+ )
615
+
616
+ task_select = gr.Dropdown(label="Select Task")
617
+ solo_task_btn = gr.Button("Execute Solo Task", variant="primary")
618
+
619
+ end_turn_btn = gr.Button("⏭️ End Turn", variant="secondary")
620
+
621
+ action_result = gr.Markdown()
622
+
623
+ # Player 2 Panel
624
+ with gr.Column():
625
+ gr.Markdown("### Player 2")
626
+ p2_display = gr.JSON(label="Status")
627
+
628
+ gr.Markdown("### Spectators")
629
+ spectators_display = gr.Markdown("*No spectators*")
630
+
631
+ refresh_btn = gr.Button("🔄 Refresh Game State")
632
+
633
+ with gr.Tab("🏆 Rankings"):
634
+ rankings_table = gr.Dataframe(
635
+ headers=["Rank", "Player", "Avg QoL", "Games", "Wins", "Win %"],
636
+ label="Top Players"
637
+ )
638
+ refresh_rankings_btn = gr.Button("🔄 Refresh Rankings")
639
+
640
+ # ===== EVENT HANDLERS =====
641
+
642
+ def create_room_handler(your_name, character_id):
643
+ import uuid
644
+ room_id = str(uuid.uuid4())[:8].upper()
645
+ player_id = str(uuid.uuid4())[:8]
646
+
647
+ db.create_room(room_id)
648
+ success, slot, msg = db.join_room(room_id, player_id, your_name, character_id)
649
+
650
+ if success:
651
+ return {
652
+ 'room_id': room_id,
653
+ 'player_id': player_id,
654
+ 'player_slot': slot,
655
+ 'is_spectator': slot is None
656
+ }, f"✅ Room created: **{room_id}**\n\n{msg}\n\nShare this Room ID with your partner!"
657
+ return {}, f"❌ Failed to create room: {msg}"
658
+
659
+ def join_room_handler(room_id, your_name, character_id):
660
+ import uuid
661
+ player_id = str(uuid.uuid4())[:8]
662
+
663
+ success, slot, msg = db.join_room(room_id, player_id, your_name, character_id)
664
+
665
+ if success:
666
+ return {
667
+ 'room_id': room_id,
668
+ 'player_id': player_id,
669
+ 'player_slot': slot,
670
+ 'is_spectator': slot is None
671
+ }, f"✅ {msg}"
672
+ return {}, f"❌ {msg}"
673
+
674
+ def refresh_game_state(session):
675
+ if not session.get('room_id'):
676
+ return ("*No game joined*", {}, {}, [], "*No spectators*", "")
677
+
678
+ state = db.get_room_state(session['room_id'])
679
+ if not state:
680
+ return ("*Room not found*", {}, {}, [], "*No spectators*", "")
681
+
682
+ room = state['room']
683
+ players = state['players']
684
+
685
+ # Status
686
+ status_md = f"""
687
+ **Room:** {session['room_id']} | **Round:** {room['current_round']}/{room['total_rounds']}
688
+ **Status:** {room['status'].upper()} | **Current Turn:** Player {room['current_turn'] + 1}
689
+ """
690
+
691
+ if room['status'] == 'finished':
692
+ status_md += f"\n\n🏆 **GAME OVER!** Winner: {room['winner_id']}"
693
+
694
+ # Players
695
+ p1_data = players[0] if len(players) > 0 else {}
696
+ p2_data = players[1] if len(players) > 1 else {}
697
+
698
+ # Tasks
699
+ task_rows = []
700
+ for task_id in state['task_ids']:
701
+ task = next((t for t in TASKS if t['id'] == task_id), None)
702
+ if task:
703
+ req_str = ', '.join([f"{k}:{v}" for k, v in task['solo'].items()])
704
+ reward_str = f"${task.get('reward', 0)}" if 'reward' in task else f"{task.get('qol', 0)} QoL"
705
+ task_rows.append([
706
+ task['id'], task['name'], task['type'],
707
+ f"{task['time']}h", req_str, reward_str
708
+ ])
709
+
710
+ # Task dropdown options
711
+ task_options = [(f"{t['id']} - {t['name']}", t['id']) for t in TASKS if t['id'] in state['task_ids']]
712
+
713
+ # Spectators
714
+ spec_md = f"👥 {len(state['spectators'])} watching: {', '.join(state['spectators'])}" if state['spectators'] else "*No spectators*"
715
+
716
+ # Turn indicator
717
+ if session.get('is_spectator'):
718
+ turn_msg = "👁️ You are spectating"
719
+ elif session.get('player_slot') == room['current_turn']:
720
+ turn_msg = "✅ **YOUR TURN!** Take an action below."
721
+ else:
722
+ turn_msg = "⏳ Waiting for partner's turn..."
723
+
724
+ return (status_md, p1_data, p2_data, task_rows, spec_md, turn_msg)
725
+
726
+ def execute_rest_handler(session, rest_type):
727
+ if not session.get('room_id') or session.get('is_spectator'):
728
+ return "❌ Not in game as player", session
729
+
730
+ state = db.get_room_state(session['room_id'])
731
+ if not state:
732
+ return "❌ Room not found", session
733
+
734
+ room = state['room']
735
+ if room['current_turn'] != session['player_slot']:
736
+ return "❌ Not your turn!", session
737
+
738
+ player = state['players'][session['player_slot']]
739
+ success, msg, updates = execute_rest(player, rest_type)
740
+
741
+ if success:
742
+ db.update_player(session['room_id'], session['player_slot'], updates)
743
+ return f"✅ {msg}", session
744
+
745
+ return f"❌ {msg}", session
746
+
747
+ def execute_solo_task_handler(session, task_id):
748
+ if not session.get('room_id') or session.get('is_spectator'):
749
+ return "❌ Not in game as player", session
750
+
751
+ if not task_id:
752
+ return "❌ Select a task first", session
753
+
754
+ state = db.get_room_state(session['room_id'])
755
+ if not state:
756
+ return "❌ Room not found", session
757
+
758
+ room = state['room']
759
+ if room['current_turn'] != session['player_slot']:
760
+ return "❌ Not your turn!", session
761
+
762
+ player = state['players'][session['player_slot']]
763
+ task = next((t for t in TASKS if t['id'] == task_id), None)
764
+
765
+ if not task:
766
+ return "❌ Task not found", session
767
+
768
+ success, msg, updates = execute_solo_task(player, task)
769
+
770
+ if success:
771
+ # Add task to completed list
772
+ completed = json.loads(player.get('completed_tasks', '[]'))
773
+ completed.append(task_id)
774
+ updates['completed_tasks'] = json.dumps(completed)
775
+
776
+ db.update_player(session['room_id'], session['player_slot'], updates)
777
+ return f"✅ {msg}", session
778
+
779
+ return f"❌ {msg}", session
780
+
781
+ def end_turn_handler(session):
782
+ if not session.get('room_id') or session.get('is_spectator'):
783
+ return "❌ Not in game as player", session
784
+
785
+ state = db.get_room_state(session['room_id'])
786
+ if not state:
787
+ return "❌ Room not found", session
788
+
789
+ room = state['room']
790
+ if room['current_turn'] != session['player_slot']:
791
+ return "❌ Not your turn!", session
792
+
793
+ db.end_turn(session['room_id'], session['player_slot'])
794
+ return "✅ Turn ended. Waiting for partner...", session
795
+
796
+ def load_rankings():
797
+ rankings = db.get_rankings(20)
798
+ rows = []
799
+ for i, r in enumerate(rankings, 1):
800
+ rows.append([
801
+ i,
802
+ r['player_name'],
803
+ f"{r['avg_qol']:.1f}",
804
+ r['total_games'],
805
+ r['total_wins'],
806
+ f"{r['win_rate']:.1f}%"
807
+ ])
808
+ return rows
809
+
810
+ # Wire up events
811
+ create_btn.click(
812
+ create_room_handler,
813
+ inputs=[your_name_input, character_select],
814
+ outputs=[session_state, join_result]
815
+ )
816
+
817
+ join_btn.click(
818
+ join_room_handler,
819
+ inputs=[room_id_input, your_name_input, character_select],
820
+ outputs=[session_state, join_result]
821
+ )
822
+
823
+ refresh_btn.click(
824
+ refresh_game_state,
825
+ inputs=[session_state],
826
+ outputs=[status_display, p1_display, p2_display, tasks_display,
827
+ spectators_display, your_turn_msg]
828
+ )
829
+
830
+ quick_nap_btn.click(
831
+ lambda s: execute_rest_handler(s, "quick_nap"),
832
+ inputs=[session_state],
833
+ outputs=[action_result, session_state]
834
+ ).then(
835
+ refresh_game_state,
836
+ inputs=[session_state],
837
+ outputs=[status_display, p1_display, p2_display, tasks_display,
838
+ spectators_display, your_turn_msg]
839
+ )
840
+
841
+ full_sleep_btn.click(
842
+ lambda s: execute_rest_handler(s, "full_sleep"),
843
+ inputs=[session_state],
844
+ outputs=[action_result, session_state]
845
+ ).then(
846
+ refresh_game_state,
847
+ inputs=[session_state],
848
+ outputs=[status_display, p1_display, p2_display, tasks_display,
849
+ spectators_display, your_turn_msg]
850
+ )
851
+
852
+ deep_rest_btn.click(
853
+ lambda s: execute_rest_handler(s, "deep_rest"),
854
+ inputs=[session_state],
855
+ outputs=[action_result, session_state]
856
+ ).then(
857
+ refresh_game_state,
858
+ inputs=[session_state],
859
+ outputs=[status_display, p1_display, p2_display, tasks_display,
860
+ spectators_display, your_turn_msg]
861
+ )
862
+
863
+ solo_task_btn.click(
864
+ execute_solo_task_handler,
865
+ inputs=[session_state, task_select],
866
+ outputs=[action_result, session_state]
867
+ ).then(
868
+ refresh_game_state,
869
+ inputs=[session_state],
870
+ outputs=[status_display, p1_display, p2_display, tasks_display,
871
+ spectators_display, your_turn_msg]
872
+ )
873
+
874
+ end_turn_btn.click(
875
+ end_turn_handler,
876
+ inputs=[session_state],
877
+ outputs=[action_result, session_state]
878
+ ).then(
879
+ refresh_game_state,
880
+ inputs=[session_state],
881
+ outputs=[status_display, p1_display, p2_display, tasks_display,
882
+ spectators_display, your_turn_msg]
883
+ )
884
+
885
+ refresh_rankings_btn.click(
886
+ load_rankings,
887
+ outputs=[rankings_table]
888
+ )
889
+
890
+ # Auto-refresh on tab switch
891
+ app.load(load_rankings, outputs=[rankings_table])
892
+
893
+ return app
894
+
895
+ # ===== MAIN =====
896
+ if __name__ == "__main__":
897
+ app = create_ui()
898
+ app.launch(
899
+ server_name="0.0.0.0",
900
+ server_port=7860,
901
+ share=False
902
+ )