Compare commits
	
		
			38 Commits
		
	
	
		
			12319fc1ab
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 9f86d060f0 | |||
| e2f4a0d2a5 | |||
| bc96cea2b9 | |||
| 8d35f76b56 | |||
| 97f9765705 | |||
| fa61e27825 | |||
| b60a0aba4f | |||
| ae02ddefb0 | |||
| f1fadd123f | |||
| 8b7927a3c5 | |||
| 62de92e7a2 | |||
| 8ad97785b8 | |||
| db112ada4c | |||
| 8542ee81e7 | |||
| f91a4e8d61 | |||
| bf1935fd7e | |||
| 784d594a3b | |||
| 14c9fcdedb | |||
| 73d815b625 | |||
| eb06c114d2 | |||
| 54f33b6572 | |||
| 91e93759e8 | |||
| 04ac674982 | |||
| 8ca15eaa78 | |||
| 8989341714 | |||
| dde690818b | |||
| f1ae12d0ec | |||
| ff61a7fad6 | |||
| 09f70223b8 | |||
| 45ed1c85c8 | |||
| 2b20582b87 | |||
| 6805e69509 | |||
| 9c5f39b669 | |||
| adb25e6ef6 | |||
| 6276f97cce | |||
| da8c64624f | |||
| e154a1fde9 | |||
| eb10933f4b | 
							
								
								
									
										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) | ||||
							
								
								
									
										
											BIN
										
									
								
								assets/fonts/Ubuntu-M.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/fonts/Ubuntu-M.ttf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/fonts/Ubuntu-R.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/fonts/Ubuntu-R.ttf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										13
									
								
								assets/tracks/simple/meta.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								assets/tracks/simple/meta.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| { | ||||
|     "name": "Simple Track", | ||||
|     "start": { | ||||
|         "pos": [ | ||||
|             30, | ||||
|             0 | ||||
|         ], | ||||
|         "direction": [ | ||||
|             0, | ||||
|             1 | ||||
|         ] | ||||
|     } | ||||
| } | ||||
							
								
								
									
										259
									
								
								assets/tracks/simple/track.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										259
									
								
								assets/tracks/simple/track.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,259 @@ | ||||
| [ | ||||
|     { | ||||
|         "type": "road", | ||||
|         "pts": [ | ||||
|             [ | ||||
|                 30.0, | ||||
|                 0.0, | ||||
|                 1.0, | ||||
|                 0.0, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 29.544, | ||||
|                 5.209, | ||||
|                 0.985, | ||||
|                 0.174, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 28.191, | ||||
|                 10.261, | ||||
|                 0.94, | ||||
|                 0.342, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 25.981, | ||||
|                 15.0, | ||||
|                 0.866, | ||||
|                 0.5, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 22.981, | ||||
|                 19.284, | ||||
|                 0.766, | ||||
|                 0.643, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 19.284, | ||||
|                 22.981, | ||||
|                 0.643, | ||||
|                 0.766, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 15.0, | ||||
|                 25.981, | ||||
|                 0.5, | ||||
|                 0.866, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 10.261, | ||||
|                 28.191, | ||||
|                 0.342, | ||||
|                 0.94, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 5.209, | ||||
|                 29.544, | ||||
|                 0.174, | ||||
|                 0.985, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 0.0, | ||||
|                 30.0, | ||||
|                 0.0, | ||||
|                 1.0, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 -5.209, | ||||
|                 29.544, | ||||
|                 -0.174, | ||||
|                 0.985, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 -10.261, | ||||
|                 28.191, | ||||
|                 -0.342, | ||||
|                 0.94, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 -15.0, | ||||
|                 25.981, | ||||
|                 -0.5, | ||||
|                 0.866, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 -19.284, | ||||
|                 22.981, | ||||
|                 -0.643, | ||||
|                 0.766, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 -22.981, | ||||
|                 19.284, | ||||
|                 -0.766, | ||||
|                 0.643, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 -25.981, | ||||
|                 15.0, | ||||
|                 -0.866, | ||||
|                 0.5, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 -28.191, | ||||
|                 10.261, | ||||
|                 -0.94, | ||||
|                 0.342, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 -29.544, | ||||
|                 5.209, | ||||
|                 -0.985, | ||||
|                 0.174, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 -30.0, | ||||
|                 0.0, | ||||
|                 -1.0, | ||||
|                 0.0, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 -29.544, | ||||
|                 -5.209, | ||||
|                 -0.985, | ||||
|                 -0.174, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 -28.191, | ||||
|                 -10.261, | ||||
|                 -0.94, | ||||
|                 -0.342, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 -25.981, | ||||
|                 -15.0, | ||||
|                 -0.866, | ||||
|                 -0.5, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 -22.981, | ||||
|                 -19.284, | ||||
|                 -0.766, | ||||
|                 -0.643, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 -19.284, | ||||
|                 -22.981, | ||||
|                 -0.643, | ||||
|                 -0.766, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 -15.0, | ||||
|                 -25.981, | ||||
|                 -0.5, | ||||
|                 -0.866, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 -10.261, | ||||
|                 -28.191, | ||||
|                 -0.342, | ||||
|                 -0.94, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 -5.209, | ||||
|                 -29.544, | ||||
|                 -0.174, | ||||
|                 -0.985, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 -0.0, | ||||
|                 -30.0, | ||||
|                 -0.0, | ||||
|                 -1.0, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 5.209, | ||||
|                 -29.544, | ||||
|                 0.174, | ||||
|                 -0.985, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 10.261, | ||||
|                 -28.191, | ||||
|                 0.342, | ||||
|                 -0.94, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 15.0, | ||||
|                 -25.981, | ||||
|                 0.5, | ||||
|                 -0.866, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 19.284, | ||||
|                 -22.981, | ||||
|                 0.643, | ||||
|                 -0.766, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 22.981, | ||||
|                 -19.284, | ||||
|                 0.766, | ||||
|                 -0.643, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 25.981, | ||||
|                 -15.0, | ||||
|                 0.866, | ||||
|                 -0.5, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 28.191, | ||||
|                 -10.261, | ||||
|                 0.94, | ||||
|                 -0.342, | ||||
|                 1 | ||||
|             ], | ||||
|             [ | ||||
|                 29.544, | ||||
|                 -5.209, | ||||
|                 0.985, | ||||
|                 -0.174, | ||||
|                 1 | ||||
|             ] | ||||
|         ] | ||||
|     } | ||||
| ] | ||||
							
								
								
									
										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 | 
							
								
								
									
										1
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								main.py
									
									
									
									
									
								
							| @@ -4,6 +4,7 @@ from src.game import Game | ||||
| def main(): | ||||
|     print("Welcome to Rally Racer !") | ||||
|     game: Game = Game() | ||||
|     game.mainloop() | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|   | ||||
| @@ -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() | ||||
							
								
								
									
										40
									
								
								scripts/track_gen.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								scripts/track_gen.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import json | ||||
| from math import radians | ||||
| from pathlib import Path | ||||
| from src.utils import ROOT | ||||
| from src.vec import Vec | ||||
|  | ||||
|  | ||||
| def gen_circle( | ||||
|     folder: Path, center: Vec, radius: float, n_sides: int, width: float = 1 | ||||
| ): | ||||
|     with open(folder / "track.json", "w") as f: | ||||
|         pts: list[tuple[float, ...]] = [] | ||||
|  | ||||
|         for i in range(n_sides): | ||||
|             angle: float = radians(i / n_sides * 360) | ||||
|             v: Vec = Vec(1, 0).rotate(angle) | ||||
|             pos: Vec = center + v * radius | ||||
|             normal: Vec = v | ||||
|             pts.append((pos.x, pos.y, normal.x, normal.y, width)) | ||||
|  | ||||
|         for i, pt in enumerate(pts): | ||||
|             pts[i] = tuple(round(v, 3) for v in pt) | ||||
|  | ||||
|         json.dump([{"type": "road", "pts": pts}], f, indent=4) | ||||
|  | ||||
|     with open(folder / "meta.json", "r") as f: | ||||
|         meta: dict = json.load(f) | ||||
|  | ||||
|     meta["start"] = {"pos": [radius, 0], "direction": [0, 1]} | ||||
|     with open(folder / "meta.json", "w") as f: | ||||
|         json.dump(meta, f, indent=4) | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     folder: Path = ROOT / "assets" / "tracks" / "simple" | ||||
|     gen_circle(folder, Vec(0, 0), 30, 36) | ||||
|  | ||||
|  | ||||
| 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 | ||||
							
								
								
									
										50
									
								
								src/camera.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/camera.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| from src.vec import Vec | ||||
|  | ||||
|  | ||||
| class Camera: | ||||
|     UNIT_RATIO = 150 | ||||
|  | ||||
|     def __init__(self) -> None: | ||||
|         self.pos: Vec = Vec() | ||||
|         self.up: Vec = Vec(0, -1) | ||||
|         self.size: Vec = Vec(600, 600) | ||||
|         self.zoom: float = 1 | ||||
|  | ||||
|     def set_pos(self, pos: Vec): | ||||
|         self.pos = pos | ||||
|  | ||||
|     def set_direction(self, up: Vec): | ||||
|         self.up = up.normalized | ||||
|  | ||||
|     def set_size(self, size: Vec): | ||||
|         self.size = size | ||||
|  | ||||
|     @property | ||||
|     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.car_screen_pos | ||||
|         delta /= self.zoom * self.UNIT_RATIO | ||||
|         dx: float = delta.x | ||||
|         dy: float = delta.y | ||||
|  | ||||
|         v1: Vec = self.up.perp * dx | ||||
|         v2: Vec = self.up * dy | ||||
|  | ||||
|         return self.pos + v1 + v2 | ||||
|  | ||||
|     def world2screen(self, world_pos: Vec) -> Vec: | ||||
|         delta: Vec = world_pos - self.pos | ||||
|         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.car_screen_pos + screen_delta | ||||
|         return screen_pos | ||||
|  | ||||
|     def size2screen(self, size: float) -> float: | ||||
|         return size * self.zoom * self.UNIT_RATIO | ||||
							
								
								
									
										175
									
								
								src/car.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								src/car.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,175 @@ | ||||
| from math import radians | ||||
| from typing import Optional | ||||
|  | ||||
| import pygame | ||||
|  | ||||
| from src.camera import Camera | ||||
| from src.remote_controller import RemoteController | ||||
| from src.utils import get_segments_intersection, segments_intersect | ||||
| from src.vec import Vec | ||||
|  | ||||
|  | ||||
| def sign(x): return 0 if x == 0 else (-1 if x < 0 else 1) | ||||
|  | ||||
|  | ||||
| class Car: | ||||
|     MAX_SPEED = 5 | ||||
|     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 = 2.5 | ||||
|     N_RAYS = 15 | ||||
|     RAYS_FOV = 180 | ||||
|     RAYS_MAX_DIST = 100 | ||||
|  | ||||
|     def __init__(self, pos: Vec, direction: Vec) -> None: | ||||
|         self.initial_pos: Vec = pos.copy() | ||||
|         self.initial_dir: Vec = direction.copy() | ||||
|         self.pos: Vec = pos | ||||
|         self.direction: Vec = direction | ||||
|         self.speed: float = 0 | ||||
|         self.forward: bool = False | ||||
|         self.backward: bool = False | ||||
|         self.left: bool = False | ||||
|         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) | ||||
|         self.controller.start_server() | ||||
|  | ||||
|     def update(self, dt: float): | ||||
|         if self.forward: | ||||
|             self.speed += self.ACCELERATION * dt | ||||
|             self.speed = min(self.MAX_SPEED, self.speed) | ||||
|  | ||||
|         if self.backward: | ||||
|             self.speed -= self.ACCELERATION * 2 * dt | ||||
|             self.speed = max(self.MAX_BACK_SPEED, self.speed) | ||||
|  | ||||
|         rotate_angle: float = 0 | ||||
|         if self.left: | ||||
|             rotate_angle -= self.ROTATE_SPEED * dt | ||||
|         if self.right: | ||||
|             rotate_angle += self.ROTATE_SPEED * dt | ||||
|  | ||||
|         # if self.backward: | ||||
|         #    rotate_angle *= -1 | ||||
|  | ||||
|         if rotate_angle != 0: | ||||
|             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, 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 | ||||
|         pt: Vec = self.pos | ||||
|         p1: Vec = pt + u + v | ||||
|         p2: Vec = pt - u + v | ||||
|         p3: Vec = pt - u - v | ||||
|         p4: Vec = pt + u - v | ||||
|         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]] = [ | ||||
|             (corners[i], corners[(i + 1) % 4]) for i in range(4) | ||||
|         ] | ||||
|  | ||||
|         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] | ||||
|                 d: Vec = pt2 - pt1 | ||||
|  | ||||
|                 for s1, s2 in sides: | ||||
|                     if segments_intersect(s1, s2, pt1, pt2): | ||||
|                         self.colliding = True | ||||
|                         self.direction = d.normalized | ||||
|                         n: Vec = self.direction.perp | ||||
|                         dist: float = (self.pos - pt1).dot(n) | ||||
|                         if dist < 0: | ||||
|                             n *= -1 | ||||
|                             dist = -dist | ||||
|                         self.speed = 0 | ||||
|                         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 | ||||
							
								
								
									
										137
									
								
								src/game.py
									
									
									
									
									
								
							
							
						
						
									
										137
									
								
								src/game.py
									
									
									
									
									
								
							| @@ -1,9 +1,142 @@ | ||||
| from math import cos, radians, sin | ||||
| import pygame | ||||
|  | ||||
| from src.camera import Camera | ||||
| from src.car import Car | ||||
| from src.track import Track | ||||
| from src.utils import ROOT | ||||
| from src.vec import Vec | ||||
|  | ||||
|  | ||||
| class Game: | ||||
|     DEFAULT_SIZE = (1280, 720) | ||||
|     BACKGROUND_COLOR = (80, 80, 80) | ||||
|     MAX_FPS = 60 | ||||
|     FPS_COLOR = (255, 0, 0) | ||||
|  | ||||
|     def __init__(self): | ||||
|     def __init__(self) -> None: | ||||
|         pygame.init() | ||||
|         self.win: pygame.Surface = pygame.display.set_mode(self.DEFAULT_SIZE) | ||||
|         self.win: pygame.Surface = pygame.display.set_mode( | ||||
|             self.DEFAULT_SIZE, pygame.RESIZABLE | ||||
|         ) | ||||
|         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.camera: Camera = Camera() | ||||
|  | ||||
|         self.clock: pygame.time.Clock = pygame.time.Clock() | ||||
|         self.font: pygame.font.Font = pygame.font.Font( | ||||
|             str(ROOT / "assets" / "fonts" / "Ubuntu-M.ttf"), 20 | ||||
|         ) | ||||
|         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): | ||||
|         for event in pygame.event.get(): | ||||
|             if event.type == pygame.QUIT: | ||||
|                 self.quit() | ||||
|             elif event.type == pygame.VIDEORESIZE: | ||||
|                 self.camera.set_size(Vec(event.w, event.h)) | ||||
|             elif event.type == pygame.KEYDOWN: | ||||
|                 if event.key == pygame.K_ESCAPE: | ||||
|                     self.quit() | ||||
|                 else: | ||||
|                     self.on_key_down(event) | ||||
|             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.show_raycasts) | ||||
|         if self.show_fps: | ||||
|             self.render_fps() | ||||
|         if self.show_speed: | ||||
|             self.render_speedometer() | ||||
|  | ||||
|         pygame.display.flip() | ||||
|  | ||||
|     def on_key_down(self, event: pygame.event.Event): | ||||
|         if event.key == pygame.K_w: | ||||
|             self.car.forward = True | ||||
|         elif event.key == pygame.K_s: | ||||
|             self.car.backward = True | ||||
|         elif event.key == pygame.K_a: | ||||
|             self.car.left = True | ||||
|         elif event.key == pygame.K_d: | ||||
|             self.car.right = True | ||||
|         elif event.key == pygame.K_f: | ||||
|             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: | ||||
|             self.car.forward = False | ||||
|         elif event.key == pygame.K_s: | ||||
|             self.car.backward = False | ||||
|         elif event.key == pygame.K_a: | ||||
|             self.car.left = False | ||||
|         elif event.key == pygame.K_d: | ||||
|             self.car.right = False | ||||
|  | ||||
|     def render_fps(self): | ||||
|         txt: pygame.Surface = self.font.render( | ||||
|             f"{self.clock.get_fps():.1f}", True, self.FPS_COLOR | ||||
|         ) | ||||
|         self.win.blit(txt, (self.win.get_width() - txt.get_width(), 0)) | ||||
|  | ||||
|     def render_speedometer(self): | ||||
|         if self.car.speed == 0: | ||||
|             return | ||||
|         angle: float = self.car.speed / self.car.MAX_SPEED * 180 | ||||
|  | ||||
|         pts1: list[tuple[float, float]] = [] | ||||
|         pts2: list[tuple[float, float]] = [] | ||||
|  | ||||
|         n: int = 30 | ||||
|         r: float = 50 | ||||
|         ox: float = r + 10 | ||||
|         oy: float = r + 10 | ||||
|         thickness: float = 5 | ||||
|         r2: float = r - thickness | ||||
|  | ||||
|         for i in range(n): | ||||
|             a: float = radians(angle * i / (n - 1)) | ||||
|             dx: float = -cos(a) | ||||
|             dy: float = -sin(a) | ||||
|             pts1.append((ox + r * dx, oy + r * dy)) | ||||
|             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 | ||||
|   | ||||
							
								
								
									
										0
									
								
								src/objects/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/objects/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										119
									
								
								src/objects/road.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								src/objects/road.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import pygame | ||||
|  | ||||
| from src.camera import Camera | ||||
| from src.track_object import TrackObject, TrackObjectType | ||||
| 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: | ||||
|         return Road([RoadPoint.load(pt) for pt in data["pts"]]) | ||||
|  | ||||
|     def render(self, surf: pygame.Surface, camera: Camera): | ||||
|         side1: list[Vec] = [] | ||||
|         side2: list[Vec] = [] | ||||
|  | ||||
|         for i, pt in enumerate(self.pts): | ||||
|             p1: Vec = pt.pos | ||||
|             p2: Vec = p1 + pt.normal * pt.width | ||||
|             p3: Vec = p1 - pt.normal * pt.width | ||||
|             side1.append(camera.world2screen(p2)) | ||||
|             side2.append(camera.world2screen(p3)) | ||||
|  | ||||
|         n: int = len(self.pts) | ||||
|         for i in range(n): | ||||
|             pygame.draw.polygon( | ||||
|                 surf, | ||||
|                 (100, 100, 100), | ||||
|                 [side1[i], side1[(i + 1) % n], side2[(i + 1) % n], side2[i]], | ||||
|             ) | ||||
|  | ||||
|         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] = [] | ||||
|         for pt in self.pts: | ||||
|             p1: Vec = pt.pos | ||||
|             p2: Vec = p1 + pt.normal * pt.width | ||||
|             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: | ||||
|         self.pos: Vec = pos | ||||
|         self.normal: Vec = normal.normalized | ||||
|         self.width: float = width | ||||
|  | ||||
|     @staticmethod | ||||
|     def load(data: list[float]) -> RoadPoint: | ||||
|         return RoadPoint(Vec(data[0], data[1]), Vec(data[2], data[3]), data[4]) | ||||
							
								
								
									
										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 = 4096 | ||||
|     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)")) | ||||
							
								
								
									
										138
									
								
								src/remote_controller.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								src/remote_controller.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,138 @@ | ||||
| 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 | ||||
|  | ||||
|  | ||||
| class RemoteController: | ||||
|     DEFAULT_PORT = 5000 | ||||
|     DATA_CHUNK_SIZE = 4096 | ||||
|  | ||||
|     CONTROL_ATTRIBUTES: dict[CarControl, str] = { | ||||
|         CarControl.FORWARD: "forward", | ||||
|         CarControl.BACKWARD: "backward", | ||||
|         CarControl.LEFT: "left", | ||||
|         CarControl.RIGHT: "right", | ||||
|     } | ||||
|  | ||||
|     SNAPSHOT_INTERVAL = 0.1 | ||||
|  | ||||
|     def __init__(self, car: Car, port: int = DEFAULT_PORT) -> None: | ||||
|         self.car: Car = car | ||||
|         self.port: int = port | ||||
|         self.server: socket.socket = socket.socket( | ||||
|             socket.AF_INET, socket.SOCK_STREAM) | ||||
|         self.server_thread: threading.Thread = threading.Thread( | ||||
|             target=self.wait_for_connections, daemon=True | ||||
|         ) | ||||
|         self.running: bool = False | ||||
|         self.queue: queue.Queue[Command] = queue.Queue() | ||||
|         self.client_thread: Optional[threading.Thread] = None | ||||
|         self.client: Optional[socket.socket] = None | ||||
|         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) | ||||
|         payload: bytes = snapshot.pack() | ||||
|         self.client.sendall(struct.pack(">I", len(payload)) + payload) | ||||
							
								
								
									
										101
									
								
								src/snapshot.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								src/snapshot.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import struct | ||||
| from dataclasses import dataclass, field | ||||
| from typing import TYPE_CHECKING, Optional | ||||
|  | ||||
| import numpy as np | ||||
|  | ||||
| from src.vec import Vec | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from src.car import Car | ||||
|  | ||||
|  | ||||
| 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 | ||||
							
								
								
									
										54
									
								
								src/track.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/track.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import json | ||||
|  | ||||
| import pygame | ||||
|  | ||||
| from src.camera import Camera | ||||
| from src.track_object import TrackObject | ||||
| from src.utils import ROOT | ||||
| from src.vec import Vec | ||||
|  | ||||
| TrackObject.init() | ||||
|  | ||||
|  | ||||
| class Track: | ||||
|     TRACKS_DIRECTORY = ROOT / "assets" / "tracks" | ||||
|  | ||||
|     def __init__(self, id: str, name: str, start_pos: Vec, start_dir: Vec) -> None: | ||||
|         self.id: str = id | ||||
|         self.name: str = name | ||||
|         self.start_pos: Vec = start_pos | ||||
|         self.start_dir: Vec = start_dir | ||||
|         self.objects: list[TrackObject] = [] | ||||
|         self.load_objects() | ||||
|  | ||||
|     @staticmethod | ||||
|     def load(name: str) -> Track: | ||||
|         with open(Track.TRACKS_DIRECTORY / name / "meta.json", "r") as f: | ||||
|             meta: dict = json.load(f) | ||||
|  | ||||
|         return Track( | ||||
|             name, | ||||
|             meta["name"], | ||||
|             Vec(*meta["start"]["pos"]), | ||||
|             Vec(*meta["start"]["direction"]), | ||||
|         ) | ||||
|  | ||||
|     def load_objects(self): | ||||
|         with open(Track.TRACKS_DIRECTORY / self.id / "track.json", "r") as f: | ||||
|             data: list = json.load(f) | ||||
|  | ||||
|         self.objects = [] | ||||
|         for obj_data in data: | ||||
|             self.objects.append(TrackObject.load(obj_data)) | ||||
|  | ||||
|     def render(self, surf: pygame.Surface, camera: Camera): | ||||
|         for object in self.objects: | ||||
|             object.render(surf, camera) | ||||
|  | ||||
|     def get_collision_polygons(self) -> list[list[Vec]]: | ||||
|         polygons: list[list[Vec]] = [] | ||||
|         for obj in self.objects: | ||||
|             polygons.extend(obj.get_collision_polygons()) | ||||
|         return polygons | ||||
							
								
								
									
										46
									
								
								src/track_object.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/track_object.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| import importlib | ||||
| import pkgutil | ||||
| from enum import StrEnum | ||||
| from typing import Optional, Self | ||||
|  | ||||
| import pygame | ||||
|  | ||||
| import src.objects | ||||
| from src.camera import Camera | ||||
| from src.vec import Vec | ||||
|  | ||||
|  | ||||
| class TrackObjectType(StrEnum): | ||||
|     Road = "road" | ||||
|  | ||||
|     Unknown = "unknown" | ||||
|  | ||||
|  | ||||
| class TrackObject: | ||||
|     REGISTRY = {} | ||||
|     type: TrackObjectType = TrackObjectType.Unknown | ||||
|  | ||||
|     @staticmethod | ||||
|     def init(): | ||||
|         package = src.objects | ||||
|         for _, modname, _ in pkgutil.walk_packages( | ||||
|             package.__path__, package.__name__ + "." | ||||
|         ): | ||||
|             importlib.import_module(modname) | ||||
|  | ||||
|     def __init_subclass__(cls, **kwargs) -> None: | ||||
|         super().__init_subclass__(**kwargs) | ||||
|         TrackObject.REGISTRY[cls.type] = cls | ||||
|  | ||||
|     @classmethod | ||||
|     def load(cls, data: dict) -> Self: | ||||
|         obj_type: Optional[TrackObjectType] = data.get("type") | ||||
|         if obj_type not in cls.REGISTRY: | ||||
|             raise ValueError(f"Unknown object tyoe: {obj_type}") | ||||
|         return cls.REGISTRY[obj_type].load(data) | ||||
|  | ||||
|     def render(self, surf: pygame.Surface, camera: Camera): | ||||
|         pass | ||||
|  | ||||
|     def get_collision_polygons(self) -> list[list[Vec]]: | ||||
|         return [] | ||||
							
								
								
									
										69
									
								
								src/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/utils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| 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))) | ||||
|  | ||||
|  | ||||
| def orientation(a: Vec, b: Vec, c: Vec) -> float: | ||||
|     return (b - a).cross(c - a) | ||||
|  | ||||
|  | ||||
| def segments_intersect(a1: Vec, a2: Vec, b1: Vec, b2: Vec) -> bool: | ||||
|     o1 = orientation(a1, a2, b1) | ||||
|     o2 = orientation(a1, a2, b2) | ||||
|     o3 = orientation(b1, b2, a1) | ||||
|     o4 = orientation(b1, b2, a2) | ||||
|  | ||||
|     # General case: segments straddle each other | ||||
|     if (o1 * o2 < 0) and (o3 * o4 < 0): | ||||
|         return True | ||||
|  | ||||
|     # Special cases: Collinear overlaps | ||||
|     if o1 == 0 and b1.within(a1, a2): | ||||
|         return True | ||||
|     if o2 == 0 and b2.within(a1, a2): | ||||
|         return True | ||||
|     if o3 == 0 and a1.within(b1, b2): | ||||
|         return True | ||||
|     if o4 == 0 and a2.within(b1, b2): | ||||
|         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) | ||||
							
								
								
									
										19
									
								
								src/vec.py
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								src/vec.py
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| from math import sqrt | ||||
| from math import cos, sin, sqrt | ||||
|  | ||||
|  | ||||
| class Vec: | ||||
| @@ -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 | ||||
|  | ||||
| @@ -55,3 +61,14 @@ class Vec: | ||||
|  | ||||
|     def __repr__(self) -> str: | ||||
|         return f"Vec({self.x}, {self.y})" | ||||
|  | ||||
|     def rotate(self, angle: float) -> Vec: | ||||
|         return Vec( | ||||
|             cos(angle) * self.x - sin(angle) * self.y, | ||||
|             sin(angle) * self.x + cos(angle) * self.y, | ||||
|         ) | ||||
|  | ||||
|     def within(self, p1: Vec, p2: Vec) -> bool: | ||||
|         x1, x2 = min(p1.x, p2.x), max(p1.x, p2.x) | ||||
|         y1, y2 = min(p1.y, p2.y), max(p1.y, p2.y) | ||||
|         return (x1 <= self.x <= x2) and (y1 <= self.y <= y2) | ||||
|   | ||||
							
								
								
									
										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