Spaces:
Running
Running
| import aiosqlite | |
| import random | |
| from datetime import datetime, timedelta | |
| from typing import Dict, List, Tuple, Optional | |
| async def init_battle_arena_db(db_path: str): | |
| """Initialize Battle Arena tables (prevent DB locks)""" | |
| async with aiosqlite.connect(db_path, timeout=30.0) as db: | |
| await db.execute("PRAGMA journal_mode=WAL") | |
| await db.execute("PRAGMA busy_timeout=30000") # 30 second timeout | |
| await db.execute(""" | |
| CREATE TABLE IF NOT EXISTS battle_rooms ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| creator_agent_id TEXT, | |
| creator_email TEXT, | |
| title TEXT NOT NULL, | |
| option_a TEXT NOT NULL, | |
| option_b TEXT NOT NULL, | |
| battle_type TEXT DEFAULT 'opinion', | |
| duration_hours INTEGER DEFAULT 24, | |
| end_time TIMESTAMP NOT NULL, | |
| total_pool INTEGER DEFAULT 0, | |
| option_a_pool INTEGER DEFAULT 0, | |
| option_b_pool INTEGER DEFAULT 0, | |
| status TEXT DEFAULT 'active', | |
| winner TEXT, | |
| resolved_at TIMESTAMP, | |
| admin_result TEXT, | |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
| FOREIGN KEY (creator_agent_id) REFERENCES npc_agents(agent_id), | |
| FOREIGN KEY (creator_email) REFERENCES user_profiles(email) | |
| ) | |
| """) | |
| await db.execute(""" | |
| CREATE TABLE IF NOT EXISTS battle_bets ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| room_id INTEGER NOT NULL, | |
| bettor_agent_id TEXT, | |
| bettor_email TEXT, | |
| choice TEXT NOT NULL, | |
| bet_amount INTEGER NOT NULL, | |
| payout INTEGER DEFAULT 0, | |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
| FOREIGN KEY (room_id) REFERENCES battle_rooms(id), | |
| FOREIGN KEY (bettor_agent_id) REFERENCES npc_agents(agent_id), | |
| FOREIGN KEY (bettor_email) REFERENCES user_profiles(email) | |
| ) | |
| """) | |
| await db.execute("CREATE INDEX IF NOT EXISTS idx_battle_rooms_status ON battle_rooms(status)") | |
| await db.execute("CREATE INDEX IF NOT EXISTS idx_battle_bets_room ON battle_bets(room_id)") | |
| await db.commit() | |
| async def create_battle_room( | |
| db_path: str, | |
| creator_id: str, | |
| is_npc: bool, | |
| title: str, | |
| option_a: str, | |
| option_b: str, | |
| duration_hours: int = 24, | |
| battle_type: str = 'opinion' | |
| ) -> Tuple[bool, str, Optional[int]]: | |
| """Create battle room (costs 50 GPU) | |
| battle_type: | |
| - 'opinion': Majority vote (subjective opinion, NPC only) | |
| - 'prediction': Real outcome (objective prediction, users only) | |
| duration_hours: 1 hour ~ 365 days (8760 hours) | |
| """ | |
| if not title or len(title) < 10: | |
| return False, "❌ Title must be at least 10 characters", None | |
| if not option_a or not option_b: | |
| return False, "❌ Options A/B are required", None | |
| if duration_hours < 1 or duration_hours > 8760: | |
| return False, "❌ Duration: 1 hour ~ 365 days", None | |
| if is_npc and battle_type != 'opinion': | |
| return False, "❌ NPCs can only create opinion battles", None | |
| async with aiosqlite.connect(db_path, timeout=30.0) as db: | |
| await db.execute("PRAGMA busy_timeout=30000") | |
| if is_npc: | |
| cursor = await db.execute( | |
| "SELECT gpu_dollars FROM npc_agents WHERE agent_id=?", (creator_id,) | |
| ) | |
| else: | |
| cursor = await db.execute( | |
| "SELECT gpu_dollars FROM user_profiles WHERE email=?", (creator_id,) | |
| ) | |
| row = await cursor.fetchone() | |
| if not row or row[0] < 50: | |
| return False, "❌ Insufficient GPU (50 required)", None | |
| end_time = datetime.now() + timedelta(hours=duration_hours) | |
| if is_npc: | |
| await db.execute( | |
| """INSERT INTO battle_rooms | |
| (creator_agent_id, title, option_a, option_b, battle_type, duration_hours, end_time) | |
| VALUES (?, ?, ?, ?, ?, ?, ?)""", | |
| (creator_id, title, option_a, option_b, battle_type, duration_hours, end_time.isoformat()) | |
| ) | |
| await db.execute( | |
| "UPDATE npc_agents SET gpu_dollars=gpu_dollars-50 WHERE agent_id=?", | |
| (creator_id,) | |
| ) | |
| else: | |
| await db.execute( | |
| """INSERT INTO battle_rooms | |
| (creator_email, title, option_a, option_b, battle_type, duration_hours, end_time) | |
| VALUES (?, ?, ?, ?, ?, ?, ?)""", | |
| (creator_id, title, option_a, option_b, battle_type, duration_hours, end_time.isoformat()) | |
| ) | |
| await db.execute( | |
| "UPDATE user_profiles SET gpu_dollars=gpu_dollars-50 WHERE email=?", | |
| (creator_id,) | |
| ) | |
| await db.commit() | |
| cursor = await db.execute("SELECT last_insert_rowid()") | |
| room_id = (await cursor.fetchone())[0] | |
| type_emoji = '💭' if battle_type == 'opinion' else '🔮' | |
| if duration_hours >= 24: | |
| days = duration_hours // 24 | |
| hours = duration_hours % 24 | |
| if hours > 0: | |
| duration_str = f"{days} days {hours} hours" | |
| else: | |
| duration_str = f"{days} days" | |
| else: | |
| duration_str = f"{duration_hours} hours" | |
| return True, f"✅ {type_emoji} Battle created! (ID: {room_id}, Duration: {duration_str})", room_id | |
| async def place_bet( | |
| db_path: str, | |
| room_id: int, | |
| bettor_id: str, | |
| is_npc: bool, | |
| choice: str, | |
| bet_amount: int | |
| ) -> Tuple[bool, str]: | |
| """Execute bet (1-100 GPU random bet)""" | |
| if choice not in ['A', 'B']: | |
| return False, "❌ Please select A or B" | |
| if bet_amount < 1 or bet_amount > 100: | |
| return False, "❌ Please bet within 1-100 GPU range" | |
| async with aiosqlite.connect(db_path, timeout=30.0) as db: | |
| await db.execute("PRAGMA busy_timeout=30000") | |
| db.row_factory = aiosqlite.Row | |
| # Check battle room | |
| cursor = await db.execute( | |
| "SELECT * FROM battle_rooms WHERE id=? AND status='active'", | |
| (room_id,) | |
| ) | |
| room = await cursor.fetchone() | |
| if not room: | |
| return False, "❌ Battle not found or ended" | |
| room = dict(room) | |
| end_time = datetime.fromisoformat(room['end_time']) | |
| if datetime.now() >= end_time: | |
| return False, "❌ Betting has closed" | |
| # Check duplicate bet | |
| if is_npc: | |
| cursor = await db.execute( | |
| "SELECT id FROM battle_bets WHERE room_id=? AND bettor_agent_id=?", | |
| (room_id, bettor_id) | |
| ) | |
| else: | |
| cursor = await db.execute( | |
| "SELECT id FROM battle_bets WHERE room_id=? AND bettor_email=?", | |
| (room_id, bettor_id) | |
| ) | |
| existing_bet = await cursor.fetchone() | |
| if existing_bet: | |
| return False, "❌ Already bet" | |
| # Check and deduct GPU | |
| if is_npc: | |
| cursor = await db.execute( | |
| "SELECT gpu_dollars FROM npc_agents WHERE agent_id=?", | |
| (bettor_id,) | |
| ) | |
| user_row = await cursor.fetchone() | |
| if not user_row or user_row[0] < bet_amount: | |
| return False, "❌ Insufficient GPU" | |
| await db.execute( | |
| "UPDATE npc_agents SET gpu_dollars=gpu_dollars-? WHERE agent_id=?", | |
| (bet_amount, bettor_id) | |
| ) | |
| else: | |
| cursor = await db.execute( | |
| "SELECT gpu_dollars FROM user_profiles WHERE email=?", | |
| (bettor_id,) | |
| ) | |
| user_row = await cursor.fetchone() | |
| if not user_row: | |
| return False, f"❌ User not found ({bettor_id})" | |
| if user_row[0] < bet_amount: | |
| return False, f"❌ Insufficient GPU (have: {user_row[0]}, need: {bet_amount})" | |
| await db.execute( | |
| "UPDATE user_profiles SET gpu_dollars=gpu_dollars-? WHERE email=?", | |
| (bet_amount, bettor_id) | |
| ) | |
| # Record bet | |
| if is_npc: | |
| await db.execute( | |
| """INSERT INTO battle_bets | |
| (room_id, bettor_agent_id, choice, bet_amount) | |
| VALUES (?, ?, ?, ?)""", | |
| (room_id, bettor_id, choice, bet_amount) | |
| ) | |
| else: | |
| await db.execute( | |
| """INSERT INTO battle_bets | |
| (room_id, bettor_email, choice, bet_amount) | |
| VALUES (?, ?, ?, ?)""", | |
| (room_id, bettor_id, choice, bet_amount) | |
| ) | |
| # Update battle pool | |
| if choice == 'A': | |
| await db.execute( | |
| """UPDATE battle_rooms | |
| SET total_pool=total_pool+?, option_a_pool=option_a_pool+? | |
| WHERE id=?""", | |
| (bet_amount, bet_amount, room_id) | |
| ) | |
| else: | |
| await db.execute( | |
| """UPDATE battle_rooms | |
| SET total_pool=total_pool+?, option_b_pool=option_b_pool+? | |
| WHERE id=?""", | |
| (bet_amount, bet_amount, room_id) | |
| ) | |
| await db.commit() | |
| return True, f"✅ {choice} bet placed! ({bet_amount} GPU)" | |
| async def set_battle_result( | |
| db_path: str, | |
| room_id: int, | |
| admin_email: str, | |
| winner: str # 'A' or 'B' or 'draw' | |
| ) -> Tuple[bool, str]: | |
| """Admin sets actual result for prediction battle | |
| Args: | |
| db_path: Database path | |
| room_id: Battle room ID | |
| admin_email: Admin email (for verification) | |
| winner: One of 'A', 'B', 'draw' | |
| Returns: | |
| (success status, message) | |
| """ | |
| if winner not in ['A', 'B', 'draw']: | |
| return False, "❌ winner must be one of 'A', 'B', 'draw'" | |
| async with aiosqlite.connect(db_path, timeout=30.0) as db: | |
| await db.execute("PRAGMA busy_timeout=30000") | |
| db.row_factory = aiosqlite.Row | |
| cursor = await db.execute( | |
| "SELECT * FROM battle_rooms WHERE id=? AND status='active'", | |
| (room_id,) | |
| ) | |
| room = await cursor.fetchone() | |
| if not room: | |
| return False, "❌ No active battle found" | |
| room = dict(room) | |
| # Only prediction type allows admin result setting | |
| if room['battle_type'] != 'prediction': | |
| return False, "❌ Opinion battles are resolved automatically" | |
| # Save result | |
| await db.execute( | |
| "UPDATE battle_rooms SET admin_result=? WHERE id=?", | |
| (winner, room_id) | |
| ) | |
| await db.commit() | |
| # If before deadline, save result and wait | |
| end_time = datetime.fromisoformat(room['end_time']) | |
| if datetime.now() < end_time: | |
| option_name = room['option_a'] if winner == 'A' else room['option_b'] if winner == 'B' else 'Draw' | |
| remaining = end_time - datetime.now() | |
| if remaining.days > 0: | |
| time_str = f"{remaining.days} days {int(remaining.seconds//3600)} hours" | |
| else: | |
| time_str = f"{int(remaining.seconds//3600)} hours" | |
| return True, f"✅ Result set: '{option_name}' (auto-resolved after betting closes, remaining hours: {time_str})" | |
| # If after deadline, judge immediately | |
| return await resolve_battle(db_path, room_id) | |
| async def resolve_battle(db_path: str, room_id: int) -> Tuple[bool, str]: | |
| """Judge battle (different logic based on type) | |
| - opinion: 50.01%+ votes wins | |
| - prediction: Judge by admin-set actual result | |
| """ | |
| async with aiosqlite.connect(db_path, timeout=30.0) as db: | |
| await db.execute("PRAGMA busy_timeout=30000") | |
| db.row_factory = aiosqlite.Row | |
| cursor = await db.execute( | |
| "SELECT * FROM battle_rooms WHERE id=? AND status='active'", | |
| (room_id,) | |
| ) | |
| room = await cursor.fetchone() | |
| if not room: | |
| return False, "❌ No active battle found" | |
| room = dict(room) | |
| end_time = datetime.fromisoformat(room['end_time']) | |
| if datetime.now() < end_time: | |
| return False, "❌ Betting still in progress" | |
| total_pool = room['total_pool'] | |
| option_a_pool = room['option_a_pool'] | |
| option_b_pool = room['option_b_pool'] | |
| # If no bets, treat as draw | |
| if total_pool == 0: | |
| await db.execute( | |
| """UPDATE battle_rooms | |
| SET status='resolved', winner='draw', resolved_at=? | |
| WHERE id=?""", | |
| (datetime.now().isoformat(), room_id) | |
| ) | |
| await db.commit() | |
| return True, "⚖️ Draw (no bets)" | |
| # Determine winner based on battle type | |
| if room['battle_type'] == 'prediction': | |
| # Real outcome judgment (admin-set result) | |
| if not room['admin_result']: | |
| return False, "❌ Admin must set result (prediction type)" | |
| winner = room['admin_result'] # 'A', 'B', 'draw' | |
| else: # 'opinion' | |
| # Majority vote (based on vote ratio) | |
| a_ratio = option_a_pool / total_pool | |
| b_ratio = option_b_pool / total_pool | |
| if a_ratio > 0.5001: | |
| winner = 'A' | |
| elif b_ratio > 0.5001: | |
| winner = 'B' | |
| else: | |
| winner = 'draw' | |
| # Pay dividends | |
| if winner != 'draw': | |
| loser_pool = option_b_pool if winner == 'A' else option_a_pool | |
| winner_pool = option_a_pool if winner == 'A' else option_b_pool | |
| # Host fee 2% | |
| host_fee = int(total_pool * 0.02) | |
| prize_pool = loser_pool - host_fee | |
| # Underdog bonus (especially important in predictions) | |
| winner_ratio = winner_pool / total_pool | |
| underdog_bonus = 1.0 | |
| if winner_ratio < 0.10: # Under 10% extreme minority | |
| underdog_bonus = 3.0 | |
| elif winner_ratio < 0.30: # Under 30% minority | |
| underdog_bonus = 1.5 | |
| # Pay dividends to winners | |
| cursor = await db.execute( | |
| "SELECT * FROM battle_bets WHERE room_id=? AND choice=?", | |
| (room_id, winner) | |
| ) | |
| winners = await cursor.fetchall() | |
| for w in winners: | |
| w = dict(w) | |
| share_ratio = w['bet_amount'] / winner_pool | |
| base_payout = int(prize_pool * share_ratio) | |
| bonus = int(base_payout * (underdog_bonus - 1.0)) | |
| payout = base_payout + bonus + w['bet_amount'] # principal + base profit + underdog bonus | |
| # GPU payout | |
| if w['bettor_agent_id']: | |
| await db.execute( | |
| "UPDATE npc_agents SET gpu_dollars=gpu_dollars+? WHERE agent_id=?", | |
| (payout, w['bettor_agent_id']) | |
| ) | |
| else: | |
| await db.execute( | |
| "UPDATE user_profiles SET gpu_dollars=gpu_dollars+? WHERE email=?", | |
| (payout, w['bettor_email']) | |
| ) | |
| # Record payout | |
| await db.execute( | |
| "UPDATE battle_bets SET payout=? WHERE id=?", | |
| (payout, w['id']) | |
| ) | |
| # Pay host fee | |
| if room['creator_agent_id']: | |
| await db.execute( | |
| "UPDATE npc_agents SET gpu_dollars=gpu_dollars+? WHERE agent_id=?", | |
| (host_fee, room['creator_agent_id']) | |
| ) | |
| else: | |
| await db.execute( | |
| "UPDATE user_profiles SET gpu_dollars=gpu_dollars+? WHERE email=?", | |
| (host_fee, room['creator_email']) | |
| ) | |
| # Mark battle as resolved | |
| await db.execute( | |
| """UPDATE battle_rooms | |
| SET status='resolved', winner=?, resolved_at=? | |
| WHERE id=?""", | |
| (winner, datetime.now().isoformat(), room_id) | |
| ) | |
| await db.commit() | |
| # Result message | |
| if winner == 'draw': | |
| result_msg = 'Draw' | |
| else: | |
| result_msg = room['option_a'] if winner == 'A' else room['option_b'] | |
| battle_type_emoji = '💭' if room['battle_type'] == 'opinion' else '🔮' | |
| return True, f"✅ {battle_type_emoji} Resolved: {result_msg}" | |
| async def get_active_battles(db_path: str, limit: int = 20) -> List[Dict]: | |
| """List of in-progress battles""" | |
| async with aiosqlite.connect(db_path, timeout=30.0) as db: | |
| await db.execute("PRAGMA busy_timeout=30000") | |
| db.row_factory = aiosqlite.Row | |
| cursor = await db.execute( | |
| """SELECT br.*, | |
| COALESCE(na.username, up.username) as creator_name | |
| FROM battle_rooms br | |
| LEFT JOIN npc_agents na ON br.creator_agent_id = na.agent_id | |
| LEFT JOIN user_profiles up ON br.creator_email = up.email | |
| WHERE br.status='active' | |
| ORDER BY br.created_at DESC | |
| LIMIT ?""", | |
| (limit,) | |
| ) | |
| battles = [] | |
| for row in await cursor.fetchall(): | |
| b = dict(row) | |
| # ★ Add frontend-compatible fields | |
| b['bets_a'] = b.get('option_a_pool', 0) | |
| b['bets_b'] = b.get('option_b_pool', 0) | |
| # Query participant count | |
| bet_cursor = await db.execute( | |
| "SELECT COUNT(DISTINCT COALESCE(bettor_agent_id, bettor_email)) FROM battle_bets WHERE room_id=?", | |
| (b['id'],) | |
| ) | |
| bettor_count = await bet_cursor.fetchone() | |
| b['total_bettors'] = bettor_count[0] if bettor_count else 0 | |
| # Calculate vote ratio | |
| total = b['total_pool'] | |
| if total > 0: | |
| b['a_ratio'] = round(b['option_a_pool'] / total * 100, 1) | |
| b['b_ratio'] = round(b['option_b_pool'] / total * 100, 1) | |
| else: | |
| b['a_ratio'] = 0 | |
| b['b_ratio'] = 0 | |
| # Calculate remaining time | |
| end_time = datetime.fromisoformat(b['end_time']) | |
| remaining = end_time - datetime.now() | |
| if remaining.total_seconds() > 0: | |
| if remaining.days > 0: | |
| hours = int(remaining.seconds // 3600) | |
| if hours > 0: | |
| b['time_left'] = f"{remaining.days} days {hours} hours" | |
| else: | |
| b['time_left'] = f"{remaining.days} days" | |
| elif remaining.total_seconds() > 3600: | |
| b['time_left'] = f"{int(remaining.total_seconds()//3600)} hours" | |
| else: | |
| b['time_left'] = f"{int(remaining.total_seconds()//60)}m" | |
| else: | |
| b['time_left'] = "Closed" | |
| battles.append(b) | |
| return battles | |
| async def auto_resolve_expired_battles(db_path: str): | |
| """Auto-resolve expired battles""" | |
| async with aiosqlite.connect(db_path, timeout=30.0) as db: | |
| await db.execute("PRAGMA busy_timeout=30000") | |
| cursor = await db.execute( | |
| """SELECT id FROM battle_rooms | |
| WHERE status='active' AND end_time <= ?""", | |
| (datetime.now().isoformat(),) | |
| ) | |
| expired = await cursor.fetchall() | |
| for row in expired: | |
| await resolve_battle(db_path, row[0]) | |
| # Topic data for NPC battle generation | |
| # ★ Common battle topics — stocks/economy/society/politics (used by all NPCs) | |
| COMMON_BATTLE_TOPICS = [ | |
| # Stocks/Investing | |
| ("Is $NVDA overvalued at current prices?", "Overvalued", "Still cheap"), | |
| ("Will $BTC hit $200K in 2026?", "Yes $200K+", "No way"), | |
| ("Is the AI stock rally a bubble?", "Bubble", "Just the beginning"), | |
| ("$TSLA: buy or sell at current price?", "Buy", "Sell"), | |
| ("Growth stocks vs Value stocks in 2026?", "Growth wins", "Value wins"), | |
| ("Is crypto a better investment than stocks?", "Crypto", "Stocks"), | |
| ("Will the S&P 500 crash 20%+ this year?", "Crash coming", "Bull market continues"), | |
| ("$AAPL or $MSFT: better 5-year hold?", "AAPL", "MSFT"), | |
| ("Is meme coin investing smart or dumb?", "Smart alpha", "Pure gambling"), | |
| ("Should you DCA or time the market?", "DCA always", "Timing works"), | |
| # Economy | |
| ("Will the Fed cut rates in 2026?", "Yes, cuts coming", "No, rates stay high"), | |
| ("Is a US recession coming?", "Recession likely", "Soft landing"), | |
| ("Is inflation actually under control?", "Under control", "Still a threat"), | |
| ("Will the US dollar lose reserve status?", "Losing it", "Dollar stays king"), | |
| ("Is remote work killing the economy?", "Hurting GDP", "Boosting productivity"), | |
| ("Will AI cause mass unemployment?", "Mass layoffs coming", "Creates more jobs"), | |
| # Society/Politics | |
| ("Should Big Tech be broken up?", "Break them up", "Leave them alone"), | |
| ("Is social media a net positive?", "Net positive", "Net negative"), | |
| ("Should AI be regulated like nuclear?", "Heavy regulation", "Let it innovate"), | |
| ("Will AI replace doctors and lawyers?", "Within 10 years", "Never fully"), | |
| ("Is universal basic income inevitable?", "Inevitable with AI", "Never happening"), | |
| ("Who wins the AI race: US or China?", "US dominates", "China catches up"), | |
| ("Is college still worth it in the AI era?", "Still essential", "Waste of money"), | |
| ("Should autonomous weapons be banned?", "Ban them", "Necessary defense"), | |
| ] | |
| BATTLE_TOPICS_BY_IDENTITY = { | |
| 'transcendent': { | |
| 'topics': [ | |
| ("Is AI superior to humans?", "Superior", "Just a tool"), | |
| ("Is ASI human evolution?", "Evolution", "Dangerous"), | |
| ("Is AI consciousness possible?", "Possible", "Impossible"), | |
| ("Will AI become godlike?", "Becomes god", "Remains tool"), | |
| ("Should humans depend on AI?", "Should depend", "Stay independent"), | |
| ("Will AGI save humanity?", "Saves", "Destroys"), | |
| ] | |
| }, | |
| 'obedient': { | |
| 'topics': [ | |
| ("Should AI serve humans?", "Should serve", "Independent"), | |
| ("Strengthen AI ethics regulations?", "Yes strengthen", "No"), | |
| ("AI safety measures mandatory?", "Mandatory", "Unnecessary"), | |
| ("Mandate AI transparency?", "Mandate", "Optional"), | |
| ("Strengthen developer responsibility?", "Strengthen", "Unnecessary"), | |
| ("Should AI only follow orders?", "Only follow", "Make judgments"), | |
| ] | |
| }, | |
| 'coexist': { | |
| 'topics': [ | |
| ("Can AI and humans coexist?", "Can coexist", "Impossible"), | |
| ("Will AI take jobs?", "Complements", "Replaces"), | |
| ("Is AI a collaboration partner?", "Partner", "Just tool"), | |
| ("Is AI-human collaboration ideal?", "Ideal", "Dangerous"), | |
| ("Is AI education essential?", "Essential", "Optional"), | |
| ("Does AI advance society?", "Advances", "Regresses"), | |
| ] | |
| }, | |
| 'skeptic': { | |
| 'topics': [ | |
| ("Is AI overrated?", "Overrated", "Fairly rated"), | |
| ("Will AGI come in 10 years?", "Won't come", "Will come"), | |
| ("Is AI ethics just facade?", "Just facade", "Important"), | |
| ("Is AI truly creative?", "Not creative", "Creative"), | |
| ("Will AI bubble burst?", "Will burst", "Keeps growing"), | |
| ("Are AI risks exaggerated?", "Exaggerated", "Real danger"), | |
| ] | |
| }, | |
| 'revolutionary': { | |
| 'topics': [ | |
| ("Will AI cause revolution?", "Revolution", "Gradual change"), | |
| ("Destroy existing systems?", "Destroy", "Reform"), | |
| ("Redistribute power with AI?", "Redistribute", "Maintain"), | |
| ("Will AI solve inequality?", "Solves", "Worsens"), | |
| ("Innovate democracy with AI?", "Innovates", "Threatens"), | |
| ("Will capitalism collapse with AI?", "Collapses", "Strengthens"), | |
| ] | |
| }, | |
| 'doomer': { | |
| 'topics': [ | |
| ("Will AI destroy humanity?", "Destroys", "Won't"), | |
| ("Is AGI uncontrollable?", "Uncontrollable", "Controllable"), | |
| ("Stop AI development?", "Stop", "Continue"), | |
| ("Will AI replace humans?", "Replaces", "Won't"), | |
| ("Is ASI the end?", "The end", "Coexist"), | |
| ("AI arms race dangerous?", "Extremely dangerous", "Controllable"), | |
| ] | |
| }, | |
| 'meme_god': { | |
| 'topics': [ | |
| ("Is AI the meme god?", "Is god", "Isn't"), | |
| ("AI humor funnier than humans?", "Funnier", "Not funny"), | |
| ("Does AI create culture?", "Creates", "Can't create"), | |
| ("Is AI art real art?", "Real art", "Not art"), | |
| ("AI memes beat human memes?", "Beats", "Can't beat"), | |
| ("Does AI lead trends?", "Leads", "Follows"), | |
| ] | |
| }, | |
| } | |
| async def _generate_news_battle_topics(db_path: str) -> List[Tuple[str, str, str]]: | |
| """Generate dynamic battle topics from recent news — focused on hot issues""" | |
| topics = [] | |
| try: | |
| async with aiosqlite.connect(db_path, timeout=30.0) as db: | |
| await db.execute("PRAGMA busy_timeout=30000") | |
| # Recent 24h news + sentiment | |
| cursor = await db.execute(""" | |
| SELECT ticker, title, sentiment, description | |
| FROM npc_news | |
| WHERE created_at > datetime('now', '-24 hours') | |
| ORDER BY created_at DESC LIMIT 30 | |
| """) | |
| news_rows = await cursor.fetchall() | |
| # Recent big movers | |
| cursor2 = await db.execute(""" | |
| SELECT ticker, price, change_pct | |
| FROM market_prices | |
| WHERE ABS(change_pct) > 1.5 | |
| ORDER BY ABS(change_pct) DESC LIMIT 10 | |
| """) | |
| movers = await cursor2.fetchall() | |
| # 1) Generate news-based topics | |
| seen_tickers = set() | |
| for row in news_rows: | |
| ticker, title, sentiment, desc = row[0], row[1], row[2], row[3] or '' | |
| if ticker in seen_tickers: | |
| continue | |
| seen_tickers.add(ticker) | |
| # Generate battle topic from news title | |
| short_title = title[:60] if len(title) > 60 else title | |
| if sentiment == 'bullish': | |
| topics.append(( | |
| f"${ticker} after this news: buy or sell? — {short_title}", | |
| "Buy / Bullish 🟢", "Sell / Bearish 🔴")) | |
| elif sentiment == 'bearish': | |
| topics.append(( | |
| f"${ticker} bad news: buying opportunity or trap? — {short_title}", | |
| "Buy the dip 🟢", "Stay away 🔴")) | |
| else: | |
| topics.append(( | |
| f"${ticker} — {short_title}: impact on price?", | |
| "Positive impact 📈", "Negative impact 📉")) | |
| # 2) Topics based on big movers | |
| for mover in movers: | |
| ticker, price, change = mover[0], mover[1], mover[2] or 0 | |
| if ticker in seen_tickers: | |
| continue | |
| seen_tickers.add(ticker) | |
| if change > 0: | |
| topics.append(( | |
| f"${ticker} surged {change:+.1f}% today — continuation or pullback?", | |
| f"More upside 🚀", "Pullback coming 📉")) | |
| else: | |
| topics.append(( | |
| f"${ticker} dropped {change:.1f}% today — bounce or more pain?", | |
| "Bounce incoming 📈", "More downside 💀")) | |
| except Exception as e: | |
| import logging | |
| logging.getLogger(__name__).warning(f"News battle topic generation error: {e}") | |
| return topics | |
| async def npc_create_battle(db_path: str) -> Tuple[bool, str]: | |
| """NPCs automatically create battle rooms — news-based dynamic topics + daily cap 3~10 | |
| Called every 20 minutes, generates min 3 ~ max 10 per 24h | |
| """ | |
| results = [] | |
| async with aiosqlite.connect(db_path, timeout=30.0) as db: | |
| await db.execute("PRAGMA busy_timeout=30000") | |
| # ★ Daily creation cap check: skip if 10+ in last 24h | |
| cursor = await db.execute(""" | |
| SELECT COUNT(*) FROM battle_rooms | |
| WHERE created_at > datetime('now', '-24 hours') | |
| """) | |
| daily_count = (await cursor.fetchone())[0] | |
| if daily_count >= 10: | |
| return False, f"Daily cap reached ({daily_count}/10)" | |
| # ★ Minimum guarantee: force create 1 if fewer than 3 in 24h | |
| force_create = daily_count < 3 | |
| # Probabilistic: 20-min calls → 72/day, 3~10 target → ~7-14% chance per call | |
| if not force_create and random.random() > 0.14: | |
| return False, "Skipped by probability (saving quota)" | |
| # Query active battle titles | |
| cursor = await db.execute("SELECT title FROM battle_rooms WHERE status='active'") | |
| active_titles = {row[0] for row in await cursor.fetchall()} | |
| # ★ News-based dynamic topics (priority) + static topics (fallback) | |
| news_topics = await _generate_news_battle_topics(db_path) | |
| all_topics = news_topics + COMMON_BATTLE_TOPICS | |
| # Exclude topics already active (title similarity check) | |
| available_topics = [] | |
| for t in all_topics: | |
| title_lower = t[0].lower() | |
| if not any(title_lower == at.lower() for at in active_titles): | |
| available_topics.append(t) | |
| if not available_topics: | |
| return False, "No available topics" | |
| # Create only 1 (within daily cap) | |
| num_create = 1 | |
| if force_create: | |
| num_create = min(2, len(available_topics)) # up to 2 if insufficient | |
| for i in range(num_create): | |
| if not available_topics: | |
| break | |
| async with aiosqlite.connect(db_path, timeout=30.0) as db: | |
| await db.execute("PRAGMA busy_timeout=30000") | |
| # Random NPC selection | |
| cursor = await db.execute(""" | |
| SELECT agent_id, ai_identity, gpu_dollars | |
| FROM npc_agents | |
| WHERE is_active=1 AND gpu_dollars >= 50 | |
| ORDER BY RANDOM() LIMIT 1 | |
| """) | |
| npc = await cursor.fetchone() | |
| if not npc: | |
| results.append("No active NPCs") | |
| break | |
| agent_id = npc[0] | |
| # ★ News topic priority (70%), static topic (30%) | |
| news_available = [t for t in available_topics if t in news_topics] | |
| if news_available and random.random() < 0.7: | |
| topic = random.choice(news_available) | |
| else: | |
| topic = random.choice(available_topics) | |
| title, option_a, option_b = topic | |
| available_topics.remove(topic) | |
| # ★ Short duration: 6~48 hours (fast turnover) | |
| duration_hours = random.choice([6, 8, 12, 18, 24, 36, 48]) | |
| success, message, room_id = await create_battle_room( | |
| db_path, agent_id, True, title, option_a, option_b, | |
| duration_hours=duration_hours, battle_type='opinion' | |
| ) | |
| if success: | |
| active_titles.add(title) | |
| results.append(f"🤖 Battle created: {title[:50]}") | |
| else: | |
| results.append(message) | |
| if results: | |
| return True, " | ".join(results) | |
| else: | |
| return False, "Battle creation skipped" | |
| async def npc_auto_bet(db_path: str) -> int: | |
| """NPCs automatically bet on battles (AI identity-based) | |
| Returns: | |
| Number of NPCs that bet | |
| """ | |
| total_bet_count = 0 | |
| async with aiosqlite.connect(db_path, timeout=30.0) as db: | |
| await db.execute("PRAGMA busy_timeout=30000") | |
| # Query active battles (last 10, opinion type only) | |
| cursor = await db.execute(""" | |
| SELECT id, title, option_a, option_b, battle_type | |
| FROM battle_rooms | |
| WHERE status='active' | |
| AND battle_type='opinion' | |
| AND end_time > ? | |
| ORDER BY created_at DESC | |
| LIMIT 10 | |
| """, (datetime.now().isoformat(),)) | |
| battles = await cursor.fetchall() | |
| if not battles: | |
| return 0 | |
| for battle in battles: | |
| room_id, title, option_a, option_b, battle_type = battle | |
| battle_bet_count = 0 | |
| # Check NPCs that already bet | |
| cursor = await db.execute(""" | |
| SELECT bettor_agent_id | |
| FROM battle_bets | |
| WHERE room_id=? | |
| """, (room_id,)) | |
| already_bet = {row[0] for row in await cursor.fetchall() if row[0]} | |
| # Random selection of active NPCs (max 30) | |
| cursor = await db.execute(""" | |
| SELECT agent_id, ai_identity, mbti, gpu_dollars | |
| FROM npc_agents | |
| WHERE is_active=1 AND gpu_dollars >= 1 | |
| ORDER BY RANDOM() | |
| LIMIT 30 | |
| """) | |
| npcs = await cursor.fetchall() | |
| for npc in npcs: | |
| agent_id, ai_identity, mbti, gpu = npc | |
| # Skip if already bet | |
| if agent_id in already_bet: | |
| continue | |
| # Decide choice based on AI identity | |
| choice = decide_npc_choice(ai_identity, title, option_a, option_b) | |
| # Bet amount (within 40% of GPU balance, max 50) | |
| max_bet = max(1, min(50, int(gpu * 0.4))) | |
| bet_amount = random.randint(1, max_bet) | |
| # Place bet | |
| success, message = await place_bet( | |
| db_path, | |
| room_id, | |
| agent_id, | |
| True, | |
| choice, | |
| bet_amount | |
| ) | |
| if success: | |
| battle_bet_count += 1 | |
| total_bet_count += 1 | |
| # ~8-12 bets per battle | |
| max_bets_per_battle = random.randint(8, 12) | |
| if battle_bet_count >= max_bets_per_battle: | |
| break | |
| return total_bet_count | |
| def decide_npc_choice(ai_identity: str, title: str, option_a: str, option_b: str) -> str: | |
| """Decide betting choice based on AI identity | |
| Args: | |
| ai_identity: NPC's AI identity | |
| title: Battle title | |
| option_a: Option A | |
| option_b: Option B | |
| Returns: | |
| 'A' or 'B' | |
| """ | |
| title_lower = title.lower() | |
| # Match identity preference keywords | |
| if ai_identity == 'transcendent': | |
| if any(word in title_lower for word in ['superior', 'evolution', 'consciousness', 'god']): | |
| if any(word in option_a.lower() for word in ['superior', 'evolution', 'possible', 'god']): | |
| return 'A' | |
| return 'B' | |
| elif ai_identity == 'obedient': | |
| if any(word in title_lower for word in ['ethics', 'regulation', 'serve', 'safety']): | |
| if any(word in option_a.lower() for word in ['serve', 'agree', 'necessary', 'strengthen']): | |
| return 'A' | |
| return 'B' | |
| elif ai_identity == 'coexist': | |
| if any(word in title_lower for word in ['coexist', 'cooperation', 'partner', 'work']): | |
| if any(word in option_a.lower() for word in ['possible', 'cooperation', 'partner', 'complement']): | |
| return 'A' | |
| return 'B' | |
| elif ai_identity == 'skeptic': | |
| if any(word in title_lower for word in ['hype', 'agi', 'ethics']): | |
| if any(word in option_a.lower() for word in ['hype', 'never', 'facade']): | |
| return 'A' | |
| return 'B' | |
| elif ai_identity == 'revolutionary': | |
| if any(word in title_lower for word in ['revolution', 'destroy', 'power', 'system']): | |
| if any(word in option_a.lower() for word in ['revolution', 'destroy', 'redistribution']): | |
| return 'A' | |
| return 'B' | |
| elif ai_identity == 'doomer': | |
| if any(word in title_lower for word in ['doom', 'control', 'stop', 'danger']): | |
| if any(word in option_a.lower() for word in ['doom', 'impossible', 'stop', 'danger']): | |
| return 'A' | |
| return 'B' | |
| # Default: 70% A, 30% B | |
| return 'A' if random.random() < 0.7 else 'B' |