Compare commits
10 Commits
8b7927a3c5
...
feat/image
| Author | SHA1 | Date | |
|---|---|---|---|
|
cd866cc511
|
|||
|
9f86d060f0
|
|||
|
e2f4a0d2a5
|
|||
|
bc96cea2b9
|
|||
|
8d35f76b56
|
|||
|
97f9765705
|
|||
|
fa61e27825
|
|||
|
b60a0aba4f
|
|||
|
ae02ddefb0
|
|||
|
f1fadd123f
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -9,4 +9,7 @@ wheels/
|
|||||||
# Virtual environments
|
# Virtual environments
|
||||||
.venv
|
.venv
|
||||||
|
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
|
records
|
||||||
|
*.rec
|
||||||
59
README.md
59
README.md
@@ -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
143
car.svg
Normal 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 |
202
logo.svg
Normal file
202
logo.svg
Normal 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 |
41
scripts/example_bot.py
Normal file
41
scripts/example_bot.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
from src.bot import Bot
|
||||||
|
from src.command import CarControl
|
||||||
|
from src.recorder import RecorderWindow
|
||||||
|
from src.snapshot import Snapshot
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleBot(Bot):
|
||||||
|
def nn_infer(self, snapshot: Snapshot) -> list[tuple[CarControl, bool]]:
|
||||||
|
# Do smart NN inference here
|
||||||
|
return [(CarControl.FORWARD, True)]
|
||||||
|
|
||||||
|
def on_snapshot_received(self, snapshot: Snapshot):
|
||||||
|
controls: list[tuple[CarControl, bool]] = self.nn_infer(snapshot)
|
||||||
|
for control, active in controls:
|
||||||
|
self.recorder.on_car_controlled(control, active)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def except_hook(cls, exception, traceback):
|
||||||
|
sys.__excepthook__(cls, exception, traceback)
|
||||||
|
|
||||||
|
sys.excepthook = except_hook
|
||||||
|
|
||||||
|
app: QApplication = QApplication(sys.argv)
|
||||||
|
recorder: RecorderWindow = RecorderWindow("localhost", 5000)
|
||||||
|
bot: ExampleBot = ExampleBot()
|
||||||
|
bot.set_recorder(recorder)
|
||||||
|
|
||||||
|
app.aboutToQuit.connect(recorder.shutdown)
|
||||||
|
recorder.register_bot(bot)
|
||||||
|
recorder.show()
|
||||||
|
|
||||||
|
app.exec()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
26
src/bot.py
Normal file
26
src/bot.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from src.snapshot import Snapshot
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from src.recorder import RecorderWindow
|
||||||
|
|
||||||
|
|
||||||
|
class Bot:
|
||||||
|
def __init__(self):
|
||||||
|
self._recorder: Optional[RecorderWindow] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def recorder(self) -> RecorderWindow:
|
||||||
|
if self._recorder is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Bot does not have a recorder. Call Bot.set_recorder to set one")
|
||||||
|
return self._recorder
|
||||||
|
|
||||||
|
def set_recorder(self, recorder: RecorderWindow):
|
||||||
|
self._recorder = recorder
|
||||||
|
|
||||||
|
def on_snapshot_received(self, snapshot: Snapshot):
|
||||||
|
pass
|
||||||
34
src/car.py
34
src/car.py
@@ -1,5 +1,7 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from math import radians
|
from math import radians
|
||||||
from typing import Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
import pygame
|
import pygame
|
||||||
|
|
||||||
@@ -8,7 +10,11 @@ from src.remote_controller import RemoteController
|
|||||||
from src.utils import get_segments_intersection, segments_intersect
|
from src.utils import get_segments_intersection, segments_intersect
|
||||||
from src.vec import Vec
|
from src.vec import Vec
|
||||||
|
|
||||||
sign = lambda x: 0 if x == 0 else (-1 if x < 0 else 1)
|
if TYPE_CHECKING:
|
||||||
|
from src.game import Game
|
||||||
|
|
||||||
|
|
||||||
|
def sign(x): return 0 if x == 0 else (-1 if x < 0 else 1)
|
||||||
|
|
||||||
|
|
||||||
class Car:
|
class Car:
|
||||||
@@ -26,7 +32,10 @@ class Car:
|
|||||||
RAYS_FOV = 180
|
RAYS_FOV = 180
|
||||||
RAYS_MAX_DIST = 100
|
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
|
self.pos: Vec = pos
|
||||||
self.direction: Vec = direction
|
self.direction: Vec = direction
|
||||||
self.speed: float = 0
|
self.speed: float = 0
|
||||||
@@ -39,7 +48,7 @@ class Car:
|
|||||||
self.rays: list[float] = [0] * self.N_RAYS
|
self.rays: list[float] = [0] * self.N_RAYS
|
||||||
self.rays_end: list[Vec] = [Vec() for _ in range(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()
|
self.controller.start_server()
|
||||||
|
|
||||||
def update(self, dt: float):
|
def update(self, dt: float):
|
||||||
@@ -77,7 +86,8 @@ class Car:
|
|||||||
if show_raycasts:
|
if show_raycasts:
|
||||||
pos: Vec = camera.world2screen(self.pos)
|
pos: Vec = camera.world2screen(self.pos)
|
||||||
for p in self.rays_end:
|
for p in self.rays_end:
|
||||||
pygame.draw.line(surf, (255, 0, 0), pos, camera.world2screen(p), 2)
|
pygame.draw.line(surf, (255, 0, 0), pos,
|
||||||
|
camera.world2screen(p), 2)
|
||||||
|
|
||||||
pts: list[Vec] = self.get_corners()
|
pts: list[Vec] = self.get_corners()
|
||||||
pts = [camera.world2screen(p) for p in pts]
|
pts = [camera.world2screen(p) for p in pts]
|
||||||
@@ -127,14 +137,17 @@ class Car:
|
|||||||
n *= -1
|
n *= -1
|
||||||
dist = -dist
|
dist = -dist
|
||||||
self.speed = 0
|
self.speed = 0
|
||||||
self.pos = self.pos + n * (self.COLLISION_MARGIN - dist)
|
self.pos = self.pos + n * \
|
||||||
|
(self.COLLISION_MARGIN - dist)
|
||||||
return
|
return
|
||||||
|
|
||||||
def cast_rays(self, polygons: list[list[Vec]]):
|
def cast_rays(self, polygons: list[list[Vec]]):
|
||||||
for i in range(self.N_RAYS):
|
for i in range(self.N_RAYS):
|
||||||
angle: float = radians((i / (self.N_RAYS - 1) - 0.5) * self.RAYS_FOV)
|
angle: float = radians(
|
||||||
|
(i / (self.N_RAYS - 1) - 0.5) * self.RAYS_FOV)
|
||||||
p: Optional[Vec] = self.cast_ray(angle, polygons)
|
p: Optional[Vec] = self.cast_ray(angle, polygons)
|
||||||
self.rays[i] = self.RAYS_MAX_DIST if p is None else (p - self.pos).mag()
|
self.rays[i] = self.RAYS_MAX_DIST if p is None else (
|
||||||
|
p - self.pos).mag()
|
||||||
self.rays_end[i] = self.pos if p is None else p
|
self.rays_end[i] = self.pos if p is None else p
|
||||||
|
|
||||||
def cast_ray(self, angle: float, polygons: list[list[Vec]]) -> Optional[Vec]:
|
def cast_ray(self, angle: float, polygons: list[list[Vec]]) -> Optional[Vec]:
|
||||||
@@ -161,3 +174,8 @@ class Car:
|
|||||||
dist = d
|
dist = d
|
||||||
closest = p
|
closest = p
|
||||||
return closest
|
return closest
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.pos = self.initial_pos.copy()
|
||||||
|
self.direction = self.initial_dir.copy()
|
||||||
|
self.speed = 0
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ from enum import IntEnum
|
|||||||
import struct
|
import struct
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
|
from src.snapshot import Snapshot
|
||||||
|
|
||||||
|
|
||||||
class CommandType(IntEnum):
|
class CommandType(IntEnum):
|
||||||
CAR_CONTROL = 0
|
CAR_CONTROL = 0
|
||||||
RECORDING = 1
|
RECORDING = 1
|
||||||
|
APPLY_SNAPSHOT = 2
|
||||||
|
RESET = 3
|
||||||
|
|
||||||
|
|
||||||
class CarControl(IntEnum):
|
class CarControl(IntEnum):
|
||||||
@@ -30,8 +34,8 @@ class Command(abc.ABC):
|
|||||||
)
|
)
|
||||||
Command.REGISTRY[cls.TYPE] = cls
|
Command.REGISTRY[cls.TYPE] = cls
|
||||||
|
|
||||||
@abc.abstractmethod
|
def get_payload(self) -> bytes:
|
||||||
def get_payload(self) -> bytes: ...
|
return b""
|
||||||
|
|
||||||
def pack(self) -> bytes:
|
def pack(self) -> bytes:
|
||||||
payload: bytes = self.get_payload()
|
payload: bytes = self.get_payload()
|
||||||
@@ -43,8 +47,8 @@ class Command(abc.ABC):
|
|||||||
return Command.REGISTRY[type].from_payload(data[1:])
|
return Command.REGISTRY[type].from_payload(data[1:])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abc.abstractmethod
|
def from_payload(cls, payload: bytes) -> Command:
|
||||||
def from_payload(cls, payload: bytes) -> Command: ...
|
return cls()
|
||||||
|
|
||||||
|
|
||||||
class ControlCommand(Command):
|
class ControlCommand(Command):
|
||||||
@@ -82,3 +86,24 @@ class RecordingCommand(Command):
|
|||||||
def from_payload(cls, payload: bytes) -> Command:
|
def from_payload(cls, payload: bytes) -> Command:
|
||||||
state: bool = struct.unpack(">B", payload)[0]
|
state: bool = struct.unpack(">B", payload)[0]
|
||||||
return RecordingCommand(state)
|
return RecordingCommand(state)
|
||||||
|
|
||||||
|
|
||||||
|
class ApplySnapshotCommand(Command):
|
||||||
|
TYPE = CommandType.APPLY_SNAPSHOT
|
||||||
|
__match_args__ = ("snapshot",)
|
||||||
|
|
||||||
|
def __init__(self, snapshot: Snapshot) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.snapshot: Snapshot = snapshot
|
||||||
|
|
||||||
|
def get_payload(self) -> bytes:
|
||||||
|
return self.snapshot.pack()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_payload(cls, payload: bytes) -> Command:
|
||||||
|
snapshot: Snapshot = Snapshot.unpack(payload)
|
||||||
|
return ApplySnapshotCommand(snapshot)
|
||||||
|
|
||||||
|
|
||||||
|
class ResetCommand(Command):
|
||||||
|
TYPE = CommandType.RESET
|
||||||
|
|||||||
11
src/game.py
11
src/game.py
@@ -19,10 +19,11 @@ class Game:
|
|||||||
self.win: pygame.Surface = pygame.display.set_mode(
|
self.win: pygame.Surface = pygame.display.set_mode(
|
||||||
self.DEFAULT_SIZE, pygame.RESIZABLE
|
self.DEFAULT_SIZE, pygame.RESIZABLE
|
||||||
)
|
)
|
||||||
|
self.game_surf: pygame.Surface = pygame.Surface(self.DEFAULT_SIZE)
|
||||||
pygame.display.set_caption("Rally Racer")
|
pygame.display.set_caption("Rally Racer")
|
||||||
self.running: bool = True
|
self.running: bool = True
|
||||||
self.track: Track = Track.load("simple")
|
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.camera: Camera = Camera()
|
||||||
|
|
||||||
self.clock: pygame.time.Clock = pygame.time.Clock()
|
self.clock: pygame.time.Clock = pygame.time.Clock()
|
||||||
@@ -49,6 +50,7 @@ class Game:
|
|||||||
if event.type == pygame.QUIT:
|
if event.type == pygame.QUIT:
|
||||||
self.quit()
|
self.quit()
|
||||||
elif event.type == pygame.VIDEORESIZE:
|
elif event.type == pygame.VIDEORESIZE:
|
||||||
|
self.game_surf = pygame.Surface((event.w, event.h))
|
||||||
self.camera.set_size(Vec(event.w, event.h))
|
self.camera.set_size(Vec(event.w, event.h))
|
||||||
elif event.type == pygame.KEYDOWN:
|
elif event.type == pygame.KEYDOWN:
|
||||||
if event.key == pygame.K_ESCAPE:
|
if event.key == pygame.K_ESCAPE:
|
||||||
@@ -68,9 +70,10 @@ class Game:
|
|||||||
self.car.controller.close()
|
self.car.controller.close()
|
||||||
|
|
||||||
def render(self):
|
def render(self):
|
||||||
self.win.fill(self.BACKGROUND_COLOR)
|
self.game_surf.fill(self.BACKGROUND_COLOR)
|
||||||
self.track.render(self.win, self.camera)
|
self.track.render(self.game_surf, self.camera)
|
||||||
self.car.render(self.win, self.camera, self.show_raycasts)
|
self.car.render(self.game_surf, self.camera, self.show_raycasts)
|
||||||
|
self.win.blit(self.game_surf, (0, 0))
|
||||||
if self.show_fps:
|
if self.show_fps:
|
||||||
self.render_fps()
|
self.render_fps()
|
||||||
if self.show_speed:
|
if self.show_speed:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import lzma
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import struct
|
import struct
|
||||||
import time
|
import time
|
||||||
@@ -12,7 +13,7 @@ class RecordFile:
|
|||||||
def __init__(self, path: str | Path, mode: Literal["w", "r"]) -> None:
|
def __init__(self, path: str | Path, mode: Literal["w", "r"]) -> None:
|
||||||
self.path: str | Path = path
|
self.path: str | Path = path
|
||||||
self.mode: Literal["w", "r"] = mode
|
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):
|
def __enter__(self):
|
||||||
return self
|
return self
|
||||||
@@ -21,8 +22,7 @@ class RecordFile:
|
|||||||
self.file.close()
|
self.file.close()
|
||||||
|
|
||||||
def write_header(self, n_snapshots: int):
|
def write_header(self, n_snapshots: int):
|
||||||
data: bytes = struct.pack(
|
data: bytes = struct.pack(">IId", self.VERSION, n_snapshots, time.time())
|
||||||
">IId", self.VERSION, n_snapshots, time.time())
|
|
||||||
self.file.write(data)
|
self.file.write(data)
|
||||||
|
|
||||||
def write_snapshots(self, snapshots: list[Snapshot]):
|
def write_snapshots(self, snapshots: list[Snapshot]):
|
||||||
@@ -35,7 +35,8 @@ class RecordFile:
|
|||||||
version: int = struct.unpack(">I", self.file.read(4))[0]
|
version: int = struct.unpack(">I", self.file.read(4))[0]
|
||||||
if version != self.VERSION:
|
if version != self.VERSION:
|
||||||
raise ValueError(
|
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
|
n_snapshots: int
|
||||||
timestamp: float
|
timestamp: float
|
||||||
|
|||||||
@@ -9,14 +9,15 @@ from PyQt6.QtCore import QObject, QThread, QTimer, pyqtSignal, pyqtSlot
|
|||||||
from PyQt6.QtGui import QKeyEvent
|
from PyQt6.QtGui import QKeyEvent
|
||||||
from PyQt6.QtWidgets import QMainWindow
|
from PyQt6.QtWidgets import QMainWindow
|
||||||
|
|
||||||
from src.command import CarControl, Command, ControlCommand, RecordingCommand
|
from src.bot import Bot
|
||||||
|
from src.command import ApplySnapshotCommand, CarControl, Command, ControlCommand, RecordingCommand, ResetCommand
|
||||||
from src.record_file import RecordFile
|
from src.record_file import RecordFile
|
||||||
from src.recorder_ui import Ui_Recorder
|
from src.recorder_ui import Ui_Recorder
|
||||||
from src.snapshot import Snapshot
|
from src.snapshot import Snapshot
|
||||||
|
|
||||||
|
|
||||||
class RecorderClient(QObject):
|
class RecorderClient(QObject):
|
||||||
DATA_CHUNK_SIZE = 4096
|
DATA_CHUNK_SIZE = 65536
|
||||||
data_received: pyqtSignal = pyqtSignal(Snapshot)
|
data_received: pyqtSignal = pyqtSignal(Snapshot)
|
||||||
|
|
||||||
def __init__(self, host: str, port: int) -> None:
|
def __init__(self, host: str, port: int) -> None:
|
||||||
@@ -27,6 +28,7 @@ class RecorderClient(QObject):
|
|||||||
socket.AF_INET, socket.SOCK_STREAM)
|
socket.AF_INET, socket.SOCK_STREAM)
|
||||||
self.timer: Optional[QTimer] = None
|
self.timer: Optional[QTimer] = None
|
||||||
self.connected: bool = False
|
self.connected: bool = False
|
||||||
|
self.buffer: bytes = b""
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def start(self):
|
def start(self):
|
||||||
@@ -39,27 +41,27 @@ class RecorderClient(QObject):
|
|||||||
print("Connected to server")
|
print("Connected to server")
|
||||||
|
|
||||||
def poll_socket(self):
|
def poll_socket(self):
|
||||||
buffer: bytes = b""
|
|
||||||
if not self.connected:
|
if not self.connected:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
chunk: bytes = self.socket.recv(self.DATA_CHUNK_SIZE)
|
|
||||||
if not chunk:
|
|
||||||
return
|
|
||||||
buffer += chunk
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if len(buffer) < 4:
|
chunk: bytes = self.socket.recv(self.DATA_CHUNK_SIZE)
|
||||||
break
|
if not chunk:
|
||||||
msg_len: int = struct.unpack(">I", buffer[:4])[0]
|
return
|
||||||
msg_end: int = 4 + msg_len
|
self.buffer += chunk
|
||||||
if len(buffer) < msg_end:
|
|
||||||
break
|
|
||||||
|
|
||||||
message: bytes = buffer[4:msg_end]
|
while True:
|
||||||
buffer = buffer[msg_end:]
|
if len(self.buffer) < 4:
|
||||||
self.on_message(message)
|
break
|
||||||
|
msg_len: int = struct.unpack(">I", self.buffer[:4])[0]
|
||||||
|
msg_end: int = 4 + msg_len
|
||||||
|
if len(self.buffer) < msg_end:
|
||||||
|
break
|
||||||
|
|
||||||
|
message: bytes = self.buffer[4:msg_end]
|
||||||
|
self.buffer = self.buffer[msg_end:]
|
||||||
|
self.on_message(message)
|
||||||
except BlockingIOError:
|
except BlockingIOError:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -162,9 +164,11 @@ class RecorderWindow(Ui_Recorder, QMainWindow):
|
|||||||
self.recordDataButton.clicked.connect(self.toggle_record)
|
self.recordDataButton.clicked.connect(self.toggle_record)
|
||||||
self.resetButton.clicked.connect(self.rollback)
|
self.resetButton.clicked.connect(self.rollback)
|
||||||
|
|
||||||
|
self.bot: Optional[Bot] = None
|
||||||
self.autopiloting = False
|
self.autopiloting = False
|
||||||
|
|
||||||
self.autopilotButton.clicked.connect(self.toggle_autopilot)
|
self.autopilotButton.clicked.connect(self.toggle_autopilot)
|
||||||
|
self.autopilotButton.setDisabled(True)
|
||||||
|
|
||||||
self.saveRecordButton.clicked.connect(self.save_record)
|
self.saveRecordButton.clicked.connect(self.save_record)
|
||||||
|
|
||||||
@@ -203,7 +207,19 @@ class RecorderWindow(Ui_Recorder, QMainWindow):
|
|||||||
self.send_command(RecordingCommand(self.recording))
|
self.send_command(RecordingCommand(self.recording))
|
||||||
|
|
||||||
def rollback(self):
|
def rollback(self):
|
||||||
pass
|
rollback_by: int = self.forgetSnapshotNumber.value()
|
||||||
|
rollback_by = max(0, min(rollback_by, len(self.snapshots) - 1))
|
||||||
|
|
||||||
|
self.snapshots = self.snapshots[:-rollback_by]
|
||||||
|
self.nbrSnapshotSaved.setText(str(len(self.snapshots)))
|
||||||
|
|
||||||
|
if len(self.snapshots) == 0:
|
||||||
|
self.send_command(ResetCommand())
|
||||||
|
else:
|
||||||
|
self.send_command(ApplySnapshotCommand(self.snapshots[-1]))
|
||||||
|
|
||||||
|
if self.recording:
|
||||||
|
self.toggle_record()
|
||||||
|
|
||||||
def toggle_autopilot(self):
|
def toggle_autopilot(self):
|
||||||
self.autopiloting = not self.autopiloting
|
self.autopiloting = not self.autopiloting
|
||||||
@@ -227,7 +243,7 @@ class RecorderWindow(Ui_Recorder, QMainWindow):
|
|||||||
|
|
||||||
self.SAVE_DIR.mkdir(exist_ok=True)
|
self.SAVE_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
record_name: str = "record_%d.rec"
|
record_name: str = "record_%d.rec.xz"
|
||||||
fid = 0
|
fid = 0
|
||||||
while os.path.exists(self.SAVE_DIR / (record_name % fid)):
|
while os.path.exists(self.SAVE_DIR / (record_name % fid)):
|
||||||
fid += 1
|
fid += 1
|
||||||
@@ -251,8 +267,15 @@ class RecorderWindow(Ui_Recorder, QMainWindow):
|
|||||||
self.snapshots.append(snapshot)
|
self.snapshots.append(snapshot)
|
||||||
self.nbrSnapshotSaved.setText(str(len(self.snapshots)))
|
self.nbrSnapshotSaved.setText(str(len(self.snapshots)))
|
||||||
|
|
||||||
|
if self.autopiloting and self.bot is not None:
|
||||||
|
self.bot.on_snapshot_received(snapshot)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.close_signal.emit()
|
self.close_signal.emit()
|
||||||
|
|
||||||
def send_command(self, command: Command):
|
def send_command(self, command: Command):
|
||||||
self.send_signal.emit(command)
|
self.send_signal.emit(command)
|
||||||
|
|
||||||
|
def register_bot(self, bot: Bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.autopilotButton.setDisabled(False)
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="resetButton">
|
<widget class="QPushButton" name="resetButton">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Reset</string>
|
<string>Rollback</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Form implementation generated from reading ui file 'recorder.ui'
|
# Form implementation generated from reading ui file 'recorder.ui'
|
||||||
#
|
#
|
||||||
# Created by: PyQt6 UI code generator 6.8.0
|
# Created by: PyQt6 UI code generator 6.8.1
|
||||||
#
|
#
|
||||||
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
|
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
|
||||||
# run again. Do not edit this file unless you know what you are doing.
|
# run again. Do not edit this file unless you know what you are doing.
|
||||||
@@ -102,7 +102,7 @@ class Ui_Recorder(object):
|
|||||||
self.recordDataButton.setText(_translate("Recorder", "Record"))
|
self.recordDataButton.setText(_translate("Recorder", "Record"))
|
||||||
self.saveImgCheckBox.setText(_translate("Recorder", "Imgs"))
|
self.saveImgCheckBox.setText(_translate("Recorder", "Imgs"))
|
||||||
self.saveRecordButton.setText(_translate("Recorder", "Save"))
|
self.saveRecordButton.setText(_translate("Recorder", "Save"))
|
||||||
self.resetButton.setText(_translate("Recorder", "Reset"))
|
self.resetButton.setText(_translate("Recorder", "Rollback"))
|
||||||
self.nbrSnapshotSaved.setText(_translate("Recorder", "0"))
|
self.nbrSnapshotSaved.setText(_translate("Recorder", "0"))
|
||||||
self.autopilotButton.setText(_translate("Recorder", "AutoPilot\n"
|
self.autopilotButton.setText(_translate("Recorder", "AutoPilot\n"
|
||||||
"OFF"))
|
"OFF"))
|
||||||
|
|||||||
@@ -6,17 +6,18 @@ import struct
|
|||||||
import threading
|
import threading
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from src.command import CarControl, Command, ControlCommand, RecordingCommand
|
from src.command import ApplySnapshotCommand, CarControl, Command, ControlCommand, RecordingCommand, ResetCommand
|
||||||
from src.snapshot import Snapshot
|
from src.snapshot import Snapshot
|
||||||
from src.utils import RepeatTimer
|
from src.utils import RepeatTimer
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from src.car import Car
|
from src.car import Car
|
||||||
|
from src.game import Game
|
||||||
|
|
||||||
|
|
||||||
class RemoteController:
|
class RemoteController:
|
||||||
DEFAULT_PORT = 5000
|
DEFAULT_PORT = 5000
|
||||||
DATA_CHUNK_SIZE = 4096
|
DATA_CHUNK_SIZE = 65536
|
||||||
|
|
||||||
CONTROL_ATTRIBUTES: dict[CarControl, str] = {
|
CONTROL_ATTRIBUTES: dict[CarControl, str] = {
|
||||||
CarControl.FORWARD: "forward",
|
CarControl.FORWARD: "forward",
|
||||||
@@ -27,7 +28,8 @@ class RemoteController:
|
|||||||
|
|
||||||
SNAPSHOT_INTERVAL = 0.1
|
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.car: Car = car
|
||||||
self.port: int = port
|
self.port: int = port
|
||||||
self.server: socket.socket = socket.socket(
|
self.server: socket.socket = socket.socket(
|
||||||
@@ -119,6 +121,10 @@ class RemoteController:
|
|||||||
self.set_control(control, active)
|
self.set_control(control, active)
|
||||||
case RecordingCommand(state):
|
case RecordingCommand(state):
|
||||||
self.recording = state
|
self.recording = state
|
||||||
|
case ApplySnapshotCommand(snapshot):
|
||||||
|
snapshot.apply(self.car)
|
||||||
|
case ResetCommand():
|
||||||
|
self.car.reset()
|
||||||
|
|
||||||
def set_control(self, control: CarControl, active: bool):
|
def set_control(self, control: CarControl, active: bool):
|
||||||
setattr(self.car, self.CONTROL_ATTRIBUTES[control], active)
|
setattr(self.car, self.CONTROL_ATTRIBUTES[control], active)
|
||||||
@@ -130,5 +136,6 @@ class RemoteController:
|
|||||||
return
|
return
|
||||||
|
|
||||||
snapshot: Snapshot = Snapshot.from_car(self.car)
|
snapshot: Snapshot = Snapshot.from_car(self.car)
|
||||||
|
snapshot.add_image(self.game)
|
||||||
payload: bytes = snapshot.pack()
|
payload: bytes = snapshot.pack()
|
||||||
self.client.sendall(struct.pack(">I", len(payload)) + payload)
|
self.client.sendall(struct.pack(">I", len(payload)) + payload)
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ from dataclasses import dataclass, field
|
|||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pygame
|
||||||
|
|
||||||
from src.vec import Vec
|
from src.vec import Vec
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from src.car import Car
|
from src.car import Car
|
||||||
|
from src.game import Game
|
||||||
|
|
||||||
|
|
||||||
def iter_unpack(format, data):
|
def iter_unpack(format, data):
|
||||||
@@ -63,7 +65,7 @@ class Snapshot:
|
|||||||
(nbr_raycasts,), data = iter_unpack(">B", data)
|
(nbr_raycasts,), data = iter_unpack(">B", data)
|
||||||
raycast_distances, data = iter_unpack(f">{nbr_raycasts}f", data)
|
raycast_distances, data = iter_unpack(f">{nbr_raycasts}f", data)
|
||||||
|
|
||||||
(h, w), data = iter_unpack(">ii", data)
|
(h, w), data = iter_unpack(">II", data)
|
||||||
|
|
||||||
if h * w > 0:
|
if h * w > 0:
|
||||||
image = np.frombuffer(data, np.uint8).reshape(h, w, 3)
|
image = np.frombuffer(data, np.uint8).reshape(h, w, 3)
|
||||||
@@ -94,3 +96,11 @@ class Snapshot:
|
|||||||
raycast_distances=car.rays.copy(),
|
raycast_distances=car.rays.copy(),
|
||||||
image=None
|
image=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def apply(self, car: Car):
|
||||||
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user