Initial commit.
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import sqlite3
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, List
|
||||
|
||||
class Database:
|
||||
"""Handles all database operations"""
|
||||
|
||||
def __init__(self, db_path: str = "data/game.db"):
|
||||
self.db_path = db_path
|
||||
Path("data").mkdir(exist_ok=True)
|
||||
|
||||
def _get_connection(self):
|
||||
"""Get database connection"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def init_db(self):
|
||||
"""Initialize database tables"""
|
||||
with self._get_connection() as conn:
|
||||
# Players table
|
||||
conn.execute('''
|
||||
CREATE TABLE IF NOT EXISTS players (
|
||||
player_id TEXT PRIMARY KEY,
|
||||
nickname TEXT UNIQUE NOT NULL,
|
||||
money INTEGER NOT NULL,
|
||||
population INTEGER NOT NULL,
|
||||
color TEXT NOT NULL,
|
||||
last_online REAL NOT NULL
|
||||
)
|
||||
''')
|
||||
|
||||
# Buildings table
|
||||
conn.execute('''
|
||||
CREATE TABLE IF NOT EXISTS buildings (
|
||||
x INTEGER NOT NULL,
|
||||
y INTEGER NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
owner_id TEXT NOT NULL,
|
||||
name TEXT,
|
||||
placed_at REAL NOT NULL,
|
||||
PRIMARY KEY (x, y),
|
||||
FOREIGN KEY (owner_id) REFERENCES players (player_id)
|
||||
)
|
||||
''')
|
||||
|
||||
conn.commit()
|
||||
|
||||
def save_game_state(self, game_state):
|
||||
"""Save complete game state to database"""
|
||||
with self._get_connection() as conn:
|
||||
# Save players
|
||||
conn.execute('DELETE FROM players')
|
||||
|
||||
for player in game_state.players.values():
|
||||
conn.execute('''
|
||||
INSERT INTO players (player_id, nickname, money, population, color, last_online)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
''', (
|
||||
player.player_id,
|
||||
player.nickname,
|
||||
player.money,
|
||||
player.population,
|
||||
player.color,
|
||||
player.last_online
|
||||
))
|
||||
|
||||
# Save buildings
|
||||
conn.execute('DELETE FROM buildings')
|
||||
|
||||
for building in game_state.buildings.values():
|
||||
conn.execute('''
|
||||
INSERT INTO buildings (x, y, type, owner_id, name, placed_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
''', (
|
||||
building.x,
|
||||
building.y,
|
||||
building.building_type.value,
|
||||
building.owner_id,
|
||||
building.name,
|
||||
building.placed_at
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
|
||||
def load_game_state(self) -> dict:
|
||||
"""Load complete game state from database"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
# Load players
|
||||
players = []
|
||||
cursor = conn.execute('SELECT * FROM players')
|
||||
for row in cursor:
|
||||
players.append({
|
||||
"player_id": row["player_id"],
|
||||
"nickname": row["nickname"],
|
||||
"money": row["money"],
|
||||
"population": row["population"],
|
||||
"color": row["color"],
|
||||
"last_online": row["last_online"]
|
||||
})
|
||||
|
||||
# Load buildings
|
||||
buildings = []
|
||||
cursor = conn.execute('SELECT * FROM buildings')
|
||||
for row in cursor:
|
||||
buildings.append({
|
||||
"x": row["x"],
|
||||
"y": row["y"],
|
||||
"type": row["type"],
|
||||
"owner_id": row["owner_id"],
|
||||
"name": row["name"],
|
||||
"placed_at": row["placed_at"]
|
||||
})
|
||||
|
||||
return {
|
||||
"players": players,
|
||||
"buildings": buildings
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error loading game state: {e}")
|
||||
return {"players": [], "buildings": []}
|
||||
@@ -0,0 +1,72 @@
|
||||
from server.game_state import GameState
|
||||
from server.models import BUILDING_CONFIGS, BuildingType
|
||||
import time
|
||||
|
||||
class EconomyEngine:
|
||||
"""Handles all economy calculations and ticks"""
|
||||
|
||||
def __init__(self, game_state: GameState):
|
||||
self.game_state = game_state
|
||||
|
||||
def tick(self):
|
||||
"""Process one economy tick for all players"""
|
||||
current_time = time.time()
|
||||
|
||||
for player in self.game_state.players.values():
|
||||
# Calculate power factor (10% if offline, 100% if online)
|
||||
time_diff = current_time - player.last_online
|
||||
if player.is_online:
|
||||
power_factor = 1.0
|
||||
else:
|
||||
power_factor = 0.1
|
||||
|
||||
# Process player economy
|
||||
self._process_player_economy(player, power_factor)
|
||||
|
||||
def _process_player_economy(self, player, power_factor: float):
|
||||
"""Process economy for a single player"""
|
||||
total_income = 0
|
||||
total_population = 0
|
||||
|
||||
# Get all player buildings
|
||||
buildings = self.game_state.get_player_buildings(player.player_id)
|
||||
|
||||
for building in buildings:
|
||||
config = BUILDING_CONFIGS[building.building_type]
|
||||
|
||||
# Calculate base income
|
||||
base_income = config.income
|
||||
|
||||
# Apply connectivity bonus for income-generating buildings
|
||||
if base_income > 0:
|
||||
zone_size = self.game_state.get_building_zone_size(building.x, building.y)
|
||||
connectivity_bonus = 1.0 + (zone_size * 0.05) # 5% per road in zone
|
||||
base_income = int(base_income * connectivity_bonus)
|
||||
|
||||
# Add to totals
|
||||
total_income += base_income
|
||||
total_population += config.population
|
||||
|
||||
# Apply power factor
|
||||
total_income = int(total_income * power_factor)
|
||||
|
||||
# Update player stats
|
||||
player.money += total_income
|
||||
player.population = max(0, total_population)
|
||||
|
||||
# Prevent negative money (but allow debt for realism)
|
||||
if player.money < -100000:
|
||||
player.money = -100000
|
||||
|
||||
def calculate_building_stats(self, player_id: str, building_type: BuildingType) -> dict:
|
||||
"""Calculate what a building would produce for a player"""
|
||||
config = BUILDING_CONFIGS[building_type]
|
||||
|
||||
return {
|
||||
"cost": config.cost,
|
||||
"income": config.income,
|
||||
"population": config.population,
|
||||
"power_required": config.power_required,
|
||||
"requires_population": config.requires_population,
|
||||
"description": config.description
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
from typing import Dict, List, Optional, Set, Tuple
|
||||
from server.models import Player, Building, BuildingType, BUILDING_CONFIGS
|
||||
import time
|
||||
|
||||
class GameState:
|
||||
"""Manages the complete game state"""
|
||||
|
||||
def __init__(self):
|
||||
self.players: Dict[str, Player] = {}
|
||||
self.buildings: Dict[Tuple[int, int], Building] = {}
|
||||
self.road_network: Set[Tuple[int, int]] = set()
|
||||
self.connected_zones: List[Set[Tuple[int, int]]] = []
|
||||
|
||||
def get_or_create_player(self, nickname: str, player_id: str) -> Player:
|
||||
"""Get existing player or create new one"""
|
||||
if player_id in self.players:
|
||||
player = self.players[player_id]
|
||||
player.is_online = True
|
||||
player.last_online = time.time()
|
||||
return player
|
||||
|
||||
player = Player(
|
||||
player_id=player_id,
|
||||
nickname=nickname,
|
||||
last_online=time.time()
|
||||
)
|
||||
self.players[player_id] = player
|
||||
return player
|
||||
|
||||
def place_building(self, player_id: str, building_type: str, x: int, y: int) -> dict:
|
||||
"""Place a building on the map"""
|
||||
# Check if tile is occupied
|
||||
if (x, y) in self.buildings:
|
||||
return {"success": False, "error": "Tile already occupied"}
|
||||
|
||||
# Get player
|
||||
player = self.players.get(player_id)
|
||||
if not player:
|
||||
return {"success": False, "error": "Player not found"}
|
||||
|
||||
# Get building config
|
||||
try:
|
||||
b_type = BuildingType(building_type)
|
||||
config = BUILDING_CONFIGS[b_type]
|
||||
except (ValueError, KeyError):
|
||||
return {"success": False, "error": "Invalid building type"}
|
||||
|
||||
# Check if player can afford
|
||||
if not player.can_afford(config.cost):
|
||||
return {"success": False, "error": "Not enough money"}
|
||||
|
||||
# Check requirements
|
||||
if config.requires_population > player.population:
|
||||
return {"success": False, "error": f"Requires {config.requires_population} population"}
|
||||
|
||||
if config.power_required and not self._has_power_plant(player_id):
|
||||
return {"success": False, "error": "Requires power plant"}
|
||||
|
||||
# Place building
|
||||
building = Building(
|
||||
building_type=b_type,
|
||||
x=x,
|
||||
y=y,
|
||||
owner_id=player_id,
|
||||
placed_at=time.time()
|
||||
)
|
||||
|
||||
self.buildings[(x, y)] = building
|
||||
player.deduct_money(config.cost)
|
||||
|
||||
# Update road network if it's a road
|
||||
if b_type == BuildingType.ROAD:
|
||||
self.road_network.add((x, y))
|
||||
self._update_connected_zones()
|
||||
|
||||
return {"success": True, "building": building.to_dict()}
|
||||
|
||||
def remove_building(self, player_id: str, x: int, y: int) -> dict:
|
||||
"""Remove a building"""
|
||||
building = self.buildings.get((x, y))
|
||||
|
||||
if not building:
|
||||
return {"success": False, "error": "No building at this location"}
|
||||
|
||||
if building.owner_id != player_id:
|
||||
return {"success": False, "error": "You don't own this building"}
|
||||
|
||||
# Remove building
|
||||
del self.buildings[(x, y)]
|
||||
|
||||
# Update road network if it was a road
|
||||
if building.building_type == BuildingType.ROAD:
|
||||
self.road_network.discard((x, y))
|
||||
self._update_connected_zones()
|
||||
|
||||
return {"success": True}
|
||||
|
||||
def edit_building_name(self, player_id: str, x: int, y: int, name: str) -> dict:
|
||||
"""Edit building name"""
|
||||
building = self.buildings.get((x, y))
|
||||
|
||||
if not building:
|
||||
return {"success": False, "error": "No building at this location"}
|
||||
|
||||
if building.owner_id != player_id:
|
||||
return {"success": False, "error": "You don't own this building"}
|
||||
|
||||
building.name = name
|
||||
return {"success": True}
|
||||
|
||||
def _has_power_plant(self, player_id: str) -> bool:
|
||||
"""Check if player has a power plant"""
|
||||
for building in self.buildings.values():
|
||||
if (building.owner_id == player_id and
|
||||
building.building_type == BuildingType.POWER_PLANT):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _update_connected_zones(self):
|
||||
"""Update connected zones based on road network using flood fill"""
|
||||
if not self.road_network:
|
||||
self.connected_zones = []
|
||||
return
|
||||
|
||||
visited = set()
|
||||
self.connected_zones = []
|
||||
|
||||
for road_pos in self.road_network:
|
||||
if road_pos in visited:
|
||||
continue
|
||||
|
||||
# Flood fill to find connected zone
|
||||
zone = set()
|
||||
stack = [road_pos]
|
||||
|
||||
while stack:
|
||||
pos = stack.pop()
|
||||
if pos in visited:
|
||||
continue
|
||||
|
||||
visited.add(pos)
|
||||
zone.add(pos)
|
||||
|
||||
# Check adjacent positions
|
||||
x, y = pos
|
||||
for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
|
||||
adj_pos = (x + dx, y + dy)
|
||||
if adj_pos in self.road_network and adj_pos not in visited:
|
||||
stack.append(adj_pos)
|
||||
|
||||
self.connected_zones.append(zone)
|
||||
|
||||
def get_building_zone_size(self, x: int, y: int) -> int:
|
||||
"""Get the size of the connected zone for a building"""
|
||||
# Find adjacent roads
|
||||
for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0), (1, 1), (-1, 1), (1, -1), (-1, -1)]:
|
||||
road_pos = (x + dx, y + dy)
|
||||
if road_pos in self.road_network:
|
||||
# Find which zone this road belongs to
|
||||
for zone in self.connected_zones:
|
||||
if road_pos in zone:
|
||||
return len(zone)
|
||||
return 0
|
||||
|
||||
def get_player_buildings(self, player_id: str) -> List[Building]:
|
||||
"""Get all buildings owned by a player"""
|
||||
return [b for b in self.buildings.values() if b.owner_id == player_id]
|
||||
|
||||
def get_state(self) -> dict:
|
||||
"""Get complete game state for broadcasting"""
|
||||
return {
|
||||
"players": {pid: p.to_dict() for pid, p in self.players.items()},
|
||||
"buildings": {f"{x},{y}": b.to_dict() for (x, y), b in self.buildings.items()}
|
||||
}
|
||||
|
||||
def load_state(self, state: dict):
|
||||
"""Load game state from database"""
|
||||
if not state:
|
||||
return
|
||||
|
||||
# Load players
|
||||
for player_data in state.get("players", []):
|
||||
player = Player(**player_data)
|
||||
player.is_online = False
|
||||
self.players[player.player_id] = player
|
||||
|
||||
# Load buildings
|
||||
for building_data in state.get("buildings", []):
|
||||
building = Building(
|
||||
building_type=BuildingType(building_data["type"]),
|
||||
x=building_data["x"],
|
||||
y=building_data["y"],
|
||||
owner_id=building_data["owner_id"],
|
||||
name=building_data.get("name"),
|
||||
placed_at=building_data.get("placed_at", 0.0)
|
||||
)
|
||||
self.buildings[(building.x, building.y)] = building
|
||||
|
||||
if building.building_type == BuildingType.ROAD:
|
||||
self.road_network.add((building.x, building.y))
|
||||
|
||||
self._update_connected_zones()
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse
|
||||
from pathlib import Path
|
||||
|
||||
from server.websocket_manager import WebSocketManager
|
||||
from server.game_state import GameState
|
||||
from server.economy import EconomyEngine
|
||||
from server.database import Database
|
||||
|
||||
# Global instances
|
||||
ws_manager = WebSocketManager()
|
||||
game_state = GameState()
|
||||
economy_engine = EconomyEngine(game_state)
|
||||
database = Database()
|
||||
|
||||
# Background task for economy ticks and persistence
|
||||
async def game_loop():
|
||||
"""Main game loop: economy ticks every 10 seconds, DB save every 10 seconds"""
|
||||
while True:
|
||||
await asyncio.sleep(10)
|
||||
|
||||
# Economy tick
|
||||
economy_engine.tick()
|
||||
|
||||
# Save to database
|
||||
database.save_game_state(game_state)
|
||||
|
||||
# Broadcast state to all players
|
||||
await ws_manager.broadcast_game_state(game_state.get_state())
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Startup and shutdown events"""
|
||||
# Startup
|
||||
database.init_db()
|
||||
game_state.load_state(database.load_game_state())
|
||||
|
||||
# Start game loop
|
||||
task = asyncio.create_task(game_loop())
|
||||
|
||||
yield
|
||||
|
||||
# Shutdown
|
||||
task.cancel()
|
||||
database.save_game_state(game_state)
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
# Mount static files
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Serve index.html"""
|
||||
return FileResponse("static/index.html")
|
||||
|
||||
@app.websocket("/ws/{nickname}")
|
||||
async def websocket_endpoint(websocket: WebSocket, nickname: str):
|
||||
"""WebSocket endpoint for real-time game communication"""
|
||||
await ws_manager.connect(websocket, nickname)
|
||||
|
||||
try:
|
||||
# Send initial game state
|
||||
player_id = await ws_manager.get_player_id(websocket)
|
||||
player = game_state.get_or_create_player(nickname, player_id)
|
||||
|
||||
await websocket.send_json({
|
||||
"type": "init",
|
||||
"player": player.to_dict(),
|
||||
"game_state": game_state.get_state()
|
||||
})
|
||||
|
||||
# Listen for messages
|
||||
while True:
|
||||
data = await websocket.receive_json()
|
||||
await handle_message(websocket, data)
|
||||
|
||||
except WebSocketDisconnect:
|
||||
await ws_manager.disconnect(websocket)
|
||||
|
||||
async def handle_message(websocket: WebSocket, data: dict):
|
||||
"""Handle incoming WebSocket messages"""
|
||||
msg_type = data.get("type")
|
||||
player_id = await ws_manager.get_player_id(websocket)
|
||||
|
||||
if msg_type == "cursor_move":
|
||||
# Broadcast cursor position
|
||||
await ws_manager.broadcast({
|
||||
"type": "cursor_move",
|
||||
"player_id": player_id,
|
||||
"x": data["x"],
|
||||
"y": data["y"]
|
||||
}, exclude=websocket)
|
||||
|
||||
elif msg_type == "place_building":
|
||||
# Place building
|
||||
result = game_state.place_building(
|
||||
player_id,
|
||||
data["building_type"],
|
||||
data["x"],
|
||||
data["y"]
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
# Broadcast to all players
|
||||
await ws_manager.broadcast({
|
||||
"type": "building_placed",
|
||||
"building": result["building"]
|
||||
})
|
||||
else:
|
||||
# Send error to player
|
||||
await websocket.send_json({
|
||||
"type": "error",
|
||||
"message": result["error"]
|
||||
})
|
||||
|
||||
elif msg_type == "remove_building":
|
||||
# Remove building
|
||||
result = game_state.remove_building(player_id, data["x"], data["y"])
|
||||
|
||||
if result["success"]:
|
||||
await ws_manager.broadcast({
|
||||
"type": "building_removed",
|
||||
"x": data["x"],
|
||||
"y": data["y"]
|
||||
})
|
||||
|
||||
elif msg_type == "edit_building":
|
||||
# Edit building name
|
||||
result = game_state.edit_building_name(
|
||||
player_id,
|
||||
data["x"],
|
||||
data["y"],
|
||||
data["name"]
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
await ws_manager.broadcast({
|
||||
"type": "building_updated",
|
||||
"x": data["x"],
|
||||
"y": data["y"],
|
||||
"name": data["name"]
|
||||
})
|
||||
|
||||
elif msg_type == "chat":
|
||||
# Broadcast chat message
|
||||
nickname = await ws_manager.get_nickname(websocket)
|
||||
await ws_manager.broadcast({
|
||||
"type": "chat",
|
||||
"nickname": nickname,
|
||||
"message": data["message"],
|
||||
"timestamp": data["timestamp"]
|
||||
})
|
||||
@@ -0,0 +1,192 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional
|
||||
from enum import Enum
|
||||
import random
|
||||
|
||||
class BuildingType(Enum):
|
||||
"""All available building types"""
|
||||
# Residential
|
||||
SMALL_HOUSE = "small_house"
|
||||
MEDIUM_HOUSE = "medium_house"
|
||||
LARGE_HOUSE = "large_house"
|
||||
|
||||
# Commercial
|
||||
SMALL_SHOP = "small_shop"
|
||||
SUPERMARKET = "supermarket"
|
||||
MALL = "mall"
|
||||
|
||||
# Industrial
|
||||
SMALL_FACTORY = "small_factory"
|
||||
LARGE_FACTORY = "large_factory"
|
||||
|
||||
# Infrastructure
|
||||
ROAD = "road"
|
||||
PARK = "park"
|
||||
PLAZA = "plaza"
|
||||
|
||||
# Special
|
||||
TOWN_HALL = "town_hall"
|
||||
POWER_PLANT = "power_plant"
|
||||
|
||||
@dataclass
|
||||
class BuildingConfig:
|
||||
"""Configuration for each building type"""
|
||||
name: str
|
||||
cost: int
|
||||
income: int # Per tick
|
||||
population: int # Positive = adds population, negative = requires jobs
|
||||
power_required: bool = False
|
||||
requires_population: int = 0
|
||||
description: str = ""
|
||||
|
||||
# Building configurations
|
||||
BUILDING_CONFIGS = {
|
||||
BuildingType.SMALL_HOUSE: BuildingConfig(
|
||||
name="Small House",
|
||||
cost=5000,
|
||||
income=-50,
|
||||
population=10,
|
||||
description="Basic residential building"
|
||||
),
|
||||
BuildingType.MEDIUM_HOUSE: BuildingConfig(
|
||||
name="Medium House",
|
||||
cost=12000,
|
||||
income=-120,
|
||||
population=25,
|
||||
description="Medium residential building"
|
||||
),
|
||||
BuildingType.LARGE_HOUSE: BuildingConfig(
|
||||
name="Large House",
|
||||
cost=25000,
|
||||
income=-250,
|
||||
population=50,
|
||||
power_required=True,
|
||||
description="Large residential building"
|
||||
),
|
||||
BuildingType.SMALL_SHOP: BuildingConfig(
|
||||
name="Small Shop",
|
||||
cost=8000,
|
||||
income=100,
|
||||
population=-5,
|
||||
requires_population=20,
|
||||
description="Small retail store"
|
||||
),
|
||||
BuildingType.SUPERMARKET: BuildingConfig(
|
||||
name="Supermarket",
|
||||
cost=25000,
|
||||
income=300,
|
||||
population=-15,
|
||||
requires_population=50,
|
||||
power_required=True,
|
||||
description="Large grocery store"
|
||||
),
|
||||
BuildingType.MALL: BuildingConfig(
|
||||
name="Shopping Mall",
|
||||
cost=80000,
|
||||
income=800,
|
||||
population=-40,
|
||||
requires_population=100,
|
||||
power_required=True,
|
||||
description="Large shopping center"
|
||||
),
|
||||
BuildingType.SMALL_FACTORY: BuildingConfig(
|
||||
name="Small Factory",
|
||||
cost=15000,
|
||||
income=200,
|
||||
population=-20,
|
||||
power_required=True,
|
||||
description="Small industrial building"
|
||||
),
|
||||
BuildingType.LARGE_FACTORY: BuildingConfig(
|
||||
name="Large Factory",
|
||||
cost=50000,
|
||||
income=500,
|
||||
population=-50,
|
||||
power_required=True,
|
||||
description="Large industrial complex"
|
||||
),
|
||||
BuildingType.ROAD: BuildingConfig(
|
||||
name="Road",
|
||||
cost=500,
|
||||
income=0,
|
||||
population=0,
|
||||
description="Connects buildings for economy boost"
|
||||
),
|
||||
BuildingType.PARK: BuildingConfig(
|
||||
name="Park",
|
||||
cost=3000,
|
||||
income=-20,
|
||||
population=5,
|
||||
description="Increases population happiness"
|
||||
),
|
||||
BuildingType.PLAZA: BuildingConfig(
|
||||
name="Plaza",
|
||||
cost=8000,
|
||||
income=-40,
|
||||
population=10,
|
||||
description="Large public space"
|
||||
),
|
||||
BuildingType.TOWN_HALL: BuildingConfig(
|
||||
name="Town Hall",
|
||||
cost=50000,
|
||||
income=-100,
|
||||
population=100,
|
||||
description="City administration building"
|
||||
),
|
||||
BuildingType.POWER_PLANT: BuildingConfig(
|
||||
name="Power Plant",
|
||||
cost=100000,
|
||||
income=-500,
|
||||
population=-30,
|
||||
description="Provides power to buildings"
|
||||
)
|
||||
}
|
||||
|
||||
@dataclass
|
||||
class Building:
|
||||
"""A placed building in the game"""
|
||||
building_type: BuildingType
|
||||
x: int
|
||||
y: int
|
||||
owner_id: str
|
||||
name: Optional[str] = None
|
||||
placed_at: float = 0.0
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"type": self.building_type.value,
|
||||
"x": self.x,
|
||||
"y": self.y,
|
||||
"owner_id": self.owner_id,
|
||||
"name": self.name
|
||||
}
|
||||
|
||||
@dataclass
|
||||
class Player:
|
||||
"""Player data"""
|
||||
player_id: str
|
||||
nickname: str
|
||||
money: int = 100000 # Starting money
|
||||
population: int = 0
|
||||
color: str = field(default_factory=lambda: f"#{random.randint(0, 0xFFFFFF):06x}")
|
||||
last_online: float = 0.0
|
||||
is_online: bool = True
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"player_id": self.player_id,
|
||||
"nickname": self.nickname,
|
||||
"money": self.money,
|
||||
"population": self.population,
|
||||
"color": self.color,
|
||||
"is_online": self.is_online
|
||||
}
|
||||
|
||||
def can_afford(self, cost: int) -> bool:
|
||||
return self.money >= cost
|
||||
|
||||
def deduct_money(self, amount: int):
|
||||
self.money -= amount
|
||||
|
||||
def add_money(self, amount: int):
|
||||
self.money += amount
|
||||
@@ -0,0 +1,90 @@
|
||||
from fastapi import WebSocket
|
||||
from typing import Dict, Set
|
||||
import uuid
|
||||
|
||||
class WebSocketManager:
|
||||
"""Manages WebSocket connections for multiplayer"""
|
||||
|
||||
def __init__(self):
|
||||
self.active_connections: Dict[str, WebSocket] = {}
|
||||
self.player_nicknames: Dict[str, str] = {}
|
||||
self.nickname_to_id: Dict[str, str] = {}
|
||||
|
||||
async def connect(self, websocket: WebSocket, nickname: str):
|
||||
"""Connect a new player"""
|
||||
await websocket.accept()
|
||||
|
||||
# Generate or reuse player ID
|
||||
if nickname in self.nickname_to_id:
|
||||
player_id = self.nickname_to_id[nickname]
|
||||
else:
|
||||
player_id = str(uuid.uuid4())
|
||||
self.nickname_to_id[nickname] = player_id
|
||||
|
||||
self.active_connections[player_id] = websocket
|
||||
self.player_nicknames[player_id] = nickname
|
||||
|
||||
# Broadcast player joined
|
||||
await self.broadcast({
|
||||
"type": "player_joined",
|
||||
"player_id": player_id,
|
||||
"nickname": nickname
|
||||
})
|
||||
|
||||
async def disconnect(self, websocket: WebSocket):
|
||||
"""Disconnect a player"""
|
||||
player_id = None
|
||||
for pid, ws in self.active_connections.items():
|
||||
if ws == websocket:
|
||||
player_id = pid
|
||||
break
|
||||
|
||||
if player_id:
|
||||
del self.active_connections[player_id]
|
||||
nickname = self.player_nicknames.pop(player_id, None)
|
||||
|
||||
# Broadcast player left
|
||||
await self.broadcast({
|
||||
"type": "player_left",
|
||||
"player_id": player_id,
|
||||
"nickname": nickname
|
||||
})
|
||||
|
||||
async def broadcast(self, message: dict, exclude: WebSocket = None):
|
||||
"""Broadcast message to all connected players"""
|
||||
disconnected = []
|
||||
|
||||
for player_id, websocket in self.active_connections.items():
|
||||
if websocket == exclude:
|
||||
continue
|
||||
|
||||
try:
|
||||
await websocket.send_json(message)
|
||||
except Exception:
|
||||
disconnected.append(player_id)
|
||||
|
||||
# Clean up disconnected websockets
|
||||
for player_id in disconnected:
|
||||
if player_id in self.active_connections:
|
||||
del self.active_connections[player_id]
|
||||
if player_id in self.player_nicknames:
|
||||
del self.player_nicknames[player_id]
|
||||
|
||||
async def broadcast_game_state(self, state: dict):
|
||||
"""Broadcast full game state"""
|
||||
await self.broadcast({
|
||||
"type": "game_state_update",
|
||||
"state": state
|
||||
})
|
||||
|
||||
async def get_player_id(self, websocket: WebSocket) -> str:
|
||||
"""Get player ID from websocket"""
|
||||
for player_id, ws in self.active_connections.items():
|
||||
if ws == websocket:
|
||||
return player_id
|
||||
return None
|
||||
|
||||
async def get_nickname(self, websocket: WebSocket) -> str:
|
||||
"""Get nickname from websocket"""
|
||||
player_id = await self.get_player_id(websocket)
|
||||
return self.player_nicknames.get(player_id, "Unknown")
|
||||
Reference in New Issue
Block a user