| | import os |
| | import json |
| | import asyncio |
| | import math |
| | import uuid |
| | import random |
| | import time |
| | from collections import defaultdict |
| | from aiohttp import web, ClientSession |
| | import socketio |
| |
|
| | |
| | PORT = int(os.getenv('SERVER_PORT', 8080)) |
| | ADMIN_PATH = os.getenv('ADMIN_PATH', '/secret-admin-panel') |
| |
|
| | |
| | DISCORD_CLIENT_ID = "1450064181972832409" |
| | DISCORD_CLIENT_SECRET = "F1Tm1JL5sDmbOn9RGT-qYKyc7Q0_9IsW" |
| | DISCORD_REDIRECT_URI = "https://orbitmc-yt.hf.space/callback" |
| |
|
| | GAME_TICK_RATE = 30 |
| | GRID_SIZE = 8 |
| | BLOCK_SIZE = 280 |
| | ROAD_WIDTH = 90 |
| | CELL_SIZE = BLOCK_SIZE + ROAD_WIDTH |
| | MAP_WIDTH = GRID_SIZE * CELL_SIZE |
| | MAP_HEIGHT = GRID_SIZE * CELL_SIZE |
| |
|
| | |
| | ENTITY_TYPES = { |
| | 'ROCK': {'r': 8, 'h': 8, 'color': '#7f8c8d', 'score': 1, 'type': 'rock'}, |
| | 'FLOWER': {'r': 6, 'h': 4, 'color': '#e84393', 'score': 1, 'type': 'flower'}, |
| | 'FENCE': {'w': 20, 'l': 4, 'h': 10, 'color': '#8e44ad', 'score': 1, 'type': 'building'}, |
| | 'HYDRANT': {'r': 6, 'h': 12, 'color': '#e74c3c', 'sideColor': '#c0392b', 'score': 1, 'type': 'cylinder'}, |
| | 'POST': {'r': 4, 'h': 40, 'color': '#bdc3c7', 'sideColor': '#7f8c8d', 'score': 2, 'type': 'cylinder'}, |
| | 'BUSH': {'r': 12, 'h': 10, 'color': '#2ecc71', 'score': 3, 'type': 'bush'}, |
| | 'TREE': {'r': 18, 'h': 70, 'color': '#27ae60', 'score': 5, 'type': 'tree'}, |
| | 'CAR': {'w': 48, 'l': 24, 'h': 12, 'cabinW': 28, 'cabinL': 20, 'cabinH': 10, 'color': '#e74c3c', 'score': 10, 'type': 'car', 'mobile': True}, |
| | 'HOUSE_S': {'w': 70, 'l': 70, 'h': 60, 'color': '#e67e22', 'sideColor': '#d35400', 'score': 50, 'type': 'building', 'windows': True}, |
| | 'HOUSE_M': {'w': 100, 'l': 90, 'h': 90, 'color': '#3498db', 'sideColor': '#2980b9', 'score': 100, 'type': 'building', 'windows': True}, |
| | 'OFFICE': {'w': 110, 'l': 110, 'h': 160, 'color': '#9b59b6', 'sideColor': '#8e44ad', 'score': 200, 'type': 'building', 'windows': True}, |
| | 'TOWER': {'w': 130, 'l': 130, 'h': 280, 'color': '#34495e', 'sideColor': '#2c3e50', 'score': 300, 'type': 'building', 'windows': True} |
| | } |
| |
|
| | sio = socketio.AsyncServer(async_mode='aiohttp', cors_allowed_origins='*') |
| | app = web.Application() |
| | sio.attach(app) |
| |
|
| | class GameState: |
| | def __init__(self): |
| | self.players = {} |
| | self.sockets = {} |
| | self.static_entities = [] |
| | |
| | self.static_grid = defaultdict(list) |
| | self.dynamic_entities = [] |
| | self.removed_entities = [] |
| | self.active = True |
| | self.user_data_file = 'user_data.json' |
| | self.persistent_data = self.load_data() |
| | self.generate_map() |
| |
|
| | def load_data(self): |
| | if os.path.exists(self.user_data_file): |
| | try: |
| | with open(self.user_data_file, 'r') as f: return json.load(f) |
| | except: return {} |
| | return {} |
| |
|
| | def save_data(self): |
| | with open(self.user_data_file, 'w') as f: |
| | json.dump(self.persistent_data, f, indent=2) |
| |
|
| | def generate_map(self): |
| | self.static_entities = [] |
| | self.static_grid.clear() |
| | self.dynamic_entities = [] |
| | |
| | |
| | for ix in range(GRID_SIZE + 1): |
| | for iy in range(GRID_SIZE + 1): |
| | x = ix * CELL_SIZE |
| | y = iy * CELL_SIZE |
| | if ix < GRID_SIZE and iy < GRID_SIZE: |
| | self.generate_block(x + ROAD_WIDTH/2, y + ROAD_WIDTH/2) |
| | |
| | |
| | lane_offset = ROAD_WIDTH / 4 |
| | for _ in range(60): |
| | self.spawn_car(lane_offset) |
| |
|
| | def add_entity(self, x, y, template, **kwargs): |
| | ent = template.copy() |
| | ent.update(kwargs) |
| | ent['x'] = x |
| | ent['y'] = y |
| | ent['id'] = str(uuid.uuid4()) |
| | ent['startX'] = x |
| | ent['startY'] = y |
| | ent['varSeed'] = random.random() |
| | ent['rotation'] = kwargs.get('rotation', 0) |
| | |
| | if ent['type'] in ['building', 'car']: |
| | ent['radius'] = math.hypot(ent['w']/2, ent.get('l', ent['w'])/2) |
| | else: |
| | ent['radius'] = ent['r'] |
| | |
| | if ent['type'] == 'flower': |
| | ent['color'] = random.choice(['#e84393', '#fd79a8', '#00b894', '#fab1a0']) |
| | |
| | |
| | self.static_entities.append(ent) |
| | gx = int(x // CELL_SIZE) |
| | gy = int(y // CELL_SIZE) |
| | self.static_grid[(gx, gy)].append(ent) |
| |
|
| | def get_nearby_static(self, x, y): |
| | |
| | gx = int(x // CELL_SIZE) |
| | gy = int(y // CELL_SIZE) |
| | nearby = [] |
| | for ix in range(gx - 1, gx + 2): |
| | for iy in range(gy - 1, gy + 2): |
| | nearby.extend(self.static_grid[(ix, iy)]) |
| | return nearby |
| |
|
| | def generate_block(self, bx, by): |
| | cx = bx + BLOCK_SIZE/2 |
| | cy = by + BLOCK_SIZE/2 |
| | b_type = random.random() |
| |
|
| | if b_type < 0.2: |
| | inset = 10 |
| | sz = BLOCK_SIZE - inset*2 |
| | for k in range(0, int(sz), 25): |
| | self.add_entity(bx + inset + k, by + inset, ENTITY_TYPES['FENCE']) |
| | self.add_entity(bx + inset + k, by + BLOCK_SIZE - inset, ENTITY_TYPES['FENCE']) |
| | for k in range(0, int(sz), 25): |
| | self.add_entity(bx + inset, by + inset + k, ENTITY_TYPES['FENCE'], w=5, l=20) |
| | self.add_entity(bx + BLOCK_SIZE - inset, by + inset + k, ENTITY_TYPES['FENCE'], w=5, l=20) |
| | |
| | for _ in range(10): |
| | ox = random.random() * (sz-40) + 20 |
| | oy = random.random() * (sz-40) + 20 |
| | tmpl = ENTITY_TYPES['TREE'] if random.random() > 0.6 else ENTITY_TYPES['FLOWER'] |
| | self.add_entity(bx+inset+ox, by+inset+oy, tmpl) |
| | self.add_entity(cx, cy, ENTITY_TYPES['HYDRANT']) |
| | |
| | elif b_type < 0.6: |
| | q = BLOCK_SIZE/4 |
| | self.add_entity(cx - q, cy - q, ENTITY_TYPES['HOUSE_S']) |
| | self.add_entity(cx + q, cy - q, ENTITY_TYPES['HOUSE_M']) |
| | self.add_entity(cx - q, cy + q, ENTITY_TYPES['HOUSE_M']) |
| | self.add_entity(cx + q, cy + q, ENTITY_TYPES['HOUSE_S']) |
| | self.add_entity(cx, cy, ENTITY_TYPES['TREE']) |
| | self.add_entity(bx + 10, cy, ENTITY_TYPES['BUSH']) |
| | self.add_entity(bx + BLOCK_SIZE - 10, cy, ENTITY_TYPES['BUSH']) |
| | |
| | else: |
| | if random.random() > 0.5: |
| | self.add_entity(cx, cy, ENTITY_TYPES['TOWER']) |
| | else: |
| | self.add_entity(cx - 50, cy, ENTITY_TYPES['OFFICE']) |
| | self.add_entity(cx + 50, cy, ENTITY_TYPES['OFFICE']) |
| | |
| | self.add_entity(bx + 20, by + 20, ENTITY_TYPES['POST']) |
| | self.add_entity(bx + BLOCK_SIZE - 20, by + 20, ENTITY_TYPES['POST']) |
| | self.add_entity(bx + 20, by + BLOCK_SIZE - 20, ENTITY_TYPES['POST']) |
| | self.add_entity(bx + BLOCK_SIZE - 20, by + BLOCK_SIZE - 20, ENTITY_TYPES['POST']) |
| |
|
| | def spawn_car(self, offset): |
| | orient = 0 if random.random() > 0.5 else 1 |
| | if orient == 0: |
| | iy = random.randint(0, GRID_SIZE) |
| | y = iy * CELL_SIZE |
| | x = random.random() * MAP_WIDTH |
| | dir = 1 if random.random() > 0.5 else -1 |
| | y += dir * offset |
| | rot = 0 if dir == 1 else math.pi |
| | else: |
| | ix = random.randint(0, GRID_SIZE) |
| | x = ix * CELL_SIZE |
| | y = random.random() * MAP_HEIGHT |
| | dir = 1 if random.random() > 0.5 else -1 |
| | x += dir * offset |
| | rot = math.pi/2 if dir == 1 else -math.pi/2 |
| |
|
| | car = ENTITY_TYPES['CAR'].copy() |
| | car.update({ |
| | 'x': x, 'y': y, 'rotation': rot, 'id': str(uuid.uuid4()), |
| | 'radius': math.hypot(24, 12), 'type': 'car', 'mobile': True, |
| | 'color': random.choice(['#e74c3c', '#3498db', '#f1c40f', '#ecf0f1', '#2c3e50']) |
| | }) |
| | self.dynamic_entities.append(car) |
| |
|
| | def add_player(self, session_id, user_info): |
| | saved = self.persistent_data.get(session_id, {}) |
| | self.players[session_id] = { |
| | 'id': session_id, |
| | 'discord_id': user_info['id'], |
| | 'username': user_info['username'], |
| | 'avatar': f"https://cdn.discordapp.com/avatars/{user_info['id']}/{user_info['avatar']}.png" if user_info['avatar'] else "", |
| | 'x': random.random() * MAP_WIDTH, |
| | 'y': random.random() * MAP_HEIGHT, |
| | 'r': saved.get('r', 35), |
| | 'target_r': saved.get('r', 35), |
| | 'score': saved.get('score', 0), |
| | 'color': '#3498db', |
| | 'input_angle': 0, 'input_force': 0, 'vx': 0, 'vy': 0, |
| | 'alive': True, 'respawn_timer': 0 |
| | } |
| | if session_id not in self.persistent_data: |
| | self.persistent_data[session_id] = {'r': 35, 'score': 0, 'username': user_info['username']} |
| | self.save_data() |
| |
|
| | def update(self, dt): |
| | if not self.active: return |
| | self.removed_entities = [] |
| |
|
| | sorted_players = sorted(self.players.values(), key=lambda p: p['r'], reverse=True) |
| |
|
| | for p in sorted_players: |
| | if not p['alive']: |
| | p['respawn_timer'] -= dt |
| | if p['respawn_timer'] <= 0: |
| | p['alive'] = True |
| | p['r'] = 35; p['target_r'] = 35; p['score'] = 0 |
| | p['x'] = random.random() * MAP_WIDTH |
| | p['y'] = random.random() * MAP_HEIGHT |
| | continue |
| |
|
| | speed = 180 * (1 - (p['r'] / 800)) |
| | p['vx'] = math.cos(p['input_angle']) * p['input_force'] * speed |
| | p['vy'] = math.sin(p['input_angle']) * p['input_force'] * speed |
| | p['x'] = max(p['r'], min(MAP_WIDTH - p['r'], p['x'] + p['vx'] * dt)) |
| | p['y'] = max(p['r'], min(MAP_HEIGHT - p['r'], p['y'] + p['vy'] * dt)) |
| | |
| | if p['r'] < p['target_r']: p['r'] += (p['target_r'] - p['r']) * dt |
| |
|
| | |
| | nearby_static = self.get_nearby_static(p['x'], p['y']) |
| | for ent in nearby_static: |
| | |
| | if abs(p['x'] - ent['x']) > p['r'] or abs(p['y'] - ent['y']) > p['r']: continue |
| | |
| | dist = math.hypot(p['x'] - ent['x'], p['y'] - ent['y']) |
| | if dist < p['r'] and p['r'] > ent['radius']: |
| | self.static_entities.remove(ent) |
| | |
| | gx, gy = int(ent['x']//CELL_SIZE), int(ent['y']//CELL_SIZE) |
| | if ent in self.static_grid[(gx, gy)]: |
| | self.static_grid[(gx, gy)].remove(ent) |
| | |
| | self.removed_entities.append(ent['id']) |
| | p['score'] += ent['score'] |
| | p['target_r'] += math.sqrt(ent['score']) * 0.3 |
| |
|
| | |
| | for car in self.dynamic_entities: |
| | dist = math.hypot(p['x'] - car['x'], p['y'] - car['y']) |
| | if dist < p['r'] and p['r'] > car['radius']: |
| | p['score'] += car['score'] |
| | p['target_r'] += math.sqrt(car['score']) * 0.3 |
| | car['x'] = random.randint(0, GRID_SIZE) * CELL_SIZE |
| | car['y'] = random.random() * MAP_HEIGHT |
| | self.removed_entities.append(car['id']) |
| |
|
| | |
| | for other in sorted_players: |
| | if p == other or not other['alive']: continue |
| | dist = math.hypot(p['x'] - other['x'], p['y'] - other['y']) |
| | if p['r'] > other['r'] * 1.1 and dist < p['r'] - other['r']*0.4: |
| | other['alive'] = False |
| | other['respawn_timer'] = 5 |
| | p['score'] += 50 + other['score'] * 0.5 |
| | p['target_r'] += 5 |
| | self.persistent_data[p['id']]['score'] = p['score'] |
| | self.persistent_data[p['id']]['r'] = p['target_r'] |
| | self.persistent_data[other['id']]['score'] = 0 |
| | self.persistent_data[other['id']]['r'] = 35 |
| | self.save_data() |
| |
|
| | |
| | for car in self.dynamic_entities: |
| | speed = 150 |
| | car['x'] += math.cos(car['rotation']) * speed * dt |
| | car['y'] += math.sin(car['rotation']) * speed * dt |
| | if car['x'] > MAP_WIDTH: car['x'] = 0 |
| | if car['x'] < 0: car['x'] = MAP_WIDTH |
| | if car['y'] > MAP_HEIGHT: car['y'] = 0 |
| | if car['y'] < 0: car['y'] = MAP_HEIGHT |
| |
|
| | game = GameState() |
| |
|
| | |
| | async def handle_index(request): return web.FileResponse('./index.html') |
| | async def handle_admin(request): return web.FileResponse('./admin.html') |
| | async def handle_login(request): |
| | return web.HTTPFound(f"https://discord.com/api/oauth2/authorize?client_id={DISCORD_CLIENT_ID}&redirect_uri={DISCORD_REDIRECT_URI}&response_type=code&scope=identify") |
| |
|
| | async def handle_callback(request): |
| | code = request.query.get('code') |
| | if not code: return web.Response(text="No code") |
| | async with ClientSession() as s: |
| | data = {'client_id': DISCORD_CLIENT_ID, 'client_secret': DISCORD_CLIENT_SECRET, 'grant_type': 'authorization_code', 'code': code, 'redirect_uri': DISCORD_REDIRECT_URI} |
| | async with s.post('https://discord.com/api/oauth2/token', data=data) as r: |
| | token_data = await r.json() |
| | if 'access_token' not in token_data: return web.Response(text="Auth failed") |
| | headers = {'Authorization': f"Bearer {token_data['access_token']}"} |
| | async with s.get('https://discord.com/api/users/@me', headers=headers) as r: |
| | user_data = await r.json() |
| | sid = str(uuid.uuid4()) |
| | game.add_player(sid, user_data) |
| | |
| | |
| | p = game.players[sid] |
| | meta = {'id': p['id'], 'username': p['username'], 'avatar': p['avatar'], 'color': p['color']} |
| | await sio.emit('player_joined', meta) |
| | |
| | return web.HTTPFound(f"/?token={sid}") |
| |
|
| | app.router.add_get('/', handle_index) |
| | app.router.add_get('/login', handle_login) |
| | app.router.add_get('/callback', handle_callback) |
| | app.router.add_get(ADMIN_PATH, handle_admin) |
| |
|
| | @sio.event |
| | async def connect(sid, environ, auth): |
| | token = auth.get('token') if auth else None |
| | if not token or token not in game.players: return False |
| | game.sockets[sid] = token |
| | |
| | |
| | all_players_meta = [ |
| | {'id': p['id'], 'username': p['username'], 'avatar': p['avatar'], 'color': p['color']} |
| | for p in game.players.values() |
| | ] |
| | |
| | await sio.emit('init_game', { |
| | 'width': MAP_WIDTH, 'height': MAP_HEIGHT, |
| | 'static_entities': game.static_entities, |
| | 'players_meta': all_players_meta |
| | }, to=sid) |
| |
|
| | @sio.event |
| | async def disconnect(sid): |
| | if sid in game.sockets: |
| | pid = game.sockets[sid] |
| | del game.sockets[sid] |
| | await sio.emit('player_left', {'id': pid}) |
| |
|
| | @sio.event |
| | async def input_data(sid, data): |
| | if sid in game.sockets: |
| | p = game.players[game.sockets[sid]] |
| | p['input_angle'] = data.get('angle', 0) |
| | p['input_force'] = min(max(data.get('force', 0), 0), 1) |
| |
|
| | @sio.event |
| | async def admin_cmd(sid, data): |
| | cmd = data.get('cmd') |
| | if cmd == 'start_tournament': |
| | game.active = True |
| | game.generate_map() |
| | for p in game.players.values(): |
| | p['r'] = 35; p['target_r'] = 35; p['score'] = 0; p['alive'] = True |
| | p['x'] = random.random() * MAP_WIDTH; p['y'] = random.random() * MAP_HEIGHT |
| | await sio.emit('tournament_reset', {'static_entities': game.static_entities}) |
| | elif cmd == 'stop_tournament': |
| | game.active = False |
| | res = [{'username': p['username'], 'score': int(p['score']), 'avatar': p['avatar']} for p in game.players.values()] |
| | await sio.emit('tournament_end', {'results': sorted(res, key=lambda x: x['score'], reverse=True)}) |
| |
|
| | async def game_loop(): |
| | while True: |
| | start = time.time() |
| | if game.active: |
| | game.update(1/GAME_TICK_RATE) |
| | |
| | |
| | |
| | state = { |
| | 'players': [ |
| | {'id': p['id'], 'x': int(p['x']), 'y': int(p['y']), 'r': int(p['r']), |
| | 'alive': p['alive'], 'score': int(p['score'])} |
| | for p in game.players.values() if p['alive'] |
| | ], |
| | 'cars': [ |
| | {'id': c['id'], 'x': int(c['x']), 'y': int(c['y']), 'rotation': round(c['rotation'], 2), |
| | 'color': c['color'], 'w': c['w'], 'l': c['l'], 'h': c['h'], |
| | 'cabinW': c.get('cabinW'), 'cabinL': c.get('cabinL'), 'type': 'car'} |
| | for c in game.dynamic_entities |
| | ], |
| | 'removed': game.removed_entities |
| | } |
| | await sio.emit('game_update', state) |
| | |
| | await asyncio.sleep(max(0, (1/GAME_TICK_RATE) - (time.time() - start))) |
| |
|
| | async def init_app(): |
| | asyncio.create_task(game_loop()) |
| | return app |
| |
|
| | if __name__ == '__main__': |
| | web.run_app(init_app(), port=PORT) |