npc0 commited on
Commit
fd10988
·
verified ·
1 Parent(s): 6caf003

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +179 -54
app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  """
2
  Life Frontier: Partner's Concerto
3
  Single-app turn-based game with SQLite state management
@@ -166,12 +167,16 @@ class GameDatabase:
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
@@ -218,13 +223,15 @@ class GameDatabase:
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"""
@@ -239,7 +246,44 @@ class GameDatabase:
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"""
@@ -452,6 +496,24 @@ db = GameDatabase()
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":
@@ -673,97 +735,139 @@ def create_ui():
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
 
@@ -774,24 +878,27 @@ def create_ui():
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)
@@ -823,63 +930,75 @@ def create_ui():
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(
@@ -887,16 +1006,22 @@ def create_ui():
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
- )
 
 
 
1
+ # ===== app.py - Single Gradio App with SQLite =====
2
  """
3
  Life Frontier: Partner's Concerto
4
  Single-app turn-based game with SQLite state management
 
167
 
168
  def create_room(self, room_id: str, total_rounds: int = 8) -> bool:
169
  with self.lock, self.get_connection() as conn:
170
+ # Cleanup old rooms before creating new one
171
+ self.cleanup_old_rooms()
172
+
173
  try:
174
  conn.execute("""
175
  INSERT INTO rooms (room_id, total_rounds, created_at, status)
176
  VALUES (?, ?, ?, 'waiting')
177
  """, (room_id, total_rounds, datetime.now().isoformat()))
178
  conn.commit()
179
+ print(f"✨ Created room {room_id}")
180
  return True
181
  except sqlite3.IntegrityError:
182
  return False
 
223
  room_id, player_id, name, character_id, player_slot
224
  ) VALUES (?, ?, ?, ?, ?)
225
  """, (room_id, player_id, name, character_id, slot))
 
226
 
227
  # If 2 players, start game
228
  if slot == 1:
229
  self._start_game(room_id, conn)
230
+ conn.commit()
231
+ return True, slot, f"✅ Joined as Player {slot + 1}! Game starting..."
232
 
233
+ conn.commit()
234
+ return True, slot, f"Joined as Player {slot + 1}. Waiting for Player 2..."
235
 
236
  def _start_game(self, room_id: str, conn):
237
  """Internal: Start game when 2 players joined"""
 
246
  UPDATE rooms SET available_tasks = ? WHERE room_id = ?
247
  """, (json.dumps(task_ids), room_id))
248
 
249
+ print(f"🎮 Game started in room {room_id}!")
250
+
251
+ def cleanup_old_rooms(self):
252
+ """Remove empty or stale rooms (no activity for 1 day)"""
253
+ with self.lock, self.get_connection() as conn:
254
+ cutoff_time = datetime.now().timestamp() - 86400 # 24 hours ago
255
+
256
+ # Find stale rooms
257
+ stale_rooms = conn.execute("""
258
+ SELECT room_id FROM rooms
259
+ WHERE status = 'waiting'
260
+ AND strftime('%s', created_at) < ?
261
+ """, (cutoff_time,)).fetchall()
262
+
263
+ # Also find empty rooms (no players after 1 hour)
264
+ empty_cutoff = datetime.now().timestamp() - 3600 # 1 hour ago
265
+ empty_rooms = conn.execute("""
266
+ SELECT r.room_id FROM rooms r
267
+ LEFT JOIN players p ON r.room_id = p.room_id
268
+ WHERE r.status = 'waiting'
269
+ AND p.room_id IS NULL
270
+ AND strftime('%s', r.created_at) < ?
271
+ """, (empty_cutoff,)).fetchall()
272
+
273
+ rooms_to_delete = set(
274
+ [r['room_id'] for r in stale_rooms] +
275
+ [r['room_id'] for r in empty_rooms]
276
+ )
277
+
278
+ for room_id in rooms_to_delete:
279
+ # Delete associated data
280
+ conn.execute("DELETE FROM players WHERE room_id = ?", (room_id,))
281
+ conn.execute("DELETE FROM spectators WHERE room_id = ?", (room_id,))
282
+ conn.execute("DELETE FROM rooms WHERE room_id = ?", (room_id,))
283
+ print(f"🧹 Cleaned up stale room: {room_id}")
284
+
285
+ conn.commit()
286
+ return len(rooms_to_delete)
287
 
288
  def get_room_state(self, room_id: str) -> Optional[Dict]:
289
  """Get complete room state including players"""
 
496
  def roll_dice() -> int:
497
  return random.randint(1, 6)
498
 
499
+ def can_afford_task(player: Dict, task: Dict, is_solo: bool = True) -> Tuple[bool, str]:
500
+ """Check if player can afford task"""
501
+ req = task['solo'] if is_solo else task['coop']
502
+
503
+ if player['time'] < task['time']:
504
+ return False, f"Not enough time (need {task['time']}h)"
505
+
506
+ for res, amount in req.items():
507
+ if res == "HP" and player['hp'] < amount:
508
+ return False, f"Not enough HP (need {amount})"
509
+ if res == "CP" and player['cp'] < amount:
510
+ return False, f"Not enough CP (need {amount})"
511
+
512
+ if 'cost' in task and player['money'] < task['cost']:
513
+ return False, f"Not enough money (need ${task['cost']})"
514
+
515
+ return True, "OK"
516
+
517
  def execute_rest(player: Dict, rest_type: str) -> Tuple[bool, str, Dict]:
518
  """Execute rest action"""
519
  if rest_type == "quick_nap":
 
735
 
736
  def refresh_game_state(session):
737
  if not session.get('room_id'):
738
+ return ("*No game joined*", "", "", 0, 0, 0, 0, 0, 0,
739
+ "", "", 0, 0, 0, 0, 0, 0, [], "", "*No spectators*", "")
740
 
741
  state = db.get_room_state(session['room_id'])
742
  if not state:
743
+ return ("*Room not found*", "", "", 0, 0, 0, 0, 0, 0,
744
+ "", "", 0, 0, 0, 0, 0, 0, [], "", "*No spectators*", "")
745
 
746
  room = state['room']
747
  players = state['players']
748
 
749
  # Status
750
  status_md = f"""
751
+ **Room:** {session['room_id']} | **Round:** {room['current_round']}/{room['total_rounds']}
752
  **Status:** {room['status'].upper()} | **Current Turn:** Player {room['current_turn'] + 1}
753
  """
754
 
755
  if room['status'] == 'finished':
756
+ winner_name = next((p['name'] for p in players if p['player_id'] == room['winner_id']), "Unknown")
757
+ status_md += f"\n\n🏆 **GAME OVER!** Winner: **{winner_name}**"
758
 
759
+ # Player 1 data
760
  p1_data = players[0] if len(players) > 0 else {}
761
+ p1_vals = (
762
+ p1_data.get('name', ''),
763
+ p1_data.get('character_id', ''),
764
+ p1_data.get('time', 0),
765
+ p1_data.get('money', 0),
766
+ p1_data.get('hp', 0),
767
+ p1_data.get('cp', 0),
768
+ p1_data.get('stress', 0),
769
+ p1_data.get('qol', 0)
770
+ )
771
+
772
+ # Player 2 data
773
  p2_data = players[1] if len(players) > 1 else {}
774
+ p2_vals = (
775
+ p2_data.get('name', ''),
776
+ p2_data.get('character_id', ''),
777
+ p2_data.get('time', 0),
778
+ p2_data.get('money', 0),
779
+ p2_data.get('hp', 0),
780
+ p2_data.get('cp', 0),
781
+ p2_data.get('stress', 0),
782
+ p2_data.get('qol', 0)
783
+ )
784
 
785
+ # Tasks with better formatting
786
  task_rows = []
787
+ task_options = []
788
  for task_id in state['task_ids']:
789
  task = next((t for t in TASKS if t['id'] == task_id), None)
790
  if task:
791
+ solo_req = ', '.join([f"{k}:{v}" for k, v in task['solo'].items()])
792
+ coop_req = ', '.join([f"{k}:{v}" for k, v in task['coop'].items()])
793
+
794
+ if 'reward' in task and task['reward'] > 0:
795
+ reward_str = f"${task['reward']}"
796
+ elif 'qol' in task:
797
+ reward_str = f"{task['qol']} QoL"
798
+ else:
799
+ reward_str = "Utility"
800
+
801
  task_rows.append([
802
  task['id'], task['name'], task['type'],
803
+ f"{task['time']}h", solo_req, coop_req, reward_str
804
  ])
805
+ task_options.append((f"{task['id']} - {task['name']}", task['id']))
 
 
806
 
807
  # Spectators
808
  spec_md = f"👥 {len(state['spectators'])} watching: {', '.join(state['spectators'])}" if state['spectators'] else "*No spectators*"
809
 
810
  # Turn indicator
811
  if session.get('is_spectator'):
812
+ turn_msg = "👁️ **You are spectating** - Refresh to see updates"
813
+ elif room['status'] == 'waiting':
814
+ turn_msg = "⏳ **Waiting for Player 2 to join...**"
815
+ elif room['status'] == 'finished':
816
+ turn_msg = "🏁 **Game Finished!** Check Rankings tab."
817
  elif session.get('player_slot') == room['current_turn']:
818
  turn_msg = "✅ **YOUR TURN!** Take an action below."
819
  else:
820
+ turn_msg = "⏳ **Partner's turn...** Click Refresh to see updates."
821
 
822
+ return (status_md, *p1_vals, *p2_vals, task_rows, task_options, spec_md, turn_msg)
823
 
824
  def execute_rest_handler(session, rest_type):
825
  if not session.get('room_id') or session.get('is_spectator'):
826
+ return "❌ Not in game as player"
827
 
828
  state = db.get_room_state(session['room_id'])
829
  if not state:
830
+ return "❌ Room not found"
831
 
832
  room = state['room']
833
+ if room['status'] != 'playing':
834
+ return "❌ Game not in progress"
835
+
836
  if room['current_turn'] != session['player_slot']:
837
+ return "❌ Not your turn!"
838
 
839
  player = state['players'][session['player_slot']]
840
  success, msg, updates = execute_rest(player, rest_type)
841
 
842
  if success:
843
  db.update_player(session['room_id'], session['player_slot'], updates)
844
+ return f"✅ {msg}"
845
 
846
+ return f"❌ {msg}"
847
 
848
  def execute_solo_task_handler(session, task_id):
849
  if not session.get('room_id') or session.get('is_spectator'):
850
+ return "❌ Not in game as player"
851
 
852
  if not task_id:
853
+ return "❌ Select a task first"
854
 
855
  state = db.get_room_state(session['room_id'])
856
  if not state:
857
+ return "❌ Room not found"
858
 
859
  room = state['room']
860
+ if room['status'] != 'playing':
861
+ return "❌ Game not in progress"
862
+
863
  if room['current_turn'] != session['player_slot']:
864
+ return "❌ Not your turn!"
865
 
866
  player = state['players'][session['player_slot']]
867
  task = next((t for t in TASKS if t['id'] == task_id), None)
868
 
869
  if not task:
870
+ return "❌ Task not found"
871
 
872
  success, msg, updates = execute_solo_task(player, task)
873
 
 
878
  updates['completed_tasks'] = json.dumps(completed)
879
 
880
  db.update_player(session['room_id'], session['player_slot'], updates)
881
+ return f"✅ {msg}"
882
 
883
+ return f"❌ {msg}"
884
 
885
  def end_turn_handler(session):
886
  if not session.get('room_id') or session.get('is_spectator'):
887
+ return "❌ Not in game as player"
888
 
889
  state = db.get_room_state(session['room_id'])
890
  if not state:
891
+ return "❌ Room not found"
892
 
893
  room = state['room']
894
+ if room['status'] != 'playing':
895
+ return "❌ Game not in progress"
896
+
897
  if room['current_turn'] != session['player_slot']:
898
+ return "❌ Not your turn!"
899
 
900
  db.end_turn(session['room_id'], session['player_slot'])
901
+ return "✅ Turn ended. Waiting for partner..."
902
 
903
  def load_rankings():
904
  rankings = db.get_rankings(20)
 
930
  refresh_btn.click(
931
  refresh_game_state,
932
  inputs=[session_state],
933
+ outputs=[status_display,
934
+ p1_name, p1_character, p1_time, p1_money, p1_hp, p1_cp, p1_stress, p1_qol,
935
+ p2_name, p2_character, p2_time, p2_money, p2_hp, p2_cp, p2_stress, p2_qol,
936
+ tasks_display, task_select, spectators_display, your_turn_msg]
937
  )
938
 
939
  quick_nap_btn.click(
940
  lambda s: execute_rest_handler(s, "quick_nap"),
941
  inputs=[session_state],
942
+ outputs=[action_result]
943
  ).then(
944
  refresh_game_state,
945
  inputs=[session_state],
946
+ outputs=[status_display,
947
+ p1_name, p1_character, p1_time, p1_money, p1_hp, p1_cp, p1_stress, p1_qol,
948
+ p2_name, p2_character, p2_time, p2_money, p2_hp, p2_cp, p2_stress, p2_qol,
949
+ tasks_display, task_select, spectators_display, your_turn_msg]
950
  )
951
 
952
  full_sleep_btn.click(
953
  lambda s: execute_rest_handler(s, "full_sleep"),
954
  inputs=[session_state],
955
+ outputs=[action_result]
956
  ).then(
957
  refresh_game_state,
958
  inputs=[session_state],
959
+ outputs=[status_display,
960
+ p1_name, p1_character, p1_time, p1_money, p1_hp, p1_cp, p1_stress, p1_qol,
961
+ p2_name, p2_character, p2_time, p2_money, p2_hp, p2_cp, p2_stress, p2_qol,
962
+ tasks_display, task_select, spectators_display, your_turn_msg]
963
  )
964
 
965
  deep_rest_btn.click(
966
  lambda s: execute_rest_handler(s, "deep_rest"),
967
  inputs=[session_state],
968
+ outputs=[action_result]
969
  ).then(
970
  refresh_game_state,
971
  inputs=[session_state],
972
+ outputs=[status_display,
973
+ p1_name, p1_character, p1_time, p1_money, p1_hp, p1_cp, p1_stress, p1_qol,
974
+ p2_name, p2_character, p2_time, p2_money, p2_hp, p2_cp, p2_stress, p2_qol,
975
+ tasks_display, task_select, spectators_display, your_turn_msg]
976
  )
977
 
978
  solo_task_btn.click(
979
  execute_solo_task_handler,
980
  inputs=[session_state, task_select],
981
+ outputs=[action_result]
982
  ).then(
983
  refresh_game_state,
984
  inputs=[session_state],
985
+ outputs=[status_display,
986
+ p1_name, p1_character, p1_time, p1_money, p1_hp, p1_cp, p1_stress, p1_qol,
987
+ p2_name, p2_character, p2_time, p2_money, p2_hp, p2_cp, p2_stress, p2_qol,
988
+ tasks_display, task_select, spectators_display, your_turn_msg]
989
  )
990
 
991
  end_turn_btn.click(
992
  end_turn_handler,
993
  inputs=[session_state],
994
+ outputs=[action_result]
995
  ).then(
996
  refresh_game_state,
997
  inputs=[session_state],
998
+ outputs=[status_display,
999
+ p1_name, p1_character, p1_time, p1_money, p1_hp, p1_cp, p1_stress, p1_qol,
1000
+ p2_name, p2_character, p2_time, p2_money, p2_hp, p2_cp, p2_stress, p2_qol,
1001
+ tasks_display, task_select, spectators_display, your_turn_msg]
1002
  )
1003
 
1004
  refresh_rankings_btn.click(
 
1006
  outputs=[rankings_table]
1007
  )
1008
 
1009
+ # Auto-load rankings on app start
1010
  app.load(load_rankings, outputs=[rankings_table])
1011
 
1012
  return app
1013
 
1014
  # ===== MAIN =====
1015
  if __name__ == "__main__":
1016
+ print("🏠 Life Frontier: Partner's Concerto")
1017
+ print("=" * 50)
1018
+ print("Starting server...")
1019
+
1020
  app = create_ui()
1021
  app.launch(
1022
  server_name="0.0.0.0",
1023
  server_port=7860,
1024
+ share=False,
1025
+ show_error=True
1026
+ )
1027
+ print("\n✨ Server started! Open http://localhost:7860")