Compare commits

...

4 Commits

10 changed files with 437 additions and 15 deletions

View File

@@ -0,0 +1,59 @@
<p align="center">
<img src="logo.png" width="300">
</p>
# Rally Racer
This repository holds a sandbox driving simulation controllable via a network interface as a machine learning and data collection challenge.
# Installation
From the root of the repository, run
```sh
uv sync
```
To run the game, you can use
```sh
uv run main.py
```
# Generality
Launching [`main.py`](main.py) starts a race with a single car on the provided track.
This track can be controlled either by keyboard (*WASD*) or by a socket interface.
An example of such interface is included in the code in [*`scripts/recorder.py`*](scripts/recorder.py). To run it, simply use the following command:
```sh
uv run -m scripts.recorder
```
# Sensing
The car sensing is available in two commodities: **raycasts** and **images**. These sensing snapshots are sent at 10 Hertz (i.e. 10 times a second). Due to this fact, correct reception of snapshot messages has to be done regularly.
# Communication protocol
A remote controller can be impemented using TCP socket connecting on localhost on port 5000.
Different commands can be issued to the race simulation to control the car.
These commands are declared in [`src/command.py`](src/command.py)
## Car controls
```python
ControlCommand(control: CarControl, active: bool)
```
To simulate key press and control the car.
# Controls
- <kbd>W</kbd> Move forward
- <kbd>S</kbd> Brake / move backward
- <kbd>A</kbd> Turn left
- <kbd>D</kbd> Turn right
- <kbd>F</kbd> Toggle FPS indicator
- <kbd>V</kbd> Toggle speedometer
- <kbd>R</kbd> Reset car
- <kbd>C</kbd> Toggle raycasts visibility
- <kbd>Esc</kbd> Quit
# Credits
This project is based on the repository [https://github.com/ISC-HEI/RallyRobotPilot_2025](https://github.com/ISC-HEI/RallyRobotPilot_2025), which is in turn based on [https://github.com/mandaw2014/Rally](https://github.com/mandaw2014/Rally)

143
car.svg Normal file
View File

@@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="64"
height="64"
viewBox="0 0 64 64.000003"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="car.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="11.313709"
inkscape:cx="42.470601"
inkscape:cy="34.957591"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
id="grid1"
units="px"
originx="0"
originy="0"
spacingx="1"
spacingy="1"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="8"
enabled="true"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.0000002,0,1 @ F,0,1,1,0,1.0000002,0,1 @ F,0,1,1,0,1.0000002,0,1 @ F,0,1,1,0,1.0000002,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.0000001,0,1 @ F,0,1,1,0,1.0000001,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0000001,0,1 @ F,0,1,1,0,1.0000001,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<g
inkscape:label="Calque 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g5"
inkscape:label="car"
transform="translate(-5.9999998,-7.9999997)">
<g
id="g4"
inkscape:label="wheels">
<path
style="fill:#000000;stroke-linecap:round;stroke-linejoin:round"
d="m 25.000001,31.999999 v -2 h 4 v 2 z"
id="path2"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#000000;stroke-linecap:round;stroke-linejoin:round"
d="m 43.000001,31.999999 v -2 h 4 v 2 z"
id="path2-3"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#000000;stroke-linecap:round;stroke-linejoin:round"
d="m 25.000001,49.999999 v -2 h 4 v 2 z"
id="path2-1"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#000000;stroke-linecap:round;stroke-linejoin:round"
d="m 43.000001,49.999999 v -2 h 4 v 2 z"
id="path2-3-2"
sodipodi:nodetypes="ccccc" />
</g>
<path
style="fill:#e14324;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
d="m 16,33 v 13.999999 a 1.1327823,1.1327823 48.562508 0 0 0.992278,1.124035 l 7.007723,0.875965 h 24 l 11.003454,-0.916955 a 1.0867997,1.0867997 132.61818 0 0 0.996546,-1.083045 v -14 A 1.0867996,1.0867996 47.381818 0 0 59.003455,31.916954 L 48.000001,31 h -24 l -7.007723,0.875965 A 1.1327823,1.1327823 131.43749 0 0 16,33 Z"
id="path1"
inkscape:path-effect="#path-effect2"
inkscape:original-d="m 16,32 v 15.999999 l 8.000001,1 h 24 l 12,-1 v -16 L 48.000001,31 h -24 z"
inkscape:label="body" />
<path
style="fill:#53170b;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
d="m 50.000001,33.500001 v 13 a 1.0867994,1.0867994 132.61819 0 1 -0.996546,1.083045 l -4.006908,0.333908 A 0.92013337,0.92013337 42.618189 0 1 44.000001,46.999999 V 33 a 0.92013291,0.92013291 137.38183 0 1 0.996546,-0.916954 l 4.006908,0.333909 a 1.0867999,1.0867999 47.381826 0 1 0.996546,1.083046 z"
id="path3"
inkscape:path-effect="#path-effect3"
inkscape:original-d="m 50.000001,32.500001 v 15 l -6,0.499998 V 32 Z"
sodipodi:nodetypes="ccccc"
inkscape:label="windshield" />
<path
style="fill:#af3116;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
d="m 29.000001,46.999999 c -4,0 -8.000001,0 -11.000001,-1 v -12 c 3,-1 7.000001,-1 11.000001,-1 z"
id="path4"
sodipodi:nodetypes="ccccc"
inkscape:label="back_window" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

202
logo.svg Normal file
View File

@@ -0,0 +1,202 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="64"
height="64"
viewBox="0 0 64 64.000003"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="logo.svg"
inkscape:export-filename="logo.png"
inkscape:export-xdpi="768"
inkscape:export-ydpi="768"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="11.313709"
inkscape:cx="31.952386"
inkscape:cy="30.803338"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
inkscape:export-bgcolor="#ffffffff">
<inkscape:grid
id="grid1"
units="px"
originx="0"
originy="0"
spacingx="1"
spacingy="1"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="8"
enabled="true"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.0000002,0,1 @ F,0,1,1,0,1.0000002,0,1 @ F,0,1,1,0,1.0000002,0,1 @ F,0,1,1,0,1.0000002,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.0000001,0,1 @ F,0,1,1,0,1.0000001,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0000001,0,1 @ F,0,1,1,0,1.0000001,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<g
inkscape:label="Calque 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g14"
transform="matrix(1.12,0,0,1.12,-6.6400002,3.1600025)">
<path
id="path13"
style="fill:none;fill-opacity:1;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
d="m 38,43.499998 -11,-21 m 0,0 -9,20 M 50,32.749996 38,43.499998 M 53,16.749997 50,32.749996 M 40.000001,7.9999998 27,22.499998 M 16,7.9999998 27,22.499998"
sodipodi:nodetypes="cccccccccccc" />
<g
id="g13">
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
id="path5"
cx="16"
cy="8"
r="3" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
id="path5-3"
cx="40"
cy="7.9999995"
r="3" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
id="path5-1"
cx="27"
cy="22.499998"
r="3" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
id="path5-6"
cx="38"
cy="43.499996"
r="3" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
id="path5-18"
cx="18"
cy="42.499996"
r="3" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
id="path5-2"
cx="50"
cy="32.749996"
r="3" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
id="path5-22"
cx="53"
cy="16.749996"
r="3" />
</g>
<g
id="g5"
inkscape:label="car"
transform="matrix(0.1163734,0.24290774,-0.24290774,0.1163734,38.296019,19.616451)"
style="display:inline">
<g
id="g4"
inkscape:label="wheels">
<path
style="fill:#000000;stroke-linecap:round;stroke-linejoin:round"
d="m 25.000001,31.999999 v -2 h 4 v 2 z"
id="path2"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#000000;stroke-linecap:round;stroke-linejoin:round"
d="m 43.000001,31.999999 v -2 h 4 v 2 z"
id="path2-3"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#000000;stroke-linecap:round;stroke-linejoin:round"
d="m 25.000001,49.999999 v -2 h 4 v 2 z"
id="path2-1"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#000000;stroke-linecap:round;stroke-linejoin:round"
d="m 43.000001,49.999999 v -2 h 4 v 2 z"
id="path2-3-2"
sodipodi:nodetypes="ccccc" />
</g>
<path
style="fill:#e14324;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
d="m 16,33 v 13.999999 a 1.1327823,1.1327823 48.562508 0 0 0.992278,1.124035 l 7.007723,0.875965 h 24 l 11.003454,-0.916955 a 1.0867997,1.0867997 132.61818 0 0 0.996546,-1.083045 v -14 A 1.0867996,1.0867996 47.381818 0 0 59.003455,31.916954 L 48.000001,31 h -24 l -7.007723,0.875965 A 1.1327823,1.1327823 131.43749 0 0 16,33 Z"
id="path1"
inkscape:path-effect="#path-effect2"
inkscape:original-d="m 16,32 v 15.999999 l 8.000001,1 h 24 l 12,-1 v -16 L 48.000001,31 h -24 z"
inkscape:label="body" />
<path
style="fill:#53170b;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
d="m 50.000001,33.500001 v 13 a 1.0867994,1.0867994 132.61819 0 1 -0.996546,1.083045 l -4.006908,0.333908 A 0.92013337,0.92013337 42.618189 0 1 44.000001,46.999999 V 33 a 0.92013291,0.92013291 137.38183 0 1 0.996546,-0.916954 l 4.006908,0.333909 a 1.0867999,1.0867999 47.381826 0 1 0.996546,1.083046 z"
id="path3"
inkscape:path-effect="#path-effect3"
inkscape:original-d="m 50.000001,32.500001 v 15 l -6,0.499998 V 32 Z"
sodipodi:nodetypes="ccccc"
inkscape:label="windshield" />
<path
style="fill:#af3116;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
d="m 29.000001,46.999999 c -4,0 -8.000001,0 -11.000001,-1 v -12 c 3,-1 7.000001,-1 11.000001,-1 z"
id="path4"
sodipodi:nodetypes="ccccc"
inkscape:label="back_window" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -1,5 +1,7 @@
from __future__ import annotations
from math import radians
from typing import Optional
from typing import TYPE_CHECKING, Optional
import pygame
@@ -8,6 +10,9 @@ from src.remote_controller import RemoteController
from src.utils import get_segments_intersection, segments_intersect
from src.vec import Vec
if TYPE_CHECKING:
from src.game import Game
def sign(x): return 0 if x == 0 else (-1 if x < 0 else 1)
@@ -27,7 +32,8 @@ class Car:
RAYS_FOV = 180
RAYS_MAX_DIST = 100
def __init__(self, pos: Vec, direction: Vec) -> None:
def __init__(self, game: Game, pos: Vec, direction: Vec) -> None:
self.game: Game = game
self.initial_pos: Vec = pos.copy()
self.initial_dir: Vec = direction.copy()
self.pos: Vec = pos
@@ -42,7 +48,7 @@ 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: RemoteController = RemoteController(self.game, self)
self.controller.start_server()
def update(self, dt: float):

View File

@@ -19,10 +19,11 @@ class Game:
self.win: pygame.Surface = pygame.display.set_mode(
self.DEFAULT_SIZE, pygame.RESIZABLE
)
self.game_surf: pygame.Surface = pygame.Surface(self.DEFAULT_SIZE)
pygame.display.set_caption("Rally Racer")
self.running: bool = True
self.track: Track = Track.load("simple")
self.car: Car = Car(self.track.start_pos, self.track.start_dir)
self.car: Car = Car(self, self.track.start_pos, self.track.start_dir)
self.camera: Camera = Camera()
self.clock: pygame.time.Clock = pygame.time.Clock()
@@ -49,6 +50,7 @@ class Game:
if event.type == pygame.QUIT:
self.quit()
elif event.type == pygame.VIDEORESIZE:
self.game_surf = pygame.Surface((event.w, event.h))
self.camera.set_size(Vec(event.w, event.h))
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
@@ -68,9 +70,10 @@ class Game:
self.car.controller.close()
def render(self):
self.win.fill(self.BACKGROUND_COLOR)
self.track.render(self.win, self.camera)
self.car.render(self.win, self.camera, self.show_raycasts)
self.game_surf.fill(self.BACKGROUND_COLOR)
self.track.render(self.game_surf, self.camera)
self.car.render(self.game_surf, self.camera, self.show_raycasts)
self.win.blit(self.game_surf, (0, 0))
if self.show_fps:
self.render_fps()
if self.show_speed:

View File

@@ -1,3 +1,4 @@
import lzma
from pathlib import Path
import struct
import time
@@ -12,7 +13,7 @@ class RecordFile:
def __init__(self, path: str | Path, mode: Literal["w", "r"]) -> None:
self.path: str | Path = path
self.mode: Literal["w", "r"] = mode
self.file = open(self.path, self.mode + "b")
self.file: lzma.LZMAFile = lzma.LZMAFile(self.path, self.mode)
def __enter__(self):
return self
@@ -21,8 +22,7 @@ class RecordFile:
self.file.close()
def write_header(self, n_snapshots: int):
data: bytes = struct.pack(
">IId", self.VERSION, n_snapshots, time.time())
data: bytes = struct.pack(">IId", self.VERSION, n_snapshots, time.time())
self.file.write(data)
def write_snapshots(self, snapshots: list[Snapshot]):
@@ -35,7 +35,8 @@ class RecordFile:
version: int = struct.unpack(">I", self.file.read(4))[0]
if version != self.VERSION:
raise ValueError(
f"Cannot parse record file with format version {version} (current version: {self.VERSION})")
f"Cannot parse record file with format version {version} (current version: {self.VERSION})"
)
n_snapshots: int
timestamp: float

View File

@@ -17,7 +17,7 @@ from src.snapshot import Snapshot
class RecorderClient(QObject):
DATA_CHUNK_SIZE = 4096
DATA_CHUNK_SIZE = 65536
data_received: pyqtSignal = pyqtSignal(Snapshot)
def __init__(self, host: str, port: int) -> None:
@@ -243,7 +243,7 @@ class RecorderWindow(Ui_Recorder, QMainWindow):
self.SAVE_DIR.mkdir(exist_ok=True)
record_name: str = "record_%d.rec"
record_name: str = "record_%d.rec.xz"
fid = 0
while os.path.exists(self.SAVE_DIR / (record_name % fid)):
fid += 1

View File

@@ -12,11 +12,12 @@ from src.utils import RepeatTimer
if TYPE_CHECKING:
from src.car import Car
from src.game import Game
class RemoteController:
DEFAULT_PORT = 5000
DATA_CHUNK_SIZE = 4096
DATA_CHUNK_SIZE = 65536
CONTROL_ATTRIBUTES: dict[CarControl, str] = {
CarControl.FORWARD: "forward",
@@ -27,7 +28,8 @@ class RemoteController:
SNAPSHOT_INTERVAL = 0.1
def __init__(self, car: Car, port: int = DEFAULT_PORT) -> None:
def __init__(self, game: Game, car: Car, port: int = DEFAULT_PORT) -> None:
self.game: Game = game
self.car: Car = car
self.port: int = port
self.server: socket.socket = socket.socket(
@@ -134,5 +136,6 @@ class RemoteController:
return
snapshot: Snapshot = Snapshot.from_car(self.car)
snapshot.add_image(self.game)
payload: bytes = snapshot.pack()
self.client.sendall(struct.pack(">I", len(payload)) + payload)

View File

@@ -5,11 +5,13 @@ from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Optional
import numpy as np
import pygame
from src.vec import Vec
if TYPE_CHECKING:
from src.car import Car
from src.game import Game
def iter_unpack(format, data):
@@ -99,3 +101,6 @@ class Snapshot:
car.pos = self.position.copy()
car.direction = self.direction.copy()
car.speed = 0
def add_image(self, game: Game):
self.image = pygame.surfarray.array3d(game.game_surf)