Spaces:
Running
Running
Update app_routes.py
Browse files- 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 |
-
## ======
|
| 1204 |
-
|
| 1205 |
-
|
| 1206 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1207 |
try:
|
| 1208 |
-
async with get_db(
|
| 1209 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 1210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1211 |
try:
|
| 1212 |
-
|
| 1213 |
-
|
| 1214 |
-
|
| 1215 |
-
|
| 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 |
-
|
| 1234 |
-
|
| 1235 |
-
|
| 1236 |
-
|
| 1237 |
-
|
| 1238 |
-
|
| 1239 |
-
|
| 1240 |
-
|
| 1241 |
-
|
| 1242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1243 |
""")
|
| 1244 |
-
|
| 1245 |
-
|
| 1246 |
-
|
| 1247 |
-
|
| 1248 |
-
|
| 1249 |
-
|
| 1250 |
-
|
| 1251 |
-
|
| 1252 |
-
|
| 1253 |
-
|
| 1254 |
-
|
| 1255 |
-
|
| 1256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1257 |
try:
|
| 1258 |
-
|
| 1259 |
-
|
| 1260 |
-
|
| 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 |
-
#
|
|
|
|
| 1278 |
try:
|
| 1279 |
-
|
| 1280 |
-
SELECT
|
| 1281 |
-
|
| 1282 |
-
|
| 1283 |
-
|
| 1284 |
-
|
| 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 |
-
|
| 1300 |
|
| 1301 |
-
|
| 1302 |
-
|
| 1303 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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():
|