Compare commits
25 Commits
8989341714
...
feat/image
| Author | SHA1 | Date | |
|---|---|---|---|
|
cd866cc511
|
|||
|
9f86d060f0
|
|||
|
e2f4a0d2a5
|
|||
|
bc96cea2b9
|
|||
|
8d35f76b56
|
|||
|
97f9765705
|
|||
|
fa61e27825
|
|||
|
b60a0aba4f
|
|||
|
ae02ddefb0
|
|||
|
f1fadd123f
|
|||
|
8b7927a3c5
|
|||
|
62de92e7a2
|
|||
|
8ad97785b8
|
|||
|
db112ada4c
|
|||
|
8542ee81e7
|
|||
|
f91a4e8d61
|
|||
|
bf1935fd7e
|
|||
|
784d594a3b
|
|||
|
14c9fcdedb
|
|||
|
73d815b625
|
|||
|
eb06c114d2
|
|||
|
54f33b6572
|
|||
|
91e93759e8
|
|||
|
04ac674982
|
|||
|
8ca15eaa78
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -9,4 +9,7 @@ wheels/
|
||||
# Virtual environments
|
||||
.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 |
@@ -4,4 +4,9 @@ version = "0.1.0"
|
||||
description = "Rally racing game for ML"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = ["pygame>=2.6.1"]
|
||||
dependencies = [
|
||||
"numpy>=2.3.4",
|
||||
"pygame>=2.6.1",
|
||||
"pyqt6>=6.9.1",
|
||||
"qasync>=0.28.0",
|
||||
]
|
||||
|
||||
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()
|
||||
23
scripts/recorder.py
Normal file
23
scripts/recorder.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
|
||||
from src.recorder import RecorderWindow
|
||||
|
||||
|
||||
def main():
|
||||
import sys
|
||||
|
||||
def except_hook(cls, exception, traceback):
|
||||
sys.__excepthook__(cls, exception, traceback)
|
||||
|
||||
sys.excepthook = except_hook
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
window = RecorderWindow("localhost", 5000)
|
||||
app.aboutToQuit.connect(window.shutdown)
|
||||
window.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
|
||||
@@ -23,8 +23,12 @@ class Camera:
|
||||
def center(self) -> Vec:
|
||||
return self.size / 2
|
||||
|
||||
@property
|
||||
def car_screen_pos(self) -> Vec:
|
||||
return Vec(self.size.x / 2, 3 * self.size.y / 4)
|
||||
|
||||
def screen2world(self, screen_pos: Vec) -> Vec:
|
||||
delta: Vec = screen_pos - self.center
|
||||
delta: Vec = screen_pos - self.car_screen_pos
|
||||
delta /= self.zoom * self.UNIT_RATIO
|
||||
dx: float = delta.x
|
||||
dy: float = delta.y
|
||||
@@ -39,5 +43,8 @@ class Camera:
|
||||
dy: float = -delta.dot(self.up)
|
||||
dx: float = delta.dot(self.up.perp)
|
||||
screen_delta: Vec = Vec(dx, dy) * self.zoom * self.UNIT_RATIO
|
||||
screen_pos: Vec = self.center + screen_delta
|
||||
screen_pos: Vec = self.car_screen_pos + screen_delta
|
||||
return screen_pos
|
||||
|
||||
def size2screen(self, size: float) -> float:
|
||||
return size * self.zoom * self.UNIT_RATIO
|
||||
|
||||
92
src/car.py
92
src/car.py
@@ -1,27 +1,41 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from math import radians
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import pygame
|
||||
|
||||
from src.camera import Camera
|
||||
from src.utils import segments_intersect
|
||||
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
|
||||
|
||||
sign = lambda x: 0 if x == 0 else (-1 if x < 0 else 1)
|
||||
|
||||
def sign(x): return 0 if x == 0 else (-1 if x < 0 else 1)
|
||||
|
||||
|
||||
class Car:
|
||||
MAX_SPEED = 5
|
||||
MAX_BACK_SPEED = -2
|
||||
MAX_BACK_SPEED = -3
|
||||
ROTATE_SPEED = 1
|
||||
COLOR = (230, 150, 80)
|
||||
CTRL_COLOR = (80, 230, 150)
|
||||
WIDTH = 0.4
|
||||
LENGTH = 0.6
|
||||
COLLISION_MARGIN = 0.4
|
||||
ACCELERATION = 2
|
||||
FRICTION = 3
|
||||
FRICTION = 2.5
|
||||
N_RAYS = 15
|
||||
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
|
||||
self.direction: Vec = direction
|
||||
self.speed: float = 0
|
||||
@@ -31,6 +45,12 @@ class Car:
|
||||
self.right: bool = False
|
||||
self.colliding: bool = False
|
||||
|
||||
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.game, self)
|
||||
self.controller.start_server()
|
||||
|
||||
def update(self, dt: float):
|
||||
if self.forward:
|
||||
self.speed += self.ACCELERATION * dt
|
||||
@@ -53,18 +73,34 @@ class Car:
|
||||
self.direction = self.direction.rotate(rotate_angle)
|
||||
|
||||
if not self.forward and not self.backward:
|
||||
fn = max if self.speed >= 0 else min
|
||||
self.speed -= sign(self.speed) * self.FRICTION * dt
|
||||
self.speed = fn(0, self.speed)
|
||||
|
||||
if abs(self.speed) < 1e-4:
|
||||
self.speed = 0
|
||||
|
||||
self.pos += self.direction * self.speed * dt
|
||||
|
||||
def render(self, surf: pygame.Surface, camera: Camera):
|
||||
def render(self, surf: pygame.Surface, camera: Camera, show_raycasts: bool = False):
|
||||
if show_raycasts:
|
||||
pos: Vec = camera.world2screen(self.pos)
|
||||
for p in self.rays_end:
|
||||
pygame.draw.line(surf, (255, 0, 0), pos,
|
||||
camera.world2screen(p), 2)
|
||||
|
||||
pts: list[Vec] = self.get_corners()
|
||||
pts = [camera.world2screen(p) for p in pts]
|
||||
pygame.draw.polygon(surf, self.COLOR, pts)
|
||||
|
||||
if self.controller.is_connected:
|
||||
pygame.draw.circle(
|
||||
surf,
|
||||
self.CTRL_COLOR,
|
||||
camera.world2screen(self.pos),
|
||||
camera.size2screen(self.WIDTH / 4),
|
||||
)
|
||||
|
||||
def get_corners(self) -> list[Vec]:
|
||||
u: Vec = self.direction * self.LENGTH / 2
|
||||
v: Vec = self.direction.perp * self.WIDTH / 2
|
||||
@@ -76,6 +112,8 @@ class Car:
|
||||
return [p1, p2, p3, p4]
|
||||
|
||||
def check_collisions(self, polygons: list[list[Vec]]):
|
||||
self.cast_rays(polygons)
|
||||
|
||||
self.colliding = False
|
||||
corners: list[Vec] = self.get_corners()
|
||||
sides: list[tuple[Vec, Vec]] = [
|
||||
@@ -99,5 +137,45 @@ class Car:
|
||||
n *= -1
|
||||
dist = -dist
|
||||
self.speed = 0
|
||||
self.pos = self.pos + n * (self.COLLISION_MARGIN - dist)
|
||||
self.pos = self.pos + n * \
|
||||
(self.COLLISION_MARGIN - dist)
|
||||
return
|
||||
|
||||
def cast_rays(self, polygons: list[list[Vec]]):
|
||||
for i in range(self.N_RAYS):
|
||||
angle: float = radians(
|
||||
(i / (self.N_RAYS - 1) - 0.5) * self.RAYS_FOV)
|
||||
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_end[i] = self.pos if p is None else p
|
||||
|
||||
def cast_ray(self, angle: float, polygons: list[list[Vec]]) -> Optional[Vec]:
|
||||
v: Vec = self.direction.normalized.rotate(angle)
|
||||
|
||||
segments: list[tuple[Vec, Vec]] = []
|
||||
for polygon in polygons:
|
||||
n_pts: int = len(polygon)
|
||||
for i in range(n_pts):
|
||||
pt1: Vec = polygon[i]
|
||||
pt2: Vec = polygon[(i + 1) % n_pts]
|
||||
segments.append((pt1, pt2))
|
||||
|
||||
p1: Vec = self.pos
|
||||
p2: Vec = p1 + v * self.RAYS_MAX_DIST
|
||||
dist: float = self.RAYS_MAX_DIST
|
||||
closest: Optional[Vec] = None
|
||||
|
||||
for q1, q2 in segments:
|
||||
p: Optional[Vec] = get_segments_intersection(p1, p2, q1, q2)
|
||||
if p is not None:
|
||||
d: float = (p - p1).mag()
|
||||
if d < dist:
|
||||
dist = d
|
||||
closest = p
|
||||
return closest
|
||||
|
||||
def reset(self):
|
||||
self.pos = self.initial_pos.copy()
|
||||
self.direction = self.initial_dir.copy()
|
||||
self.speed = 0
|
||||
|
||||
109
src/command.py
Normal file
109
src/command.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
from enum import IntEnum
|
||||
import struct
|
||||
from typing import Type
|
||||
|
||||
from src.snapshot import Snapshot
|
||||
|
||||
|
||||
class CommandType(IntEnum):
|
||||
CAR_CONTROL = 0
|
||||
RECORDING = 1
|
||||
APPLY_SNAPSHOT = 2
|
||||
RESET = 3
|
||||
|
||||
|
||||
class CarControl(IntEnum):
|
||||
FORWARD = 0
|
||||
BACKWARD = 1
|
||||
LEFT = 2
|
||||
RIGHT = 3
|
||||
|
||||
|
||||
class Command(abc.ABC):
|
||||
TYPE: CommandType
|
||||
REGISTRY: dict[CommandType, Type[Command]] = {}
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
super().__init_subclass__()
|
||||
if cls.TYPE in Command.REGISTRY:
|
||||
raise ValueError(
|
||||
f"Command type {cls.TYPE} already registered by {Command.REGISTRY[cls.TYPE]}"
|
||||
)
|
||||
Command.REGISTRY[cls.TYPE] = cls
|
||||
|
||||
def get_payload(self) -> bytes:
|
||||
return b""
|
||||
|
||||
def pack(self) -> bytes:
|
||||
payload: bytes = self.get_payload()
|
||||
return struct.pack(">B", self.TYPE) + payload
|
||||
|
||||
@staticmethod
|
||||
def unpack(data: bytes) -> Command:
|
||||
type: CommandType = CommandType(data[0])
|
||||
return Command.REGISTRY[type].from_payload(data[1:])
|
||||
|
||||
@classmethod
|
||||
def from_payload(cls, payload: bytes) -> Command:
|
||||
return cls()
|
||||
|
||||
|
||||
class ControlCommand(Command):
|
||||
TYPE = CommandType.CAR_CONTROL
|
||||
__match_args__ = ("control", "active")
|
||||
|
||||
def __init__(self, control: CarControl, active: bool) -> None:
|
||||
super().__init__()
|
||||
self.control: CarControl = control
|
||||
self.active: bool = active
|
||||
|
||||
def get_payload(self) -> bytes:
|
||||
return struct.pack(">B", (self.control << 1) | self.active)
|
||||
|
||||
@classmethod
|
||||
def from_payload(cls, payload: bytes) -> Command:
|
||||
value: int = payload[0]
|
||||
active: bool = (value & 1) == 1
|
||||
control: int = value >> 1
|
||||
return ControlCommand(CarControl(control), active)
|
||||
|
||||
|
||||
class RecordingCommand(Command):
|
||||
TYPE = CommandType.RECORDING
|
||||
__match_args__ = ("state",)
|
||||
|
||||
def __init__(self, state: bool) -> None:
|
||||
super().__init__()
|
||||
self.state: bool = state
|
||||
|
||||
def get_payload(self) -> bytes:
|
||||
return struct.pack(">B", self.state)
|
||||
|
||||
@classmethod
|
||||
def from_payload(cls, payload: bytes) -> Command:
|
||||
state: bool = struct.unpack(">B", payload)[0]
|
||||
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
|
||||
32
src/game.py
32
src/game.py
@@ -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()
|
||||
@@ -31,24 +32,25 @@ class Game:
|
||||
)
|
||||
self.show_fps: bool = True
|
||||
self.show_speed: bool = True
|
||||
self.show_raycasts: bool = True
|
||||
|
||||
def mainloop(self):
|
||||
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()
|
||||
self.render()
|
||||
self.clock.tick(60)
|
||||
|
||||
def process_pygame_events(self):
|
||||
self.camera.set_pos(self.car.pos)
|
||||
self.camera.set_direction(self.car.direction)
|
||||
self.camera.set_size(Vec(*self.win.get_size()))
|
||||
for event in pygame.event.get():
|
||||
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:
|
||||
@@ -58,13 +60,20 @@ class Game:
|
||||
elif event.type == pygame.KEYUP:
|
||||
self.on_key_up(event)
|
||||
|
||||
def update_camera(self):
|
||||
self.camera.set_pos(self.car.pos)
|
||||
self.camera.set_direction(self.car.direction)
|
||||
self.camera.set_size(Vec(*self.win.get_size()))
|
||||
|
||||
def quit(self):
|
||||
self.running = False
|
||||
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.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:
|
||||
@@ -85,6 +94,10 @@ class Game:
|
||||
self.show_fps = not self.show_fps
|
||||
elif event.key == pygame.K_v:
|
||||
self.show_speed = not self.show_speed
|
||||
elif event.key == pygame.K_c:
|
||||
self.show_raycasts = not self.show_raycasts
|
||||
elif event.key == pygame.K_r:
|
||||
self.reset()
|
||||
|
||||
def on_key_up(self, event: pygame.event.Event):
|
||||
if event.key == pygame.K_w:
|
||||
@@ -125,3 +138,8 @@ class Game:
|
||||
pts2.append((ox + r2 * dx, oy + r2 * dy))
|
||||
|
||||
pygame.draw.polygon(self.win, (200, 200, 200), pts1 + pts2[::-1])
|
||||
|
||||
def reset(self):
|
||||
self.car.pos = self.track.start_pos
|
||||
self.car.direction = self.track.start_dir
|
||||
self.car.speed = 0
|
||||
|
||||
@@ -10,9 +10,14 @@ from src.vec import Vec
|
||||
class Road(TrackObject):
|
||||
type = TrackObjectType.Road
|
||||
|
||||
STRIP_LENGTH = 0.5
|
||||
STRIP_GAP = 0.5
|
||||
|
||||
def __init__(self, pts: list[RoadPoint]) -> None:
|
||||
super().__init__()
|
||||
self.pts: list[RoadPoint] = pts
|
||||
self.strips: list[tuple[Vec, Vec]] = []
|
||||
self.compute_strips()
|
||||
|
||||
@classmethod
|
||||
def load(cls, data: dict) -> Road:
|
||||
@@ -40,6 +45,15 @@ class Road(TrackObject):
|
||||
pygame.draw.lines(surf, (255, 255, 255), True, side1)
|
||||
pygame.draw.lines(surf, (255, 255, 255), True, side2)
|
||||
|
||||
for p1, p2 in self.strips:
|
||||
pygame.draw.line(
|
||||
surf,
|
||||
(255, 255, 255),
|
||||
camera.world2screen(p1),
|
||||
camera.world2screen(p2),
|
||||
6,
|
||||
)
|
||||
|
||||
def get_collision_polygons(self) -> list[list[Vec]]:
|
||||
side1: list[Vec] = []
|
||||
side2: list[Vec] = []
|
||||
@@ -49,9 +63,50 @@ class Road(TrackObject):
|
||||
p3: Vec = p1 - pt.normal * pt.width
|
||||
side1.append(p2)
|
||||
side2.append(p3)
|
||||
|
||||
return [side1, side2]
|
||||
|
||||
def compute_strips(self):
|
||||
n: int = len(self.pts)
|
||||
vecs: list[Vec] = [
|
||||
self.pts[(i + 1) % n].pos - pt.pos for i, pt in enumerate(self.pts)
|
||||
]
|
||||
lengths: list[float] = [v.mag() for v in vecs]
|
||||
cum_sums: list[float] = [0]
|
||||
for l in lengths:
|
||||
cum_sums.append(cum_sums[-1] + l)
|
||||
self.strips = []
|
||||
total_length: float = sum(lengths)
|
||||
|
||||
def get_pt(length: float) -> tuple[int, float]:
|
||||
length %= total_length
|
||||
for i, cs in list(enumerate(cum_sums))[::-1]:
|
||||
if cs <= length:
|
||||
return (i, (length - cs) / lengths[i])
|
||||
raise ValueError()
|
||||
|
||||
l0: float = 0
|
||||
while l0 < total_length:
|
||||
l1: float = l0 + self.STRIP_LENGTH
|
||||
i0, t0 = get_pt(l0)
|
||||
i1, t1 = get_pt(l1)
|
||||
p0: Vec = self.pts[i0].pos + vecs[i0] * t0
|
||||
p1: Vec = self.pts[i1].pos + vecs[i1] * t1
|
||||
if i0 == i1:
|
||||
self.strips.append((p0, p1))
|
||||
elif (i0 + 1) % n == i1:
|
||||
pm: Vec = self.pts[i1].pos
|
||||
self.strips.append((p0, pm))
|
||||
self.strips.append((pm, p1))
|
||||
else:
|
||||
self.strips.append((p0, self.pts[(i0 + 1) % n].pos))
|
||||
i = (i0 + 1) % n
|
||||
while i != i1:
|
||||
i2 = (i + 1) % n
|
||||
self.strips.append((self.pts[i].pos, self.pts[i2].pos))
|
||||
i = i2
|
||||
self.strips.append((self.pts[i1].pos, p1))
|
||||
l0 = l1 + self.STRIP_GAP
|
||||
|
||||
|
||||
class RoadPoint:
|
||||
def __init__(self, pos: Vec, normal: Vec, width: float) -> None:
|
||||
|
||||
50
src/record_file.py
Normal file
50
src/record_file.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import lzma
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import time
|
||||
from typing import Literal
|
||||
|
||||
from src.snapshot import Snapshot
|
||||
|
||||
|
||||
class RecordFile:
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self, path: str | Path, mode: Literal["w", "r"]) -> None:
|
||||
self.path: str | Path = path
|
||||
self.mode: Literal["w", "r"] = mode
|
||||
self.file: lzma.LZMAFile = lzma.LZMAFile(self.path, self.mode)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
self.file.close()
|
||||
|
||||
def write_header(self, n_snapshots: int):
|
||||
data: bytes = struct.pack(">IId", self.VERSION, n_snapshots, time.time())
|
||||
self.file.write(data)
|
||||
|
||||
def write_snapshots(self, snapshots: list[Snapshot]):
|
||||
self.write_header(len(snapshots))
|
||||
for snapshot in snapshots:
|
||||
data: bytes = snapshot.pack()
|
||||
self.file.write(struct.pack(">I", len(data)) + data)
|
||||
|
||||
def read_snapshots(self) -> list[Snapshot]:
|
||||
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})"
|
||||
)
|
||||
|
||||
n_snapshots: int
|
||||
timestamp: float
|
||||
n_snapshots, timestamp = struct.unpack(">Id", self.file.read(12))
|
||||
snapshots: list[Snapshot] = []
|
||||
|
||||
for _ in range(n_snapshots):
|
||||
size: int = struct.unpack(">I", self.file.read(4))[0]
|
||||
snapshots.append(Snapshot.unpack(self.file.read(size)))
|
||||
|
||||
return snapshots
|
||||
281
src/recorder.py
Normal file
281
src/recorder.py
Normal file
@@ -0,0 +1,281 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import struct
|
||||
from typing import Optional
|
||||
|
||||
from PyQt6 import uic
|
||||
from PyQt6.QtCore import QObject, QThread, QTimer, pyqtSignal, pyqtSlot
|
||||
from PyQt6.QtGui import QKeyEvent
|
||||
from PyQt6.QtWidgets import QMainWindow
|
||||
|
||||
from src.bot import Bot
|
||||
from src.command import ApplySnapshotCommand, CarControl, Command, ControlCommand, RecordingCommand, ResetCommand
|
||||
from src.record_file import RecordFile
|
||||
from src.recorder_ui import Ui_Recorder
|
||||
from src.snapshot import Snapshot
|
||||
|
||||
|
||||
class RecorderClient(QObject):
|
||||
DATA_CHUNK_SIZE = 65536
|
||||
data_received: pyqtSignal = pyqtSignal(Snapshot)
|
||||
|
||||
def __init__(self, host: str, port: int) -> None:
|
||||
super().__init__()
|
||||
self.host: str = host
|
||||
self.port: int = port
|
||||
self.socket: socket.socket = socket.socket(
|
||||
socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.timer: Optional[QTimer] = None
|
||||
self.connected: bool = False
|
||||
self.buffer: bytes = b""
|
||||
|
||||
@pyqtSlot()
|
||||
def start(self):
|
||||
self.socket.connect((self.host, self.port))
|
||||
self.socket.setblocking(False)
|
||||
self.connected = True
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self.poll_socket)
|
||||
self.timer.start(50)
|
||||
print("Connected to server")
|
||||
|
||||
def poll_socket(self):
|
||||
if not self.connected:
|
||||
return
|
||||
|
||||
try:
|
||||
while True:
|
||||
chunk: bytes = self.socket.recv(self.DATA_CHUNK_SIZE)
|
||||
if not chunk:
|
||||
return
|
||||
self.buffer += chunk
|
||||
|
||||
while True:
|
||||
if len(self.buffer) < 4:
|
||||
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:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Socket error: {e}")
|
||||
self.shutdown()
|
||||
|
||||
def on_message(self, message: bytes):
|
||||
snapshot: Snapshot = Snapshot.unpack(message)
|
||||
self.data_received.emit(snapshot)
|
||||
|
||||
@pyqtSlot(object)
|
||||
def send_command(self, command):
|
||||
if self.connected:
|
||||
try:
|
||||
payload: bytes = command.pack()
|
||||
self.socket.sendall(struct.pack(">I", len(payload)) + payload)
|
||||
except Exception as e:
|
||||
print(f"An exception occured: {e}")
|
||||
self.shutdown()
|
||||
else:
|
||||
print("Not connected")
|
||||
|
||||
@pyqtSlot()
|
||||
def shutdown(self):
|
||||
print("Shutting down client")
|
||||
if self.timer is not None:
|
||||
self.timer.stop()
|
||||
self.timer = None
|
||||
self.connected = False
|
||||
self.socket.close()
|
||||
|
||||
|
||||
class ThreadedSaver(QThread):
|
||||
def __init__(self, path: str | Path, snapshots: list[Snapshot]):
|
||||
super().__init__()
|
||||
self.path: str | Path = path
|
||||
self.snapshots: list[Snapshot] = snapshots
|
||||
|
||||
def run(self):
|
||||
with RecordFile(self.path, "w") as f:
|
||||
f.write_snapshots(self.snapshots)
|
||||
|
||||
|
||||
class RecorderWindow(Ui_Recorder, QMainWindow):
|
||||
close_signal: pyqtSignal = pyqtSignal()
|
||||
send_signal: pyqtSignal = pyqtSignal(object)
|
||||
|
||||
SAVE_DIR: Path = Path(__file__).parent.parent / "records"
|
||||
|
||||
COMMAND_DIRECTIONS: dict[str, CarControl] = {
|
||||
"w": CarControl.FORWARD,
|
||||
"s": CarControl.BACKWARD,
|
||||
"d": CarControl.RIGHT,
|
||||
"a": CarControl.LEFT,
|
||||
}
|
||||
|
||||
def __init__(self, host: str, port: int) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.host: str = host
|
||||
self.port: int = port
|
||||
self.client_thread: QThread = QThread()
|
||||
self.client: RecorderClient = RecorderClient(self.host, self.port)
|
||||
self.client.data_received.connect(self.on_snapshot_received)
|
||||
self.client.moveToThread(self.client_thread)
|
||||
self.client_thread.started.connect(self.client.start)
|
||||
self.close_signal.connect(self.client.shutdown)
|
||||
self.send_signal.connect(self.client.send_command)
|
||||
|
||||
uic.load_ui.loadUi("src/recorder.ui", self)
|
||||
|
||||
self.forwardButton.pressed.connect(
|
||||
lambda: self.on_car_controlled(CarControl.FORWARD, True)
|
||||
)
|
||||
self.forwardButton.released.connect(
|
||||
lambda: self.on_car_controlled(CarControl.FORWARD, False)
|
||||
)
|
||||
|
||||
self.backwardButton.pressed.connect(
|
||||
lambda: self.on_car_controlled(CarControl.BACKWARD, True)
|
||||
)
|
||||
self.backwardButton.released.connect(
|
||||
lambda: self.on_car_controlled(CarControl.BACKWARD, False)
|
||||
)
|
||||
|
||||
self.rightButton.pressed.connect(
|
||||
lambda: self.on_car_controlled(CarControl.RIGHT, True)
|
||||
)
|
||||
self.rightButton.released.connect(
|
||||
lambda: self.on_car_controlled(CarControl.RIGHT, False)
|
||||
)
|
||||
|
||||
self.leftButton.pressed.connect(
|
||||
lambda: self.on_car_controlled(CarControl.LEFT, True)
|
||||
)
|
||||
self.leftButton.released.connect(
|
||||
lambda: self.on_car_controlled(CarControl.LEFT, False)
|
||||
)
|
||||
|
||||
self.recordDataButton.clicked.connect(self.toggle_record)
|
||||
self.resetButton.clicked.connect(self.rollback)
|
||||
|
||||
self.bot: Optional[Bot] = None
|
||||
self.autopiloting = False
|
||||
|
||||
self.autopilotButton.clicked.connect(self.toggle_autopilot)
|
||||
self.autopilotButton.setDisabled(True)
|
||||
|
||||
self.saveRecordButton.clicked.connect(self.save_record)
|
||||
|
||||
self.saving_worker: Optional[ThreadedSaver] = None
|
||||
self.recording = False
|
||||
|
||||
self.snapshots: list[Snapshot] = []
|
||||
self.client_thread.start()
|
||||
|
||||
def on_car_controlled(self, control: CarControl, active: bool):
|
||||
self.send_command(ControlCommand(control, active))
|
||||
|
||||
def keyPressEvent(self, event): # type: ignore
|
||||
if event.isAutoRepeat():
|
||||
return
|
||||
|
||||
if isinstance(event, QKeyEvent):
|
||||
key_text = event.text()
|
||||
ctrl: Optional[CarControl] = self.COMMAND_DIRECTIONS.get(key_text)
|
||||
if ctrl is not None:
|
||||
self.on_car_controlled(ctrl, True)
|
||||
|
||||
def keyReleaseEvent(self, event): # type: ignore
|
||||
if event.isAutoRepeat():
|
||||
return
|
||||
if isinstance(event, QKeyEvent):
|
||||
key_text = event.text()
|
||||
ctrl: Optional[CarControl] = self.COMMAND_DIRECTIONS.get(key_text)
|
||||
if ctrl is not None:
|
||||
self.on_car_controlled(ctrl, False)
|
||||
|
||||
def toggle_record(self):
|
||||
self.recording = not self.recording
|
||||
self.recordDataButton.setText(
|
||||
"Recording..." if self.recording else "Record")
|
||||
self.send_command(RecordingCommand(self.recording))
|
||||
|
||||
def rollback(self):
|
||||
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):
|
||||
self.autopiloting = not self.autopiloting
|
||||
self.autopilotButton.setText(
|
||||
"AutoPilot:\n" + ("ON" if self.autopiloting else "OFF")
|
||||
)
|
||||
|
||||
def save_record(self):
|
||||
if self.saving_worker is not None:
|
||||
print("Already saving !")
|
||||
return
|
||||
|
||||
if len(self.snapshots) == 0:
|
||||
print("No data to save !")
|
||||
return
|
||||
|
||||
if self.recording:
|
||||
self.toggle_record()
|
||||
|
||||
self.saveRecordButton.setText("Saving ...")
|
||||
|
||||
self.SAVE_DIR.mkdir(exist_ok=True)
|
||||
|
||||
record_name: str = "record_%d.rec.xz"
|
||||
fid = 0
|
||||
while os.path.exists(self.SAVE_DIR / (record_name % fid)):
|
||||
fid += 1
|
||||
|
||||
self.saving_worker = ThreadedSaver(
|
||||
self.SAVE_DIR / (record_name % fid), self.snapshots)
|
||||
self.snapshots = []
|
||||
self.nbrSnapshotSaved.setText("0")
|
||||
self.saving_worker.finished.connect(self.on_record_save_done)
|
||||
self.saving_worker.start()
|
||||
|
||||
def on_record_save_done(self):
|
||||
if self.saving_worker is None:
|
||||
return
|
||||
print("Recorded data saved to", self.saving_worker.path)
|
||||
self.saving_worker = None
|
||||
self.saveRecordButton.setText("Save")
|
||||
|
||||
@pyqtSlot(Snapshot)
|
||||
def on_snapshot_received(self, snapshot: Snapshot):
|
||||
self.snapshots.append(snapshot)
|
||||
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):
|
||||
self.close_signal.emit()
|
||||
|
||||
def send_command(self, command: Command):
|
||||
self.send_signal.emit(command)
|
||||
|
||||
def register_bot(self, bot: Bot):
|
||||
self.bot = bot
|
||||
self.autopilotButton.setDisabled(False)
|
||||
157
src/recorder.ui
Normal file
157
src/recorder.ui
Normal file
@@ -0,0 +1,157 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Recorder</class>
|
||||
<widget class="QWidget" name="Recorder">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>303</width>
|
||||
<height>233</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Recorder</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="gridLayoutWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>301</width>
|
||||
<height>231</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout" columnstretch="0,0,0">
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="forwardButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Forward (W)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QPushButton" name="leftButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Left (A)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QPushButton" name="rightButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Right (D)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="recordDataButton">
|
||||
<property name="autoFillBackground">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Record</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="saveImgCheckBox">
|
||||
<property name="text">
|
||||
<string>Imgs</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="saveRecordButton">
|
||||
<property name="text">
|
||||
<string>Save</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QPushButton" name="resetButton">
|
||||
<property name="text">
|
||||
<string>Rollback</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="forgetSnapshotNumber">
|
||||
<property name="minimum">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="nbrSnapshotSaved">
|
||||
<property name="text">
|
||||
<string>0</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QPushButton" name="autopilotButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>AutoPilot
|
||||
OFF</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QPushButton" name="backwardButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Backward (S)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
109
src/recorder_ui.py
Normal file
109
src/recorder_ui.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# Form implementation generated from reading ui file 'recorder.ui'
|
||||
#
|
||||
# Created by: PyQt6 UI code generator 6.8.1
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
from PyQt6 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class Ui_Recorder(object):
|
||||
def setupUi(self, Recorder):
|
||||
Recorder.setObjectName("Recorder")
|
||||
Recorder.resize(303, 233)
|
||||
self.gridLayoutWidget = QtWidgets.QWidget(parent=Recorder)
|
||||
self.gridLayoutWidget.setGeometry(QtCore.QRect(0, 0, 301, 231))
|
||||
self.gridLayoutWidget.setObjectName("gridLayoutWidget")
|
||||
self.gridLayout = QtWidgets.QGridLayout(self.gridLayoutWidget)
|
||||
self.gridLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.forwardButton = QtWidgets.QPushButton(parent=self.gridLayoutWidget)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.forwardButton.sizePolicy().hasHeightForWidth())
|
||||
self.forwardButton.setSizePolicy(sizePolicy)
|
||||
self.forwardButton.setObjectName("forwardButton")
|
||||
self.gridLayout.addWidget(self.forwardButton, 0, 1, 1, 1)
|
||||
self.leftButton = QtWidgets.QPushButton(parent=self.gridLayoutWidget)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.leftButton.sizePolicy().hasHeightForWidth())
|
||||
self.leftButton.setSizePolicy(sizePolicy)
|
||||
self.leftButton.setObjectName("leftButton")
|
||||
self.gridLayout.addWidget(self.leftButton, 3, 0, 1, 1)
|
||||
self.rightButton = QtWidgets.QPushButton(parent=self.gridLayoutWidget)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.rightButton.sizePolicy().hasHeightForWidth())
|
||||
self.rightButton.setSizePolicy(sizePolicy)
|
||||
self.rightButton.setObjectName("rightButton")
|
||||
self.gridLayout.addWidget(self.rightButton, 3, 2, 1, 1)
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout()
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.recordDataButton = QtWidgets.QPushButton(parent=self.gridLayoutWidget)
|
||||
self.recordDataButton.setAutoFillBackground(False)
|
||||
self.recordDataButton.setObjectName("recordDataButton")
|
||||
self.verticalLayout.addWidget(self.recordDataButton)
|
||||
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||
self.saveImgCheckBox = QtWidgets.QCheckBox(parent=self.gridLayoutWidget)
|
||||
self.saveImgCheckBox.setObjectName("saveImgCheckBox")
|
||||
self.horizontalLayout.addWidget(self.saveImgCheckBox)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
||||
self.saveRecordButton = QtWidgets.QPushButton(parent=self.gridLayoutWidget)
|
||||
self.saveRecordButton.setObjectName("saveRecordButton")
|
||||
self.verticalLayout.addWidget(self.saveRecordButton)
|
||||
self.gridLayout.addLayout(self.verticalLayout, 0, 2, 1, 1)
|
||||
self.verticalLayout_2 = QtWidgets.QVBoxLayout()
|
||||
self.verticalLayout_2.setObjectName("verticalLayout_2")
|
||||
self.resetButton = QtWidgets.QPushButton(parent=self.gridLayoutWidget)
|
||||
self.resetButton.setObjectName("resetButton")
|
||||
self.verticalLayout_2.addWidget(self.resetButton)
|
||||
self.forgetSnapshotNumber = QtWidgets.QSpinBox(parent=self.gridLayoutWidget)
|
||||
self.forgetSnapshotNumber.setMinimum(10)
|
||||
self.forgetSnapshotNumber.setObjectName("forgetSnapshotNumber")
|
||||
self.verticalLayout_2.addWidget(self.forgetSnapshotNumber)
|
||||
self.nbrSnapshotSaved = QtWidgets.QLabel(parent=self.gridLayoutWidget)
|
||||
self.nbrSnapshotSaved.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
self.nbrSnapshotSaved.setObjectName("nbrSnapshotSaved")
|
||||
self.verticalLayout_2.addWidget(self.nbrSnapshotSaved)
|
||||
self.gridLayout.addLayout(self.verticalLayout_2, 0, 0, 1, 1)
|
||||
self.autopilotButton = QtWidgets.QPushButton(parent=self.gridLayoutWidget)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.autopilotButton.sizePolicy().hasHeightForWidth())
|
||||
self.autopilotButton.setSizePolicy(sizePolicy)
|
||||
self.autopilotButton.setObjectName("autopilotButton")
|
||||
self.gridLayout.addWidget(self.autopilotButton, 3, 1, 1, 1)
|
||||
self.backwardButton = QtWidgets.QPushButton(parent=self.gridLayoutWidget)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.backwardButton.sizePolicy().hasHeightForWidth())
|
||||
self.backwardButton.setSizePolicy(sizePolicy)
|
||||
self.backwardButton.setObjectName("backwardButton")
|
||||
self.gridLayout.addWidget(self.backwardButton, 4, 1, 1, 1)
|
||||
|
||||
self.retranslateUi(Recorder)
|
||||
QtCore.QMetaObject.connectSlotsByName(Recorder)
|
||||
|
||||
def retranslateUi(self, Recorder):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
Recorder.setWindowTitle(_translate("Recorder", "Recorder"))
|
||||
self.forwardButton.setText(_translate("Recorder", "Forward (W)"))
|
||||
self.leftButton.setText(_translate("Recorder", "Left (A)"))
|
||||
self.rightButton.setText(_translate("Recorder", "Right (D)"))
|
||||
self.recordDataButton.setText(_translate("Recorder", "Record"))
|
||||
self.saveImgCheckBox.setText(_translate("Recorder", "Imgs"))
|
||||
self.saveRecordButton.setText(_translate("Recorder", "Save"))
|
||||
self.resetButton.setText(_translate("Recorder", "Rollback"))
|
||||
self.nbrSnapshotSaved.setText(_translate("Recorder", "0"))
|
||||
self.autopilotButton.setText(_translate("Recorder", "AutoPilot\n"
|
||||
"OFF"))
|
||||
self.backwardButton.setText(_translate("Recorder", "Backward (S)"))
|
||||
141
src/remote_controller.py
Normal file
141
src/remote_controller.py
Normal file
@@ -0,0 +1,141 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import queue
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from src.command import ApplySnapshotCommand, CarControl, Command, ControlCommand, RecordingCommand, ResetCommand
|
||||
from src.snapshot import Snapshot
|
||||
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 = 65536
|
||||
|
||||
CONTROL_ATTRIBUTES: dict[CarControl, str] = {
|
||||
CarControl.FORWARD: "forward",
|
||||
CarControl.BACKWARD: "backward",
|
||||
CarControl.LEFT: "left",
|
||||
CarControl.RIGHT: "right",
|
||||
}
|
||||
|
||||
SNAPSHOT_INTERVAL = 0.1
|
||||
|
||||
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(
|
||||
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
|
||||
self.snapshot_timer: RepeatTimer = RepeatTimer(
|
||||
interval=self.SNAPSHOT_INTERVAL, function=self.take_snapshot)
|
||||
self.snapshot_timer.start()
|
||||
self.recording: bool = False
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
return self.client is not 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.snapshot_timer.cancel()
|
||||
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)
|
||||
case RecordingCommand(state):
|
||||
self.recording = state
|
||||
case ApplySnapshotCommand(snapshot):
|
||||
snapshot.apply(self.car)
|
||||
case ResetCommand():
|
||||
self.car.reset()
|
||||
|
||||
def set_control(self, control: CarControl, active: bool):
|
||||
setattr(self.car, self.CONTROL_ATTRIBUTES[control], active)
|
||||
|
||||
def take_snapshot(self):
|
||||
if self.client is None:
|
||||
return
|
||||
if not self.recording:
|
||||
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)
|
||||
106
src/snapshot.py
Normal file
106
src/snapshot.py
Normal file
@@ -0,0 +1,106 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
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):
|
||||
nbr_bytes = struct.calcsize(format)
|
||||
return struct.unpack(format, data[:nbr_bytes]), data[nbr_bytes:]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Snapshot:
|
||||
controls: tuple[bool, bool, bool, bool] = (False, False, False, False)
|
||||
position: Vec = field(default_factory=Vec)
|
||||
direction: Vec = field(default_factory=Vec)
|
||||
speed: float = 0
|
||||
raycast_distances: list[float] | tuple[float, ...] = field(
|
||||
default_factory=list)
|
||||
image: Optional[np.ndarray] = None
|
||||
|
||||
def pack(self):
|
||||
data: bytes = b""
|
||||
data += struct.pack(">BBBB", *self.controls)
|
||||
data += struct.pack(
|
||||
">fffff",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.direction.x,
|
||||
self.direction.y,
|
||||
self.speed,
|
||||
)
|
||||
|
||||
nbr_raycasts: int = len(self.raycast_distances)
|
||||
data += struct.pack(f">B{nbr_raycasts}f",
|
||||
nbr_raycasts, *self.raycast_distances)
|
||||
|
||||
if self.image is not None:
|
||||
data += struct.pack(">II",
|
||||
self.image.shape[0], self.image.shape[1])
|
||||
data += self.image.tobytes()
|
||||
else:
|
||||
data += struct.pack(">II", 0, 0)
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def unpack(data: bytes) -> Snapshot:
|
||||
controls, data = iter_unpack(">BBBB", data)
|
||||
(x, y, dx, dy, s), data = iter_unpack(">fffff", data)
|
||||
position = Vec(x, y)
|
||||
direction = Vec(dx, dy)
|
||||
speed = s
|
||||
|
||||
(nbr_raycasts,), data = iter_unpack(">B", data)
|
||||
raycast_distances, data = iter_unpack(f">{nbr_raycasts}f", data)
|
||||
|
||||
(h, w), data = iter_unpack(">II", data)
|
||||
|
||||
if h * w > 0:
|
||||
image = np.frombuffer(data, np.uint8).reshape(h, w, 3)
|
||||
else:
|
||||
image = None
|
||||
|
||||
return Snapshot(
|
||||
controls=controls,
|
||||
position=position,
|
||||
direction=direction,
|
||||
speed=speed,
|
||||
raycast_distances=raycast_distances,
|
||||
image=image,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_car(car: Car) -> Snapshot:
|
||||
return Snapshot(
|
||||
controls=(
|
||||
car.forward,
|
||||
car.backward,
|
||||
car.left,
|
||||
car.right
|
||||
),
|
||||
position=car.pos.copy(),
|
||||
direction=car.direction.copy(),
|
||||
speed=car.speed,
|
||||
raycast_distances=car.rays.copy(),
|
||||
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)
|
||||
39
src/utils.py
39
src/utils.py
@@ -1,10 +1,12 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from threading import Timer
|
||||
from typing import Optional
|
||||
|
||||
from src.vec import Vec
|
||||
|
||||
|
||||
ROOT = Path(os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)))
|
||||
ROOT = Path(os.path.abspath(os.path.join(
|
||||
os.path.dirname(__file__), os.pardir)))
|
||||
|
||||
|
||||
def orientation(a: Vec, b: Vec, c: Vec) -> float:
|
||||
@@ -32,3 +34,36 @@ def segments_intersect(a1: Vec, a2: Vec, b1: Vec, b2: Vec) -> bool:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_segments_intersection(a1: Vec, a2: Vec, b1: Vec, b2: Vec) -> Optional[Vec]:
|
||||
da: Vec = a2 - a1
|
||||
db: Vec = b2 - b1
|
||||
dp: Vec = a1 - b1
|
||||
dap: Vec = da.perp
|
||||
denom: float = dap.dot(db)
|
||||
|
||||
if abs(denom) < 1e-9:
|
||||
o1: float = da.cross(-dp)
|
||||
if abs(o1) < 1e-9:
|
||||
for p in [b1, b2]:
|
||||
if p.within(a1, a2):
|
||||
return p
|
||||
for p in [a1, a2]:
|
||||
if p.within(b1, b2):
|
||||
return p
|
||||
return None
|
||||
return None
|
||||
|
||||
num: float = dap.dot(dp)
|
||||
t: float = num / denom
|
||||
intersection: Vec = b1 + db * t
|
||||
if intersection.within(a1, a2) and intersection.within(b1, b2):
|
||||
return intersection
|
||||
return None
|
||||
|
||||
|
||||
class RepeatTimer(Timer):
|
||||
def run(self):
|
||||
while not self.finished.wait(self.interval):
|
||||
self.function(*self.args, **self.kwargs)
|
||||
|
||||
@@ -8,6 +8,9 @@ class Vec:
|
||||
self.x: float = x
|
||||
self.y: float = y
|
||||
|
||||
def copy(self) -> Vec:
|
||||
return Vec(self.x, self.y)
|
||||
|
||||
def __add__(self, other: float | Vec) -> Vec:
|
||||
if isinstance(other, Vec):
|
||||
return Vec(self.x + other.x, self.y + other.y)
|
||||
@@ -24,6 +27,9 @@ class Vec:
|
||||
def __truediv__(self, value: float) -> Vec:
|
||||
return Vec(self.x / value, self.y / value)
|
||||
|
||||
def __neg__(self) -> Vec:
|
||||
return Vec(-self.x, -self.y)
|
||||
|
||||
def dot(self, other: Vec) -> float:
|
||||
return self.x * other.x + self.y * other.y
|
||||
|
||||
|
||||
119
uv.lock
generated
119
uv.lock
generated
@@ -2,6 +2,58 @@ version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.3.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/57/7e/b72610cc91edf138bc588df5150957a4937221ca6058b825b4725c27be62/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", size = 20950335, upload-time = "2025-10-15T16:16:10.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", size = 14179878, upload-time = "2025-10-15T16:16:12.595Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/01/5a67cb785bda60f45415d09c2bc245433f1c68dd82eef9c9002c508b5a65/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", size = 5108673, upload-time = "2025-10-15T16:16:14.877Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/cd/8428e23a9fcebd33988f4cb61208fda832800ca03781f471f3727a820704/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", size = 6641438, upload-time = "2025-10-15T16:16:16.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", size = 14281290, upload-time = "2025-10-15T16:16:18.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/7e/7d306ff7cb143e6d975cfa7eb98a93e73495c4deabb7d1b5ecf09ea0fd69/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", size = 16636543, upload-time = "2025-10-15T16:16:21.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/6a/8cfc486237e56ccfb0db234945552a557ca266f022d281a2f577b98e955c/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", size = 16056117, upload-time = "2025-10-15T16:16:23.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/0e/42cb5e69ea901e06ce24bfcc4b5664a56f950a70efdcf221f30d9615f3f3/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", size = 18577788, upload-time = "2025-10-15T16:16:27.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/92/41c3d5157d3177559ef0a35da50f0cda7fa071f4ba2306dd36818591a5bc/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", size = 6282620, upload-time = "2025-10-15T16:16:29.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/97/fd421e8bc50766665ad35536c2bb4ef916533ba1fdd053a62d96cc7c8b95/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", size = 12784672, upload-time = "2025-10-15T16:16:31.589Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/df/5474fb2f74970ca8eb978093969b125a84cc3d30e47f82191f981f13a8a0/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", size = 10196702, upload-time = "2025-10-15T16:16:33.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/83/66ac031464ec1767ea3ed48ce40f615eb441072945e98693bec0bcd056cc/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", size = 21049003, upload-time = "2025-10-15T16:16:36.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/99/5b14e0e686e61371659a1d5bebd04596b1d72227ce36eed121bb0aeab798/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", size = 14302980, upload-time = "2025-10-15T16:16:39.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/44/e9486649cd087d9fc6920e3fc3ac2aba10838d10804b1e179fb7cbc4e634/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", size = 5231472, upload-time = "2025-10-15T16:16:41.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/51/902b24fa8887e5fe2063fd61b1895a476d0bbf46811ab0c7fdf4bd127345/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", size = 6739342, upload-time = "2025-10-15T16:16:43.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/f1/4de9586d05b1962acdcdb1dc4af6646361a643f8c864cef7c852bf509740/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", size = 14354338, upload-time = "2025-10-15T16:16:46.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/06/1c16103b425de7969d5a76bdf5ada0804b476fed05d5f9e17b777f1cbefd/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", size = 16702392, upload-time = "2025-10-15T16:16:48.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/b2/65f4dc1b89b5322093572b6e55161bb42e3e0487067af73627f795cc9d47/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", size = 16134998, upload-time = "2025-10-15T16:16:51.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/11/94ec578896cdb973aaf56425d6c7f2aff4186a5c00fac15ff2ec46998b46/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", size = 18651574, upload-time = "2025-10-15T16:16:53.429Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/b7/7efa763ab33dbccf56dade36938a77345ce8e8192d6b39e470ca25ff3cd0/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb", size = 6413135, upload-time = "2025-10-15T16:16:55.992Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/70/aba4c38e8400abcc2f345e13d972fb36c26409b3e644366db7649015f291/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", size = 12928582, upload-time = "2025-10-15T16:16:57.943Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/63/871fad5f0073fc00fbbdd7232962ea1ac40eeaae2bba66c76214f7954236/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", size = 10266691, upload-time = "2025-10-15T16:17:00.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580, upload-time = "2025-10-15T16:17:02.509Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056, upload-time = "2025-10-15T16:17:04.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555, upload-time = "2025-10-15T16:17:07.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581, upload-time = "2025-10-15T16:17:09.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186, upload-time = "2025-10-15T16:17:11.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601, upload-time = "2025-10-15T16:17:14.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219, upload-time = "2025-10-15T16:17:17.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702, upload-time = "2025-10-15T16:17:19.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136, upload-time = "2025-10-15T16:17:22.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542, upload-time = "2025-10-15T16:17:24.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213, upload-time = "2025-10-15T16:17:26.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280, upload-time = "2025-10-15T16:17:29.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930, upload-time = "2025-10-15T16:17:32.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504, upload-time = "2025-10-15T16:17:34.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405, upload-time = "2025-10-15T16:17:36.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866, upload-time = "2025-10-15T16:17:38.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296, upload-time = "2025-10-15T16:17:41.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046, upload-time = "2025-10-15T16:17:43.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691, upload-time = "2025-10-15T16:17:46.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782, upload-time = "2025-10-15T16:17:48.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301, upload-time = "2025-10-15T16:17:50.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygame"
|
||||
version = "2.6.1"
|
||||
@@ -17,13 +69,78 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/11/17f7f319ca91824b86557e9303e3b7a71991ef17fd45286bf47d7f0a38e6/pygame-2.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:813af4fba5d0b2cb8e58f5d95f7910295c34067dcc290d34f1be59c48bd1ea6a", size = 10620084, upload-time = "2024-09-29T11:48:51.587Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyqt6"
|
||||
version = "6.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyqt6-qt6" },
|
||||
{ name = "pyqt6-sip" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/32/1b/567f46eb43ca961efd38d7a0b73efb70d7342854f075fd919179fdb2a571/pyqt6-6.9.1.tar.gz", hash = "sha256:50642be03fb40f1c2111a09a1f5a0f79813e039c15e78267e6faaf8a96c1c3a6", size = 1067230, upload-time = "2025-06-06T08:49:30.307Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/c4/fc2a69cf3df09b213185ef5a677c3940cd20e7855d29e40061a685b9c6ee/pyqt6-6.9.1-cp39-abi3-macosx_10_14_universal2.whl", hash = "sha256:33c23d28f6608747ecc8bfd04c8795f61631af9db4fb1e6c2a7523ec4cc916d9", size = 59770566, upload-time = "2025-06-06T08:48:20.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/78/92f3c46440a83ebe22ae614bd6792e7b052bcb58ff128f677f5662015184/pyqt6-6.9.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:37884df27f774e2e1c0c96fa41e817a222329b80ffc6241725b0dc8c110acb35", size = 37804959, upload-time = "2025-06-06T08:48:39.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/5e/e77fa2761d809cd08d724f44af01a4b6ceb0ff9648e43173187b0e4fac4e/pyqt6-6.9.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:055870b703c1a49ca621f8a89e2ec4d848e6c739d39367eb9687af3b056d9aa3", size = 40414608, upload-time = "2025-06-06T08:49:00.26Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/09/69cf80456b6a985e06dd24ed0c2d3451e43567bf2807a5f3a86ef7a74a2e/pyqt6-6.9.1-cp39-abi3-win_amd64.whl", hash = "sha256:15b95bd273bb6288b070ed7a9503d5ff377aa4882dd6d175f07cad28cdb21da0", size = 25717996, upload-time = "2025-06-06T08:49:13.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/b3/0839d8fd18b86362a4de384740f2f6b6885b5d06fda7720f8a335425e316/pyqt6-6.9.1-cp39-abi3-win_arm64.whl", hash = "sha256:08792c72d130a02e3248a120f0b9bbb4bf4319095f92865bc5b365b00518f53d", size = 25212132, upload-time = "2025-06-06T08:49:27.41Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyqt6-qt6"
|
||||
version = "6.9.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/6f/fe2cd9cb2201c685be2f50c8c915df97848cac3dca4bad44bc3aed56fc63/pyqt6_qt6-6.9.2-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:183b62be49216da80c7df1931d74885610a88f74812489d29610d13b7c215a1c", size = 66568266, upload-time = "2025-09-01T11:43:31.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/1d/47dc51b4383b350f4ff6b1db461b01eba580030683ffa65475b4fdd9b80d/pyqt6_qt6-6.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7897fb74ee21bdc87b5ccf84e94f4a551377e792fd180a9211c17eb41c3338a3", size = 60859706, upload-time = "2025-09-01T11:43:36.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/07/21f7dc188e35b46631707f3b40ace5643a0e03a8e1e446854826d08a04ae/pyqt6_qt6-6.9.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:9abfc0ee4a8293a6442128ae3f87f68e82e2a949d7b9caabd98c86ba5679ab48", size = 82322871, upload-time = "2025-09-01T11:43:41.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/c0/da658e735817feaa35ddfddb4c5d699291e8b8e3138e69ad7ae1a38a7db8/pyqt6_qt6-6.9.2-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:940aac6462532578e8ddefe0494cd17e33a85e0f3cfb21c612f56ab9ad7bc871", size = 80826693, upload-time = "2025-09-01T11:43:46.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/3a/d811ed1aa579b93ab56188d1371b05eacb4188599d83e72b761263a10f92/pyqt6_qt6-6.9.2-py3-none-win_amd64.whl", hash = "sha256:f9289768039bef4a63e5949b7f8cfbbddc3b6d24bd58c21ba0f2921bed8d1c08", size = 74147171, upload-time = "2025-09-01T11:43:53.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/59/7db6c5ddcb60ef3ecca2040274a30e8bc35b569c49e25e1cf2ef9f159426/pyqt6_qt6-6.9.2-py3-none-win_arm64.whl", hash = "sha256:8f82944ef68c8f8c78aa8eca4832c7bc05116c6de00a3bad8af5a0d63d1caafb", size = 54534019, upload-time = "2025-09-01T11:43:58.763Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyqt6-sip"
|
||||
version = "13.10.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/4a/96daf6c2e4f689faae9bd8cebb52754e76522c58a6af9b5ec86a2e8ec8b4/pyqt6_sip-13.10.2.tar.gz", hash = "sha256:464ad156bf526500ce6bd05cac7a82280af6309974d816739b4a9a627156fafe", size = 92548, upload-time = "2025-05-23T12:26:49.901Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/1e/979ea64c98ca26979d8ce11e9a36579e17d22a71f51d7366d6eec3c82c13/pyqt6_sip-13.10.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8b5d06a0eac36038fa8734657d99b5fe92263ae7a0cd0a67be6acfe220a063e1", size = 112227, upload-time = "2025-05-23T12:26:38.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/21/84c230048e3bfef4a9209d16e56dcd2ae10590d03a31556ae8b5f1dcc724/pyqt6_sip-13.10.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad376a6078da37b049fdf9d6637d71b52727e65c4496a80b753ddc8d27526aca", size = 322920, upload-time = "2025-05-23T12:26:39.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/1e/c6a28a142f14e735088534cc92951c3f48cccd77cdd4f3b10d7996be420f/pyqt6_sip-13.10.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3dde8024d055f496eba7d44061c5a1ba4eb72fc95e5a9d7a0dbc908317e0888b", size = 303833, upload-time = "2025-05-23T12:26:41.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/63/e5adf350c1c3123d4865c013f164c5265512fa79f09ad464fb2fdf9f9e61/pyqt6_sip-13.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:0b097eb58b4df936c4a2a88a2f367c8bb5c20ff049a45a7917ad75d698e3b277", size = 53527, upload-time = "2025-05-23T12:26:42.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/74/2df4195306d050fbf4963fb5636108a66e5afa6dc05fd9e81e51ec96c384/pyqt6_sip-13.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:cc6a1dfdf324efaac6e7b890a608385205e652845c62130de919fd73a6326244", size = 45373, upload-time = "2025-05-23T12:26:43.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/57/74b4eb7a51b9133958daa8409b55de95e44feb694d4e2e3eba81a070ca20/pyqt6_sip-13.10.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8a76a06a8e5c5b1f17a3f6f3c834ca324877e07b960b18b8b9bbfd9c536ec658", size = 112354, upload-time = "2025-10-08T08:44:00.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/cb/fdef02e0d6ee8443a9683a43650d61c6474b634b6ae6e1c6f097da6310bf/pyqt6_sip-13.10.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9128d770a611200529468397d710bc972f1dcfe12bfcbb09a3ccddcd4d54fa5b", size = 323488, upload-time = "2025-10-08T08:44:01.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/5b/8ede8d6234c3ea884cbd097d7d47ff9910fb114efe041af62b4453acd23b/pyqt6_sip-13.10.2-cp314-cp314-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d820a0fae7315932c08f27dc0a7e33e0f50fe351001601a8eb9cf6f22b04562e", size = 303881, upload-time = "2025-10-08T08:44:04.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/44/b5e78b072d1594643b0f1ff348f2bf54d4adb5a3f9b9f0989c54e33238d6/pyqt6_sip-13.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:3213bb6e102d3842a3bb7e59d5f6e55f176c80880ff0b39d0dac0cfe58313fb3", size = 55098, upload-time = "2025-10-08T08:44:08.943Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/91/357e9fcef5d830c3d50503d35e0357818aca3540f78748cc214dfa015d00/pyqt6_sip-13.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:ce33ff1f94960ad4b08035e39fa0c3c9a67070bec39ffe3e435c792721504726", size = 46088, upload-time = "2025-10-08T08:44:10.014Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qasync"
|
||||
version = "0.28.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ec/b2/5be08597dbbf331edb69478eae2f8dd511834cebf56a183b442e7437f8e0/qasync-0.28.0.tar.gz", hash = "sha256:6f7f1f18971f59cb259b107218269ba56e3ad475ec456e54714b426a6e30b71d", size = 14010, upload-time = "2025-08-28T01:31:36.785Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/84/0ce4cd946f6e958428c87d5accac35df70f81607e45ba4919947d0762d63/qasync-0.28.0-py3-none-any.whl", hash = "sha256:21faba8d047c717008378f5ac29ea58c32a8128528629e4afd57c59b768dba0f", size = 16188, upload-time = "2025-08-28T01:31:35.591Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rally-racer"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
{ name = "pygame" },
|
||||
{ name = "pyqt6" },
|
||||
{ name = "qasync" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "pygame", specifier = ">=2.6.1" }]
|
||||
requires-dist = [
|
||||
{ name = "numpy", specifier = ">=2.3.4" },
|
||||
{ name = "pygame", specifier = ">=2.6.1" },
|
||||
{ name = "pyqt6", specifier = ">=6.9.1" },
|
||||
{ name = "qasync", specifier = ">=0.28.0" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user