diff --git a/nanocity.py b/nanocity.py
new file mode 100644
index 0000000..62e8fe4
--- /dev/null
+++ b/nanocity.py
@@ -0,0 +1,887 @@
+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 = """
+
+
+
+ City Tycoon
+
+
+
+
+
+
CITY TYCOON
+
+
+
+
+
+
+
+
+
+
+
+
RENAME
+
+
+
+
+
+
+
+"""
+
+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)