from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles
import sqlite3
import json
import asyncio
from datetime import datetime
from typing import Dict, Set
import random
app = FastAPI()
# Database initialization
def init_db():
conn = sqlite3.connect('city_game.db')
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS users
(username TEXT PRIMARY KEY, money REAL, color TEXT, last_login TEXT)''')
c.execute('''CREATE TABLE IF NOT EXISTS objects
(id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, type TEXT,
x INTEGER, y INTEGER, name TEXT, profit_rate REAL)''')
c.execute('''CREATE TABLE IF NOT EXISTS chat
(id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, message TEXT, timestamp TEXT)''')
conn.commit()
conn.close()
init_db()
# Active connections
connections: Dict[str, WebSocket] = {}
user_cursors: Dict[str, tuple] = {}
# Economic system
BUILDING_COSTS = {
"house": 50000, "flat": 120000, "store": 80000, "hospital": 250000,
"road": 1000, "tree": 500, "fountain": 3000, "office": 150000
}
BUILDING_PROFITS = {
"house": 500, "flat": 1200, "store": 800, "hospital": 2000,
"office": 1500, "road": 0, "tree": 0, "fountain": 0
}
STARTING_MONEY = 150000
COLORS = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#FFA07A", "#98D8C8",
"#F7DC6F", "#BB8FCE", "#85C1E2", "#F8B739", "#52D7A7"]
def get_user_color(username):
conn = sqlite3.connect('city_game.db')
c = conn.cursor()
c.execute("SELECT color FROM users WHERE username=?", (username,))
result = c.fetchone()
conn.close()
if result:
return result[0]
return random.choice(COLORS)
def get_or_create_user(username):
conn = sqlite3.connect('city_game.db')
c = conn.cursor()
c.execute("SELECT * FROM users WHERE username=?", (username,))
user = c.fetchone()
if not user:
color = random.choice(COLORS)
c.execute("INSERT INTO users VALUES (?, ?, ?, ?)",
(username, STARTING_MONEY, color, datetime.now().isoformat()))
conn.commit()
user = (username, STARTING_MONEY, color, datetime.now().isoformat())
conn.close()
return {"username": user[0], "money": user[1], "color": user[2]}
def save_object(username, obj_type, x, y, name=""):
conn = sqlite3.connect('city_game.db')
c = conn.cursor()
profit = BUILDING_PROFITS.get(obj_type, 0)
c.execute("INSERT INTO objects (username, type, x, y, name, profit_rate) VALUES (?, ?, ?, ?, ?, ?)",
(username, obj_type, x, y, name, profit))
conn.commit()
obj_id = c.lastrowid
conn.close()
return obj_id
def load_objects():
conn = sqlite3.connect('city_game.db')
c = conn.cursor()
c.execute("SELECT id, username, type, x, y, name FROM objects")
objects = c.fetchall()
conn.close()
return [{"id": o[0], "username": o[1], "type": o[2], "x": o[3], "y": o[4], "name": o[5]} for o in objects]
def delete_object(obj_id, username):
conn = sqlite3.connect('city_game.db')
c = conn.cursor()
c.execute("SELECT username FROM objects WHERE id=?", (obj_id,))
result = c.fetchone()
if result and result[0] == username:
c.execute("DELETE FROM objects WHERE id=?", (obj_id,))
conn.commit()
conn.close()
return True
conn.close()
return False
async def broadcast(message, exclude=None):
for username, ws in connections.items():
if username != exclude:
try:
await ws.send_json(message)
except:
pass
# HTML Frontend
HTML_CONTENT = """<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>City Tycoon</title>
<link rel="stylesheet" href="/static/app.css">
</head>
<body>
<div id="login-screen">
<div class="tt-window">
<div class="tt-title">CITY TYCOON</div>
<input type="text" id="username-input" class="tt-input" placeholder="Enter username" maxlength="20">
<button class="tt-button" onclick="login()">START</button>
</div>
</div>
<div id="game-screen" style="display:none;">
<div id="hud">
<div id="player-info"></div>
<div id="money-info"></div>
</div>
<canvas id="game-canvas"></canvas>
<div id="toolbar"></div>
<div id="chat-container">
<div id="chat-messages"></div>
<input type="text" id="chat-input" placeholder="Press Enter to chat...">
</div>
<div id="context-menu" class="context-menu" style="display:none;">
<div class="context-item" onclick="renameObject()">Rename</div>
<div class="context-item" onclick="demolishObject()">Demolish</div>
</div>
<div id="rename-dialog" class="tt-window" style="display:none;">
<div class="tt-title">RENAME</div>
<input type="text" id="rename-input" class="tt-input" maxlength="30">
<button class="tt-button" onclick="confirmRename()">OK</button>
<button class="tt-button" onclick="cancelRename()">CANCEL</button>
</div>
</div>
<script src="/static/app.js"></script>
</body>
</html>"""
CSS_CONTENT = """* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Courier New', monospace;
}
body {
overflow: hidden;
background: #000;
}
#login-screen {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #1a1a1a;
}
.tt-window {
background: #2a2a2a;
border: 2px solid #555;
padding: 20px;
min-width: 300px;
}
.tt-title {
color: #fff;
font-size: 20px;
font-weight: bold;
margin-bottom: 15px;
text-align: center;
}
.tt-input {
width: 100%;
background: #000;
border: 2px solid #555;
color: #fff;
padding: 8px;
font-size: 14px;
margin-bottom: 10px;
}
.tt-button {
background: #444;
border: 2px solid #666;
color: #fff;
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
margin: 2px;
}
.tt-button:hover {
background: #555;
}
.tt-button:active {
background: #333;
}
.tt-button:disabled {
background: #222;
color: #555;
cursor: not-allowed;
}
#game-screen {
width: 100vw;
height: 100vh;
position: relative;
}
#game-canvas {
display: block;
cursor: crosshair;
}
#hud {
position: absolute;
top: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.7);
border: 2px solid #555;
padding: 10px;
color: #fff;
font-size: 12px;
z-index: 100;
}
#toolbar {
position: absolute;
top: 20%;
right: 10px;
width: 150px;
background: rgba(0, 0, 0, 0.8);
border: 2px solid #555;
padding: 5px;
max-height: 70%;
overflow-y: auto;
z-index: 100;
}
.tool-item {
background: #333;
border: 2px solid #555;
padding: 8px;
margin: 3px 0;
cursor: pointer;
color: #fff;
font-size: 11px;
text-align: center;
}
.tool-item:hover {
background: #444;
}
.tool-item.selected {
background: #555;
border-color: #888;
}
.tool-item.disabled {
background: #222;
color: #555;
cursor: not-allowed;
}
#chat-container {
position: absolute;
bottom: 10px;
left: 10px;
width: 400px;
height: 150px;
background: rgba(0, 0, 0, 0.7);
border: 2px solid #555;
z-index: 100;
}
#chat-messages {
height: calc(100% - 30px);
overflow-y: auto;
padding: 5px;
color: #fff;
font-size: 11px;
}
#chat-input {
width: 100%;
height: 25px;
background: #000;
border: none;
border-top: 2px solid #555;
color: #fff;
padding: 5px;
font-size: 11px;
}
.context-menu {
position: absolute;
background: #2a2a2a;
border: 2px solid #555;
z-index: 200;
}
.context-item {
padding: 8px 15px;
color: #fff;
font-size: 12px;
cursor: pointer;
}
.context-item:hover {
background: #444;
}
#rename-dialog {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 300;
}"""
JS_CONTENT = """let ws = null;
let username = '';
let playerColor = '';
let playerMoney = 0;
let canvas, ctx;
let camera = { x: 0, y: 0, zoom: 1 };
let objects = [];
let selectedTool = null;
let isDragging = false;
let dragStart = { x: 0, y: 0 };
let hoveredTile = null;
let contextMenuObject = null;
const TILE_SIZE = 32;
const RENDER_SECTORS = 12;
let sectorCache = {};
let otherCursors = {};
const TOOLS = [
{ type: 'road', name: 'Road', cost: 1000 },
{ type: 'house', name: 'House', cost: 50000 },
{ type: 'flat', name: 'Flat', cost: 120000 },
{ type: 'store', name: 'Store', cost: 80000 },
{ type: 'office', name: 'Office', cost: 150000 },
{ type: 'hospital', name: 'Hospital', cost: 250000 },
{ type: 'tree', name: 'Tree', cost: 500 },
{ type: 'fountain', name: 'Fountain', cost: 3000 }
];
function login() {
const input = document.getElementById('username-input');
username = input.value.trim();
if (!username) return;
document.getElementById('login-screen').style.display = 'none';
document.getElementById('game-screen').style.display = 'block';
initGame();
connectWebSocket();
}
function initGame() {
canvas = document.getElementById('game-canvas');
ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
setupToolbar();
setupEventListeners();
render();
}
function connectWebSocket() {
ws = new WebSocket(`ws://127.0.0.1:8595/ws/${username}`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleMessage(data);
};
ws.onclose = () => {
setTimeout(connectWebSocket, 3000);
};
}
function handleMessage(data) {
if (data.type === 'init') {
playerColor = data.color;
playerMoney = data.money;
objects = data.objects;
updateHUD();
updateToolbar();
} else if (data.type === 'object_placed') {
objects.push(data.object);
if (data.object.username === username) {
playerMoney = data.money;
updateHUD();
updateToolbar();
}
} else if (data.type === 'object_deleted') {
objects = objects.filter(o => o.id !== data.id);
if (data.username === username) {
playerMoney = data.money;
updateHUD();
updateToolbar();
}
} else if (data.type === 'object_renamed') {
const obj = objects.find(o => o.id === data.id);
if (obj) obj.name = data.name;
} else if (data.type === 'chat') {
addChatMessage(data.username, data.message, data.timestamp);
} else if (data.type === 'cursor') {
otherCursors[data.username] = { x: data.x, y: data.y, color: data.color };
} else if (data.type === 'money_update') {
if (data.username === username) {
playerMoney = data.money;
updateHUD();
updateToolbar();
}
}
}
function setupToolbar() {
const toolbar = document.getElementById('toolbar');
toolbar.innerHTML = '';
TOOLS.forEach(tool => {
const div = document.createElement('div');
div.className = 'tool-item';
div.textContent = `${tool.name}\\n$${tool.cost.toLocaleString()}`;
div.onclick = () => selectTool(tool);
div.dataset.type = tool.type;
toolbar.appendChild(div);
});
}
function selectTool(tool) {
if (tool.cost > playerMoney) return;
selectedTool = tool;
updateToolbar();
}
function updateToolbar() {
document.querySelectorAll('.tool-item').forEach(item => {
const tool = TOOLS.find(t => t.type === item.dataset.type);
item.classList.remove('selected', 'disabled');
if (tool.cost > playerMoney) {
item.classList.add('disabled');
}
if (selectedTool && selectedTool.type === item.dataset.type) {
item.classList.add('selected');
}
});
}
function setupEventListeners() {
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('contextmenu', handleContextMenu);
canvas.addEventListener('wheel', handleWheel);
const chatInput = document.getElementById('chat-input');
chatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && chatInput.value.trim()) {
ws.send(JSON.stringify({ type: 'chat', message: chatInput.value.trim() }));
chatInput.value = '';
}
});
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
}
function handleMouseDown(e) {
if (e.button === 2) {
isDragging = true;
dragStart = { x: e.clientX - camera.x, y: e.clientY - camera.y };
} else if (e.button === 0 && selectedTool && hoveredTile) {
placeObject(hoveredTile.x, hoveredTile.y);
}
}
function handleMouseMove(e) {
if (isDragging) {
camera.x = e.clientX - dragStart.x;
camera.y = e.clientY - dragStart.y;
} else {
hoveredTile = screenToTile(e.clientX, e.clientY);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'cursor',
x: hoveredTile ? hoveredTile.x : -999,
y: hoveredTile ? hoveredTile.y : -999
}));
}
}
}
function handleMouseUp(e) {
if (e.button === 2) {
isDragging = false;
}
}
function handleContextMenu(e) {
e.preventDefault();
const tile = screenToTile(e.clientX, e.clientY);
if (!tile) return;
const obj = objects.find(o => o.x === tile.x && o.y === tile.y && o.username === username);
if (obj) {
contextMenuObject = obj;
const menu = document.getElementById('context-menu');
menu.style.display = 'block';
menu.style.left = e.clientX + 'px';
menu.style.top = e.clientY + 'px';
}
}
function handleWheel(e) {
e.preventDefault();
const oldZoom = camera.zoom;
camera.zoom *= e.deltaY > 0 ? 0.9 : 1.1;
camera.zoom = Math.max(0.5, Math.min(2, camera.zoom));
const zoomRatio = camera.zoom / oldZoom;
camera.x = e.clientX - (e.clientX - camera.x) * zoomRatio;
camera.y = e.clientY - (e.clientY - camera.y) * zoomRatio;
}
function placeObject(x, y) {
if (!selectedTool || objects.some(o => o.x === x && o.y === y)) return;
ws.send(JSON.stringify({
type: 'place_object',
object_type: selectedTool.type,
x: x,
y: y
}));
}
function renameObject() {
document.getElementById('context-menu').style.display = 'none';
document.getElementById('rename-dialog').style.display = 'block';
document.getElementById('rename-input').value = contextMenuObject.name || '';
document.getElementById('rename-input').focus();
}
function demolishObject() {
document.getElementById('context-menu').style.display = 'none';
ws.send(JSON.stringify({
type: 'demolish',
id: contextMenuObject.id
}));
}
function confirmRename() {
const name = document.getElementById('rename-input').value.trim();
ws.send(JSON.stringify({
type: 'rename',
id: contextMenuObject.id,
name: name
}));
cancelRename();
}
function cancelRename() {
document.getElementById('rename-dialog').style.display = 'none';
}
function screenToTile(screenX, screenY) {
const worldX = (screenX - camera.x) / camera.zoom;
const worldY = (screenY - camera.y) / camera.zoom;
return {
x: Math.floor(worldX / TILE_SIZE),
y: Math.floor(worldY / TILE_SIZE)
};
}
function tileToScreen(tileX, tileY) {
return {
x: tileX * TILE_SIZE * camera.zoom + camera.x,
y: tileY * TILE_SIZE * camera.zoom + camera.y
};
}
function updateHUD() {
document.getElementById('player-info').textContent = `Player: ${username}`;
document.getElementById('money-info').textContent = `Money: $${playerMoney.toLocaleString()}`;
}
function addChatMessage(user, message, timestamp) {
const messages = document.getElementById('chat-messages');
const time = timestamp.split('T')[1].substring(0, 5);
const div = document.createElement('div');
div.textContent = `[${time}] [${user}]: ${message}`;
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
}
function render() {
ctx.fillStyle = '#90EE90';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const startTile = screenToTile(0, 0);
const endTile = screenToTile(canvas.width, canvas.height);
for (let y = startTile.y - 1; y <= endTile.y + 1; y++) {
for (let x = startTile.x - 1; x <= endTile.x + 1; x++) {
const pos = tileToScreen(x, y);
ctx.strokeStyle = '#70CC70';
ctx.lineWidth = 1;
ctx.strokeRect(pos.x, pos.y, TILE_SIZE * camera.zoom, TILE_SIZE * camera.zoom);
if (hoveredTile && hoveredTile.x === x && hoveredTile.y === y && selectedTool) {
ctx.fillStyle = 'rgba(255, 255, 0, 0.3)';
ctx.fillRect(pos.x, pos.y, TILE_SIZE * camera.zoom, TILE_SIZE * camera.zoom);
}
}
}
objects.forEach(obj => {
if (obj.x >= startTile.x - 1 && obj.x <= endTile.x + 1 &&
obj.y >= startTile.y - 1 && obj.y <= endTile.y + 1) {
drawObject(obj);
}
});
Object.entries(otherCursors).forEach(([user, cursor]) => {
if (cursor.x >= startTile.x && cursor.x <= endTile.x &&
cursor.y >= startTile.y && cursor.y <= endTile.y) {
const pos = tileToScreen(cursor.x, cursor.y);
ctx.fillStyle = cursor.color;
ctx.globalAlpha = 0.5;
ctx.fillRect(pos.x, pos.y, TILE_SIZE * camera.zoom, TILE_SIZE * camera.zoom);
ctx.globalAlpha = 1;
}
});
requestAnimationFrame(render);
}
function drawObject(obj) {
const pos = tileToScreen(obj.x, obj.y);
const size = TILE_SIZE * camera.zoom;
const color = obj.username === username ? playerColor : getStoredColor(obj.username);
ctx.fillStyle = color;
if (obj.type === 'road') {
ctx.fillStyle = '#555';
ctx.fillRect(pos.x, pos.y, size, size);
} else if (obj.type === 'house') {
ctx.fillRect(pos.x + size * 0.1, pos.y + size * 0.3, size * 0.8, size * 0.6);
ctx.beginPath();
ctx.moveTo(pos.x + size * 0.5, pos.y + size * 0.1);
ctx.lineTo(pos.x + size * 0.9, pos.y + size * 0.3);
ctx.lineTo(pos.x + size * 0.1, pos.y + size * 0.3);
ctx.closePath();
ctx.fill();
} else if (obj.type === 'flat') {
ctx.fillRect(pos.x + size * 0.1, pos.y + size * 0.1, size * 0.8, size * 0.8);
ctx.fillStyle = darkenColor(color);
ctx.fillRect(pos.x + size * 0.1, pos.y + size * 0.1, size * 0.8, size * 0.15);
} else if (obj.type === 'store') {
ctx.fillRect(pos.x + size * 0.1, pos.y + size * 0.2, size * 0.8, size * 0.7);
ctx.fillStyle = '#FFF';
ctx.fillRect(pos.x + size * 0.3, pos.y + size * 0.4, size * 0.15, size * 0.3);
} else if (obj.type === 'hospital') {
ctx.fillRect(pos.x + size * 0.1, pos.y + size * 0.2, size * 0.8, size * 0.7);
ctx.fillStyle = '#FFF';
ctx.fillRect(pos.x + size * 0.4, pos.y + size * 0.35, size * 0.2, size * 0.5);
ctx.fillRect(pos.x + size * 0.25, pos.y + size * 0.5, size * 0.5, size * 0.2);
} else if (obj.type === 'office') {
ctx.fillRect(pos.x + size * 0.15, pos.y + size * 0.15, size * 0.7, size * 0.75);
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 2; j++) {
ctx.fillStyle = '#FFF';
ctx.fillRect(pos.x + size * (0.25 + j * 0.3), pos.y + size * (0.25 + i * 0.2), size * 0.15, size * 0.12);
}
}
} else if (obj.type === 'tree') {
ctx.fillStyle = '#654321';
ctx.fillRect(pos.x + size * 0.4, pos.y + size * 0.5, size * 0.2, size * 0.4);
ctx.fillStyle = '#228B22';
ctx.beginPath();
ctx.arc(pos.x + size * 0.5, pos.y + size * 0.4, size * 0.3, 0, Math.PI * 2);
ctx.fill();
} else if (obj.type === 'fountain') {
ctx.fillStyle = '#4682B4';
ctx.beginPath();
ctx.arc(pos.x + size * 0.5, pos.y + size * 0.5, size * 0.35, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#87CEEB';
ctx.beginPath();
ctx.arc(pos.x + size * 0.5, pos.y + size * 0.5, size * 0.2, 0, Math.PI * 2);
ctx.fill();
}
if (obj.name) {
ctx.fillStyle = color;
ctx.font = `${10 * camera.zoom}px monospace`;
ctx.textAlign = 'center';
ctx.fillText(obj.name, pos.x + size * 0.5, pos.y - 5 * camera.zoom);
}
}
function getStoredColor(username) {
return '#888';
}
function darkenColor(color) {
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
return `rgb(${Math.floor(r * 0.7)}, ${Math.floor(g * 0.7)}, ${Math.floor(b * 0.7)})`;
}
document.addEventListener('click', (e) => {
if (!e.target.closest('#context-menu') && !e.target.closest('#rename-dialog')) {
document.getElementById('context-menu').style.display = 'none';
}
});"""
@app.get("/")
async def get_index():
return HTMLResponse(content=HTML_CONTENT)
@app.get("/static/app.js")
async def get_js():
from fastapi.responses import Response
return Response(content=JS_CONTENT, media_type="application/javascript")
@app.get("/static/app.css")
async def get_css():
from fastapi.responses import Response
return Response(content=CSS_CONTENT, media_type="text/css")
@app.websocket("/ws/{username}")
async def websocket_endpoint(websocket: WebSocket, username: str):
await websocket.accept()
connections[username] = websocket
user_data = get_or_create_user(username)
objects_data = load_objects()
await websocket.send_json({
"type": "init",
"username": username,
"color": user_data["color"],
"money": user_data["money"],
"objects": objects_data
})
try:
while True:
data = await websocket.receive_json()
if data["type"] == "place_object":
cost = BUILDING_COSTS.get(data["object_type"], 0)
if user_data["money"] >= cost:
user_data["money"] -= cost
obj_id = save_object(username, data["object_type"], data["x"], data["y"])
conn = sqlite3.connect('city_game.db')
c = conn.cursor()
c.execute("UPDATE users SET money=? WHERE username=?", (user_data["money"], username))
conn.commit()
conn.close()
new_obj = {
"id": obj_id,
"username": username,
"type": data["object_type"],
"x": data["x"],
"y": data["y"],
"name": ""
}
await broadcast({
"type": "object_placed",
"object": new_obj,
"money": user_data["money"]
})
elif data["type"] == "demolish":
if delete_object(data["id"], username):
await broadcast({"type": "object_deleted", "id": data["id"], "username": username})
elif data["type"] == "rename":
conn = sqlite3.connect('city_game.db')
c = conn.cursor()
c.execute("UPDATE objects SET name=? WHERE id=? AND username=?",
(data["name"], data["id"], username))
conn.commit()
conn.close()
await broadcast({"type": "object_renamed", "id": data["id"], "name": data["name"]})
elif data["type"] == "chat":
timestamp = datetime.now().isoformat()
conn = sqlite3.connect('city_game.db')
c = conn.cursor()
c.execute("INSERT INTO chat (username, message, timestamp) VALUES (?, ?, ?)",
(username, data["message"], timestamp))
conn.commit()
conn.close()
await broadcast({
"type": "chat",
"username": username,
"message": data["message"],
"timestamp": timestamp
})
elif data["type"] == "cursor":
await broadcast({
"type": "cursor",
"username": username,
"x": data["x"],
"y": data["y"],
"color": user_data["color"]
}, exclude=username)
except WebSocketDisconnect:
connections.pop(username, None)
user_cursors.pop(username, None)
# Economic system background task
async def economic_loop():
while True:
await asyncio.sleep(300)
conn = sqlite3.connect('city_game.db')
c = conn.cursor()
c.execute("""SELECT username, SUM(profit_rate) as total_profit
FROM objects GROUP BY username""")
profits = c.fetchall()
for username, profit in profits:
c.execute("SELECT money FROM users WHERE username=?", (username,))
current_money = c.fetchone()[0]
new_money = current_money + profit
c.execute("UPDATE users SET money=? WHERE username=?", (new_money, username))
if username in connections:
await connections[username].send_json({
"type": "money_update",
"username": username,
"money": new_money
})
conn.commit()
conn.close()
@app.on_event("startup")
async def startup_event():
asyncio.create_task(economic_loop())
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8595)