Created
June 29, 2026 16:10
-
-
Save EncodeTheCode/7e581832ea01387f12c3ae0a696f17b0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import asyncio | |
| import websockets | |
| import json | |
| import time | |
| PORT = 4445 | |
| TICK_RATE = 20 | |
| TIMEOUT = 5.0 | |
| # ========================= | |
| # PLAYER CLASS (FULL) | |
| # ========================= | |
| class Player: | |
| def __init__(self, ws): | |
| self.ws = ws | |
| # identity | |
| self.user_id = None | |
| self.authenticated = False | |
| # position (current) | |
| self.x = self.y = self.z = 0.0 | |
| # position (previous for interpolation) | |
| self.px = self.py = self.pz = 0.0 | |
| # gameplay | |
| self.health = 100 | |
| self.armor = 0 | |
| self.weapon_id = 0 | |
| self.primary_ammo = 30 | |
| self.secondary_ammo = 90 | |
| self.walking = False | |
| self.running = False | |
| # networking | |
| self.last_seen = time.time() | |
| self.ping = 0.0 | |
| # ========================= | |
| # AUTH | |
| # ========================= | |
| async def auth(self, msg): | |
| self.user_id = msg.get("user_id") | |
| token = msg.get("token") | |
| if self.user_id and token and len(token) > 5: | |
| self.authenticated = True | |
| await self.ws.send(json.dumps({ | |
| "type": "auth_ok", | |
| "user_id": self.user_id | |
| })) | |
| # ========================= | |
| # APPLY MOVEMENT (with interp support) | |
| # ========================= | |
| def move(self, msg): | |
| # store previous BEFORE moving (IMPORTANT for interpolation) | |
| self.px, self.py, self.pz = self.x, self.y, self.z | |
| self.x += float(msg.get("dx", 0)) | |
| self.y += float(msg.get("dy", 0)) | |
| self.z += float(msg.get("dz", 0)) | |
| self.walking = bool(msg.get("walking", False)) | |
| self.running = bool(msg.get("running", False)) | |
| # ========================= | |
| # COMBAT | |
| # ========================= | |
| def combat(self, msg): | |
| self.weapon_id = msg.get("weapon_id", self.weapon_id) | |
| self.primary_ammo = msg.get("primary_ammo", self.primary_ammo) | |
| self.secondary_ammo = msg.get("secondary_ammo", self.secondary_ammo) | |
| # ========================= | |
| # DAMAGE | |
| # ========================= | |
| def damage(self, msg): | |
| dmg = int(msg.get("amount", 0)) | |
| absorbed = min(self.armor, dmg) | |
| self.armor -= absorbed | |
| dmg -= absorbed | |
| self.health = max(0, self.health - dmg) | |
| # ========================= | |
| # PING | |
| # ========================= | |
| def ping_update(self, msg): | |
| client_time = float(msg.get("t", time.time())) | |
| self.ping = time.time() - client_time | |
| # ========================= | |
| # TOUCH | |
| # ========================= | |
| def touch(self): | |
| self.last_seen = time.time() | |
| # ========================= | |
| # SERIALIZE (SEND TO CLIENT) | |
| # includes interpolation data | |
| # ========================= | |
| def serialize(self): | |
| return { | |
| "user_id": self.user_id, | |
| "x": self.x, | |
| "y": self.y, | |
| "z": self.z, | |
| "px": self.px, | |
| "py": self.py, | |
| "pz": self.pz, | |
| "health": self.health, | |
| "armor": self.armor, | |
| "weapon_id": self.weapon_id, | |
| "primary_ammo": self.primary_ammo, | |
| "secondary_ammo": self.secondary_ammo, | |
| "walking": self.walking, | |
| "running": self.running, | |
| "ping": round(self.ping, 3) | |
| } | |
| # ========================= | |
| # GAME SERVER | |
| # ========================= | |
| class GameServer: | |
| def __init__(self): | |
| self.players = {} | |
| def get_player(self, ws): | |
| if ws not in self.players: | |
| self.players[ws] = Player(ws) | |
| return self.players[ws] | |
| # ========================= | |
| # HANDLE MESSAGE | |
| # ========================= | |
| async def handle(self, ws, msg): | |
| player = self.get_player(ws) | |
| player.touch() | |
| t = msg.get("type") | |
| # AUTH FIRST | |
| if not player.authenticated: | |
| if t == "auth": | |
| await player.auth(msg) | |
| return | |
| if t == "update": | |
| player.move(msg) | |
| elif t == "combat": | |
| player.combat(msg) | |
| elif t == "damage": | |
| player.damage(msg) | |
| elif t == "ping": | |
| player.ping_update(msg) | |
| # ========================= | |
| # CLEANUP | |
| # ========================= | |
| def cleanup(self): | |
| now = time.time() | |
| dead = [] | |
| for ws, p in self.players.items(): | |
| if now - p.last_seen > TIMEOUT: | |
| dead.append(ws) | |
| for ws in dead: | |
| del self.players[ws] | |
| # ========================= | |
| # WORLD SNAPSHOT | |
| # ========================= | |
| def world(self): | |
| return { | |
| "type": "world", | |
| "players": { | |
| str(id(p.ws)): p.serialize() | |
| for p in self.players.values() | |
| } | |
| } | |
| server = GameServer() | |
| # ========================= | |
| # WS HANDLER | |
| # ========================= | |
| async def handler(ws): | |
| try: | |
| async for message in ws: | |
| msg = json.loads(message) | |
| await server.handle(ws, msg) | |
| finally: | |
| if ws in server.players: | |
| del server.players[ws] | |
| # ========================= | |
| # BROADCAST LOOP | |
| # ========================= | |
| async def broadcast(): | |
| interval = 1 / TICK_RATE | |
| while True: | |
| server.cleanup() | |
| packet = json.dumps(server.world()) | |
| dead = [] | |
| for p in list(server.players.values()): | |
| try: | |
| await p.ws.send(packet) | |
| except: | |
| dead.append(p.ws) | |
| for ws in dead: | |
| server.players.pop(ws, None) | |
| await asyncio.sleep(interval) | |
| async def main(): | |
| async with websockets.serve(handler, "0.0.0.0", PORT): | |
| print(f"Server running ws://localhost:{PORT}") | |
| await broadcast() | |
| asyncio.run(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment