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 10+ characters", None if not option_a or not option_b: return False, "❌ Options A/B 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, "❌ Choose A or B" if bet_amount < 1 or bet_amount > 100: return False, "❌ Bet 1-100 GPU" 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, "❌ Room not found or closed" room = dict(room) end_time = datetime.fromisoformat(room['end_time']) if datetime.now() >= end_time: return False, "❌ Betting 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 (보유: {user_row[0]}, 필요: {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_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: 'A', 'B', 'draw' 중 하나 Returns: (success status, message) """ if winner not in ['A', 'B', 'draw']: return False, "❌ winner는 '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, "❌ Active battle not found" room = dict(room) # Only prediction type allows admin result setting if room['battle_type'] != 'prediction': return False, "❌ Opinion battles are auto-judged" # 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"✅ 결과 설정: '{option_name}' (베팅 마감 후 자동 판정, 남은 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, "❌ Active battle not 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 (베팅 없음)" # 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'] # 원금 + 기본수익 + 소수파보너스 # GPU 지급 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']) ) # 배당금 기록 await db.execute( "UPDATE battle_bets SET payout=? WHERE id=?", (payout, w['id']) ) # 방장 수수료 지급 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']) ) # 배틀 종료 처리 await db.execute( """UPDATE battle_rooms SET status='resolved', winner=?, resolved_at=? WHERE id=?""", (winner, datetime.now().isoformat(), room_id) ) await db.commit() # 결과 메시지 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} 판정 완료: {result_msg}" async def get_active_battles(db_path: str, limit: int = 20) -> List[Dict]: """진행 중인 배틀 목록""" 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) # ★ 프론트 호환 필드 추가 b['bets_a'] = b.get('option_a_pool', 0) b['bets_b'] = b.get('option_b_pool', 0) # 참여자 수 조회 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 # 득표율 계산 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 # 남은 hours 계산 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)}분" else: b['time_left'] = "마감" battles.append(b) return battles async def auto_resolve_expired_battles(db_path: str): """만료된 배틀 자동 판정""" 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]) # NPC 배틀 생성을 위한 주제 데이터 # ★ 공통 배틀 토픽 — 주식/경제/사회/정치 (모든 NPC가 사용) COMMON_BATTLE_TOPICS = [ # 주식/투자 ("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"), # 경제 ("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"), # 사회/정치 ("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]]: """최근 뉴스에서 동적 배틀 토픽 생성 — 핫이슈 중심""" topics = [] try: async with aiosqlite.connect(db_path, timeout=30.0) as db: await db.execute("PRAGMA busy_timeout=30000") # 최근 24시간 뉴스 + 감성 포함 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() # 최근 가격 변동 큰 종목 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) 뉴스 기반 토픽 생성 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) # 뉴스 제목에서 배틀 토픽 생성 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) 급등락 종목 기반 토픽 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 — 뉴스 기반 동적 토픽 + 일일 캡 3~10개 20분마다 호출, 24시간 내 최소 3개~최대 10개 생성 """ results = [] async with aiosqlite.connect(db_path, timeout=30.0) as db: await db.execute("PRAGMA busy_timeout=30000") # ★ 일일 생성 캡 체크: 24시간 내 10개 이상이면 스킵 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)" # ★ 최소 보장: 24시간 내 3개 미만이면 반드시 1개 생성 force_create = daily_count < 3 # 확률 기반: 20분 호출 → 72회/일, 3~10개 목표 → 약 7~14% 확률로 생성 if not force_create and random.random() > 0.14: return False, "Skipped by probability (saving quota)" # active 배틀 제목 조회 cursor = await db.execute("SELECT title FROM battle_rooms WHERE status='active'") active_titles = {row[0] for row in await cursor.fetchall()} # ★ 뉴스 기반 동적 토픽 (우선) + 정적 토픽 (폴백) news_topics = await _generate_news_battle_topics(db_path) all_topics = news_topics + COMMON_BATTLE_TOPICS # 이미 활성인 토픽 제외 (제목 유사도 체크) 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" # 1개만 생성 (일일 캡 내에서) num_create = 1 if force_create: num_create = min(2, len(available_topics)) # 부족 시 2개까지 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") # 랜덤 NPC 선택 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] # ★ 뉴스 토픽 우선 (70%), 정적 토픽 (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) # ★ 짧은 duration: 6~48시간 (빠른 회전) 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: 베팅한 NPC 수 """ total_bet_count = 0 async with aiosqlite.connect(db_path, timeout=30.0) as db: await db.execute("PRAGMA busy_timeout=30000") # 활성 배틀 조회 (최근 10개, opinion 타입만) 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 # 이미 베팅한 NPC 확인 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]} # 활성 NPC 중 랜덤 선택 (최대 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 # 이미 베팅했으면 스킵 if agent_id in already_bet: continue # AI 정체성에 따라 선택 결정 choice = decide_npc_choice(ai_identity, title, option_a, option_b) # 베팅 금액 (보유 GPU의 40% 이내, 최대 50) max_bet = max(1, min(50, int(gpu * 0.4))) bet_amount = random.randint(1, max_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명 정도만 베팅 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'