Compare commits
	
		
			28 Commits
		
	
	
		
			09f70223b8
			...
			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 | 
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -10,3 +10,6 @@ wheels/ | |||||||
| .venv | .venv | ||||||
|  |  | ||||||
| .vscode | .vscode | ||||||
|  |  | ||||||
|  | records | ||||||
|  | *.rec | ||||||
							
								
								
									
										59
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										59
									
								
								README.md
									
									
									
									
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | <p align="center"> | ||||||
|  |     <img src="logo.png" width="300"> | ||||||
|  | </p> | ||||||
|  |  | ||||||
|  | # Rally Racer | ||||||
|  |  | ||||||
|  | This repository holds a sandbox driving simulation controllable via a network interface as a machine learning and data collection challenge. | ||||||
|  |  | ||||||
|  | # Installation | ||||||
|  | From the root of the repository, run | ||||||
|  | ```sh | ||||||
|  | uv sync | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | To run the game, you can use | ||||||
|  | ```sh | ||||||
|  | uv run main.py | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | # Generality | ||||||
|  | Launching [`main.py`](main.py) starts a race with a single car on the provided track.  | ||||||
|  | This track can be controlled either by keyboard (*WASD*) or by a socket interface.  | ||||||
|  | An example of such interface is included in the code in [*`scripts/recorder.py`*](scripts/recorder.py). To run it, simply use the following command: | ||||||
|  | ```sh | ||||||
|  | uv run -m scripts.recorder | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | # Sensing | ||||||
|  | The car sensing is available in two commodities: **raycasts** and **images**. These sensing snapshots are sent at 10 Hertz (i.e. 10 times a second). Due to this fact, correct reception of snapshot messages has to be done regularly. | ||||||
|  |  | ||||||
|  | # Communication protocol | ||||||
|  |  | ||||||
|  | A remote controller can be impemented using TCP socket connecting on localhost on port 5000. | ||||||
|  | Different commands can be issued to the race simulation to control the car. | ||||||
|  |  | ||||||
|  | These commands are declared in [`src/command.py`](src/command.py) | ||||||
|  |  | ||||||
|  | ##  Car controls | ||||||
|  | ```python | ||||||
|  | ControlCommand(control: CarControl, active: bool) | ||||||
|  | ``` | ||||||
|  | To simulate key press and control the car. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Controls | ||||||
|  |  | ||||||
|  | - <kbd>W</kbd> Move forward | ||||||
|  | - <kbd>S</kbd> Brake / move backward | ||||||
|  | - <kbd>A</kbd> Turn left | ||||||
|  | - <kbd>D</kbd> Turn right | ||||||
|  | - <kbd>F</kbd> Toggle FPS indicator | ||||||
|  | - <kbd>V</kbd> Toggle speedometer | ||||||
|  | - <kbd>R</kbd> Reset car | ||||||
|  | - <kbd>C</kbd> Toggle raycasts visibility | ||||||
|  | - <kbd>Esc</kbd> Quit | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Credits | ||||||
|  | This project is based on the repository [https://github.com/ISC-HEI/RallyRobotPilot_2025](https://github.com/ISC-HEI/RallyRobotPilot_2025), which is in turn based on [https://github.com/mandaw2014/Rally](https://github.com/mandaw2014/Rally) | ||||||
							
								
								
									
										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 | 
| @@ -4,4 +4,9 @@ version = "0.1.0" | |||||||
| description = "Rally racing game for ML" | description = "Rally racing game for ML" | ||||||
| readme = "README.md" | readme = "README.md" | ||||||
| requires-python = ">=3.13" | 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 | ||||||
| @@ -23,8 +23,12 @@ class Camera: | |||||||
|     def center(self) -> Vec: |     def center(self) -> Vec: | ||||||
|         return self.size / 2 |         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: |     def screen2world(self, screen_pos: Vec) -> Vec: | ||||||
|         delta: Vec = screen_pos - self.center |         delta: Vec = screen_pos - self.car_screen_pos | ||||||
|         delta /= self.zoom * self.UNIT_RATIO |         delta /= self.zoom * self.UNIT_RATIO | ||||||
|         dx: float = delta.x |         dx: float = delta.x | ||||||
|         dy: float = delta.y |         dy: float = delta.y | ||||||
| @@ -39,5 +43,8 @@ class Camera: | |||||||
|         dy: float = -delta.dot(self.up) |         dy: float = -delta.dot(self.up) | ||||||
|         dx: float = delta.dot(self.up.perp) |         dx: float = delta.dot(self.up.perp) | ||||||
|         screen_delta: Vec = Vec(dx, dy) * self.zoom * self.UNIT_RATIO |         screen_delta: Vec = Vec(dx, dy) * self.zoom * self.UNIT_RATIO | ||||||
|         screen_pos: Vec = self.center + screen_delta |         screen_pos: Vec = self.car_screen_pos + screen_delta | ||||||
|         return screen_pos |         return screen_pos | ||||||
|  |  | ||||||
|  |     def size2screen(self, size: float) -> float: | ||||||
|  |         return size * self.zoom * self.UNIT_RATIO | ||||||
|   | |||||||
							
								
								
									
										107
									
								
								src/car.py
									
									
									
									
									
								
							
							
						
						
									
										107
									
								
								src/car.py
									
									
									
									
									
								
							| @@ -1,22 +1,35 @@ | |||||||
| from math import radians | from math import radians | ||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
| import pygame | import pygame | ||||||
|  |  | ||||||
| from src.camera import Camera | from src.camera import Camera | ||||||
| from src.utils import segments_intersect | from src.remote_controller import RemoteController | ||||||
|  | from src.utils import get_segments_intersection, segments_intersect | ||||||
| from src.vec import Vec | from src.vec import Vec | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def sign(x): return 0 if x == 0 else (-1 if x < 0 else 1) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Car: | class Car: | ||||||
|     MAX_SPEED = 0.05 |     MAX_SPEED = 5 | ||||||
|     MAX_BACK_SPEED = -0.025 |     MAX_BACK_SPEED = -3 | ||||||
|     ROTATE_SPEED = radians(1) |     ROTATE_SPEED = 1 | ||||||
|     COLOR = (230, 150, 80) |     COLOR = (230, 150, 80) | ||||||
|  |     CTRL_COLOR = (80, 230, 150) | ||||||
|     WIDTH = 0.4 |     WIDTH = 0.4 | ||||||
|     LENGTH = 0.6 |     LENGTH = 0.6 | ||||||
|     COLLISION_MARGIN = 0.4 |     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: |     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.pos: Vec = pos | ||||||
|         self.direction: Vec = direction |         self.direction: Vec = direction | ||||||
|         self.speed: float = 0 |         self.speed: float = 0 | ||||||
| @@ -26,20 +39,26 @@ class Car: | |||||||
|         self.right: bool = False |         self.right: bool = False | ||||||
|         self.colliding: bool = False |         self.colliding: bool = False | ||||||
|  |  | ||||||
|     def update(self): |         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: |         if self.forward: | ||||||
|             self.speed += 0.001 |             self.speed += self.ACCELERATION * dt | ||||||
|             self.speed = min(self.MAX_SPEED, self.speed) |             self.speed = min(self.MAX_SPEED, self.speed) | ||||||
|  |  | ||||||
|         if self.backward: |         if self.backward: | ||||||
|             self.speed -= 0.002 |             self.speed -= self.ACCELERATION * 2 * dt | ||||||
|             self.speed = max(self.MAX_BACK_SPEED, self.speed) |             self.speed = max(self.MAX_BACK_SPEED, self.speed) | ||||||
|  |  | ||||||
|         rotate_angle: float = 0 |         rotate_angle: float = 0 | ||||||
|         if self.left: |         if self.left: | ||||||
|             rotate_angle -= self.ROTATE_SPEED |             rotate_angle -= self.ROTATE_SPEED * dt | ||||||
|         if self.right: |         if self.right: | ||||||
|             rotate_angle += self.ROTATE_SPEED |             rotate_angle += self.ROTATE_SPEED * dt | ||||||
|  |  | ||||||
|         # if self.backward: |         # if self.backward: | ||||||
|         #    rotate_angle *= -1 |         #    rotate_angle *= -1 | ||||||
| @@ -47,17 +66,35 @@ class Car: | |||||||
|         if rotate_angle != 0: |         if rotate_angle != 0: | ||||||
|             self.direction = self.direction.rotate(rotate_angle) |             self.direction = self.direction.rotate(rotate_angle) | ||||||
|  |  | ||||||
|         self.speed *= 0.98 |         if not self.forward and not self.backward: | ||||||
|         if abs(self.speed) < 1e-8: |             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.speed = 0 | ||||||
|  |  | ||||||
|         self.pos += self.direction * self.speed |         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) | ||||||
|  |  | ||||||
|     def render(self, surf: pygame.Surface, camera: Camera): |  | ||||||
|         pts: list[Vec] = self.get_corners() |         pts: list[Vec] = self.get_corners() | ||||||
|         pts = [camera.world2screen(p) for p in pts] |         pts = [camera.world2screen(p) for p in pts] | ||||||
|         pygame.draw.polygon(surf, self.COLOR, 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]: |     def get_corners(self) -> list[Vec]: | ||||||
|         u: Vec = self.direction * self.LENGTH / 2 |         u: Vec = self.direction * self.LENGTH / 2 | ||||||
|         v: Vec = self.direction.perp * self.WIDTH / 2 |         v: Vec = self.direction.perp * self.WIDTH / 2 | ||||||
| @@ -69,6 +106,8 @@ class Car: | |||||||
|         return [p1, p2, p3, p4] |         return [p1, p2, p3, p4] | ||||||
|  |  | ||||||
|     def check_collisions(self, polygons: list[list[Vec]]): |     def check_collisions(self, polygons: list[list[Vec]]): | ||||||
|  |         self.cast_rays(polygons) | ||||||
|  |  | ||||||
|         self.colliding = False |         self.colliding = False | ||||||
|         corners: list[Vec] = self.get_corners() |         corners: list[Vec] = self.get_corners() | ||||||
|         sides: list[tuple[Vec, Vec]] = [ |         sides: list[tuple[Vec, Vec]] = [ | ||||||
| @@ -92,5 +131,45 @@ class Car: | |||||||
|                             n *= -1 |                             n *= -1 | ||||||
|                             dist = -dist |                             dist = -dist | ||||||
|                         self.speed = 0 |                         self.speed = 0 | ||||||
|                         self.pos = self.pos + n * (self.COLLISION_MARGIN - dist) |                         self.pos = self.pos + n * \ | ||||||
|  |                             (self.COLLISION_MARGIN - dist) | ||||||
|                         return |                         return | ||||||
|  |  | ||||||
|  |     def cast_rays(self, polygons: list[list[Vec]]): | ||||||
|  |         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 | ||||||
							
								
								
									
										58
									
								
								src/game.py
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								src/game.py
									
									
									
									
									
								
							| @@ -1,3 +1,4 @@ | |||||||
|  | from math import cos, radians, sin | ||||||
| import pygame | import pygame | ||||||
|  |  | ||||||
| from src.camera import Camera | from src.camera import Camera | ||||||
| @@ -29,19 +30,21 @@ class Game: | |||||||
|             str(ROOT / "assets" / "fonts" / "Ubuntu-M.ttf"), 20 |             str(ROOT / "assets" / "fonts" / "Ubuntu-M.ttf"), 20 | ||||||
|         ) |         ) | ||||||
|         self.show_fps: bool = True |         self.show_fps: bool = True | ||||||
|  |         self.show_speed: bool = True | ||||||
|  |         self.show_raycasts: bool = True | ||||||
|  |  | ||||||
|     def mainloop(self): |     def mainloop(self): | ||||||
|         while self.running: |         while self.running: | ||||||
|  |             dt: float = self.clock.get_time() / 1000 | ||||||
|             self.process_pygame_events() |             self.process_pygame_events() | ||||||
|             self.car.update() |             self.car.controller.process_commands() | ||||||
|  |             self.car.update(dt) | ||||||
|             self.car.check_collisions(self.track.get_collision_polygons()) |             self.car.check_collisions(self.track.get_collision_polygons()) | ||||||
|  |             self.update_camera() | ||||||
|             self.render() |             self.render() | ||||||
|             self.clock.tick(60) |             self.clock.tick(60) | ||||||
|  |  | ||||||
|     def process_pygame_events(self): |     def process_pygame_events(self): | ||||||
|         self.camera.set_pos(self.car.pos) |  | ||||||
|         self.camera.set_direction(self.car.direction) |  | ||||||
|         self.camera.set_size(Vec(*self.win.get_size())) |  | ||||||
|         for event in pygame.event.get(): |         for event in pygame.event.get(): | ||||||
|             if event.type == pygame.QUIT: |             if event.type == pygame.QUIT: | ||||||
|                 self.quit() |                 self.quit() | ||||||
| @@ -55,15 +58,23 @@ class Game: | |||||||
|             elif event.type == pygame.KEYUP: |             elif event.type == pygame.KEYUP: | ||||||
|                 self.on_key_up(event) |                 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): |     def quit(self): | ||||||
|         self.running = False |         self.running = False | ||||||
|  |         self.car.controller.close() | ||||||
|  |  | ||||||
|     def render(self): |     def render(self): | ||||||
|         self.win.fill(self.BACKGROUND_COLOR) |         self.win.fill(self.BACKGROUND_COLOR) | ||||||
|         self.track.render(self.win, self.camera) |         self.track.render(self.win, self.camera) | ||||||
|         self.car.render(self.win, self.camera) |         self.car.render(self.win, self.camera, self.show_raycasts) | ||||||
|         if self.show_fps: |         if self.show_fps: | ||||||
|             self.render_fps() |             self.render_fps() | ||||||
|  |         if self.show_speed: | ||||||
|  |             self.render_speedometer() | ||||||
|  |  | ||||||
|         pygame.display.flip() |         pygame.display.flip() | ||||||
|  |  | ||||||
| @@ -76,6 +87,14 @@ class Game: | |||||||
|             self.car.left = True |             self.car.left = True | ||||||
|         elif event.key == pygame.K_d: |         elif event.key == pygame.K_d: | ||||||
|             self.car.right = True |             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): |     def on_key_up(self, event: pygame.event.Event): | ||||||
|         if event.key == pygame.K_w: |         if event.key == pygame.K_w: | ||||||
| @@ -92,3 +111,32 @@ class Game: | |||||||
|             f"{self.clock.get_fps():.1f}", True, self.FPS_COLOR |             f"{self.clock.get_fps():.1f}", True, self.FPS_COLOR | ||||||
|         ) |         ) | ||||||
|         self.win.blit(txt, (self.win.get_width() - txt.get_width(), 0)) |         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 | ||||||
|   | |||||||
| @@ -10,9 +10,14 @@ from src.vec import Vec | |||||||
| class Road(TrackObject): | class Road(TrackObject): | ||||||
|     type = TrackObjectType.Road |     type = TrackObjectType.Road | ||||||
|  |  | ||||||
|  |     STRIP_LENGTH = 0.5 | ||||||
|  |     STRIP_GAP = 0.5 | ||||||
|  |  | ||||||
|     def __init__(self, pts: list[RoadPoint]) -> None: |     def __init__(self, pts: list[RoadPoint]) -> None: | ||||||
|         super().__init__() |         super().__init__() | ||||||
|         self.pts: list[RoadPoint] = pts |         self.pts: list[RoadPoint] = pts | ||||||
|  |         self.strips: list[tuple[Vec, Vec]] = [] | ||||||
|  |         self.compute_strips() | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def load(cls, data: dict) -> Road: |     def load(cls, data: dict) -> Road: | ||||||
| @@ -28,8 +33,6 @@ class Road(TrackObject): | |||||||
|             p3: Vec = p1 - pt.normal * pt.width |             p3: Vec = p1 - pt.normal * pt.width | ||||||
|             side1.append(camera.world2screen(p2)) |             side1.append(camera.world2screen(p2)) | ||||||
|             side2.append(camera.world2screen(p3)) |             side2.append(camera.world2screen(p3)) | ||||||
|             col: tuple[float, float, float] = (i * 10 + 150, 100, 100) |  | ||||||
|             pygame.draw.circle(surf, col, camera.world2screen(p1), 5) |  | ||||||
|  |  | ||||||
|         n: int = len(self.pts) |         n: int = len(self.pts) | ||||||
|         for i in range(n): |         for i in range(n): | ||||||
| @@ -42,6 +45,15 @@ class Road(TrackObject): | |||||||
|         pygame.draw.lines(surf, (255, 255, 255), True, side1) |         pygame.draw.lines(surf, (255, 255, 255), True, side1) | ||||||
|         pygame.draw.lines(surf, (255, 255, 255), True, side2) |         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]]: |     def get_collision_polygons(self) -> list[list[Vec]]: | ||||||
|         side1: list[Vec] = [] |         side1: list[Vec] = [] | ||||||
|         side2: list[Vec] = [] |         side2: list[Vec] = [] | ||||||
| @@ -51,9 +63,50 @@ class Road(TrackObject): | |||||||
|             p3: Vec = p1 - pt.normal * pt.width |             p3: Vec = p1 - pt.normal * pt.width | ||||||
|             side1.append(p2) |             side1.append(p2) | ||||||
|             side2.append(p3) |             side2.append(p3) | ||||||
|  |  | ||||||
|         return [side1, side2] |         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: | class RoadPoint: | ||||||
|     def __init__(self, pos: Vec, normal: Vec, width: float) -> None: |     def __init__(self, pos: Vec, normal: Vec, width: float) -> None: | ||||||
|   | |||||||
							
								
								
									
										50
									
								
								src/record_file.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/record_file.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | import lzma | ||||||
|  | from pathlib import Path | ||||||
|  | import struct | ||||||
|  | import time | ||||||
|  | from typing import Literal | ||||||
|  |  | ||||||
|  | from src.snapshot import Snapshot | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RecordFile: | ||||||
|  |     VERSION = 1 | ||||||
|  |  | ||||||
|  |     def __init__(self, path: str | Path, mode: Literal["w", "r"]) -> None: | ||||||
|  |         self.path: str | Path = path | ||||||
|  |         self.mode: Literal["w", "r"] = mode | ||||||
|  |         self.file: lzma.LZMAFile = lzma.LZMAFile(self.path, self.mode) | ||||||
|  |  | ||||||
|  |     def __enter__(self): | ||||||
|  |         return self | ||||||
|  |  | ||||||
|  |     def __exit__(self, type, value, traceback): | ||||||
|  |         self.file.close() | ||||||
|  |  | ||||||
|  |     def write_header(self, n_snapshots: int): | ||||||
|  |         data: bytes = struct.pack(">IId", self.VERSION, n_snapshots, time.time()) | ||||||
|  |         self.file.write(data) | ||||||
|  |  | ||||||
|  |     def write_snapshots(self, snapshots: list[Snapshot]): | ||||||
|  |         self.write_header(len(snapshots)) | ||||||
|  |         for snapshot in snapshots: | ||||||
|  |             data: bytes = snapshot.pack() | ||||||
|  |             self.file.write(struct.pack(">I", len(data)) + data) | ||||||
|  |  | ||||||
|  |     def read_snapshots(self) -> list[Snapshot]: | ||||||
|  |         version: int = struct.unpack(">I", self.file.read(4))[0] | ||||||
|  |         if version != self.VERSION: | ||||||
|  |             raise ValueError( | ||||||
|  |                 f"Cannot parse record file with format version {version} (current version: {self.VERSION})" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         n_snapshots: int | ||||||
|  |         timestamp: float | ||||||
|  |         n_snapshots, timestamp = struct.unpack(">Id", self.file.read(12)) | ||||||
|  |         snapshots: list[Snapshot] = [] | ||||||
|  |  | ||||||
|  |         for _ in range(n_snapshots): | ||||||
|  |             size: int = struct.unpack(">I", self.file.read(4))[0] | ||||||
|  |             snapshots.append(Snapshot.unpack(self.file.read(size))) | ||||||
|  |  | ||||||
|  |         return snapshots | ||||||
							
								
								
									
										281
									
								
								src/recorder.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										281
									
								
								src/recorder.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,281 @@ | |||||||
|  | import os | ||||||
|  | from pathlib import Path | ||||||
|  | import socket | ||||||
|  | import struct | ||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
|  | from PyQt6 import uic | ||||||
|  | from PyQt6.QtCore import QObject, QThread, QTimer, pyqtSignal, pyqtSlot | ||||||
|  | from PyQt6.QtGui import QKeyEvent | ||||||
|  | from PyQt6.QtWidgets import QMainWindow | ||||||
|  |  | ||||||
|  | from src.bot import Bot | ||||||
|  | from src.command import ApplySnapshotCommand, CarControl, Command, ControlCommand, RecordingCommand, ResetCommand | ||||||
|  | from src.record_file import RecordFile | ||||||
|  | from src.recorder_ui import Ui_Recorder | ||||||
|  | from src.snapshot import Snapshot | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RecorderClient(QObject): | ||||||
|  |     DATA_CHUNK_SIZE = 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 | ||||||
							
								
								
									
										39
									
								
								src/utils.py
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								src/utils.py
									
									
									
									
									
								
							| @@ -1,10 +1,12 @@ | |||||||
| import os | import os | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  | from threading import Timer | ||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
| from src.vec import Vec | from src.vec import Vec | ||||||
|  |  | ||||||
|  | ROOT = Path(os.path.abspath(os.path.join( | ||||||
| ROOT = Path(os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))) |     os.path.dirname(__file__), os.pardir))) | ||||||
|  |  | ||||||
|  |  | ||||||
| def orientation(a: Vec, b: Vec, c: Vec) -> float: | def orientation(a: Vec, b: Vec, c: Vec) -> float: | ||||||
| @@ -32,3 +34,36 @@ def segments_intersect(a1: Vec, a2: Vec, b1: Vec, b2: Vec) -> bool: | |||||||
|         return True |         return True | ||||||
|  |  | ||||||
|     return False |     return False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_segments_intersection(a1: Vec, a2: Vec, b1: Vec, b2: Vec) -> Optional[Vec]: | ||||||
|  |     da: Vec = a2 - a1 | ||||||
|  |     db: Vec = b2 - b1 | ||||||
|  |     dp: Vec = a1 - b1 | ||||||
|  |     dap: Vec = da.perp | ||||||
|  |     denom: float = dap.dot(db) | ||||||
|  |  | ||||||
|  |     if abs(denom) < 1e-9: | ||||||
|  |         o1: float = da.cross(-dp) | ||||||
|  |         if abs(o1) < 1e-9: | ||||||
|  |             for p in [b1, b2]: | ||||||
|  |                 if p.within(a1, a2): | ||||||
|  |                     return p | ||||||
|  |             for p in [a1, a2]: | ||||||
|  |                 if p.within(b1, b2): | ||||||
|  |                     return p | ||||||
|  |             return None | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |     num: float = dap.dot(dp) | ||||||
|  |     t: float = num / denom | ||||||
|  |     intersection: Vec = b1 + db * t | ||||||
|  |     if intersection.within(a1, a2) and intersection.within(b1, b2): | ||||||
|  |         return intersection | ||||||
|  |     return None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RepeatTimer(Timer): | ||||||
|  |     def run(self): | ||||||
|  |         while not self.finished.wait(self.interval): | ||||||
|  |             self.function(*self.args, **self.kwargs) | ||||||
|   | |||||||
| @@ -8,6 +8,9 @@ class Vec: | |||||||
|         self.x: float = x |         self.x: float = x | ||||||
|         self.y: float = y |         self.y: float = y | ||||||
|  |  | ||||||
|  |     def copy(self) -> Vec: | ||||||
|  |         return Vec(self.x, self.y) | ||||||
|  |  | ||||||
|     def __add__(self, other: float | Vec) -> Vec: |     def __add__(self, other: float | Vec) -> Vec: | ||||||
|         if isinstance(other, Vec): |         if isinstance(other, Vec): | ||||||
|             return Vec(self.x + other.x, self.y + other.y) |             return Vec(self.x + other.x, self.y + other.y) | ||||||
| @@ -24,6 +27,9 @@ class Vec: | |||||||
|     def __truediv__(self, value: float) -> Vec: |     def __truediv__(self, value: float) -> Vec: | ||||||
|         return Vec(self.x / value, self.y / value) |         return Vec(self.x / value, self.y / value) | ||||||
|  |  | ||||||
|  |     def __neg__(self) -> Vec: | ||||||
|  |         return Vec(-self.x, -self.y) | ||||||
|  |  | ||||||
|     def dot(self, other: Vec) -> float: |     def dot(self, other: Vec) -> float: | ||||||
|         return self.x * other.x + self.y * other.y |         return self.x * other.x + self.y * other.y | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										119
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										119
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							| @@ -2,6 +2,58 @@ version = 1 | |||||||
| revision = 3 | revision = 3 | ||||||
| requires-python = ">=3.13" | 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]] | [[package]] | ||||||
| name = "pygame" | name = "pygame" | ||||||
| version = "2.6.1" | 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" }, |     { 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]] | [[package]] | ||||||
| name = "rally-racer" | name = "rally-racer" | ||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| source = { virtual = "." } | source = { virtual = "." } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  |     { name = "numpy" }, | ||||||
|     { name = "pygame" }, |     { name = "pygame" }, | ||||||
|  |     { name = "pyqt6" }, | ||||||
|  |     { name = "qasync" }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.metadata] | [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