Spaces:
Running
Running
fix: tz-naive datetime crash + initial-backup safety + English-only sweep
Browse files- 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
|
| 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, "❌
|
| 154 |
if bet_amount < 1 or bet_amount > 100:
|
| 155 |
-
return False, "❌
|
| 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, "❌
|
| 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 (
|
| 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}
|
| 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
|
| 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, "❌
|
| 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
|
| 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"✅
|
| 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, "❌
|
| 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}
|
| 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 |
-
#
|
| 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 |
-
#
|
| 532 |
-
# ★
|
| 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 |
-
#
|
| 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 —
|
| 706 |
-
20
|
| 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 |
-
# ★
|
| 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 |
-
# ★
|
| 723 |
force_create = daily_count < 3
|
| 724 |
|
| 725 |
-
#
|
| 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 |
-
#
|
| 748 |
num_create = 1
|
| 749 |
if force_create:
|
| 750 |
-
num_create = min(2, len(available_topics)) #
|
| 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 |
-
#
|
| 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 |
-
# ★
|
| 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 |
-
# ★
|
| 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 |
-
|
| 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 |
-
#
|
| 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 |
-
#
|
| 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 |
-
#
|
| 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 |
-
#
|
| 858 |
choice = decide_npc_choice(ai_identity, title, option_a, option_b)
|
| 859 |
-
|
| 860 |
-
#
|
| 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 |
-
#
|
| 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 topics — stocks/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
|