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
""" 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)