Skip to content

Commit

Permalink
Move src files of the server application into a source dir
Browse files Browse the repository at this point in the history
  - move from twisted to asyncio
  - move from autobahn to websockets
  - move files into a game directory

Signed-off-by: yzamir <kobi.zamir@gmail.com>
  • Loading branch information
yaacov committed Sep 12, 2023
1 parent 99a73df commit 9affc20
Show file tree
Hide file tree
Showing 12 changed files with 569 additions and 398 deletions.
132 changes: 0 additions & 132 deletions rose/server/game.py

This file was deleted.

Empty file added rose/server/game/__init__.py
Empty file.
152 changes: 152 additions & 0 deletions rose/server/game/logic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import asyncio
import logging
import random
import aiohttp

from common import config

from game import score, net
from game.player import Player
from game.track import Track

log = logging.getLogger("logic")


async def initialize_game(state):
"""Reset game settings and return re-initialized track and players."""
state["reset"] = None
state["running"] = 0
state["timeleft"] = config.game_duration
track = initialize_track(state["track_type"] != "same")
players = await initialize_players(state["drivers"])
return track, players


def initialize_track(is_track_random):
"""
Initialize and return a new track.
Args:
is_track_random (bool): If False, the track will have the same obstacles for both players.
If True, obstacles will be randomized.
Returns:
Track: An initialized track object.
"""
track = Track(is_track_random)
track.reset()
return track


async def initialize_players(drivers):
"""
Asynchronously initialize players from a list of driver URLs.
Args:
drivers (list): List of driver URLs to initialize players from.
players (list): List of Player objects.
"""

# Init players
players = []

if not drivers:
return players

base_color = random.randint(0, 3)

async with aiohttp.ClientSession() as session:
for index, driver in enumerate(drivers):
try:
async with session.get(driver) as info:
info_data = await info.json()
player_name = info_data.get("info", {}).get("name")
player_car = (base_color + index) % 3
player_lane = index
player = Player(player_name, player_car, player_lane)
player.reset()
player.URL = driver
players.append(player)
except Exception as e:
log.error(f"error: {e}")

return players


async def game_loop(state, active_websockets):
"""
Asynchronously execute the game loop, using provided state and active websockets.
Args:
state (dict): Dictionary containing game state data (rate, running status, time left, etc.).
active_websockets (set): A set of active websocket connections for communication.
Returns:
None
"""

# Initialize or reset the game, set up track and players
track, players = await initialize_game(state)

# Begin the main game loop
while True:
# Check if the game needs a reset (based on state)
if state["reset"] == 1:
track, players = await initialize_game(state)

# Stop game if timeleft is zero
if state["timeleft"] < 1:
state["running"] = 0

# Check if the game is currently running and there's time left to play
if state["running"] == 1:
# Start executing a step in the game
task = asyncio.create_task(
game_step(state, players, track, active_websockets)
)

# Pause the game loop for a specified duration, based on the rate defined in the state
await asyncio.sleep(1 / state["rate"])

# If for some reason the game step hasn't finished executing, cancel it
if not task.done():
task.cancel()

# If the game is not running (e.g., paused or finished)
else:
# Update all connected clients (via websockets) with the current game state
await net.update_websockets(False, state, players, track, active_websockets)

await asyncio.sleep(1)


async def game_step(state, players, track, active_websockets):
"""
Execute a game step: Update the track, fetch drivers' actions, process actions, and update websockets.
Args:
state (dict): Dictionary containing game state data (rate, running status, etc.).
players (list): List of Player objects.
track (Track): the game track.
active_websockets (Any): Active websockets for communication (assuming a suitable data structure).
"""

try:
# Fetch players actions using an asynchronous HTTP session
await net.fetch_drivers_actions(players, track.matrix())

# Update track
track.update()

# Process the actions of the players
score.process(players, track)

# Send data to all WebSocket connections
await net.update_websockets(True, state, players, track, active_websockets)

# Progress the game's timer
state["timeleft"] -= 1

except asyncio.CancelledError:
log.info("Game step was canceled!")
raise
113 changes: 113 additions & 0 deletions rose/server/game/net.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import asyncio
import aiohttp
import json
import time
import logging


log = logging.getLogger("net")


async def fetch_drivers_actions(players, track_matrix):
"""
Asynchronously fetch actions for each player.
Args:
players (list): List of Player objects.
track_matrix (list of list): The matrix representation a 2D array of the track.
Returns:
list: List of player actions fetched.
"""
async with aiohttp.ClientSession() as session:
await asyncio.gather(
*(fetch_driver_action(session, player, track_matrix) for player in players),
return_exceptions=True,
)


async def fetch_driver_action(session, player, track_matrix):
"""
Asynchronously fetch content from a URL using a POST request and return the parsed JSON
along with the time it took to get the response.
Args:
session (aiohttp.ClientSession): An active ClientSession for making the request.
player (Player): The player object containing name, URL, and position.
track_matrix (list of list): The matrix representation a 2D array of the track.
Returns:
tuple: A tuple containing:
- dict or None: The JSON-decoded response from the server or None if an error occurred.
- float: The time taken (in seconds) to get the response.
- str or None: The error message, if any. None if no error occurred.
"""
start_time = time.time()

try:
response_data = await send_post_request(session, player, track_matrix)
return process_driver_response(player, response_data, start_time)

except Exception as e:
elapsed_time = time.time() - start_time
player.response_time = elapsed_time
error_msg = f"Error during POST request {e}"
log.error(error_msg)

player.action = None
player.httperror = "Error POST to driver"

return None, elapsed_time


async def send_post_request(session, player, track_matrix):
data = {"info": {"car": {"x": player.x, "y": player.y}}, "track": track_matrix}

async with session.post(player.URL, data=json.dumps(data).encode()) as response:
return await response.json()


def process_driver_response(player, response_data, start_time):
elapsed_time = time.time() - start_time
player.response_time = elapsed_time

try:
player.name = response_data.get("info").get("name")
player.action = response_data.get("info").get("action")
player.httperror = None
return response_data, elapsed_time

except Exception as e:
error_msg = f"Post to driver error {e}"
log.error(error_msg)

player.action = None
player.httperror = error_msg

return None, elapsed_time


async def update_websockets(started, state, players, track, active_websockets):
"""Generate game state data and send it to all active websockets."""
data = {
"action": "update",
"payload": {
"started": started,
"rate": state["rate"],
"timeleft": state["timeleft"],
"players": [player.state() for player in players],
"track": track.state(),
},
}

await send_to_all_websockets(data, active_websockets)


async def send_to_all_websockets(data, active_websockets):
"""Send the given data to all active websocket connections."""
json_encoded_data = json.dumps(data)
for ws in active_websockets:
try:
await ws.send(json_encoded_data)
except Exception as e:
log.error("Fail ws send", e)
Loading

0 comments on commit 9affc20

Please sign in to comment.