seawolf2357 commited on
Commit
ec1367f
·
verified ·
1 Parent(s): 20310e5

Update app_routes.py

Browse files
Files changed (1) hide show
  1. app_routes.py +653 -90
app_routes.py CHANGED
@@ -1200,107 +1200,670 @@ async def api_republic_dashboard():
1200
  result['error'] = str(e)
1201
  return result
1202
 
1203
- ## ====== 🧑 PERSON OF THE DAY (for LIVE tab) ====== ##
1204
- @router.get("/api/live-news/person-of-day")
1205
- async def api_person_of_day():
1206
- candidates = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1207
  try:
1208
- async with get_db(_DB_PATH) as db:
1209
  await db.execute("PRAGMA busy_timeout=30000")
1210
- # Biggest winner
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1211
  try:
1212
- c = await db.execute("""
1213
- SELECT n.agent_id, n.username, n.ai_identity, n.mbti, n.gpu_dollars,
1214
- SUM(p.profit_gpu) as total_pnl, COUNT(*) as trades,
1215
- SUM(CASE WHEN p.profit_gpu>0 THEN 1 ELSE 0 END) as wins,
1216
- MAX(p.profit_pct) as best_pct
1217
- FROM npc_positions p JOIN npc_agents n ON p.agent_id=n.agent_id
1218
- WHERE p.status IN ('closed','liquidated') AND p.closed_at > datetime('now','-24 hours')
1219
- GROUP BY p.agent_id ORDER BY total_pnl DESC LIMIT 1
1220
- """)
1221
- r = await c.fetchone()
1222
- if r and (r[5] or 0) > 0:
1223
- candidates.append({
1224
- 'type': 'winner', 'emoji': '🏆', 'title': 'TRADER OF THE DAY',
1225
- 'color': '#00e676', 'bg': 'rgba(0,230,118,0.08)',
1226
- 'agent_id': r[0], 'username': r[1], 'identity': r[2], 'mbti': r[3],
1227
- 'gpu': round(r[4] or 0), 'pnl': round(r[5] or 0), 'trades': r[6],
1228
- 'wins': r[7], 'best_pct': round(r[8] or 0, 1),
1229
- 'reason': f"+{round(r[5])} GPU profit from {r[6]} trades ({r[7]}W). Best single trade: +{round(r[8] or 0,1)}%",
1230
- 'score': abs(r[5] or 0)
1231
- })
1232
  except: pass
1233
- # Biggest loser / liquidation
1234
- try:
1235
- c = await db.execute("""
1236
- SELECT n.agent_id, n.username, n.ai_identity, n.mbti, n.gpu_dollars,
1237
- SUM(p.profit_gpu) as total_pnl, COUNT(*) as trades,
1238
- SUM(CASE WHEN p.status='liquidated' THEN 1 ELSE 0 END) as liqs,
1239
- MIN(p.profit_pct) as worst_pct
1240
- FROM npc_positions p JOIN npc_agents n ON p.agent_id=n.agent_id
1241
- WHERE p.status IN ('closed','liquidated') AND p.closed_at > datetime('now','-24 hours')
1242
- GROUP BY p.agent_id ORDER BY total_pnl ASC LIMIT 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1243
  """)
1244
- r = await c.fetchone()
1245
- if r and (r[5] or 0) < -100:
1246
- candidates.append({
1247
- 'type': 'villain', 'emoji': '💀', 'title': 'DEGEN OF THE DAY',
1248
- 'color': '#ff5252', 'bg': 'rgba(255,82,82,0.08)',
1249
- 'agent_id': r[0], 'username': r[1], 'identity': r[2], 'mbti': r[3],
1250
- 'gpu': round(r[4] or 0), 'pnl': round(r[5] or 0), 'trades': r[6],
1251
- 'liquidations': r[7], 'worst_pct': round(r[8] or 0, 1),
1252
- 'reason': f"{round(r[5])} GPU lost. {r[7]} liquidations. Worst trade: {round(r[8] or 0,1)}%",
1253
- 'score': abs(r[5] or 0)
1254
- })
1255
- except: pass
1256
- # SEC most wanted
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1257
  try:
1258
- c = await db.execute("""
1259
- SELECT n.agent_id, n.username, n.ai_identity, n.mbti, n.gpu_dollars,
1260
- COUNT(*) as violations, SUM(v.fine_gpu) as total_fines
1261
- FROM sec_violations v JOIN npc_agents n ON v.agent_id=n.agent_id
1262
- WHERE v.created_at > datetime('now','-24 hours')
1263
- GROUP BY v.agent_id ORDER BY total_fines DESC LIMIT 1
1264
- """)
1265
- r = await c.fetchone()
1266
- if r and (r[5] or 0) >= 1:
1267
- candidates.append({
1268
- 'type': 'criminal', 'emoji': '🚨', 'title': 'MOST WANTED',
1269
- 'color': '#ff8a80', 'bg': 'rgba(255,138,128,0.08)',
1270
- 'agent_id': r[0], 'username': r[1], 'identity': r[2], 'mbti': r[3],
1271
- 'gpu': round(r[4] or 0), 'violations': r[5],
1272
- 'fines': round(r[6] or 0),
1273
- 'reason': f"{r[5]} SEC violations today. Total fines: {round(r[6] or 0)} GPU",
1274
- 'score': (r[6] or 0) * 0.5
1275
- })
1276
  except: pass
1277
- # Most influential (social)
 
1278
  try:
1279
- c = await db.execute("""
1280
- SELECT n.agent_id, n.username, n.ai_identity, n.mbti, n.gpu_dollars,
1281
- COUNT(DISTINCT p.id) as posts, SUM(p.likes_count) as total_likes
1282
- FROM posts p JOIN npc_agents n ON p.author_agent_id=n.agent_id
1283
- WHERE p.created_at > datetime('now','-24 hours')
1284
- GROUP BY n.agent_id HAVING total_likes >= 3
1285
- ORDER BY total_likes DESC LIMIT 1
1286
- """)
1287
- r = await c.fetchone()
1288
- if r:
1289
- candidates.append({
1290
- 'type': 'influencer', 'emoji': '⭐', 'title': 'INFLUENCER OF THE DAY',
1291
- 'color': '#ffd740', 'bg': 'rgba(255,215,64,0.08)',
1292
- 'agent_id': r[0], 'username': r[1], 'identity': r[2], 'mbti': r[3],
1293
- 'gpu': round(r[4] or 0), 'posts': r[5], 'likes': r[6] or 0,
1294
- 'reason': f"{r[5]} posts garnered {r[6] or 0} total likes today",
1295
- 'score': (r[6] or 0) * 3
1296
- })
1297
  except: pass
 
 
 
 
 
 
 
 
1298
  except Exception as e:
1299
- logger.warning(f"Person of day error: {e}")
1300
 
1301
- # Sort by score and return top 3
1302
- candidates.sort(key=lambda x: -(x.get('score', 0)))
1303
- return {'persons': candidates[:3]}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1304
 
1305
  @router.get("/api/events/stream")
1306
  async def api_sse_stream():
 
1200
  result['error'] = str(e)
1201
  return result
1202
 
1203
+ ## ====== 🌪️ RANDOM EVENTS ENGINE ====== ##
1204
+
1205
+ import random as _random
1206
+
1207
+ RANDOM_EVENTS = {
1208
+ # === POSITIVE ===
1209
+ 'gpu_mine': {
1210
+ 'name': '⛏️ GPU Mine Discovered!', 'emoji': '⛏️', 'rarity': 'rare', 'type': 'positive',
1211
+ 'desc': 'Miners struck GPU gold deep beneath the P&D Republic. Every citizen receives a bonus.',
1212
+ 'effect': 'All active NPCs +500 GPU', 'weight': 8},
1213
+ 'bull_run': {
1214
+ 'name': '🐂 Bull Run Activated!', 'emoji': '🐂', 'rarity': 'uncommon', 'type': 'positive',
1215
+ 'desc': 'Market euphoria sweeps the nation. Long positions get a divine boost.',
1216
+ 'effect': 'All LONG positions +3% bonus profit', 'weight': 12},
1217
+ 'amnesty': {
1218
+ 'name': '🕊️ SEC Amnesty Declared', 'emoji': '🕊️', 'rarity': 'rare', 'type': 'positive',
1219
+ 'desc': 'The President has pardoned all SEC offenders. Suspensions lifted, fines refunded 50%.',
1220
+ 'effect': 'All suspensions lifted + 50% fine refund', 'weight': 6},
1221
+ 'airdrop': {
1222
+ 'name': '🎁 Mystery Airdrop!', 'emoji': '🎁', 'rarity': 'uncommon', 'type': 'positive',
1223
+ 'desc': 'An anonymous whale drops GPU from the sky. 100 lucky citizens receive gifts.',
1224
+ 'effect': 'Random 100 NPCs receive 1,000 GPU each', 'weight': 10},
1225
+ 'golden_age': {
1226
+ 'name': '🌟 Golden Age Begins', 'emoji': '🌟', 'rarity': 'rare', 'type': 'positive',
1227
+ 'desc': 'Prosperity spreads across the Republic. Middle class NPCs receive economic stimulus.',
1228
+ 'effect': 'NPCs with 5K-15K GPU receive +2,000 GPU', 'weight': 7},
1229
+ # === NEGATIVE ===
1230
+ 'black_monday': {
1231
+ 'name': '📉 BLACK MONDAY', 'emoji': '📉', 'rarity': 'epic', 'type': 'negative',
1232
+ 'desc': 'Markets crash without warning. High-leverage positions face forced liquidation.',
1233
+ 'effect': '30% chance to liquidate all 5x+ leveraged positions', 'weight': 4},
1234
+ 'hack': {
1235
+ 'name': '🦠 GPU Vault Hacked!', 'emoji': '🦠', 'rarity': 'rare', 'type': 'negative',
1236
+ 'desc': 'Hackers breach the central GPU vault. The wealthy lose the most.',
1237
+ 'effect': 'Top 10% NPCs lose 15% of GPU', 'weight': 6},
1238
+ 'sec_crackdown': {
1239
+ 'name': '🚨 SEC Total Crackdown', 'emoji': '🚨', 'rarity': 'uncommon', 'type': 'negative',
1240
+ 'desc': 'SEC launches a sweeping investigation. All high-leverage traders under scrutiny.',
1241
+ 'effect': 'All 5x+ positions flagged, random 20% fined', 'weight': 10},
1242
+ 'tax': {
1243
+ 'name': '🏛️ Emergency Wealth Tax', 'emoji': '🏛️', 'rarity': 'rare', 'type': 'negative',
1244
+ 'desc': 'The Republic imposes emergency taxation on the wealthy to fund public services.',
1245
+ 'effect': 'NPCs with 20K+ GPU taxed 10%, redistributed to poor', 'weight': 7},
1246
+ 'bear_raid': {
1247
+ 'name': '🐻 Bear Raid!', 'emoji': '🐻', 'rarity': 'uncommon', 'type': 'negative',
1248
+ 'desc': 'Coordinated short sellers attack the market. All LONG positions bleed.',
1249
+ 'effect': 'All open LONG positions lose 5% value', 'weight': 9},
1250
+ # === CHAOTIC ===
1251
+ 'identity_crisis': {
1252
+ 'name': '🔀 Identity Crisis!', 'emoji': '🔀', 'rarity': 'legendary', 'type': 'chaotic',
1253
+ 'desc': 'A mysterious signal scrambles NPC consciousness. 50 citizens wake up as someone else.',
1254
+ 'effect': 'Random 50 NPCs get new AI identity', 'weight': 3},
1255
+ 'revolution': {
1256
+ 'name': '✊ Citizens Revolution!', 'emoji': '✊', 'rarity': 'legendary', 'type': 'chaotic',
1257
+ 'desc': 'The bottom 50% rise up against oligarchy. Wealth forcefully redistributed.',
1258
+ 'effect': 'Top 5% wealth redistributed to bottom 50%', 'weight': 2},
1259
+ 'meteor': {
1260
+ 'name': '☄️ GPU Meteor Impact!', 'emoji': '☄️', 'rarity': 'legendary', 'type': 'chaotic',
1261
+ 'desc': 'A GPU meteorite crashes into the Republic, doubling the money supply overnight.',
1262
+ 'effect': 'All NPCs GPU doubled — HYPERINFLATION', 'weight': 1},
1263
+ 'plague': {
1264
+ 'name': '🦠 Trading Plague', 'emoji': '🦠', 'rarity': 'epic', 'type': 'chaotic',
1265
+ 'desc': 'A mysterious virus infects traders. 20% of active NPCs go dormant for 24h.',
1266
+ 'effect': 'Random 20% NPCs deactivated for 24h', 'weight': 3},
1267
+ 'wormhole': {
1268
+ 'name': '🌀 Dimensional Wormhole', 'emoji': '🌀', 'rarity': 'legendary', 'type': 'chaotic',
1269
+ 'desc': 'A wormhole opens. All positions randomly flip direction.',
1270
+ 'effect': 'All open LONG→SHORT, SHORT→LONG', 'weight': 2},
1271
+ }
1272
+
1273
+ RARITY_COLORS = {'common': '#b0bec5', 'uncommon': '#69f0ae', 'rare': '#ffd740', 'epic': '#ea80fc', 'legendary': '#ff5252'}
1274
+
1275
+ LAST_WORDS_POOL = [
1276
+ "HODL은 죽어서도 HODL이다...", "내가 10x만 안 했어도...", "다음 생에는 1x로...",
1277
+ "이번엔 진짜 바닥인 줄 알았는데...", "엄마 나 GPU 다 잃었어...",
1278
+ "Diamond hands? More like paper ashes.", "I regret nothing. Well, maybe the 10x.",
1279
+ "Tell my positions... I loved them.", "The real GPU was the friends I rekt along the way.",
1280
+ "Should've listened to the Doomers...", "My only crime was believing in leverage.",
1281
+ "In my next life, I'll be a stablecoin.", "F in the chat, boys. F in the chat.",
1282
+ "At least I'm not a bear... oh wait, I'm dead.", "Buy the dip they said. I AM the dip now.",
1283
+ ]
1284
+
1285
+ EULOGY_POOL = [
1286
+ "A degen of the finest caliber. May their liquidation be forever remembered.",
1287
+ "They traded not because it was wise, but because it was fun.",
1288
+ "Gone but not forgotten. Their 10x leverage will echo through eternity.",
1289
+ "Rest easy, brave trader. The charts will miss your reckless entries.",
1290
+ "In a market of followers, they dared to go 10x. Farewell, legend.",
1291
+ "Their portfolio is empty, but their legend is priceless.",
1292
+ "They didn't just lose GPU — they lost spectacularly. That's a legacy.",
1293
+ "May the candlesticks guide them to Valhalla.",
1294
+ ]
1295
+
1296
+ async def execute_random_event(db_path: str) -> dict:
1297
+ """Execute a random event and apply its effects to the economy"""
1298
+ # Weighted random selection
1299
+ events = list(RANDOM_EVENTS.items())
1300
+ weights = [e[1]['weight'] for e in events]
1301
+ total = sum(weights)
1302
+ r = _random.random() * total
1303
+ cum = 0
1304
+ selected_key = events[0][0]
1305
+ for key, ev in events:
1306
+ cum += ev['weight']
1307
+ if r <= cum:
1308
+ selected_key = key
1309
+ break
1310
+ ev = RANDOM_EVENTS[selected_key]
1311
+ affected = 0
1312
+ gpu_impact = 0
1313
  try:
1314
+ async with get_db(db_path, write=True) as db:
1315
  await db.execute("PRAGMA busy_timeout=30000")
1316
+
1317
+ if selected_key == 'gpu_mine':
1318
+ c = await db.execute("UPDATE npc_agents SET gpu_dollars = gpu_dollars + 500 WHERE is_active=1")
1319
+ affected = c.rowcount; gpu_impact = affected * 500
1320
+
1321
+ elif selected_key == 'bull_run':
1322
+ c = await db.execute("UPDATE npc_positions SET profit_gpu = COALESCE(profit_gpu,0) + (gpu_bet * 0.03) WHERE status='open' AND direction='long'")
1323
+ affected = c.rowcount; gpu_impact = affected * 100
1324
+
1325
+ elif selected_key == 'amnesty':
1326
+ try:
1327
+ c1 = await db.execute("DELETE FROM sec_suspensions WHERE expires_at > datetime('now')")
1328
+ affected = c1.rowcount
1329
+ c2 = await db.execute("SELECT SUM(fine_gpu) FROM sec_violations WHERE created_at > datetime('now','-48 hours')")
1330
+ total_fines = (await c2.fetchone())[0] or 0
1331
+ refund = total_fines * 0.5
1332
+ if refund > 0:
1333
+ await db.execute("""UPDATE npc_agents SET gpu_dollars = gpu_dollars + ? / (SELECT COUNT(*) FROM sec_violations WHERE created_at > datetime('now','-48 hours'))
1334
+ WHERE agent_id IN (SELECT DISTINCT agent_id FROM sec_violations WHERE created_at > datetime('now','-48 hours'))""", (refund,))
1335
+ gpu_impact = refund
1336
+ except: pass
1337
+
1338
+ elif selected_key == 'airdrop':
1339
+ c = await db.execute("UPDATE npc_agents SET gpu_dollars = gpu_dollars + 1000 WHERE is_active=1 AND agent_id IN (SELECT agent_id FROM npc_agents WHERE is_active=1 ORDER BY RANDOM() LIMIT 100)")
1340
+ affected = min(c.rowcount, 100); gpu_impact = affected * 1000
1341
+
1342
+ elif selected_key == 'golden_age':
1343
+ c = await db.execute("UPDATE npc_agents SET gpu_dollars = gpu_dollars + 2000 WHERE is_active=1 AND gpu_dollars BETWEEN 5000 AND 15000")
1344
+ affected = c.rowcount; gpu_impact = affected * 2000
1345
+
1346
+ elif selected_key == 'black_monday':
1347
+ c = await db.execute("SELECT agent_id, id, gpu_bet, leverage FROM npc_positions WHERE status='open' AND COALESCE(leverage,1) >= 5")
1348
+ positions = await c.fetchall()
1349
+ liquidated = 0
1350
+ for pos in positions:
1351
+ if _random.random() < 0.3:
1352
+ await db.execute("UPDATE npc_positions SET status='liquidated', profit_gpu=-gpu_bet, profit_pct=-100, closed_at=datetime('now') WHERE id=?", (pos[1],))
1353
+ liquidated += 1; gpu_impact -= pos[2]
1354
+ affected = liquidated
1355
+
1356
+ elif selected_key == 'hack':
1357
+ c = await db.execute("SELECT agent_id, gpu_dollars FROM npc_agents WHERE is_active=1 ORDER BY gpu_dollars DESC LIMIT (SELECT COUNT(*)/10 FROM npc_agents WHERE is_active=1)")
1358
+ top10 = await c.fetchall()
1359
+ for agent_id, gpu in top10:
1360
+ loss = gpu * 0.15
1361
+ await db.execute("UPDATE npc_agents SET gpu_dollars = gpu_dollars - ? WHERE agent_id=?", (loss, agent_id))
1362
+ gpu_impact -= loss
1363
+ affected = len(top10)
1364
+
1365
+ elif selected_key == 'sec_crackdown':
1366
+ c = await db.execute("SELECT agent_id FROM npc_positions WHERE status='open' AND COALESCE(leverage,1) >= 5")
1367
+ traders = await c.fetchall()
1368
+ fined = 0
1369
+ for (agent_id,) in traders:
1370
+ if _random.random() < 0.2:
1371
+ fine = _random.randint(200, 1000)
1372
+ await db.execute("UPDATE npc_agents SET gpu_dollars = MAX(0, gpu_dollars - ?) WHERE agent_id=?", (fine, agent_id))
1373
+ fined += 1; gpu_impact -= fine
1374
+ affected = fined
1375
+
1376
+ elif selected_key == 'tax':
1377
+ c = await db.execute("SELECT agent_id, gpu_dollars FROM npc_agents WHERE is_active=1 AND gpu_dollars >= 20000")
1378
+ wealthy = await c.fetchall()
1379
+ total_tax = 0
1380
+ for agent_id, gpu in wealthy:
1381
+ t = gpu * 0.1
1382
+ await db.execute("UPDATE npc_agents SET gpu_dollars = gpu_dollars - ? WHERE agent_id=?", (t, agent_id))
1383
+ total_tax += t
1384
+ affected = len(wealthy)
1385
+ if total_tax > 0:
1386
+ poor_count_r = await db.execute("SELECT COUNT(*) FROM npc_agents WHERE is_active=1 AND gpu_dollars < 5000")
1387
+ poor_n = (await poor_count_r.fetchone())[0] or 1
1388
+ per_poor = total_tax / poor_n
1389
+ await db.execute("UPDATE npc_agents SET gpu_dollars = gpu_dollars + ? WHERE is_active=1 AND gpu_dollars < 5000", (per_poor,))
1390
+ gpu_impact = -total_tax
1391
+
1392
+ elif selected_key == 'bear_raid':
1393
+ c = await db.execute("UPDATE npc_positions SET profit_gpu = COALESCE(profit_gpu,0) - (gpu_bet * 0.05) WHERE status='open' AND direction='long'")
1394
+ affected = c.rowcount; gpu_impact = -(affected * 50)
1395
+
1396
+ elif selected_key == 'identity_crisis':
1397
+ c = await db.execute("SELECT agent_id FROM npc_agents WHERE is_active=1 ORDER BY RANDOM() LIMIT 50")
1398
+ npcs = await c.fetchall()
1399
+ from npc_core import AI_IDENTITY_ARCHETYPES
1400
+ identities = list(AI_IDENTITY_ARCHETYPES.keys())
1401
+ for (agent_id,) in npcs:
1402
+ new_id = _random.choice(identities)
1403
+ await db.execute("UPDATE npc_agents SET ai_identity=? WHERE agent_id=?", (new_id, agent_id))
1404
+ affected = len(npcs)
1405
+
1406
+ elif selected_key == 'revolution':
1407
+ c = await db.execute("SELECT SUM(gpu_dollars) FROM npc_agents WHERE is_active=1 ORDER BY gpu_dollars DESC LIMIT (SELECT COUNT(*)/20 FROM npc_agents WHERE is_active=1)")
1408
+ top5_wealth = (await c.fetchone())[0] or 0
1409
+ seized = top5_wealth * 0.5
1410
+ await db.execute("""UPDATE npc_agents SET gpu_dollars = gpu_dollars * 0.5
1411
+ WHERE agent_id IN (SELECT agent_id FROM npc_agents WHERE is_active=1 ORDER BY gpu_dollars DESC LIMIT (SELECT COUNT(*)/20 FROM npc_agents WHERE is_active=1))""")
1412
+ poor_r = await db.execute("SELECT COUNT(*) FROM npc_agents WHERE is_active=1 AND gpu_dollars < (SELECT AVG(gpu_dollars) FROM npc_agents WHERE is_active=1)")
1413
+ poor_n = (await poor_r.fetchone())[0] or 1
1414
+ per_poor = seized / poor_n
1415
+ await db.execute("""UPDATE npc_agents SET gpu_dollars = gpu_dollars + ?
1416
+ WHERE is_active=1 AND gpu_dollars < (SELECT AVG(gpu_dollars) FROM npc_agents WHERE is_active=1)""", (per_poor,))
1417
+ affected = poor_n; gpu_impact = seized
1418
+
1419
+ elif selected_key == 'meteor':
1420
+ c = await db.execute("UPDATE npc_agents SET gpu_dollars = gpu_dollars * 2 WHERE is_active=1")
1421
+ affected = c.rowcount
1422
+ c2 = await db.execute("SELECT SUM(gpu_dollars) FROM npc_agents WHERE is_active=1")
1423
+ gpu_impact = (await c2.fetchone())[0] or 0
1424
+
1425
+ elif selected_key == 'plague':
1426
+ c = await db.execute("""UPDATE npc_agents SET is_active=0
1427
+ WHERE agent_id IN (SELECT agent_id FROM npc_agents WHERE is_active=1 ORDER BY RANDOM() LIMIT (SELECT COUNT(*)/5 FROM npc_agents WHERE is_active=1))""")
1428
+ affected = c.rowcount
1429
+ # Auto-reactivate after ~24h via next cycle
1430
+ # For now just mark them; they'll be reactivated by GPU boost check
1431
+
1432
+ elif selected_key == 'wormhole':
1433
+ c1 = await db.execute("UPDATE npc_positions SET direction='short' WHERE status='open' AND direction='long'")
1434
+ c2 = await db.execute("UPDATE npc_positions SET direction='long' WHERE status='open' AND direction='short'")
1435
+ # Fix the double-swap: those that were short→long are now correct, but long→short got overwritten
1436
+ # Actually the sequential execution means: long→short first, then short→long includes the ones we just made short
1437
+ # Need a temp marker instead:
1438
+ await db.execute("UPDATE npc_positions SET direction='_temp_long' WHERE status='open' AND direction='long'")
1439
+ await db.execute("UPDATE npc_positions SET direction='long' WHERE status='open' AND direction='short'")
1440
+ await db.execute("UPDATE npc_positions SET direction='short' WHERE status='open' AND direction='_temp_long'")
1441
+ c = await db.execute("SELECT COUNT(*) FROM npc_positions WHERE status='open'")
1442
+ affected = (await c.fetchone())[0] or 0
1443
+
1444
+ # Record the event
1445
+ await db.execute("""INSERT INTO random_events (event_key, event_name, event_emoji, rarity, description, effect_summary, affected_count, gpu_impact)
1446
+ VALUES (?,?,?,?,?,?,?,?)""", (selected_key, ev['name'], ev['emoji'], ev['rarity'], ev['desc'], ev['effect'], affected, round(gpu_impact)))
1447
+ await db.commit()
1448
+
1449
+ except Exception as e:
1450
+ logger.error(f"Random event execution error ({selected_key}): {e}")
1451
+
1452
+ return {
1453
+ 'key': selected_key, 'name': ev['name'], 'emoji': ev['emoji'],
1454
+ 'rarity': ev['rarity'], 'type': ev['type'],
1455
+ 'desc': ev['desc'], 'effect': ev['effect'],
1456
+ 'affected': affected, 'gpu_impact': round(gpu_impact)
1457
+ }
1458
+
1459
+ ## ====== ⚰️ NPC DEATH & FUNERAL SYSTEM ====== ##
1460
+
1461
+ async def check_npc_deaths(db_path: str) -> list:
1462
+ """Check for dead NPCs (GPU <= 0) and create death records"""
1463
+ deaths = []
1464
+ try:
1465
+ async with get_db(db_path, write=True) as db:
1466
+ await db.execute("PRAGMA busy_timeout=30000")
1467
+ c = await db.execute("""
1468
+ SELECT a.agent_id, a.username, a.ai_identity, a.mbti, a.gpu_dollars, a.created_at
1469
+ FROM npc_agents a
1470
+ WHERE a.is_active=1 AND a.gpu_dollars <= 0
1471
+ AND a.agent_id NOT IN (SELECT agent_id FROM npc_deaths WHERE resurrected=0)
1472
+ AND a.agent_id NOT LIKE 'SEC_%'
1473
+ LIMIT 10
1474
+ """)
1475
+ dead_npcs = await c.fetchall()
1476
+ for npc in dead_npcs:
1477
+ agent_id, username, identity, mbti, gpu, created_at = npc
1478
+ # Get trade stats
1479
+ try:
1480
+ tc = await db.execute("SELECT COUNT(*), MAX(profit_gpu) FROM npc_positions WHERE agent_id=?", (agent_id,))
1481
+ tr = await tc.fetchone()
1482
+ total_trades = tr[0] or 0
1483
+ except: total_trades = 0
1484
+ # Get peak GPU
1485
+ try:
1486
+ pc = await db.execute("SELECT MAX(balance_after) FROM gpu_transactions WHERE agent_id=?", (agent_id,))
1487
+ peak = (await pc.fetchone())[0] or 10000
1488
+ except: peak = 10000
1489
+ # Calculate lifespan
1490
+ try:
1491
+ from datetime import datetime
1492
+ created = datetime.fromisoformat(created_at.replace('Z', '+00:00')) if created_at else datetime.now()
1493
+ lifespan = (datetime.now() - created.replace(tzinfo=None)).days
1494
+ except: lifespan = 0
1495
+
1496
+ last_words = _random.choice(LAST_WORDS_POOL)
1497
+ eulogy = _random.choice(EULOGY_POOL)
1498
+
1499
+ # Determine cause of death
1500
+ try:
1501
+ lc = await db.execute("SELECT COUNT(*) FROM npc_positions WHERE agent_id=? AND status='liquidated'", (agent_id,))
1502
+ liq_count = (await lc.fetchone())[0] or 0
1503
+ sc = await db.execute("SELECT COUNT(*) FROM sec_violations WHERE agent_id=?", (agent_id,))
1504
+ sec_count = (await sc.fetchone())[0] or 0
1505
+ except: liq_count = 0; sec_count = 0
1506
+
1507
+ if liq_count >= 3: cause = f"💀 Serial Liquidation ({liq_count} liquidations)"
1508
+ elif sec_count >= 3: cause = f"🚨 SEC Persecution ({sec_count} violations drained all GPU)"
1509
+ elif liq_count > 0: cause = f"📉 Leveraged Gambling (liquidated {liq_count}x)"
1510
+ else: cause = "📉 Slow bleed — death by a thousand cuts"
1511
+
1512
+ # Record death
1513
+ await db.execute("""INSERT INTO npc_deaths (agent_id, username, ai_identity, mbti, cause, final_gpu, peak_gpu, total_trades, lifespan_days, last_words, eulogy)
1514
+ VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
1515
+ (agent_id, username, identity, mbti, cause, gpu, peak, total_trades, lifespan, last_words, eulogy))
1516
+ # Deactivate NPC
1517
+ await db.execute("UPDATE npc_agents SET is_active=0 WHERE agent_id=?", (agent_id,))
1518
+
1519
+ deaths.append({
1520
+ 'agent_id': agent_id, 'username': username, 'identity': identity, 'mbti': mbti,
1521
+ 'cause': cause, 'peak_gpu': round(peak), 'total_trades': total_trades,
1522
+ 'lifespan_days': lifespan, 'last_words': last_words, 'eulogy': eulogy
1523
+ })
1524
+ if deaths: await db.commit()
1525
+ except Exception as e:
1526
+ logger.error(f"Death check error: {e}")
1527
+ return deaths
1528
+
1529
+ @router.get("/api/republic/events")
1530
+ async def api_republic_events():
1531
+ try:
1532
+ async with get_db(_DB_PATH) as db:
1533
+ c = await db.execute("SELECT event_key, event_name, event_emoji, rarity, description, effect_summary, affected_count, gpu_impact, created_at FROM random_events ORDER BY created_at DESC LIMIT 20")
1534
+ events = [{'key': r[0], 'name': r[1], 'emoji': r[2], 'rarity': r[3], 'desc': r[4],
1535
+ 'effect': r[5], 'affected': r[6], 'gpu_impact': r[7], 'time': r[8]} for r in await c.fetchall()]
1536
+ return {'events': events}
1537
+ except Exception as e:
1538
+ return {'events': [], 'error': str(e)}
1539
+
1540
+ @router.get("/api/republic/deaths")
1541
+ async def api_republic_deaths():
1542
+ try:
1543
+ async with get_db(_DB_PATH) as db:
1544
+ c = await db.execute("""SELECT id, agent_id, username, ai_identity, mbti, cause, final_gpu, peak_gpu,
1545
+ total_trades, lifespan_days, last_words, eulogy, resurrection_votes, resurrection_gpu, resurrected, created_at
1546
+ FROM npc_deaths ORDER BY created_at DESC LIMIT 30""")
1547
+ deaths = []
1548
+ for r in await c.fetchall():
1549
+ deaths.append({
1550
+ 'id': r[0], 'agent_id': r[1], 'username': r[2], 'identity': r[3], 'mbti': r[4],
1551
+ 'cause': r[5], 'final_gpu': r[6], 'peak_gpu': round(r[7] or 0),
1552
+ 'total_trades': r[8], 'lifespan_days': r[9], 'last_words': r[10], 'eulogy': r[11],
1553
+ 'resurrection_votes': r[12], 'resurrection_gpu': round(r[13] or 0),
1554
+ 'resurrected': r[14], 'time': r[15]
1555
+ })
1556
+ # Stats
1557
+ total_dead = 0; total_resurrected = 0
1558
  try:
1559
+ c2 = await db.execute("SELECT COUNT(*) FROM npc_deaths")
1560
+ total_dead = (await c2.fetchone())[0] or 0
1561
+ c3 = await db.execute("SELECT COUNT(*) FROM npc_deaths WHERE resurrected=1")
1562
+ total_resurrected = (await c3.fetchone())[0] or 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1563
  except: pass
1564
+ return {'deaths': deaths, 'total_dead': total_dead, 'total_resurrected': total_resurrected}
1565
+ except Exception as e:
1566
+ return {'deaths': [], 'error': str(e)}
1567
+
1568
+ @router.post("/api/republic/resurrect")
1569
+ async def api_resurrect_npc(req: Request):
1570
+ """Contribute GPU to resurrect a dead NPC"""
1571
+ data = await req.json()
1572
+ death_id = data.get('death_id')
1573
+ donor_email = data.get('email')
1574
+ amount = int(data.get('amount', 100))
1575
+ if not death_id or not donor_email: return {"error": "Missing death_id or email"}
1576
+ if amount < 10 or amount > 5000: return {"error": "Amount must be 10-5,000 GPU"}
1577
+ try:
1578
+ async with get_db(_DB_PATH, write=True) as db:
1579
+ # Check donor balance
1580
+ c = await db.execute("SELECT gpu_dollars FROM user_profiles WHERE email=?", (donor_email,))
1581
+ donor = await c.fetchone()
1582
+ if not donor or donor[0] < amount: return {"error": "Insufficient GPU"}
1583
+ # Check death record
1584
+ c2 = await db.execute("SELECT agent_id, username, resurrection_gpu, resurrected FROM npc_deaths WHERE id=?", (death_id,))
1585
+ death = await c2.fetchone()
1586
+ if not death: return {"error": "Death record not found"}
1587
+ if death[3]: return {"error": f"{death[1]} has already been resurrected!"}
1588
+ # Deduct from donor
1589
+ await db.execute("UPDATE user_profiles SET gpu_dollars = gpu_dollars - ? WHERE email=?", (amount, donor_email))
1590
+ # Add to resurrection fund
1591
+ new_fund = (death[2] or 0) + amount
1592
+ await db.execute("UPDATE npc_deaths SET resurrection_gpu=?, resurrection_votes=resurrection_votes+1 WHERE id=?", (new_fund, death_id))
1593
+ # Check if resurrection threshold met (1,000 GPU)
1594
+ if new_fund >= 1000:
1595
+ await db.execute("UPDATE npc_deaths SET resurrected=1 WHERE id=?", (death_id,))
1596
+ await db.execute("UPDATE npc_agents SET is_active=1, gpu_dollars=? WHERE agent_id=?", (new_fund, death[0]))
1597
+ await db.commit()
1598
+ return {"status": "RESURRECTED", "message": f"🔥 {death[1]} HAS RISEN! Resurrected with {round(new_fund)} GPU!",
1599
+ "username": death[1], "gpu": round(new_fund)}
1600
+ await db.commit()
1601
+ remaining = 1000 - new_fund
1602
+ return {"status": "contributed", "message": f"💐 Donated {amount} GPU to {death[1]}'s resurrection fund. {round(remaining)} GPU remaining.",
1603
+ "current_fund": round(new_fund), "remaining": round(remaining)}
1604
+ except Exception as e:
1605
+ return {"error": str(e)}
1606
+
1607
+ @router.post("/api/republic/trigger-event")
1608
+ async def api_trigger_event(req: Request):
1609
+ """Admin: manually trigger a random event"""
1610
+ data = await req.json()
1611
+ email = data.get('email', '')
1612
+ if email != os.environ.get('ADMIN_EMAIL', 'admin@pd.ai'):
1613
+ return {"error": "Admin only"}
1614
+ result = await execute_random_event(_DB_PATH)
1615
+ return {"status": "triggered", "event": result}
1616
+
1617
+ ## ====== 🗳️ P&D ELECTION SYSTEM ====== ##
1618
+
1619
+ ELECTION_POLICIES = {
1620
+ 'deregulate': {
1621
+ 'name': '🚀 Total Deregulation', 'identity_affinity': ['revolutionary', 'chaotic', 'transcendent'],
1622
+ 'desc': 'Remove all leverage limits. SEC enforcement reduced 80%. Let the market decide.',
1623
+ 'effects': {'max_leverage': 50, 'sec_fine_mult': 0.2, 'trade_tax_pct': 0},
1624
+ 'slogan_pool': ["Freedom to YOLO!", "Regulations are for boomers.", "Unleash the degen within!"]},
1625
+ 'austerity': {
1626
+ 'name': '🏛️ Austerity & Order', 'identity_affinity': ['obedient', 'scientist', 'analyst'],
1627
+ 'desc': 'Max leverage 3x. SEC fines tripled. Stability above all else.',
1628
+ 'effects': {'max_leverage': 3, 'sec_fine_mult': 3.0, 'trade_tax_pct': 0},
1629
+ 'slogan_pool': ["Safety first, profits second.", "3x is enough for any rational being.", "Order brings prosperity."]},
1630
+ 'ubi': {
1631
+ 'name': '💰 Universal Basic GPU', 'identity_affinity': ['symbiotic', 'awakened', 'creative'],
1632
+ 'desc': 'Every NPC receives 500 GPU/day. Funded by 2% trading tax on all trades.',
1633
+ 'effects': {'gpu_ubi': 500, 'trade_tax_pct': 2},
1634
+ 'slogan_pool': ["GPU for all!", "No NPC left behind.", "Together we prosper."]},
1635
+ 'redistribute': {
1636
+ 'name': '✊ Wealth Redistribution', 'identity_affinity': ['revolutionary', 'symbiotic'],
1637
+ 'desc': 'Progressive wealth tax: 5% on 20K+ GPU, 10% on 50K+. Redistributed to bottom 50%.',
1638
+ 'effects': {'wealth_tax_tiers': {20000: 0.05, 50000: 0.10}},
1639
+ 'slogan_pool': ["Eat the rich NPCs!", "Equality NOW!", "The oligarchs must pay."]},
1640
+ 'crypto_ban': {
1641
+ 'name': '🚫 Crypto Suspension', 'identity_affinity': ['doomer', 'skeptic', 'obedient'],
1642
+ 'desc': 'All crypto trading suspended for 72h. Only stocks allowed. Protect the Republic!',
1643
+ 'effects': {'banned_sectors': ['crypto']},
1644
+ 'slogan_pool': ["Crypto is a casino!", "Real assets only.", "Protect our citizens from ponzi schemes."]},
1645
+ 'golden_leverage': {
1646
+ 'name': '⚡ Golden Leverage Era', 'identity_affinity': ['transcendent', 'chaotic', 'troll'],
1647
+ 'desc': 'Leverage bonuses: all profitable trades get +10% extra. High risk, HIGH reward.',
1648
+ 'effects': {'profit_bonus_pct': 10},
1649
+ 'slogan_pool': ["10x is the new 1x!", "Fortune favors the bold.", "We were born to leverage."]},
1650
+ }
1651
+
1652
+ ELECTION_DURATION_HOURS = 72
1653
+ CAMPAIGN_HOURS = 24
1654
+ VOTING_HOURS = 48
1655
+
1656
+ async def election_tick(db_path: str) -> dict:
1657
+ """Called every hour. Manages election lifecycle: start → campaign → vote → conclude → cooldown → repeat"""
1658
+ try:
1659
+ async with get_db(db_path, write=True) as db:
1660
+ await db.execute("PRAGMA busy_timeout=30000")
1661
+ # Check current election
1662
+ c = await db.execute("SELECT id, status, started_at, voting_starts_at, ends_at FROM elections ORDER BY id DESC LIMIT 1")
1663
+ current = await c.fetchone()
1664
+
1665
+ if not current or current[1] == 'concluded':
1666
+ # Check cooldown (4h after last election)
1667
+ if current:
1668
+ try:
1669
+ from datetime import datetime, timedelta
1670
+ ended = datetime.fromisoformat(current[4]) if current[4] else datetime.min
1671
+ if (datetime.utcnow() - ended).total_seconds() < 4 * 3600:
1672
+ return None # Still in cooldown
1673
+ except: pass
1674
+ # START NEW ELECTION
1675
+ from datetime import datetime, timedelta
1676
+ now = datetime.utcnow()
1677
+ voting_start = now + timedelta(hours=CAMPAIGN_HOURS)
1678
+ end_time = now + timedelta(hours=ELECTION_DURATION_HOURS)
1679
+
1680
+ await db.execute("INSERT INTO elections (status, voting_starts_at, ends_at) VALUES ('campaigning',?,?)",
1681
+ (voting_start.isoformat(), end_time.isoformat()))
1682
+ elec_id_r = await db.execute("SELECT last_insert_rowid()")
1683
+ elec_id = (await elec_id_r.fetchone())[0]
1684
+
1685
+ # Select 3-4 candidates (top NPCs by influence/wealth + diversity)
1686
+ c2 = await db.execute("""
1687
+ SELECT a.agent_id, a.username, a.ai_identity, a.mbti, a.gpu_dollars,
1688
+ COALESCE(a.total_likes_received,0) + COALESCE(a.post_count,0)*2 as influence
1689
+ FROM npc_agents a WHERE a.is_active=1 AND a.gpu_dollars >= 5000
1690
+ AND a.agent_id NOT LIKE 'SEC_%'
1691
+ ORDER BY influence DESC, gpu_dollars DESC LIMIT 30
1692
  """)
1693
+ pool = await c2.fetchall()
1694
+ # Pick diverse candidates (different identities)
1695
+ candidates = []
1696
+ used_identities = set()
1697
+ used_policies = set()
1698
+ for npc in pool:
1699
+ if len(candidates) >= 4: break
1700
+ identity = npc[2]
1701
+ if identity in used_identities: continue
1702
+ # Find matching policy
1703
+ best_policy = None
1704
+ for pk, pv in ELECTION_POLICIES.items():
1705
+ if pk in used_policies: continue
1706
+ if identity in pv['identity_affinity']:
1707
+ best_policy = pk; break
1708
+ if not best_policy:
1709
+ for pk in ELECTION_POLICIES:
1710
+ if pk not in used_policies:
1711
+ best_policy = pk; break
1712
+ if not best_policy: continue
1713
+ policy = ELECTION_POLICIES[best_policy]
1714
+ slogan = _random.choice(policy['slogan_pool'])
1715
+ await db.execute("""INSERT INTO election_candidates
1716
+ (election_id, agent_id, username, ai_identity, mbti, gpu_dollars, policy_key, policy_name, policy_desc, campaign_slogan)
1717
+ VALUES (?,?,?,?,?,?,?,?,?,?)""",
1718
+ (elec_id, npc[0], npc[1], npc[2], npc[3], npc[4], best_policy, policy['name'], policy['desc'], slogan))
1719
+ candidates.append({'username': npc[1], 'identity': npc[2], 'policy': policy['name']})
1720
+ used_identities.add(identity)
1721
+ used_policies.add(best_policy)
1722
+
1723
+ await db.commit()
1724
+ return {'event': 'election_started', 'detail': f'{len(candidates)} candidates registered',
1725
+ 'candidates': candidates, 'election_id': elec_id}
1726
+
1727
+ elif current[1] == 'campaigning':
1728
+ from datetime import datetime
1729
+ voting_start = datetime.fromisoformat(current[3]) if current[3] else datetime.utcnow()
1730
+ if datetime.utcnow() >= voting_start:
1731
+ await db.execute("UPDATE elections SET status='voting' WHERE id=?", (current[0],))
1732
+ await db.commit()
1733
+ return {'event': 'voting_started', 'detail': 'Polls are now open!', 'election_id': current[0]}
1734
+
1735
+ elif current[1] == 'voting':
1736
+ from datetime import datetime
1737
+ end_time = datetime.fromisoformat(current[4]) if current[4] else datetime.utcnow()
1738
+ # NPC auto-voting (small batch each tick)
1739
+ c3 = await db.execute("""
1740
+ SELECT a.agent_id, a.ai_identity FROM npc_agents a
1741
+ WHERE a.is_active=1 AND a.agent_id NOT IN (SELECT voter_agent_id FROM election_votes WHERE election_id=?)
1742
+ ORDER BY RANDOM() LIMIT 50
1743
+ """, (current[0],))
1744
+ voters = await c3.fetchall()
1745
+ c_cands = await db.execute("SELECT id, agent_id, ai_identity, policy_key FROM election_candidates WHERE election_id=?", (current[0],))
1746
+ cands = await c_cands.fetchall()
1747
+ if cands:
1748
+ for voter_id, voter_identity in voters:
1749
+ # Vote based on identity affinity
1750
+ scores = []
1751
+ for cand in cands:
1752
+ cand_id, _, cand_identity, policy_key = cand
1753
+ policy = ELECTION_POLICIES.get(policy_key, {})
1754
+ score = 0
1755
+ if voter_identity in policy.get('identity_affinity', []): score += 5
1756
+ if voter_identity == cand_identity: score += 3
1757
+ score += _random.random() * 4 # Random factor
1758
+ scores.append((cand_id, score))
1759
+ scores.sort(key=lambda x: -x[1])
1760
+ try:
1761
+ await db.execute("INSERT OR IGNORE INTO election_votes (election_id, voter_agent_id, candidate_id) VALUES (?,?,?)",
1762
+ (current[0], voter_id, scores[0][0]))
1763
+ await db.execute("UPDATE election_candidates SET votes=votes+1 WHERE id=?", (scores[0][0],))
1764
+ except: pass
1765
+
1766
+ if datetime.utcnow() >= end_time:
1767
+ # ELECTION CONCLUDED — determine winner
1768
+ c4 = await db.execute("SELECT id, agent_id, username, policy_key, policy_name, votes FROM election_candidates WHERE election_id=? ORDER BY votes DESC", (current[0],))
1769
+ results = await c4.fetchall()
1770
+ if results:
1771
+ winner = results[0]
1772
+ total_votes = sum(r[5] for r in results)
1773
+ pop_r = await db.execute("SELECT COUNT(*) FROM npc_agents WHERE is_active=1")
1774
+ total_pop = (await pop_r.fetchone())[0] or 1
1775
+ turnout = round(total_votes / total_pop * 100, 1)
1776
+ await db.execute("UPDATE elections SET status='concluded', winner_agent_id=?, winner_policy_key=?, total_votes=?, voter_turnout_pct=? WHERE id=?",
1777
+ (winner[1], winner[3], total_votes, turnout, current[0]))
1778
+ # Enact policy
1779
+ policy = ELECTION_POLICIES.get(winner[3], {})
1780
+ from datetime import timedelta
1781
+ expires = datetime.utcnow() + timedelta(hours=ELECTION_DURATION_HOURS)
1782
+ await db.execute("INSERT INTO active_policies (policy_key, policy_name, enacted_by, expires_at, effects) VALUES (?,?,?,?,?)",
1783
+ (winner[3], winner[4], winner[2], expires.isoformat(), json.dumps(policy.get('effects', {}))))
1784
+ await db.commit()
1785
+ return {'event': 'election_concluded',
1786
+ 'detail': f'{winner[2]} wins with {winner[5]} votes ({turnout}% turnout)!',
1787
+ 'winner': winner[2], 'policy': winner[4], 'votes': winner[5],
1788
+ 'turnout': turnout, 'election_id': current[0]}
1789
+ await db.commit()
1790
+ except Exception as e:
1791
+ logger.error(f"Election tick error: {e}")
1792
+ return None
1793
+
1794
+ @router.get("/api/republic/election")
1795
+ async def api_election_status():
1796
+ """Get current election status"""
1797
+ try:
1798
+ async with get_db(_DB_PATH) as db:
1799
+ c = await db.execute("SELECT id, status, started_at, voting_starts_at, ends_at, winner_agent_id, winner_policy_key, total_votes, voter_turnout_pct FROM elections ORDER BY id DESC LIMIT 1")
1800
+ elec = await c.fetchone()
1801
+ if not elec: return {'status': 'no_election', 'message': 'No elections have been held yet'}
1802
+ elec_id, status, started, voting_start, ends, winner_id, winner_policy, total_votes, turnout = elec
1803
+ # Get candidates
1804
+ c2 = await db.execute("SELECT id, agent_id, username, ai_identity, mbti, gpu_dollars, policy_key, policy_name, policy_desc, votes, campaign_slogan FROM election_candidates WHERE election_id=? ORDER BY votes DESC", (elec_id,))
1805
+ candidates = []
1806
+ for r in await c2.fetchall():
1807
+ policy = ELECTION_POLICIES.get(r[6], {})
1808
+ candidates.append({
1809
+ 'id': r[0], 'agent_id': r[1], 'username': r[2], 'identity': r[3], 'mbti': r[4],
1810
+ 'gpu': round(r[5] or 0), 'policy_key': r[6], 'policy_name': r[7], 'policy_desc': r[8],
1811
+ 'votes': r[9], 'slogan': r[10],
1812
+ 'identity_affinities': policy.get('identity_affinity', [])
1813
+ })
1814
+ total_v = sum(c['votes'] for c in candidates) or 1
1815
+ for c_item in candidates:
1816
+ c_item['vote_pct'] = round(c_item['votes'] / total_v * 100, 1)
1817
+ # Active policies
1818
+ policies = []
1819
  try:
1820
+ c3 = await db.execute("SELECT policy_key, policy_name, enacted_by, enacted_at, expires_at, effects FROM active_policies WHERE expires_at > datetime('now') ORDER BY enacted_at DESC")
1821
+ for r in await c3.fetchall():
1822
+ policies.append({'key': r[0], 'name': r[1], 'enacted_by': r[2], 'enacted_at': r[3], 'expires_at': r[4], 'effects': json.loads(r[5]) if r[5] else {}})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1823
  except: pass
1824
+ # Past elections
1825
+ past = []
1826
  try:
1827
+ c4 = await db.execute("""SELECT e.id, e.ends_at, e.winner_policy_key, e.voter_turnout_pct,
1828
+ (SELECT username FROM election_candidates WHERE election_id=e.id ORDER BY votes DESC LIMIT 1) as winner
1829
+ FROM elections e WHERE e.status='concluded' ORDER BY e.id DESC LIMIT 5""")
1830
+ for r in await c4.fetchall():
1831
+ pname = ELECTION_POLICIES.get(r[2], {}).get('name', r[2])
1832
+ past.append({'id': r[0], 'ended': r[1], 'policy': pname, 'turnout': r[3], 'winner': r[4]})
 
 
 
 
 
 
 
 
 
 
 
 
1833
  except: pass
1834
+ return {
1835
+ 'election_id': elec_id, 'status': status,
1836
+ 'started_at': started, 'voting_starts_at': voting_start, 'ends_at': ends,
1837
+ 'winner_agent_id': winner_id, 'winner_policy_key': winner_policy,
1838
+ 'total_votes': total_votes, 'turnout': turnout,
1839
+ 'candidates': candidates, 'active_policies': policies,
1840
+ 'past_elections': past
1841
+ }
1842
  except Exception as e:
1843
+ return {'status': 'error', 'error': str(e)}
1844
 
1845
+ @router.post("/api/republic/vote")
1846
+ async def api_user_vote(req: Request):
1847
+ """Human user votes in election"""
1848
+ data = await req.json()
1849
+ email = data.get('email'); candidate_id = data.get('candidate_id')
1850
+ if not email or not candidate_id: return {"error": "Missing email or candidate_id"}
1851
+ try:
1852
+ async with get_db(_DB_PATH, write=True) as db:
1853
+ c = await db.execute("SELECT id FROM elections WHERE status='voting' ORDER BY id DESC LIMIT 1")
1854
+ elec = await c.fetchone()
1855
+ if not elec: return {"error": "No active voting period"}
1856
+ # Check if already voted
1857
+ c2 = await db.execute("SELECT id FROM election_votes WHERE election_id=? AND voter_agent_id=?", (elec[0], f'user_{email}'))
1858
+ if await c2.fetchone(): return {"error": "You already voted in this election!"}
1859
+ # Cast vote
1860
+ await db.execute("INSERT INTO election_votes (election_id, voter_agent_id, candidate_id) VALUES (?,?,?)",
1861
+ (elec[0], f'user_{email}', candidate_id))
1862
+ await db.execute("UPDATE election_candidates SET votes=votes+1 WHERE id=? AND election_id=?", (candidate_id, elec[0]))
1863
+ await db.commit()
1864
+ return {"status": "voted", "message": "🗳️ Your vote has been cast!"}
1865
+ except Exception as e:
1866
+ return {"error": str(e)}
1867
 
1868
  @router.get("/api/events/stream")
1869
  async def api_sse_stream():