yt / main.py
Nafiul Huda
Update main.py
a34d80f verified
Raw History Blame Contribute Delete
17.6 kB
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
# --- CONFIGURATION ---
PORT = int(os.getenv('SERVER_PORT', 8080))
ADMIN_PATH = os.getenv('ADMIN_PATH', '/secret-admin-panel')
# !!! REPLACE WITH YOUR DISCORD APP DETAILS !!!
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
# --- ASSET DEFINITIONS ---
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 = []
# Spatial Hash Grid for Static Entities: key=(gridX, gridY), val=[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 = []
# Grid Generation
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)
# Car Generation
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'])
# Add to main list AND spatial grid
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):
# Return entities in the 3x3 grid cells around the point
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: # Park
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: # Residential
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: # Downtown
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
# Eat Static (OPTIMIZED: Use Spatial Hash)
nearby_static = self.get_nearby_static(p['x'], p['y'])
for ent in nearby_static:
# Basic check before expensive sqrt
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)
# Also remove from grid for consistency (slow, but eats are rare compared to frames)
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
# Eat Cars
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'])
# Eat Players
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()
# Update Cars
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()
# --- WEB HANDLERS ---
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)
# Broadcast new player metadata to everyone
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
# Send Static Info (Map + All Player Metadata)
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)
# OPTIMIZED PACKET: Strip static data (avatar, username, color, car dimensions)
# Only send dynamic data (id, x, y, r, score, alive)
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'], # Keep car static for now as they respawn often
'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)