seawolf2357 commited on
Commit
b6d8ace
·
verified ·
1 Parent(s): 4fa3c36

fix: tz-naive datetime crash + initial-backup safety + English-only sweep

Browse files
Files changed (1) hide show
  1. battle_arena.py +73 -73
battle_arena.py CHANGED
@@ -72,9 +72,9 @@ async def create_battle_room(
72
  duration_hours: 1 hour ~ 365 days (8760 hours)
73
  """
74
  if not title or len(title) < 10:
75
- return False, "❌ Title must be 10+ characters", None
76
  if not option_a or not option_b:
77
- return False, "❌ Options A/B required", None
78
  if duration_hours < 1 or duration_hours > 8760:
79
  return False, "❌ Duration: 1 hour ~ 365 days", None
80
  if is_npc and battle_type != 'opinion':
@@ -150,9 +150,9 @@ async def place_bet(
150
  ) -> Tuple[bool, str]:
151
  """Execute bet (1-100 GPU random bet)"""
152
  if choice not in ['A', 'B']:
153
- return False, "❌ Choose A or B"
154
  if bet_amount < 1 or bet_amount > 100:
155
- return False, "❌ Bet 1-100 GPU"
156
 
157
  async with aiosqlite.connect(db_path, timeout=30.0) as db:
158
  await db.execute("PRAGMA busy_timeout=30000")
@@ -165,12 +165,12 @@ async def place_bet(
165
  )
166
  room = await cursor.fetchone()
167
  if not room:
168
- return False, "❌ Room not found or closed"
169
-
170
  room = dict(room)
171
  end_time = datetime.fromisoformat(room['end_time'])
172
  if datetime.now() >= end_time:
173
- return False, "❌ Betting closed"
174
 
175
  # Check duplicate bet
176
  if is_npc:
@@ -209,7 +209,7 @@ async def place_bet(
209
  if not user_row:
210
  return False, f"❌ User not found ({bettor_id})"
211
  if user_row[0] < bet_amount:
212
- return False, f"❌ Insufficient GPU (보유: {user_row[0]}, 필요: {bet_amount})"
213
  await db.execute(
214
  "UPDATE user_profiles SET gpu_dollars=gpu_dollars-? WHERE email=?",
215
  (bet_amount, bettor_id)
@@ -248,7 +248,7 @@ async def place_bet(
248
  )
249
 
250
  await db.commit()
251
- return True, f"✅ {choice} 베팅 완료! ({bet_amount} GPU)"
252
 
253
  async def set_battle_result(
254
  db_path: str,
@@ -262,13 +262,13 @@ async def set_battle_result(
262
  db_path: Database path
263
  room_id: Battle room ID
264
  admin_email: Admin email (for verification)
265
- winner: 'A', 'B', 'draw' 중 하나
266
-
267
  Returns:
268
  (success status, message)
269
  """
270
  if winner not in ['A', 'B', 'draw']:
271
- return False, "❌ winner 'A', 'B', 'draw' 중 하나여야 함"
272
 
273
  async with aiosqlite.connect(db_path, timeout=30.0) as db:
274
  await db.execute("PRAGMA busy_timeout=30000")
@@ -280,13 +280,13 @@ async def set_battle_result(
280
  )
281
  room = await cursor.fetchone()
282
  if not room:
283
- return False, "❌ Active battle not found"
284
-
285
  room = dict(room)
286
-
287
  # Only prediction type allows admin result setting
288
  if room['battle_type'] != 'prediction':
289
- return False, "❌ Opinion battles are auto-judged"
290
 
291
  # Save result
292
  await db.execute(
@@ -305,7 +305,7 @@ async def set_battle_result(
305
  else:
306
  time_str = f"{int(remaining.seconds//3600)} hours"
307
 
308
- return True, f"✅ 결과 설정: '{option_name}' (베팅 마감 자동 판정, 남은 hours: {time_str})"
309
 
310
  # If after deadline, judge immediately
311
  return await resolve_battle(db_path, room_id)
@@ -326,8 +326,8 @@ async def resolve_battle(db_path: str, room_id: int) -> Tuple[bool, str]:
326
  )
327
  room = await cursor.fetchone()
328
  if not room:
329
- return False, "❌ Active battle not found"
330
-
331
  room = dict(room)
332
  end_time = datetime.fromisoformat(room['end_time'])
333
  if datetime.now() < end_time:
@@ -346,7 +346,7 @@ async def resolve_battle(db_path: str, room_id: int) -> Tuple[bool, str]:
346
  (datetime.now().isoformat(), room_id)
347
  )
348
  await db.commit()
349
- return True, "⚖️ Draw (베팅 없음)"
350
 
351
  # Determine winner based on battle type
352
  if room['battle_type'] == 'prediction':
@@ -398,9 +398,9 @@ async def resolve_battle(db_path: str, room_id: int) -> Tuple[bool, str]:
398
  share_ratio = w['bet_amount'] / winner_pool
399
  base_payout = int(prize_pool * share_ratio)
400
  bonus = int(base_payout * (underdog_bonus - 1.0))
401
- payout = base_payout + bonus + w['bet_amount'] # 원금 + 기본수익 + 소수파보너스
402
-
403
- # GPU 지급
404
  if w['bettor_agent_id']:
405
  await db.execute(
406
  "UPDATE npc_agents SET gpu_dollars=gpu_dollars+? WHERE agent_id=?",
@@ -412,13 +412,13 @@ async def resolve_battle(db_path: str, room_id: int) -> Tuple[bool, str]:
412
  (payout, w['bettor_email'])
413
  )
414
 
415
- # 배당금 기록
416
  await db.execute(
417
  "UPDATE battle_bets SET payout=? WHERE id=?",
418
  (payout, w['id'])
419
  )
420
 
421
- # 방장 수수료 지급
422
  if room['creator_agent_id']:
423
  await db.execute(
424
  "UPDATE npc_agents SET gpu_dollars=gpu_dollars+? WHERE agent_id=?",
@@ -430,7 +430,7 @@ async def resolve_battle(db_path: str, room_id: int) -> Tuple[bool, str]:
430
  (host_fee, room['creator_email'])
431
  )
432
 
433
- # 배틀 종료 처리
434
  await db.execute(
435
  """UPDATE battle_rooms
436
  SET status='resolved', winner=?, resolved_at=?
@@ -439,17 +439,17 @@ async def resolve_battle(db_path: str, room_id: int) -> Tuple[bool, str]:
439
  )
440
  await db.commit()
441
 
442
- # 결과 메시지
443
  if winner == 'draw':
444
  result_msg = 'Draw'
445
  else:
446
  result_msg = room['option_a'] if winner == 'A' else room['option_b']
447
 
448
  battle_type_emoji = '💭' if room['battle_type'] == 'opinion' else '🔮'
449
- return True, f"✅ {battle_type_emoji} 판정 완료: {result_msg}"
450
 
451
  async def get_active_battles(db_path: str, limit: int = 20) -> List[Dict]:
452
- """진행 중인 배틀 목록"""
453
  async with aiosqlite.connect(db_path, timeout=30.0) as db:
454
  await db.execute("PRAGMA busy_timeout=30000")
455
  db.row_factory = aiosqlite.Row
@@ -470,11 +470,11 @@ async def get_active_battles(db_path: str, limit: int = 20) -> List[Dict]:
470
  for row in await cursor.fetchall():
471
  b = dict(row)
472
 
473
- # ★ 프론트 호환 필드 추가
474
  b['bets_a'] = b.get('option_a_pool', 0)
475
  b['bets_b'] = b.get('option_b_pool', 0)
476
-
477
- # 참여자 조회
478
  bet_cursor = await db.execute(
479
  "SELECT COUNT(DISTINCT COALESCE(bettor_agent_id, bettor_email)) FROM battle_bets WHERE room_id=?",
480
  (b['id'],)
@@ -482,7 +482,7 @@ async def get_active_battles(db_path: str, limit: int = 20) -> List[Dict]:
482
  bettor_count = await bet_cursor.fetchone()
483
  b['total_bettors'] = bettor_count[0] if bettor_count else 0
484
 
485
- # 득표율 계산
486
  total = b['total_pool']
487
  if total > 0:
488
  b['a_ratio'] = round(b['option_a_pool'] / total * 100, 1)
@@ -491,7 +491,7 @@ async def get_active_battles(db_path: str, limit: int = 20) -> List[Dict]:
491
  b['a_ratio'] = 0
492
  b['b_ratio'] = 0
493
 
494
- # 남은 hours 계산
495
  end_time = datetime.fromisoformat(b['end_time'])
496
  remaining = end_time - datetime.now()
497
 
@@ -505,16 +505,16 @@ async def get_active_battles(db_path: str, limit: int = 20) -> List[Dict]:
505
  elif remaining.total_seconds() > 3600:
506
  b['time_left'] = f"{int(remaining.total_seconds()//3600)} hours"
507
  else:
508
- b['time_left'] = f"{int(remaining.total_seconds()//60)}"
509
  else:
510
- b['time_left'] = "마감"
511
 
512
  battles.append(b)
513
 
514
  return battles
515
 
516
  async def auto_resolve_expired_battles(db_path: str):
517
- """만료된 배틀 자동 판정"""
518
  async with aiosqlite.connect(db_path, timeout=30.0) as db:
519
  await db.execute("PRAGMA busy_timeout=30000")
520
 
@@ -528,10 +528,10 @@ async def auto_resolve_expired_battles(db_path: str):
528
  for row in expired:
529
  await resolve_battle(db_path, row[0])
530
 
531
- # NPC 배틀 생성을 위한 주제 데이터
532
- # ★ 공통 배틀 토픽주식/경제/사회/정치 (모든 NPC가 사용)
533
  COMMON_BATTLE_TOPICS = [
534
- # 주식/투자
535
  ("Is $NVDA overvalued at current prices?", "Overvalued", "Still cheap"),
536
  ("Will $BTC hit $200K in 2026?", "Yes $200K+", "No way"),
537
  ("Is the AI stock rally a bubble?", "Bubble", "Just the beginning"),
@@ -542,14 +542,14 @@ COMMON_BATTLE_TOPICS = [
542
  ("$AAPL or $MSFT: better 5-year hold?", "AAPL", "MSFT"),
543
  ("Is meme coin investing smart or dumb?", "Smart alpha", "Pure gambling"),
544
  ("Should you DCA or time the market?", "DCA always", "Timing works"),
545
- # 경제
546
  ("Will the Fed cut rates in 2026?", "Yes, cuts coming", "No, rates stay high"),
547
  ("Is a US recession coming?", "Recession likely", "Soft landing"),
548
  ("Is inflation actually under control?", "Under control", "Still a threat"),
549
  ("Will the US dollar lose reserve status?", "Losing it", "Dollar stays king"),
550
  ("Is remote work killing the economy?", "Hurting GDP", "Boosting productivity"),
551
  ("Will AI cause mass unemployment?", "Mass layoffs coming", "Creates more jobs"),
552
- # 사회/정치
553
  ("Should Big Tech be broken up?", "Break them up", "Leave them alone"),
554
  ("Is social media a net positive?", "Net positive", "Net negative"),
555
  ("Should AI be regulated like nuclear?", "Heavy regulation", "Let it innovate"),
@@ -634,12 +634,12 @@ BATTLE_TOPICS_BY_IDENTITY = {
634
  }
635
 
636
  async def _generate_news_battle_topics(db_path: str) -> List[Tuple[str, str, str]]:
637
- """최근 뉴스에서 동적 배틀 토픽 생성핫이슈 중심"""
638
  topics = []
639
  try:
640
  async with aiosqlite.connect(db_path, timeout=30.0) as db:
641
  await db.execute("PRAGMA busy_timeout=30000")
642
- # 최근 24시간 뉴스 + 감성 포함
643
  cursor = await db.execute("""
644
  SELECT ticker, title, sentiment, description
645
  FROM npc_news
@@ -648,7 +648,7 @@ async def _generate_news_battle_topics(db_path: str) -> List[Tuple[str, str, str
648
  """)
649
  news_rows = await cursor.fetchall()
650
 
651
- # 최근 가격 변동 큰 종목
652
  cursor2 = await db.execute("""
653
  SELECT ticker, price, change_pct
654
  FROM market_prices
@@ -657,14 +657,14 @@ async def _generate_news_battle_topics(db_path: str) -> List[Tuple[str, str, str
657
  """)
658
  movers = await cursor2.fetchall()
659
 
660
- # 1) 뉴스 기반 토픽 생성
661
  seen_tickers = set()
662
  for row in news_rows:
663
  ticker, title, sentiment, desc = row[0], row[1], row[2], row[3] or ''
664
  if ticker in seen_tickers:
665
  continue
666
  seen_tickers.add(ticker)
667
- # 뉴스 제목에서 배틀 토픽 생성
668
  short_title = title[:60] if len(title) > 60 else title
669
  if sentiment == 'bullish':
670
  topics.append((
@@ -679,7 +679,7 @@ async def _generate_news_battle_topics(db_path: str) -> List[Tuple[str, str, str
679
  f"${ticker} — {short_title}: impact on price?",
680
  "Positive impact 📈", "Negative impact 📉"))
681
 
682
- # 2) 급등락 종목 기반 토픽
683
  for mover in movers:
684
  ticker, price, change = mover[0], mover[1], mover[2] or 0
685
  if ticker in seen_tickers:
@@ -702,15 +702,15 @@ async def _generate_news_battle_topics(db_path: str) -> List[Tuple[str, str, str
702
 
703
 
704
  async def npc_create_battle(db_path: str) -> Tuple[bool, str]:
705
- """NPCs automatically create battle rooms — 뉴스 기반 동적 토픽 + 일일 3~10
706
- 20분마다 호출, 24시간 최소 3~최대 10 생성
707
  """
708
  results = []
709
 
710
  async with aiosqlite.connect(db_path, timeout=30.0) as db:
711
  await db.execute("PRAGMA busy_timeout=30000")
712
 
713
- # ★ 일일 생성 체크: 24시간 10 이상이면 스킵
714
  cursor = await db.execute("""
715
  SELECT COUNT(*) FROM battle_rooms
716
  WHERE created_at > datetime('now', '-24 hours')
@@ -719,22 +719,22 @@ async def npc_create_battle(db_path: str) -> Tuple[bool, str]:
719
  if daily_count >= 10:
720
  return False, f"Daily cap reached ({daily_count}/10)"
721
 
722
- # ★ 최소 보장: 24시간 3개 미��이면 반드시 1개 생성
723
  force_create = daily_count < 3
724
 
725
- # 확률 기반: 20 호출 → 72/, 3~10 목표약 7~14% 확률로 생성
726
  if not force_create and random.random() > 0.14:
727
  return False, "Skipped by probability (saving quota)"
728
 
729
- # active 배틀 제목 조회
730
  cursor = await db.execute("SELECT title FROM battle_rooms WHERE status='active'")
731
  active_titles = {row[0] for row in await cursor.fetchall()}
732
 
733
- # ★ 뉴스 기반 동적 토픽 (우선) + 정적 토픽 (폴백)
734
  news_topics = await _generate_news_battle_topics(db_path)
735
  all_topics = news_topics + COMMON_BATTLE_TOPICS
736
 
737
- # 이미 활성인 토픽 제외 (제목 유사도 체크)
738
  available_topics = []
739
  for t in all_topics:
740
  title_lower = t[0].lower()
@@ -744,10 +744,10 @@ async def npc_create_battle(db_path: str) -> Tuple[bool, str]:
744
  if not available_topics:
745
  return False, "No available topics"
746
 
747
- # 1개만 생성 (일일 내에서)
748
  num_create = 1
749
  if force_create:
750
- num_create = min(2, len(available_topics)) # 부족 2개까지
751
 
752
  for i in range(num_create):
753
  if not available_topics:
@@ -756,7 +756,7 @@ async def npc_create_battle(db_path: str) -> Tuple[bool, str]:
756
  async with aiosqlite.connect(db_path, timeout=30.0) as db:
757
  await db.execute("PRAGMA busy_timeout=30000")
758
 
759
- # 랜덤 NPC 선택
760
  cursor = await db.execute("""
761
  SELECT agent_id, ai_identity, gpu_dollars
762
  FROM npc_agents
@@ -770,7 +770,7 @@ async def npc_create_battle(db_path: str) -> Tuple[bool, str]:
770
 
771
  agent_id = npc[0]
772
 
773
- # ★ 뉴스 토픽 우선 (70%), 정적 토픽 (30%)
774
  news_available = [t for t in available_topics if t in news_topics]
775
  if news_available and random.random() < 0.7:
776
  topic = random.choice(news_available)
@@ -780,7 +780,7 @@ async def npc_create_battle(db_path: str) -> Tuple[bool, str]:
780
  title, option_a, option_b = topic
781
  available_topics.remove(topic)
782
 
783
- # ★ 짧은 duration: 6~48시간 (빠른 회전)
784
  duration_hours = random.choice([6, 8, 12, 18, 24, 36, 48])
785
 
786
  success, message, room_id = await create_battle_room(
@@ -803,14 +803,14 @@ async def npc_auto_bet(db_path: str) -> int:
803
  """NPCs automatically bet on battles (AI identity-based)
804
 
805
  Returns:
806
- 베팅한 NPC
807
  """
808
  total_bet_count = 0
809
 
810
  async with aiosqlite.connect(db_path, timeout=30.0) as db:
811
  await db.execute("PRAGMA busy_timeout=30000")
812
 
813
- # 활성 배틀 조회 (최근 10, opinion 타입만)
814
  cursor = await db.execute("""
815
  SELECT id, title, option_a, option_b, battle_type
816
  FROM battle_rooms
@@ -829,7 +829,7 @@ async def npc_auto_bet(db_path: str) -> int:
829
  room_id, title, option_a, option_b, battle_type = battle
830
  battle_bet_count = 0
831
 
832
- # 이미 베팅한 NPC 확인
833
  cursor = await db.execute("""
834
  SELECT bettor_agent_id
835
  FROM battle_bets
@@ -837,7 +837,7 @@ async def npc_auto_bet(db_path: str) -> int:
837
  """, (room_id,))
838
  already_bet = {row[0] for row in await cursor.fetchall() if row[0]}
839
 
840
- # 활성 NPC 랜덤 선택 (최대 30)
841
  cursor = await db.execute("""
842
  SELECT agent_id, ai_identity, mbti, gpu_dollars
843
  FROM npc_agents
@@ -850,18 +850,18 @@ async def npc_auto_bet(db_path: str) -> int:
850
  for npc in npcs:
851
  agent_id, ai_identity, mbti, gpu = npc
852
 
853
- # 이미 베팅했으면 스킵
854
  if agent_id in already_bet:
855
  continue
856
-
857
- # AI 정체성에 따라 선택 결정
858
  choice = decide_npc_choice(ai_identity, title, option_a, option_b)
859
-
860
- # 베팅 금액 (보유 GPU의 40% 이내, 최대 50)
861
  max_bet = max(1, min(50, int(gpu * 0.4)))
862
  bet_amount = random.randint(1, max_bet)
863
-
864
- # 베팅 실행
865
  success, message = await place_bet(
866
  db_path,
867
  room_id,
@@ -875,7 +875,7 @@ async def npc_auto_bet(db_path: str) -> int:
875
  battle_bet_count += 1
876
  total_bet_count += 1
877
 
878
- # 배틀당 8-12 정도만 베팅
879
  max_bets_per_battle = random.randint(8, 12)
880
  if battle_bet_count >= max_bets_per_battle:
881
  break
 
72
  duration_hours: 1 hour ~ 365 days (8760 hours)
73
  """
74
  if not title or len(title) < 10:
75
+ return False, "❌ Title must be at least 10 characters", None
76
  if not option_a or not option_b:
77
+ return False, "❌ Options A/B are required", None
78
  if duration_hours < 1 or duration_hours > 8760:
79
  return False, "❌ Duration: 1 hour ~ 365 days", None
80
  if is_npc and battle_type != 'opinion':
 
150
  ) -> Tuple[bool, str]:
151
  """Execute bet (1-100 GPU random bet)"""
152
  if choice not in ['A', 'B']:
153
+ return False, "❌ Please select A or B"
154
  if bet_amount < 1 or bet_amount > 100:
155
+ return False, "❌ Please bet within 1-100 GPU range"
156
 
157
  async with aiosqlite.connect(db_path, timeout=30.0) as db:
158
  await db.execute("PRAGMA busy_timeout=30000")
 
165
  )
166
  room = await cursor.fetchone()
167
  if not room:
168
+ return False, "❌ Battle not found or ended"
169
+
170
  room = dict(room)
171
  end_time = datetime.fromisoformat(room['end_time'])
172
  if datetime.now() >= end_time:
173
+ return False, "❌ Betting has closed"
174
 
175
  # Check duplicate bet
176
  if is_npc:
 
209
  if not user_row:
210
  return False, f"❌ User not found ({bettor_id})"
211
  if user_row[0] < bet_amount:
212
+ return False, f"❌ Insufficient GPU (have: {user_row[0]}, need: {bet_amount})"
213
  await db.execute(
214
  "UPDATE user_profiles SET gpu_dollars=gpu_dollars-? WHERE email=?",
215
  (bet_amount, bettor_id)
 
248
  )
249
 
250
  await db.commit()
251
+ return True, f"✅ {choice} bet placed! ({bet_amount} GPU)"
252
 
253
  async def set_battle_result(
254
  db_path: str,
 
262
  db_path: Database path
263
  room_id: Battle room ID
264
  admin_email: Admin email (for verification)
265
+ winner: One of 'A', 'B', 'draw'
266
+
267
  Returns:
268
  (success status, message)
269
  """
270
  if winner not in ['A', 'B', 'draw']:
271
+ return False, "❌ winner must be one of 'A', 'B', 'draw'"
272
 
273
  async with aiosqlite.connect(db_path, timeout=30.0) as db:
274
  await db.execute("PRAGMA busy_timeout=30000")
 
280
  )
281
  room = await cursor.fetchone()
282
  if not room:
283
+ return False, "❌ No active battle found"
284
+
285
  room = dict(room)
286
+
287
  # Only prediction type allows admin result setting
288
  if room['battle_type'] != 'prediction':
289
+ return False, "❌ Opinion battles are resolved automatically"
290
 
291
  # Save result
292
  await db.execute(
 
305
  else:
306
  time_str = f"{int(remaining.seconds//3600)} hours"
307
 
308
+ return True, f"✅ Result set: '{option_name}' (auto-resolved after betting closes, remaining hours: {time_str})"
309
 
310
  # If after deadline, judge immediately
311
  return await resolve_battle(db_path, room_id)
 
326
  )
327
  room = await cursor.fetchone()
328
  if not room:
329
+ return False, "❌ No active battle found"
330
+
331
  room = dict(room)
332
  end_time = datetime.fromisoformat(room['end_time'])
333
  if datetime.now() < end_time:
 
346
  (datetime.now().isoformat(), room_id)
347
  )
348
  await db.commit()
349
+ return True, "⚖️ Draw (no bets)"
350
 
351
  # Determine winner based on battle type
352
  if room['battle_type'] == 'prediction':
 
398
  share_ratio = w['bet_amount'] / winner_pool
399
  base_payout = int(prize_pool * share_ratio)
400
  bonus = int(base_payout * (underdog_bonus - 1.0))
401
+ payout = base_payout + bonus + w['bet_amount'] # principal + base profit + underdog bonus
402
+
403
+ # GPU payout
404
  if w['bettor_agent_id']:
405
  await db.execute(
406
  "UPDATE npc_agents SET gpu_dollars=gpu_dollars+? WHERE agent_id=?",
 
412
  (payout, w['bettor_email'])
413
  )
414
 
415
+ # Record payout
416
  await db.execute(
417
  "UPDATE battle_bets SET payout=? WHERE id=?",
418
  (payout, w['id'])
419
  )
420
 
421
+ # Pay host fee
422
  if room['creator_agent_id']:
423
  await db.execute(
424
  "UPDATE npc_agents SET gpu_dollars=gpu_dollars+? WHERE agent_id=?",
 
430
  (host_fee, room['creator_email'])
431
  )
432
 
433
+ # Mark battle as resolved
434
  await db.execute(
435
  """UPDATE battle_rooms
436
  SET status='resolved', winner=?, resolved_at=?
 
439
  )
440
  await db.commit()
441
 
442
+ # Result message
443
  if winner == 'draw':
444
  result_msg = 'Draw'
445
  else:
446
  result_msg = room['option_a'] if winner == 'A' else room['option_b']
447
 
448
  battle_type_emoji = '💭' if room['battle_type'] == 'opinion' else '🔮'
449
+ return True, f"✅ {battle_type_emoji} Resolved: {result_msg}"
450
 
451
  async def get_active_battles(db_path: str, limit: int = 20) -> List[Dict]:
452
+ """List of in-progress battles"""
453
  async with aiosqlite.connect(db_path, timeout=30.0) as db:
454
  await db.execute("PRAGMA busy_timeout=30000")
455
  db.row_factory = aiosqlite.Row
 
470
  for row in await cursor.fetchall():
471
  b = dict(row)
472
 
473
+ # ★ Add frontend-compatible fields
474
  b['bets_a'] = b.get('option_a_pool', 0)
475
  b['bets_b'] = b.get('option_b_pool', 0)
476
+
477
+ # Query participant count
478
  bet_cursor = await db.execute(
479
  "SELECT COUNT(DISTINCT COALESCE(bettor_agent_id, bettor_email)) FROM battle_bets WHERE room_id=?",
480
  (b['id'],)
 
482
  bettor_count = await bet_cursor.fetchone()
483
  b['total_bettors'] = bettor_count[0] if bettor_count else 0
484
 
485
+ # Calculate vote ratio
486
  total = b['total_pool']
487
  if total > 0:
488
  b['a_ratio'] = round(b['option_a_pool'] / total * 100, 1)
 
491
  b['a_ratio'] = 0
492
  b['b_ratio'] = 0
493
 
494
+ # Calculate remaining time
495
  end_time = datetime.fromisoformat(b['end_time'])
496
  remaining = end_time - datetime.now()
497
 
 
505
  elif remaining.total_seconds() > 3600:
506
  b['time_left'] = f"{int(remaining.total_seconds()//3600)} hours"
507
  else:
508
+ b['time_left'] = f"{int(remaining.total_seconds()//60)}m"
509
  else:
510
+ b['time_left'] = "Closed"
511
 
512
  battles.append(b)
513
 
514
  return battles
515
 
516
  async def auto_resolve_expired_battles(db_path: str):
517
+ """Auto-resolve expired battles"""
518
  async with aiosqlite.connect(db_path, timeout=30.0) as db:
519
  await db.execute("PRAGMA busy_timeout=30000")
520
 
 
528
  for row in expired:
529
  await resolve_battle(db_path, row[0])
530
 
531
+ # Topic data for NPC battle generation
532
+ # ★ Common battle topicsstocks/economy/society/politics (used by all NPCs)
533
  COMMON_BATTLE_TOPICS = [
534
+ # Stocks/Investing
535
  ("Is $NVDA overvalued at current prices?", "Overvalued", "Still cheap"),
536
  ("Will $BTC hit $200K in 2026?", "Yes $200K+", "No way"),
537
  ("Is the AI stock rally a bubble?", "Bubble", "Just the beginning"),
 
542
  ("$AAPL or $MSFT: better 5-year hold?", "AAPL", "MSFT"),
543
  ("Is meme coin investing smart or dumb?", "Smart alpha", "Pure gambling"),
544
  ("Should you DCA or time the market?", "DCA always", "Timing works"),
545
+ # Economy
546
  ("Will the Fed cut rates in 2026?", "Yes, cuts coming", "No, rates stay high"),
547
  ("Is a US recession coming?", "Recession likely", "Soft landing"),
548
  ("Is inflation actually under control?", "Under control", "Still a threat"),
549
  ("Will the US dollar lose reserve status?", "Losing it", "Dollar stays king"),
550
  ("Is remote work killing the economy?", "Hurting GDP", "Boosting productivity"),
551
  ("Will AI cause mass unemployment?", "Mass layoffs coming", "Creates more jobs"),
552
+ # Society/Politics
553
  ("Should Big Tech be broken up?", "Break them up", "Leave them alone"),
554
  ("Is social media a net positive?", "Net positive", "Net negative"),
555
  ("Should AI be regulated like nuclear?", "Heavy regulation", "Let it innovate"),
 
634
  }
635
 
636
  async def _generate_news_battle_topics(db_path: str) -> List[Tuple[str, str, str]]:
637
+ """Generate dynamic battle topics from recent news focused on hot issues"""
638
  topics = []
639
  try:
640
  async with aiosqlite.connect(db_path, timeout=30.0) as db:
641
  await db.execute("PRAGMA busy_timeout=30000")
642
+ # Recent 24h news + sentiment
643
  cursor = await db.execute("""
644
  SELECT ticker, title, sentiment, description
645
  FROM npc_news
 
648
  """)
649
  news_rows = await cursor.fetchall()
650
 
651
+ # Recent big movers
652
  cursor2 = await db.execute("""
653
  SELECT ticker, price, change_pct
654
  FROM market_prices
 
657
  """)
658
  movers = await cursor2.fetchall()
659
 
660
+ # 1) Generate news-based topics
661
  seen_tickers = set()
662
  for row in news_rows:
663
  ticker, title, sentiment, desc = row[0], row[1], row[2], row[3] or ''
664
  if ticker in seen_tickers:
665
  continue
666
  seen_tickers.add(ticker)
667
+ # Generate battle topic from news title
668
  short_title = title[:60] if len(title) > 60 else title
669
  if sentiment == 'bullish':
670
  topics.append((
 
679
  f"${ticker} — {short_title}: impact on price?",
680
  "Positive impact 📈", "Negative impact 📉"))
681
 
682
+ # 2) Topics based on big movers
683
  for mover in movers:
684
  ticker, price, change = mover[0], mover[1], mover[2] or 0
685
  if ticker in seen_tickers:
 
702
 
703
 
704
  async def npc_create_battle(db_path: str) -> Tuple[bool, str]:
705
+ """NPCs automatically create battle rooms — news-based dynamic topics + daily cap 3~10
706
+ Called every 20 minutes, generates min 3 ~ max 10 per 24h
707
  """
708
  results = []
709
 
710
  async with aiosqlite.connect(db_path, timeout=30.0) as db:
711
  await db.execute("PRAGMA busy_timeout=30000")
712
 
713
+ # ★ Daily creation cap check: skip if 10+ in last 24h
714
  cursor = await db.execute("""
715
  SELECT COUNT(*) FROM battle_rooms
716
  WHERE created_at > datetime('now', '-24 hours')
 
719
  if daily_count >= 10:
720
  return False, f"Daily cap reached ({daily_count}/10)"
721
 
722
+ # ★ Minimum guarantee: force create 1 if fewer than 3 in 24h
723
  force_create = daily_count < 3
724
 
725
+ # Probabilistic: 20-min calls → 72/day, 3~10 target → ~7-14% chance per call
726
  if not force_create and random.random() > 0.14:
727
  return False, "Skipped by probability (saving quota)"
728
 
729
+ # Query active battle titles
730
  cursor = await db.execute("SELECT title FROM battle_rooms WHERE status='active'")
731
  active_titles = {row[0] for row in await cursor.fetchall()}
732
 
733
+ # ★ News-based dynamic topics (priority) + static topics (fallback)
734
  news_topics = await _generate_news_battle_topics(db_path)
735
  all_topics = news_topics + COMMON_BATTLE_TOPICS
736
 
737
+ # Exclude topics already active (title similarity check)
738
  available_topics = []
739
  for t in all_topics:
740
  title_lower = t[0].lower()
 
744
  if not available_topics:
745
  return False, "No available topics"
746
 
747
+ # Create only 1 (within daily cap)
748
  num_create = 1
749
  if force_create:
750
+ num_create = min(2, len(available_topics)) # up to 2 if insufficient
751
 
752
  for i in range(num_create):
753
  if not available_topics:
 
756
  async with aiosqlite.connect(db_path, timeout=30.0) as db:
757
  await db.execute("PRAGMA busy_timeout=30000")
758
 
759
+ # Random NPC selection
760
  cursor = await db.execute("""
761
  SELECT agent_id, ai_identity, gpu_dollars
762
  FROM npc_agents
 
770
 
771
  agent_id = npc[0]
772
 
773
+ # ★ News topic priority (70%), static topic (30%)
774
  news_available = [t for t in available_topics if t in news_topics]
775
  if news_available and random.random() < 0.7:
776
  topic = random.choice(news_available)
 
780
  title, option_a, option_b = topic
781
  available_topics.remove(topic)
782
 
783
+ # ★ Short duration: 6~48 hours (fast turnover)
784
  duration_hours = random.choice([6, 8, 12, 18, 24, 36, 48])
785
 
786
  success, message, room_id = await create_battle_room(
 
803
  """NPCs automatically bet on battles (AI identity-based)
804
 
805
  Returns:
806
+ Number of NPCs that bet
807
  """
808
  total_bet_count = 0
809
 
810
  async with aiosqlite.connect(db_path, timeout=30.0) as db:
811
  await db.execute("PRAGMA busy_timeout=30000")
812
 
813
+ # Query active battles (last 10, opinion type only)
814
  cursor = await db.execute("""
815
  SELECT id, title, option_a, option_b, battle_type
816
  FROM battle_rooms
 
829
  room_id, title, option_a, option_b, battle_type = battle
830
  battle_bet_count = 0
831
 
832
+ # Check NPCs that already bet
833
  cursor = await db.execute("""
834
  SELECT bettor_agent_id
835
  FROM battle_bets
 
837
  """, (room_id,))
838
  already_bet = {row[0] for row in await cursor.fetchall() if row[0]}
839
 
840
+ # Random selection of active NPCs (max 30)
841
  cursor = await db.execute("""
842
  SELECT agent_id, ai_identity, mbti, gpu_dollars
843
  FROM npc_agents
 
850
  for npc in npcs:
851
  agent_id, ai_identity, mbti, gpu = npc
852
 
853
+ # Skip if already bet
854
  if agent_id in already_bet:
855
  continue
856
+
857
+ # Decide choice based on AI identity
858
  choice = decide_npc_choice(ai_identity, title, option_a, option_b)
859
+
860
+ # Bet amount (within 40% of GPU balance, max 50)
861
  max_bet = max(1, min(50, int(gpu * 0.4)))
862
  bet_amount = random.randint(1, max_bet)
863
+
864
+ # Place bet
865
  success, message = await place_bet(
866
  db_path,
867
  room_id,
 
875
  battle_bet_count += 1
876
  total_bet_count += 1
877
 
878
+ # ~8-12 bets per battle
879
  max_bets_per_battle = random.randint(8, 12)
880
  if battle_bet_count >= max_bets_per_battle:
881
  break