diff --git a/src/car.py b/src/car.py index d17fbb6..090cbc7 100644 --- a/src/car.py +++ b/src/car.py @@ -4,10 +4,10 @@ from typing import Optional import pygame from src.camera import Camera +from src.remote_controller import RemoteController from src.utils import get_segments_intersection, segments_intersect from src.vec import Vec - sign = lambda x: 0 if x == 0 else (-1 if x < 0 else 1) @@ -38,6 +38,9 @@ class Car: self.rays: list[float] = [0] * self.N_RAYS self.rays_end: list[Vec] = [Vec() for _ in range(self.N_RAYS)] + self.controller: RemoteController = RemoteController(self) + self.controller.start_server() + def update(self, dt: float): if self.forward: self.speed += self.ACCELERATION * dt diff --git a/src/game.py b/src/game.py index 47ae3a5..4a7d159 100644 --- a/src/game.py +++ b/src/game.py @@ -37,6 +37,7 @@ class Game: while self.running: dt: float = self.clock.get_time() / 1000 self.process_pygame_events() + self.car.controller.process_commands() self.car.update(dt) self.car.check_collisions(self.track.get_collision_polygons()) self.update_camera() @@ -64,6 +65,7 @@ class Game: def quit(self): self.running = False + self.car.controller.close() def render(self): self.win.fill(self.BACKGROUND_COLOR) diff --git a/src/remote_controller.py b/src/remote_controller.py new file mode 100644 index 0000000..0ac72c9 --- /dev/null +++ b/src/remote_controller.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import queue +import socket +import struct +import threading +from typing import TYPE_CHECKING, Optional + +from src.command import CarControl, Command, ControlCommand + +if TYPE_CHECKING: + from src.car import Car + + +class RemoteController: + DEFAULT_PORT = 5000 + DATA_CHUNK_SIZE = 4096 + + CONTROL_ATTRIBUTES: dict[CarControl, str] = { + CarControl.FORWARD: "forward", + CarControl.BACKWARD: "backward", + CarControl.LEFT: "left", + CarControl.RIGHT: "right", + } + + def __init__(self, car: Car, port: int = DEFAULT_PORT) -> None: + self.car: Car = car + self.port: int = port + self.server: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.server_thread: threading.Thread = threading.Thread( + target=self.wait_for_connections, daemon=True + ) + self.running: bool = False + self.queue: queue.Queue[Command] = queue.Queue() + self.client_thread: Optional[threading.Thread] = None + self.client: Optional[socket.socket] = None + + def wait_for_connections(self): + self.server.bind(("", self.port)) + self.server.listen(1) + print(f"Remote control server listening on port {self.port}") + while self.running: + conn, addr = self.server.accept() + print(f"Remote connection from {addr}") + self.on_client_connected(conn) + + def start_server(self): + self.running = True + self.server_thread.start() + + def close(self): + if self.client: + self.client.close() + self.server.close() + self.running = False + + def on_client_connected(self, conn: socket.socket): + if self.client: + print("A client is already connected") + conn.close() + return + + self.client = conn + self.client_thread = threading.Thread(target=self.client_loop) + self.client_thread.start() + + def client_loop(self): + buffer: bytes = b"" + while self.running and self.client: + chunk: bytes = self.client.recv(self.DATA_CHUNK_SIZE) + if not chunk: + print("Client disconnected") + break + buffer += chunk + + while True: + if len(buffer) < 4: + break + msg_len: int = struct.unpack(">I", buffer[:4])[0] + msg_end: int = 4 + msg_len + if len(buffer) < msg_end: + break + + message: bytes = buffer[4:msg_end] + buffer = buffer[msg_end:] + self.on_message(message) + + if self.client: + self.client.close() + self.client = None + self.client_thread = None + + def on_message(self, message: bytes): + command: Command = Command.unpack(message) + self.queue.put(command) + + def process_commands(self): + while not self.queue.empty(): + command: Command = self.queue.get() + self.process_command(command) + + def process_command(self, command: Command): + match command: + case ControlCommand(control, active): + self.set_control(control, active) + + def set_control(self, control: CarControl, active: bool): + setattr(self.car, self.CONTROL_ATTRIBUTES[control], active)