├── .github
└── FUNDING.yml
├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── public
├── 01-cars-before-01.gif
├── 01-cars-before-02.gif
├── 01-cars-before-03.gif
├── 02-car-after-01.gif
├── 02-car-after-02.gif
├── 02-car-after-03.gif
├── article
│ ├── images
│ │ ├── 01-cover-00.jpg
│ │ ├── 01-cover-01.jpg
│ │ ├── 02-cars-before-01.gif
│ │ ├── 02-cars-before-02.gif
│ │ ├── 02-cars-before-03.gif
│ │ ├── 03-car-after-01.gif
│ │ ├── 03-car-after-02.gif
│ │ ├── 03-car-after-03.gif
│ │ ├── 03-car-muscles-01.gif
│ │ ├── 03-car-muscles-03.png
│ │ ├── 04-sensors-01.jpg
│ │ ├── 04-sensors-02.gif
│ │ ├── 05-sigmoid-01.png
│ │ ├── 05-sigmoid-01.svg
│ │ ├── 05-sigmoid-02.png
│ │ ├── 05-sigmoid-03.png
│ │ ├── 06-floating-point-conversion-01.png
│ │ ├── 06-genome-examples.png
│ │ ├── 07-genetic-algorithm-flow-01.png
│ │ ├── 08-distance-to-parkin-lot.png
│ │ ├── 09-fitness-function.png
│ │ ├── 10-loss-history-00.png
│ │ └── 11-fin.png
│ ├── index.md
│ └── index.ru.md
├── favicon.ico
├── index.html
├── models
│ ├── beetle.glb
│ └── wheel.glb
├── robots.txt
├── site-meta-image-01.jpg
└── site-meta-image-02.jpg
├── serve.json
├── src
├── App.tsx
├── checkpoints
│ ├── README.md
│ ├── ckpt--population-1000--generation-25.json
│ ├── ckpt--population-1000--generation-36.json
│ └── ckpt--population-1000--generation-45.json
├── components
│ ├── evolution
│ │ ├── AutomaticParkingAnalytics.tsx
│ │ ├── BestGenomes.tsx
│ │ ├── EvolutionAnalytics.tsx
│ │ ├── EvolutionBoardParams.tsx
│ │ ├── EvolutionCheckpointSaver.tsx
│ │ ├── EvolutionTabAutomatic.tsx
│ │ ├── EvolutionTabEvolution.tsx
│ │ ├── EvolutionTabManual.tsx
│ │ ├── EvolutionTabs.tsx
│ │ ├── EvolutionTiming.tsx
│ │ ├── GenomePreview.tsx
│ │ ├── LossHistory.tsx
│ │ ├── PopulationTable.tsx
│ │ ├── constants
│ │ │ ├── evolution.ts
│ │ │ └── genomes.ts
│ │ └── utils
│ │ │ └── evolution.ts
│ ├── screens
│ │ └── HomeScreen.tsx
│ ├── shared
│ │ ├── ErrorBoundary.tsx
│ │ ├── FadeIn.css
│ │ ├── FadeIn.tsx
│ │ ├── Footer.tsx
│ │ ├── FormElementsRow.tsx
│ │ ├── Header.tsx
│ │ ├── Hint.tsx
│ │ ├── Layout.css
│ │ ├── Layout.tsx
│ │ ├── MainNav.tsx
│ │ ├── Row.tsx
│ │ ├── Timer.css
│ │ └── Timer.tsx
│ └── world
│ │ ├── World.tsx
│ │ ├── car
│ │ ├── Car.tsx
│ │ ├── CarLabel.tsx
│ │ ├── Chassis.tsx
│ │ ├── ChassisModel.tsx
│ │ ├── ChassisModelSimple.tsx
│ │ ├── SensorRay.tsx
│ │ ├── Sensors.tsx
│ │ ├── Wheel.tsx
│ │ ├── WheelModel.tsx
│ │ ├── WheelModelSimple.tsx
│ │ └── constants.ts
│ │ ├── cars
│ │ ├── DynamicCars.tsx
│ │ └── StaticCars.tsx
│ │ ├── constants
│ │ ├── cars.ts
│ │ ├── models.ts
│ │ ├── parking.ts
│ │ ├── performance.ts
│ │ └── world.ts
│ │ ├── controllers
│ │ ├── CarJoystickController.tsx
│ │ └── CarKeyboardController.tsx
│ │ ├── parkings
│ │ ├── ParkingAutomatic.tsx
│ │ └── ParkingManual.tsx
│ │ ├── surroundings
│ │ ├── Ground.tsx
│ │ └── ParkingSpot.tsx
│ │ ├── types
│ │ ├── car.ts
│ │ └── models.ts
│ │ └── utils
│ │ ├── controllers.ts
│ │ ├── events.ts
│ │ ├── materials.ts
│ │ ├── models.ts
│ │ └── uuid.ts
├── constants
│ ├── app.ts
│ ├── links.ts
│ └── routes.ts
├── hooks
│ └── useKeyPress.ts
├── index.tsx
├── libs
│ ├── __tests__
│ │ ├── carGenetic.test.ts
│ │ └── genetic.test.ts
│ ├── carGenetic.ts
│ ├── genetic.ts
│ └── math
│ │ ├── __tests__
│ │ ├── floats.test.ts
│ │ ├── geometry.test.ts
│ │ ├── polynomial.test.ts
│ │ ├── probability.test.ts
│ │ └── sigmoid.test.ts
│ │ ├── floats.ts
│ │ ├── geometry.ts
│ │ ├── polynomial.ts
│ │ ├── probability.ts
│ │ └── sigmoid.ts
├── react-app-env.d.ts
├── setupTests.ts
├── types
│ └── vectors.ts
└── utils
│ ├── colors.ts
│ ├── logger.ts
│ ├── storage.ts
│ └── url.ts
└── tsconfig.json
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # @see: https://docs.github.com/en/github/administering-a-repository/displaying-a-sponsor-button-in-your-repository
2 | github: trekhleb
3 | patreon: trekhleb
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 | .idea
3 |
4 | # dependencies
5 | /node_modules
6 | /.pnp
7 | .pnp.js
8 |
9 | # testing
10 | /coverage
11 |
12 | # production
13 | /build
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Oleksii Trekhleb
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🧬 Self-Parking Car Evolution
2 |
3 | Training the car to do self-parking using a genetic algorithm.
4 |
5 | > - 🚕 [Launch the demo](https://trekhleb.dev/self-parking-car-evolution)
6 | > - 📃 [Read about how it works](https://trekhleb.dev/blog/2021/self-parking-car-evolution/)
7 |
8 | [](https://trekhleb.dev/self-parking-car-evolution)
9 |
10 | This is an experimental project with the aim to learn the basics of how [genetic algorithm](https://en.wikipedia.org/wiki/Genetic_algorithm) works by teaching the cars to do the self-parking. The evolution process is happening directly in the browser. You may check the [evolution source-code](https://github.com/trekhleb/self-parking-car-evolution/tree/master/src/libs) (in TypeScript) or read the [explanation of how it works](https://trekhleb.dev/blog/2021/self-parking-car-evolution/) in my blog-post.
11 |
12 | **At the beginning of the evolution** the generation of cars has random genomes which make them behave something like this:
13 |
14 | 
15 |
16 | **On the 40th generation** the cars start learning what the self-parking is and start getting closer to the parking spot (although hitting the other cars along the way):
17 |
18 | 
19 |
20 | Another example with a bit more challenging starting point:
21 |
22 | 
23 |
24 | ## Genetic Source-Code
25 |
26 | The `≈92%` of the code in this repository relates to the UI logic (3D simulation of the cars world, form controls for the evolution training process, etc.).
27 |
28 | However, the actual [code that implements a genetic algorithm](https://github.com/trekhleb/self-parking-car-evolution/tree/master/src/libs) takes less than `<500` lines of code.
29 |
30 | ## Development Details
31 |
32 | The project is a [React](https://create-react-app.dev/) application written on TypeScript. Styled with [BaseWeb](https://baseweb.design/).
33 |
34 | The 3D world simulation is made with [Three.js](https://threejs.org/) library using [@react-three/fiber](https://github.com/pmndrs/react-three-fiber) wrapper. The physics is simulated with [Cannon.js](https://github.com/schteppe/cannon.js) using [cannon-es](https://github.com/pmndrs/cannon-es) wrapper.
35 |
36 | The whole evolution simulation is happening directly in the browser.
37 |
38 | To launch the project, fork/clone it and run the following commands:
39 |
40 | ```shell
41 | npm install
42 | npm run start
43 | ```
44 |
45 | The website will be available on `http://localhost:3000/self-parking-car-evolution`.
46 |
47 | **Hints:**
48 |
49 | - You may upload one of the [pre-trained checkpoints](https://github.com/trekhleb/self-parking-car-evolution/tree/master/src/checkpoints) to avoid starting the evolution from scratch.
50 | - Use the `?debug=true` URL param to see the FPS performance monitor and debugging logs in the console (i.e. `http://localhost:3000/self-parking-car-evolution?debug=true`).
51 | - Training progress is being saved to the local storage for each generation (not for each batch/group).
52 |
53 | ## Author
54 |
55 | - [@trekhleb](https://trekhleb.dev)
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "self-parking-car-evolution",
3 | "version": "0.1.0",
4 | "private": false,
5 | "author": "Oleksii Trekhleb (https://trekhleb.dev)",
6 | "homepage": "https://trekhleb.dev/self-parking-car-evolution",
7 | "repository": {
8 | "type": "git",
9 | "url": "git+https://github.com/trekhleb/self-parking-car-evolution.git"
10 | },
11 | "dependencies": {
12 | "@nivo/core": "^0.72.0",
13 | "@nivo/line": "^0.72.0",
14 | "@react-three/cannon": "^1.1.1",
15 | "@react-three/drei": "^4.3.3",
16 | "@react-three/fiber": "^6.0.19",
17 | "@testing-library/jest-dom": "^5.12.0",
18 | "@testing-library/react": "^11.2.6",
19 | "@testing-library/user-event": "^12.8.3",
20 | "@types/file-saver": "^2.0.3",
21 | "@types/jest": "^26.0.23",
22 | "@types/lodash": "^4.14.170",
23 | "@types/node": "^12.20.10",
24 | "@types/react": "^17.0.4",
25 | "@types/react-dom": "^17.0.3",
26 | "@types/react-router-dom": "^5.1.7",
27 | "@types/styletron-engine-atomic": "^1.1.0",
28 | "@types/styletron-react": "^5.0.2",
29 | "@types/styletron-standard": "^2.0.1",
30 | "@types/three": "^0.127.1",
31 | "baseui": "^10.0.0",
32 | "cannon-es": "^0.17.0",
33 | "file-saver": "^2.0.5",
34 | "gh-pages": "^3.1.0",
35 | "lodash": "^4.17.21",
36 | "nice-color-palettes": "^3.0.0",
37 | "react": "^17.0.2",
38 | "react-dom": "^17.0.2",
39 | "react-icons": "^4.2.0",
40 | "react-nipple": "^1.0.2",
41 | "react-router-dom": "^5.2.0",
42 | "react-scripts": "4.0.3",
43 | "styletron-engine-atomic": "^1.4.8",
44 | "styletron-react": "^6.0.1",
45 | "three": "^0.128.0",
46 | "three-mesh-bvh": "^0.3.7",
47 | "typescript": "^4.2.4",
48 | "web-vitals": "^1.1.1"
49 | },
50 | "devDependencies": {
51 | "serve": "^12.0.0"
52 | },
53 | "scripts": {
54 | "start": "react-scripts start",
55 | "build": "react-scripts build",
56 | "test": "react-scripts test",
57 | "lint": "eslint 'src/**/*.{js,ts,tsx}'",
58 | "eject": "react-scripts eject",
59 | "predeploy": "npm run build",
60 | "deploy": "gh-pages -d ./build",
61 | "serve-build": "serve --config=serve.json --no-clipboard --no-compression --cors -p 5000"
62 | },
63 | "eslintConfig": {
64 | "extends": [
65 | "react-app",
66 | "react-app/jest"
67 | ]
68 | },
69 | "browserslist": {
70 | "production": [
71 | ">0.2%",
72 | "not dead",
73 | "not op_mini all"
74 | ],
75 | "development": [
76 | "last 1 chrome version",
77 | "last 1 firefox version",
78 | "last 1 safari version"
79 | ]
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/public/01-cars-before-01.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/01-cars-before-01.gif
--------------------------------------------------------------------------------
/public/01-cars-before-02.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/01-cars-before-02.gif
--------------------------------------------------------------------------------
/public/01-cars-before-03.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/01-cars-before-03.gif
--------------------------------------------------------------------------------
/public/02-car-after-01.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/02-car-after-01.gif
--------------------------------------------------------------------------------
/public/02-car-after-02.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/02-car-after-02.gif
--------------------------------------------------------------------------------
/public/02-car-after-03.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/02-car-after-03.gif
--------------------------------------------------------------------------------
/public/article/images/01-cover-00.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/01-cover-00.jpg
--------------------------------------------------------------------------------
/public/article/images/01-cover-01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/01-cover-01.jpg
--------------------------------------------------------------------------------
/public/article/images/02-cars-before-01.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/02-cars-before-01.gif
--------------------------------------------------------------------------------
/public/article/images/02-cars-before-02.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/02-cars-before-02.gif
--------------------------------------------------------------------------------
/public/article/images/02-cars-before-03.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/02-cars-before-03.gif
--------------------------------------------------------------------------------
/public/article/images/03-car-after-01.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/03-car-after-01.gif
--------------------------------------------------------------------------------
/public/article/images/03-car-after-02.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/03-car-after-02.gif
--------------------------------------------------------------------------------
/public/article/images/03-car-after-03.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/03-car-after-03.gif
--------------------------------------------------------------------------------
/public/article/images/03-car-muscles-01.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/03-car-muscles-01.gif
--------------------------------------------------------------------------------
/public/article/images/03-car-muscles-03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/03-car-muscles-03.png
--------------------------------------------------------------------------------
/public/article/images/04-sensors-01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/04-sensors-01.jpg
--------------------------------------------------------------------------------
/public/article/images/04-sensors-02.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/04-sensors-02.gif
--------------------------------------------------------------------------------
/public/article/images/05-sigmoid-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/05-sigmoid-01.png
--------------------------------------------------------------------------------
/public/article/images/05-sigmoid-01.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/article/images/05-sigmoid-02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/05-sigmoid-02.png
--------------------------------------------------------------------------------
/public/article/images/05-sigmoid-03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/05-sigmoid-03.png
--------------------------------------------------------------------------------
/public/article/images/06-floating-point-conversion-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/06-floating-point-conversion-01.png
--------------------------------------------------------------------------------
/public/article/images/06-genome-examples.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/06-genome-examples.png
--------------------------------------------------------------------------------
/public/article/images/07-genetic-algorithm-flow-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/07-genetic-algorithm-flow-01.png
--------------------------------------------------------------------------------
/public/article/images/08-distance-to-parkin-lot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/08-distance-to-parkin-lot.png
--------------------------------------------------------------------------------
/public/article/images/09-fitness-function.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/09-fitness-function.png
--------------------------------------------------------------------------------
/public/article/images/10-loss-history-00.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/10-loss-history-00.png
--------------------------------------------------------------------------------
/public/article/images/11-fin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/article/images/11-fin.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Self-Parking Car Evolution
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/public/models/beetle.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/models/beetle.glb
--------------------------------------------------------------------------------
/public/models/wheel.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/models/wheel.glb
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/site-meta-image-01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/site-meta-image-01.jpg
--------------------------------------------------------------------------------
/public/site-meta-image-02.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trekhleb/self-parking-car-evolution/b9fb92d4d78ac935945beb9aa82ae83997c295ad/public/site-meta-image-02.jpg
--------------------------------------------------------------------------------
/serve.json:
--------------------------------------------------------------------------------
1 | {
2 | "public": "build",
3 | "rewrites": [
4 | {
5 | "source": "/self-parking-car-evolution/favicon.ico",
6 | "destination": "/favicon.ico"
7 | },
8 | {
9 | "source": "/self-parking-car-evolution/models/:model_name",
10 | "destination": "/models/:model_name"
11 | },
12 | {
13 | "source": "/self-parking-car-evolution/static/:resource_type/:resource",
14 | "destination": "/static/:resource_type/:resource"
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { HashRouter, Switch, Route } from 'react-router-dom';
3 |
4 | import Layout from './components/shared/Layout';
5 | import { routes } from './constants/routes';
6 | import HomeScreen from './components/screens/HomeScreen';
7 |
8 | function App() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | export default App;
23 |
--------------------------------------------------------------------------------
/src/checkpoints/README.md:
--------------------------------------------------------------------------------
1 | # Self-Parking Car Evolution Checkpoints
2 |
3 | Checkpoint is a `json` file that contain the history of the evolution and the list of genomes from the latest generation
4 |
5 | To read more about self-parking car evolution go to the [main README of the project](https://github.com/trekhleb/self-parking-car-evolution).
6 |
--------------------------------------------------------------------------------
/src/components/evolution/AutomaticParkingAnalytics.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Block } from 'baseui/block';
3 | import { Button, SIZE as BUTTON_SIZE, KIND as BUTTON_KIND, SHAPE as BUTTON_SHAPE } from 'baseui/button';
4 | import { Checkbox, LABEL_PLACEMENT } from 'baseui/checkbox';
5 | import { ButtonGroup, MODE as BUTTON_GROUP_MODE, SIZE as BUTTON_GROUP_SIZE } from 'baseui/button-group';
6 |
7 | import EvolutionTiming from './EvolutionTiming';
8 | import BestGenomes from './BestGenomes';
9 | import { Genome } from '../../libs/genetic';
10 | import { FormControl } from 'baseui/form-control';
11 | import Row from '../shared/Row';
12 | import Hint from '../shared/Hint';
13 | import { DynamicCarsPosition } from '../world/constants/cars';
14 |
15 | type AutomaticParkingAnalyticsProps = {
16 | genomes: Genome[],
17 | generationLifetimeMs: number,
18 | batchVersion: string,
19 | bestGenome: Genome | null,
20 | minLoss: number | null,
21 | carsBatchIndex: number | null,
22 | performanceBoost: boolean,
23 | selectedGenomeIndex: number,
24 | carsPosition: DynamicCarsPosition,
25 | onCarsPositionChange: (carsPosition: DynamicCarsPosition) => void,
26 | onChangeGenomeIndex: (index: number) => void,
27 | onBestGenomeEdit?: (genome: Genome) => void,
28 | onPerformanceBoost: (state: boolean) => void,
29 | };
30 |
31 | function AutomaticParkingAnalytics(props: AutomaticParkingAnalyticsProps) {
32 | const {
33 | genomes,
34 | bestGenome,
35 | generationLifetimeMs,
36 | batchVersion,
37 | minLoss,
38 | carsBatchIndex,
39 | selectedGenomeIndex,
40 | performanceBoost,
41 | carsPosition,
42 | onCarsPositionChange,
43 | onChangeGenomeIndex,
44 | onPerformanceBoost,
45 | onBestGenomeEdit = (genome: Genome) => {},
46 | } = props;
47 |
48 | const timingDetails = (
49 |
50 |
57 |
58 | );
59 |
60 | const carLicencePlates = genomes.map((genome: Genome, genomeIndex: number) => (
61 |
67 |
77 |
78 | ));
79 |
80 | const carsSwitcher = (
81 |
82 | 'Select the pre-trained car genome'}
84 | >
85 |
86 | {carLicencePlates}
87 |
88 |
89 |
90 | );
91 |
92 | const performanceBooster = (
93 |
94 |
95 | onPerformanceBoost(e.target.checked)}
99 | labelPlacement={LABEL_PLACEMENT.right}
100 | >
101 |
102 |
103 | Performance boost
104 |
105 |
108 |
109 |
110 |
111 |
112 | );
113 |
114 | const selectedCarsPosition: Record = {
115 | 'middle': 0,
116 | 'front': 1,
117 | 'rear': 3,
118 | };
119 |
120 | const carsPositionFromIndex = (positionIndex: number): DynamicCarsPosition => {
121 | // @ts-ignore
122 | const positions: DynamicCarsPosition[] = Object.keys(selectedCarsPosition);
123 | return positions[positionIndex];
124 | };
125 |
126 | const carsStartPositionChanger = (
127 |
128 |
129 | {
134 | onCarsPositionChange(carsPositionFromIndex(index));
135 | }}
136 | >
137 |
138 |
139 |
140 |
141 |
142 | );
143 |
144 | return (
145 | <>
146 | {timingDetails}
147 |
148 |
153 |
154 | {carsStartPositionChanger}
155 |
156 |
157 | {performanceBooster}
158 |
159 |
160 |
161 | {carsSwitcher}
162 |
163 |
170 | >
171 | );
172 | }
173 |
174 | export default AutomaticParkingAnalytics;
175 |
--------------------------------------------------------------------------------
/src/components/evolution/BestGenomes.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Panel, StatelessAccordion } from 'baseui/accordion';
3 | import { Genome } from '../../libs/genetic';
4 | import GenomePreview from './GenomePreview';
5 | import { CarLicencePlateType } from '../world/types/car';
6 |
7 | const GENOME_PANELS = {
8 | firstBestGenome: 'first-best-genome',
9 | secondBestGenome: 'second-best-genome',
10 | };
11 |
12 | type BestGenomesProps = {
13 | bestGenomePanelTitle?: string,
14 | bestGenome: Genome | null,
15 | bestCarLicencePlate?: CarLicencePlateType | null,
16 | minLoss?: number | null,
17 | secondBestGenomePanelTitle?: string,
18 | secondBestGenome?: Genome | null,
19 | secondBestCarLicencePlate?: CarLicencePlateType | null,
20 | secondMinLoss?: number | null,
21 | editable?: boolean,
22 | onBestGenomeEdit?: (genome: Genome) => void,
23 | };
24 |
25 | function BestGenomes(props: BestGenomesProps): React.ReactElement {
26 | const {
27 | bestGenomePanelTitle = '1st Best Car Genome',
28 | bestGenome,
29 | bestCarLicencePlate,
30 | minLoss,
31 | secondBestGenomePanelTitle = '2nd Best Car Genome',
32 | secondBestGenome,
33 | secondBestCarLicencePlate,
34 | secondMinLoss,
35 | editable = false,
36 | onBestGenomeEdit = (genome: Genome) => {},
37 | } = props;
38 |
39 | const [genomeExpandedTabs, setGenomeExpandedTabs] = React.useState([
40 | GENOME_PANELS.firstBestGenome
41 | ]);
42 |
43 | const onPanelChange = (
44 | {key, expanded}: {key: React.Key, expanded: React.Key[]}
45 | ) => {
46 | const newGenomeExpandedTabs = [...genomeExpandedTabs];
47 | const openedTabIndex = newGenomeExpandedTabs.indexOf(key);
48 | if (openedTabIndex === -1) {
49 | newGenomeExpandedTabs.push(key);
50 | } else {
51 | newGenomeExpandedTabs.splice(openedTabIndex);
52 | }
53 | setGenomeExpandedTabs(newGenomeExpandedTabs);
54 | };
55 |
56 | const bestGenomePreview = (
57 |
64 | );
65 |
66 | const secondBestGenomePreview = secondBestGenome !== undefined ? (
67 |
72 | ) : null;
73 |
74 | const panels = [];
75 |
76 | const firstBestGenomePanel = (
77 |
78 | {bestGenomePreview}
79 |
80 | );
81 |
82 | const secondBestGenomePanel = secondBestGenomePreview ? (
83 |
84 | {secondBestGenomePreview}
85 |
86 | ) : null;
87 |
88 | panels.push(firstBestGenomePanel);
89 | if (secondBestGenomePanel) {
90 | panels.push(secondBestGenomePanel);
91 | }
92 |
93 | return (
94 |
98 | {panels}
99 |
100 | );
101 | }
102 |
103 | export default BestGenomes;
104 |
--------------------------------------------------------------------------------
/src/components/evolution/EvolutionAnalytics.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Block } from 'baseui/block';
3 |
4 | import PopulationTable, { CarsInProgressType, CarsLossType } from './PopulationTable';
5 | import EvolutionBoardParams from './EvolutionBoardParams';
6 | import EvolutionTiming from './EvolutionTiming';
7 | import LossHistory from './LossHistory';
8 | import BestGenomes from './BestGenomes';
9 | import { CarLicencePlateType, CarsType } from '../world/types/car';
10 | import { Genome, Percentage, Probability } from '../../libs/genetic';
11 |
12 | type EvolutionAnalyticsProps = {
13 | generationIndex: number | null,
14 | carsBatchIndex: number | null,
15 | totalBatches: number | null,
16 | worldIndex: number,
17 | generationLifetimeMs: number,
18 | generationSize: number,
19 | carsBatchSize: number,
20 | mutationProbability: Probability,
21 | performanceBoost: boolean,
22 | needToRetry: boolean,
23 | longLivingChampionsPercentage: Percentage,
24 | generationLifetime: number,
25 | batchVersion: string,
26 | onGenerationSizeChange: (size: number) => void,
27 | onBatchSizeChange: (size: number) => void,
28 | onGenerationLifetimeChange: (time: number) => void,
29 | onReset: () => void,
30 | onMutationProbabilityChange: (probability: Probability) => void,
31 | onLongLivingChampionsPercentageChange: (percentage: Percentage) => void,
32 | onPerformanceBoost: (state: boolean) => void,
33 | lossHistory: number[],
34 | avgLossHistory: number[],
35 | cars: CarsType,
36 | carsInProgress: CarsInProgressType,
37 | carsLoss: CarsLossType[],
38 | bestGenome: Genome | null,
39 | bestCarLicencePlate: CarLicencePlateType | null,
40 | minLoss: number | null,
41 | secondBestGenome: Genome | null,
42 | secondBestCarLicencePlate: CarLicencePlateType | null,
43 | secondMinLoss: number | null,
44 | };
45 |
46 | function EvolutionAnalytics(props: EvolutionAnalyticsProps) {
47 | const {
48 | generationIndex,
49 | carsBatchIndex,
50 | totalBatches,
51 | needToRetry,
52 | mutationProbability,
53 | longLivingChampionsPercentage,
54 | worldIndex,
55 | generationLifetimeMs,
56 | generationSize,
57 | performanceBoost,
58 | carsBatchSize,
59 | generationLifetime,
60 | batchVersion,
61 | onGenerationSizeChange,
62 | onBatchSizeChange,
63 | onGenerationLifetimeChange,
64 | onReset,
65 | onMutationProbabilityChange,
66 | onLongLivingChampionsPercentageChange,
67 | onPerformanceBoost,
68 | lossHistory,
69 | avgLossHistory,
70 | cars,
71 | carsInProgress,
72 | carsLoss,
73 | bestGenome,
74 | bestCarLicencePlate,
75 | minLoss,
76 | secondBestGenome,
77 | secondBestCarLicencePlate,
78 | secondMinLoss,
79 | } = props;
80 |
81 | const timingDetails = (
82 |
83 |
92 |
93 | );
94 |
95 | const evolutionParams = (
96 |
97 |
112 |
113 | );
114 |
115 | const lossHistoryChart = (
116 |
117 |
121 |
122 | );
123 |
124 | const populationTable = (
125 |
126 |
135 |
136 | );
137 |
138 | return (
139 | <>
140 | {timingDetails}
141 | {evolutionParams}
142 |
143 |
144 | {lossHistoryChart}
145 |
146 |
147 | {populationTable}
148 |
149 |
150 |
158 | >
159 | );
160 | }
161 |
162 | export default EvolutionAnalytics;
163 |
--------------------------------------------------------------------------------
/src/components/evolution/EvolutionCheckpointSaver.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Block } from 'baseui/block';
3 | import {
4 | Button,
5 | SIZE as BUTTON_SIZE,
6 | SHAPE as BUTTON_SHAPE,
7 | KIND as BUTTON_KIND,
8 | } from 'baseui/button';
9 | import { BiDownload, BiUpload } from 'react-icons/all';
10 | import { saveAs } from 'file-saver';
11 | import {
12 | Modal,
13 | ModalHeader,
14 | ModalBody,
15 | SIZE,
16 | ROLE
17 | } from 'baseui/modal';
18 | import { FileUploader } from 'baseui/file-uploader';
19 | import { Notification, KIND as NOTIFICATION_KIND } from 'baseui/notification';
20 | import { Paragraph3 } from 'baseui/typography';
21 |
22 | import Row from '../shared/Row';
23 | import { Generation, Percentage, Probability } from '../../libs/genetic';
24 | import { CHECKPOINTS_PATH } from '../../constants/links';
25 | import demoCheckpoint from '../../checkpoints/ckpt--population-1000--generation-36.json';
26 |
27 | export type EvolutionCheckpoint = {
28 | dateTime: string,
29 | generationIndex: number,
30 | lossHistory: number[],
31 | avgLossHistory: number[],
32 | performanceBoost: boolean,
33 | generationSize: number,
34 | generationLifetime: number,
35 | carsBatchSize: number,
36 | mutationProbability: Probability,
37 | longLivingChampionsPercentage: Percentage,
38 | generation: Generation,
39 | };
40 |
41 | type EvolutionCheckpointSaverProps = {
42 | onRestoreFromCheckpoint: (checkpoint: EvolutionCheckpoint) => void,
43 | onCheckpointToFile: () => EvolutionCheckpoint,
44 | };
45 |
46 | function EvolutionCheckpointSaver(props: EvolutionCheckpointSaverProps) {
47 | const {
48 | onRestoreFromCheckpoint,
49 | onCheckpointToFile,
50 | } = props;
51 |
52 | const [showCheckpointModal, setShowCheckpointModal] = useState(false);
53 | const [checkpointIsProcessing, setCheckpointIsProcessing] = useState(false);
54 | const [checkpointErrorMessage, setCheckpointErrorMessage] = useState(null);
55 |
56 | const onSaveEvolution = () => {
57 | const checkpoint: EvolutionCheckpoint = onCheckpointToFile();
58 | const fileName = `ckpt--population-${checkpoint.generationSize}--generation-${checkpoint.generationIndex}.json`;
59 | const checkpointString: string = JSON.stringify(checkpoint);
60 | const checkpointBlob = new Blob(
61 | [checkpointString],
62 | { type: 'application/json' },
63 | );
64 | saveAs(checkpointBlob, fileName);
65 | };
66 |
67 | const onCheckpointModalOpen = () => {
68 | setCheckpointErrorMessage(null);
69 | setCheckpointIsProcessing(false);
70 | setShowCheckpointModal(true);
71 | };
72 |
73 | const onCheckpointModalClose = () => {
74 | setShowCheckpointModal(false);
75 | };
76 |
77 | const onCancelCheckpointUpload = () => {
78 | setCheckpointIsProcessing(false);
79 | };
80 |
81 | const onFileDrop = (acceptedFiles: File[]) => {
82 | try {
83 | setCheckpointIsProcessing(true);
84 |
85 | const onFileReaderLoaded = (event: Event) => {
86 | // @ts-ignore
87 | const checkpoint: EvolutionCheckpoint = JSON.parse(event.target.result);
88 | onRestoreFromCheckpoint(checkpoint);
89 | setCheckpointIsProcessing(false);
90 | onCheckpointModalClose();
91 | };
92 |
93 | const fileReader = new FileReader();
94 | fileReader.onload = onFileReaderLoaded;
95 | fileReader.readAsText(acceptedFiles[0]);
96 | } catch (error: any) {
97 | setCheckpointErrorMessage(error.message);
98 | setCheckpointIsProcessing(false);
99 | }
100 | };
101 |
102 | const onUseDemoCheckpoint = () => {
103 | try {
104 | // @ts-ignore
105 | onRestoreFromCheckpoint(demoCheckpoint);
106 | onCheckpointModalClose();
107 | } catch (error: any) {
108 | setCheckpointErrorMessage(error.message);
109 | }
110 | };
111 |
112 | const checkpointError = checkpointErrorMessage ? (
113 |
119 | {checkpointErrorMessage}
120 |
121 | ) : null;
122 |
123 | const checkpointModal = (
124 |
133 | Restore evolution from the checkpoint file
134 |
135 | {checkpointError}
136 |
137 | Checkpoint is a json
file that contain the history of the evolution and the list of genomes from the latest generation.
138 |
139 |
140 | You may save your own evolution progress to the checkpoint file or use one of the pre-trained checkpoints.
141 |
142 |
143 |
144 |
152 |
153 |
154 |
161 |
162 |
163 | );
164 |
165 | return (
166 | <>
167 |
168 |
169 |
177 |
178 |
179 |
180 |
188 |
189 |
190 |
191 | {checkpointModal}
192 | >
193 | );
194 | }
195 |
196 | export default EvolutionCheckpointSaver;
197 |
--------------------------------------------------------------------------------
/src/components/evolution/EvolutionTabAutomatic.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import { Block } from 'baseui/block';
3 | import { useSnackbar, DURATION } from 'baseui/snackbar';
4 | import { Check } from 'baseui/icon';
5 | import { Notification } from 'baseui/notification';
6 |
7 | import { Generation, Genome } from '../../libs/genetic';
8 | import { CarLicencePlateType, CarType } from '../world/types/car';
9 | import {
10 | SECOND,
11 | TRAINED_CAR_GENERATION_LIFETIME
12 | } from './EvolutionBoardParams';
13 | import { generationToCars } from './utils/evolution';
14 | import { loggerBuilder } from '../../utils/logger';
15 | import { BEST_GENOMES } from './constants/genomes';
16 | import AutomaticParkingAnalytics from './AutomaticParkingAnalytics';
17 | import World from '../world/World';
18 | import ParkingAutomatic from '../world/parkings/ParkingAutomatic';
19 | import { DynamicCarsPosition, DYNAMIC_CARS_POSITION_MIDDLE } from '../world/constants/cars';
20 | import { DYNAMIC_CARS_POSITION_FRONT } from '../world/constants/cars';
21 | import { getIntSearchParam, getStringSearchParam, setSearchParam } from '../../utils/url';
22 |
23 | const defaultGenomeIndex = 0;
24 |
25 | const GENOME_IDX_URL_PARAM = 'genome_idx';
26 | const START_POSITION_URL_PARAM = 'position';
27 | const DEFAULT_START_POSITION = DYNAMIC_CARS_POSITION_FRONT;
28 |
29 | function EvolutionTabAutomatic() {
30 | const {enqueue} = useSnackbar();
31 |
32 | const bestTrainedCarLossRef = useRef(null);
33 | const onTrainedCarLossUpdate = (licensePlate: CarLicencePlateType, loss: number) => {
34 | bestTrainedCarLossRef.current = loss;
35 | };
36 |
37 | const [performanceBoost, setPerformanceBoost] = useState(false);
38 |
39 | const [selectedGenomeIndex, setSelectedGenomeIndex] = useState(
40 | getIntSearchParam(GENOME_IDX_URL_PARAM, defaultGenomeIndex)
41 | );
42 |
43 | const [dynamicCarsPosition, setDynamicCarsPosition] = useState(getCarsPositionFromURL());
44 |
45 | const bestDefaultTrainedGeneration: Generation = [
46 | BEST_GENOMES[dynamicCarsPosition][defaultGenomeIndex],
47 | ];
48 |
49 | const [bestTrainedCarLoss, setBestTrainedCarLoss] = useState(null);
50 | const [bestTrainedCarCycleIndex, setBestTrainedCarCycleIndex] = useState(0);
51 | const [bestTrainedGeneration, setBestTrainedGeneration] = useState(bestDefaultTrainedGeneration);
52 | const [bestTrainedCars, setBestTrainedCars] = useState(
53 | Object.values(
54 | generationToCars({
55 | generation: bestDefaultTrainedGeneration,
56 | generationIndex: 0,
57 | onLossUpdate: onTrainedCarLossUpdate,
58 | })
59 | )
60 | );
61 |
62 | const automaticParkingLifetimeTimer = useRef(null);
63 |
64 | const logger = loggerBuilder({ context: 'AutomaticTab' });
65 |
66 | const automaticParkingCycleLifetimeMs = TRAINED_CAR_GENERATION_LIFETIME * SECOND;
67 | const automaticWorldVersion = `automatic-${bestTrainedCarCycleIndex}`;
68 |
69 | const onAutomaticCycleLifetimeEnd = () => {
70 | logger.info(`Automatic cycle #${bestTrainedCarCycleIndex} lifetime ended`);
71 | setBestTrainedCarLoss(bestTrainedCarLossRef.current);
72 | setBestTrainedCarCycleIndex(bestTrainedCarCycleIndex + 1);
73 | };
74 |
75 | const cancelAutomaticCycleTimer = () => {
76 | logger.info('Trying to cancel automatic parking cycle timer');
77 | if (automaticParkingLifetimeTimer.current === null) {
78 | return;
79 | }
80 | clearTimeout(automaticParkingLifetimeTimer.current);
81 | automaticParkingLifetimeTimer.current = null;
82 | };
83 |
84 | const countDownAutomaticParkingCycleLifetime = (onLifetimeEnd: () => void) => {
85 | logger.info(`Automatic parking cycle started`);
86 | cancelAutomaticCycleTimer();
87 | automaticParkingLifetimeTimer.current = setTimeout(onLifetimeEnd, automaticParkingCycleLifetimeMs);
88 | };
89 |
90 | const onPerformanceBoost = (state: boolean) => {
91 | setPerformanceBoost(state);
92 | };
93 |
94 | const onBestGenomeEdit = (editedGenome: Genome) => {
95 | logger.info('Updating genome', editedGenome);
96 |
97 | const updatedGeneration: Generation = [editedGenome];
98 |
99 | setBestTrainedGeneration(updatedGeneration);
100 |
101 | setBestTrainedCars(Object.values(
102 | generationToCars({
103 | generation: updatedGeneration,
104 | generationIndex: 0,
105 | onLossUpdate: onTrainedCarLossUpdate,
106 | })
107 | ));
108 |
109 | bestTrainedCarLossRef.current = null;
110 | setBestTrainedCarLoss(null);
111 | setBestTrainedCarCycleIndex(bestTrainedCarCycleIndex + 1);
112 |
113 | countDownAutomaticParkingCycleLifetime(onAutomaticCycleLifetimeEnd);
114 |
115 | enqueue({
116 | message: 'Genome has been updated and applied to the displayed car',
117 | startEnhancer: ({size}) => ,
118 | }, DURATION.medium);
119 | };
120 |
121 | const onChangeGenomeIndex = (index: number) => {
122 | setSelectedGenomeIndex(index);
123 | onBestGenomeEdit(BEST_GENOMES[dynamicCarsPosition][index]);
124 | setSearchParam(GENOME_IDX_URL_PARAM, `${index}`);
125 | };
126 |
127 | const onCarsPositionChange = (position: DynamicCarsPosition) => {
128 | setDynamicCarsPosition(position);
129 | setSelectedGenomeIndex(defaultGenomeIndex);
130 | onBestGenomeEdit(BEST_GENOMES[position][defaultGenomeIndex]);
131 | setSearchParam(START_POSITION_URL_PARAM, position);
132 | setSearchParam(GENOME_IDX_URL_PARAM, `${defaultGenomeIndex}`);
133 | };
134 |
135 | // Start the automatic parking cycles.
136 | useEffect(() => {
137 | countDownAutomaticParkingCycleLifetime(onAutomaticCycleLifetimeEnd);
138 | return () => {
139 | cancelAutomaticCycleTimer();
140 | };
141 | // eslint-disable-next-line react-hooks/exhaustive-deps
142 | }, [bestTrainedCarCycleIndex]);
143 |
144 | return (
145 |
146 |
150 |
157 |
158 |
159 |
160 | See the trained (almost) self-parking car in action
161 | You may also update genome values to see how it affects the car's behavior
162 |
163 |
164 |
179 |
180 | );
181 | }
182 |
183 | function getCarsPositionFromURL(): DynamicCarsPosition {
184 | // @ts-ignore
185 | const carPositionFromUrl: DynamicCarsPosition = getStringSearchParam(
186 | START_POSITION_URL_PARAM,
187 | DEFAULT_START_POSITION
188 | );
189 | if ([DYNAMIC_CARS_POSITION_FRONT, DYNAMIC_CARS_POSITION_MIDDLE].includes(carPositionFromUrl)) {
190 | return carPositionFromUrl;
191 | }
192 | return DEFAULT_START_POSITION;
193 | }
194 |
195 | export default EvolutionTabAutomatic;
196 |
--------------------------------------------------------------------------------
/src/components/evolution/EvolutionTabManual.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Block } from 'baseui/block';
3 | import { Notification } from 'baseui/notification';
4 |
5 | import World from '../world/World';
6 | import ParkingManual from '../world/parkings/ParkingManual';
7 |
8 | function EvolutionTabManual() {
9 | return (
10 |
11 |
16 |
21 |
22 |
23 |
24 | Try to park the car by yourself
25 | WASD keys to drive, SPACE to break, Joystick for mobile
26 |
27 |
28 |
29 | );
30 | }
31 |
32 | export default EvolutionTabManual;
33 |
--------------------------------------------------------------------------------
/src/components/evolution/EvolutionTabs.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Tab, Tabs } from 'baseui/tabs';
3 | import { Block } from 'baseui/block';
4 |
5 | import { StyleObject } from 'styletron-standard';
6 | import ErrorBoundary from '../shared/ErrorBoundary';
7 | import { getSearchParam, setSearchParam } from '../../utils/url';
8 | import EvolutionTabManual from './EvolutionTabManual';
9 | import EvolutionTabEvolution from './EvolutionTabEvolution';
10 | import EvolutionTabAutomatic from './EvolutionTabAutomatic';
11 | import { BiDna } from 'react-icons/bi';
12 | import { FaRegHandSpock } from 'react-icons/fa';
13 | import { RiGuideLine } from 'react-icons/ri';
14 |
15 | const WORLD_SEARCH_PARAM = 'parking';
16 |
17 | const TAB_KEYS: Record = {
18 | evolution: 'evolution',
19 | automatic: 'automatic',
20 | manual: 'manual',
21 | };
22 |
23 | const tabBarStyle: StyleObject = {
24 | paddingLeft: 0,
25 | paddingRight: 0,
26 | overflow: 'hidden',
27 | };
28 |
29 | const tabContentStyle: StyleObject = {
30 | paddingLeft: 0,
31 | paddingRight: 0,
32 | paddingTop: 0,
33 | paddingBottom: 0,
34 | };
35 |
36 | const tabStyle: StyleObject = {
37 | marginLeft: 0,
38 | marginRight: 0,
39 | paddingLeft: '20px',
40 | paddingRight: '20px',
41 | };
42 |
43 | function EvolutionTabs() {
44 | let worldKey: string = getSearchParam(WORLD_SEARCH_PARAM) || TAB_KEYS.evolution;
45 | if (!TAB_KEYS.hasOwnProperty(worldKey)) {
46 | worldKey = TAB_KEYS.evolution;
47 | }
48 |
49 | const [activeWorldKey, setActiveWorldKey] = useState(worldKey);
50 |
51 | const onTabSwitch = ({ activeKey }: {activeKey: React.Key}) => {
52 | setActiveWorldKey(activeKey);
53 | setSearchParam(WORLD_SEARCH_PARAM, `${activeKey}`);
54 | }
55 |
56 | return (
57 |
66 | }
71 | title="Parking Evolution"
72 | />
73 | )}
74 | >
75 |
76 |
77 |
78 |
79 |
80 | }
85 | title="Automatic Parking"
86 | />
87 | )}
88 | >
89 |
90 |
91 |
92 |
93 |
94 | }
99 | title="Manual Parking"
100 | />
101 | )}
102 | >
103 |
104 |
105 |
106 |
107 |
108 | );
109 | }
110 |
111 | type TabTitleProps = {
112 | icon: React.ReactNode,
113 | title: string,
114 | };
115 |
116 | const TabTitle = (props: TabTitleProps) => {
117 | const {icon, title} = props;
118 | return (
119 |
124 |
130 | {icon}
131 |
132 |
133 | {title}
134 |
135 |
136 | );
137 | };
138 |
139 | export default EvolutionTabs;
140 |
--------------------------------------------------------------------------------
/src/components/evolution/EvolutionTiming.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Label3 } from 'baseui/typography';
3 | import {Tag, VARIANT as TAG_VARIANT} from 'baseui/tag';
4 | import { Block } from 'baseui/block';
5 | import {Notification, KIND as NOTIFICATION_KIND} from 'baseui/notification';
6 | import { VscDebugRestart } from 'react-icons/all';
7 |
8 | import Timer from '../shared/Timer';
9 |
10 | type EvolutionTimingProps = {
11 | generationIndex?: number | null,
12 | totalBatches?: number | null,
13 | batchIndex?: number | null,
14 | generationLifetimeMs?: number,
15 | batchVersion?: string,
16 | worldVersion?: string,
17 | retry?: boolean,
18 | groupLabel?: string,
19 | batchLifetimeLabel?: string,
20 | };
21 |
22 | function EvolutionTiming(props: EvolutionTimingProps) {
23 | const {
24 | generationIndex,
25 | batchIndex,
26 | totalBatches,
27 | generationLifetimeMs,
28 | batchVersion,
29 | worldVersion,
30 | retry = false,
31 | groupLabel = 'Group',
32 | batchLifetimeLabel = 'Group lifetime',
33 | } = props;
34 |
35 | const batchesCounter = retry ? (
36 |
37 |
38 |
39 | ) : (
40 | <>
41 | #{(batchIndex || 0) + 1}
42 | {totalBatches && ( / {totalBatches})}
43 | >
44 | );
45 |
46 | const generationInfo = generationIndex !== undefined ? (
47 |
48 |
49 | #{(generationIndex || 0) + 1}
50 |
51 |
52 | ) : null;
53 |
54 | const groupInfo = batchIndex !== undefined ? (
55 |
56 |
57 | {batchesCounter}
58 |
59 |
60 | ) : null;
61 |
62 | const groupLifetime = generationLifetimeMs !== undefined && batchVersion !== undefined ? (
63 |
64 |
65 |
66 |
67 |
68 | ) : null;
69 |
70 | const worldAge = worldVersion !== undefined ? (
71 |
72 |
73 |
74 |
75 |
76 | ) : null;
77 |
78 | return (
79 |
87 |
95 | {generationInfo}
96 | {groupInfo}
97 | {groupLifetime}
98 | {worldAge}
99 |
100 |
101 | );
102 | }
103 |
104 | type TimingColumnProps = {
105 | caption: string,
106 | children: React.ReactNode,
107 | };
108 |
109 | function TimingColumn(props: TimingColumnProps) {
110 | const {caption, children} = props;
111 | return (
112 |
119 |
125 | {caption}:
126 |
127 |
128 | {children}
129 |
130 |
131 | );
132 | }
133 |
134 | export default EvolutionTiming;
135 |
--------------------------------------------------------------------------------
/src/components/evolution/GenomePreview.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Block } from 'baseui/block';
3 | import { CSSProperties, FormEvent, useState } from 'react';
4 | import { Textarea } from 'baseui/textarea';
5 | import { Button, SHAPE as BUTTON_SHAPE, KIND as BUTTON_KIND, SIZE as BUTTON_SIZE } from 'baseui/button';
6 | import { BiEdit, FaRegSave } from 'react-icons/all';
7 |
8 | import { Gene, Genome } from '../../libs/genetic';
9 | import { CarLicencePlateType } from '../world/types/car';
10 | import { FormControl } from 'baseui/form-control';
11 | import { formatLossValue } from './utils/evolution';
12 | import { CAR_SENSORS_NUM, carLossToFitness, decodeGenome, FormulaCoefficients } from '../../libs/carGenetic';
13 | import { FITNESS_ALPHA } from './constants/evolution';
14 |
15 | type GenomePreviewProps = {
16 | genome: Genome | null,
17 | title?: string,
18 | licencePlate?: CarLicencePlateType | null,
19 | loss?: number | null,
20 | editable?: boolean,
21 | onGenomeEdit?: (genome: Genome) => void,
22 | };
23 |
24 | const genomeSeparator = ' ';
25 |
26 | const commonGenomeStyles: CSSProperties = {
27 | paddingTop: '15px',
28 | paddingRight: '15px',
29 | paddingBottom: '15px',
30 | paddingLeft: '15px',
31 | fontSize: '12px',
32 | backgroundColor: '#FFFFFF',
33 | lineHeight: '20px',
34 | fontFamily: 'monospace',
35 | };
36 |
37 | function GenomePreview(props: GenomePreviewProps) {
38 | const {
39 | title,
40 | genome,
41 | licencePlate,
42 | loss,
43 | editable = false,
44 | onGenomeEdit = (genome: Genome) => {},
45 | } = props;
46 |
47 | const [shortEngineFormula] = useState(true);
48 | const [shortWheelsFormula] = useState(true);
49 |
50 | const [isEditableGenome, setIsEditableGenome] = useState(false);
51 | const [editedGenome, setEditedGenome] = useState(genome);
52 | const [genomeError, setGenomeError] = useState(null);
53 |
54 | const onGenomeUpdate = (genomeString: string) => {
55 | if (!genome) {
56 | return;
57 | }
58 |
59 | const genomeFromString: Genome = genomeString
60 | .trim()
61 | .split('')
62 | .filter((geneString: string) => ['0', '1'].includes(geneString))
63 | .map((geneString: string) => {
64 | const gene: Gene = geneString === '0' ? 0 : 1;
65 | return gene;
66 | });
67 | setEditedGenome(genomeFromString);
68 |
69 | if (genomeFromString.length !== genome.length) {
70 | setGenomeError(`Genome must have ${genome.length} genes. Currently it has ${genomeFromString.length} genes.`);
71 | } else {
72 | setGenomeError(null);
73 | }
74 | };
75 |
76 | const onEditToggle = () => {
77 | if (editedGenome && isEditableGenome && !genomeError) {
78 | onGenomeEdit(editedGenome)
79 | }
80 | setIsEditableGenome(!isEditableGenome);
81 | };
82 |
83 | const genomeCaption = (
84 |
85 | {genome && (
86 |
87 | Genes: {genome.length}
88 |
89 | )}
90 | {licencePlate && (
91 |
92 | Licence plate: {licencePlate}
93 |
94 | )}
95 | {loss && (
96 |
97 | Loss: {formatLossValue(loss)}
98 |
99 | )}
100 | {loss && (
101 |
102 | Fitness: {formatLossValue(carLossToFitness(loss, FITNESS_ALPHA))}
103 |
104 | )}
105 |
106 | );
107 |
108 | const label = title || 'Car genome';
109 |
110 | const genomeEditButtonIcon = !isEditableGenome ? (
111 |
112 | ) : null;
113 |
114 | const genomeSaveButtonIcon = isEditableGenome ? (
115 |
116 | ) : null;
117 |
118 | const genomeEditButtons = editable ? (
119 |
126 |
153 |
154 | ) : null;
155 |
156 | const genomePreviewLabel = (
157 |
158 | {label}
159 |
160 | );
161 |
162 | const genomeString = (genome || []).join(genomeSeparator);
163 | const genomePreviewOutput = (
164 |
168 |
169 | {genomeString}
170 |
171 |
172 | );
173 |
174 | const editedGenomeString = (editedGenome || []).join(genomeSeparator);
175 | const genomeEditableOutput = (
176 | genomePreviewLabel}
178 | caption={(
179 |
180 | Copy/paste whole genome or double-click the specific gene and change it
181 |
182 | )}
183 | error={genomeError}
184 | >
185 |
203 | );
204 |
205 | const genomeOutput = isEditableGenome ? genomeEditableOutput : genomePreviewOutput;
206 |
207 | let decodedEngineFormula = null;
208 | let decodedWheelsFormula = null;
209 | if (genome) {
210 | const { engineFormulaCoefficients, wheelsFormulaCoefficients } = decodeGenome(genome);
211 | decodedEngineFormula = (
212 |
220 | );
221 | decodedWheelsFormula = (
222 |
230 | );
231 | }
232 |
233 | const blocksMarginBottom = '30px';
234 |
235 | return (
236 |
237 |
238 | {genomeEditButtons}
239 | {genomeOutput}
240 |
241 |
242 |
243 | {decodedEngineFormula}
244 |
245 |
246 |
247 | {decodedWheelsFormula}
248 |
249 |
250 | );
251 | }
252 |
253 | type CoefficientsProps = {
254 | label: string,
255 | caption: string,
256 | coefficients: FormulaCoefficients,
257 | shortNumbers: boolean,
258 | };
259 |
260 | function Coefficients(props: CoefficientsProps) {
261 | const {coefficients, label, caption, shortNumbers} = props;
262 | const coefficientsString = coefficients
263 | .map((coefficient: number) => formatCoefficient(coefficient, shortNumbers))
264 | .join(', ');
265 | return (
266 |
267 | label}
269 | caption={() => caption}
270 | >
271 |
272 | {coefficientsString}
273 |
274 |
275 |
276 | );
277 | }
278 |
279 | type CodeBlockProps = {
280 | children: React.ReactNode,
281 | };
282 |
283 | function CodeBlock(props: CodeBlockProps) {
284 | const {children} = props;
285 | return (
286 | <>
287 |
292 |
293 | {children}
294 |
295 |
296 | >
297 | );
298 | }
299 |
300 | function formatCoefficient(coefficient: number, shortNumber: boolean = true): number {
301 | if (shortNumber) {
302 | return Math.ceil(coefficient * 1000) / 1000;
303 | }
304 | return coefficient;
305 | }
306 |
307 | export default GenomePreview;
308 |
--------------------------------------------------------------------------------
/src/components/evolution/LossHistory.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useState } from 'react';
3 | import { Block } from 'baseui/block';
4 | import { Datum, Point, ResponsiveLine, Serie } from '@nivo/line';
5 | import { Checkbox } from 'baseui/checkbox';
6 |
7 | import { formatLossValue } from './utils/evolution';
8 |
9 | type LossHistoryProps = {
10 | history: number[],
11 | avgHistory: number[],
12 | };
13 |
14 | // @see: Nivo docs: https://nivo.rocks/line
15 | function LossHistory(props: LossHistoryProps) {
16 | const {history, avgHistory} = props;
17 |
18 | const [showAvgHistory, setShowAvgHistory] = useState(true);
19 | const [showMinLoss, setShowMinLoss] = useState(true);
20 |
21 | const emptyStateHistoryData: [number] = [0];
22 | const historyData: Datum[] = (history.length ? history : emptyStateHistoryData).map(
23 | (loss: number, generationIndex: number): Datum => {
24 | const miss = loss === Infinity ? null : formatLossValue(loss);
25 | return {
26 | x: generationIndex,
27 | y: miss,
28 | };
29 | }
30 | );
31 |
32 | const emptyStateAvgHistoryData: [number] = [0];
33 | const avgHistoryData: Datum[] = (avgHistory.length ? avgHistory : emptyStateAvgHistoryData).map(
34 | (loss: number, generationIndex: number): Datum => {
35 | const miss = loss === Infinity ? null : formatLossValue(loss);
36 | return {
37 | x: generationIndex,
38 | y: miss,
39 | };
40 | }
41 | );
42 |
43 | const chartData: Serie[] = [];
44 |
45 | const minLossSeriesId = 'Min Loss';
46 | const avgLossSeriesId = 'P50 Avg Loss';
47 |
48 | if (showMinLoss) {
49 | chartData.push({
50 | id: minLossSeriesId,
51 | data: historyData,
52 | color: 'black',
53 | });
54 | }
55 |
56 | if (showAvgHistory) {
57 | chartData.push({
58 | id: avgLossSeriesId,
59 | data: avgHistoryData,
60 | color: '#AAAAAA',
61 | });
62 | }
63 |
64 | const chart = (
65 | {
87 | return datum.color || 'black';
88 | }}
89 | pointBorderWidth={1}
90 | pointBorderColor={'white'}
91 | useMesh={true}
92 | enableCrosshair={true}
93 | enableSlices={false}
94 | colors={(datum: Datum) => {
95 | return datum.color || 'black';
96 | }}
97 | tooltip={({point}: {point: Point}) => {
98 | const {data, serieId} = point;
99 | return (
100 |
106 |
107 |
108 | Generation: {data.xFormatted}
109 |
110 |
111 |
112 |
113 | {serieId}: {data.yFormatted}
114 |
115 |
116 |
117 | );
118 | }}
119 | legends={[
120 | {
121 | anchor: 'top-right',
122 | direction: 'column',
123 | justify: false,
124 | translateX: -10,
125 | translateY: 0,
126 | itemsSpacing: 0,
127 | itemDirection: 'left-to-right',
128 | itemWidth: 80,
129 | itemHeight: 20,
130 | itemOpacity: 1,
131 | symbolSize: 8,
132 | symbolShape: 'circle',
133 | symbolBorderColor: 'rgba(0, 0, 0, .5)',
134 | }
135 | ]}
136 | />
137 | );
138 |
139 | const chartControls = (
140 |
141 |
142 | setShowMinLoss(!showMinLoss)}
146 | >
147 | {minLossSeriesId}
148 |
149 |
150 |
151 |
152 | setShowAvgHistory(!showAvgHistory)}
156 | >
157 | {avgLossSeriesId}
158 |
159 |
160 |
161 | );
162 |
163 | return (
164 |
165 |
172 | {chart}
173 |
174 |
175 | {chartControls}
176 |
177 |
178 | );
179 | }
180 |
181 | export default LossHistory;
182 |
--------------------------------------------------------------------------------
/src/components/evolution/PopulationTable.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Block } from 'baseui/block';
3 | import { Table, DIVIDER, SIZE as TABLE_SIZE } from 'baseui/table-semantic';
4 | import { Tag, VARIANT as TAG_VARIANT, KIND as TAG_KIND } from 'baseui/tag';
5 | import { StyledSpinnerNext } from 'baseui/spinner';
6 | import { withStyle } from 'baseui';
7 |
8 | import { CarLicencePlateType, CarsType, CarType } from '../world/types/car';
9 | import { formatLossValue } from './utils/evolution';
10 | import FadeIn from '../shared/FadeIn';
11 | import { FITNESS_ALPHA, LOSS_VALUE_BAD_THRESHOLD, LOSS_VALUE_GOOD_THRESHOLD } from './constants/evolution';
12 | import { carLossToFitness } from '../../libs/carGenetic';
13 |
14 | export type CarsInProgressType = Record;
15 | export type CarsLossType = Record;
16 |
17 | type PopulationTableProps = {
18 | cars: CarsType,
19 | carsInProgress: CarsInProgressType,
20 | carsLoss: CarsLossType,
21 | };
22 |
23 | const sortTable = true;
24 |
25 | const CellSpinner = withStyle(StyledSpinnerNext, {
26 | width: '18px',
27 | height: '18px',
28 | borderLeftWidth: '3px',
29 | borderRightWidth: '3px',
30 | borderTopWidth: '3px',
31 | borderBottomWidth: '3px',
32 | borderTopColor: 'black',
33 | });
34 |
35 | function PopulationTable(props: PopulationTableProps) {
36 | const { cars, carsInProgress, carsLoss } = props;
37 | const carsArray: CarType[] = Object.values(cars);
38 |
39 | const columns = [
40 | 'Licence Plate',
41 | 'Loss',
42 | 'Fitness',
43 | ];
44 |
45 | const rowsData: React.ReactNode[][] = carsArray
46 | .sort((carA: CarType, carB: CarType): number => {
47 | if (!sortTable) {
48 | return 0;
49 | }
50 | const lossA = getCarLoss(carsLoss, carA);
51 | const lossB = getCarLoss(carsLoss, carB);
52 | if (lossA === null && lossB !== null) {
53 | return 1;
54 | }
55 | if (lossA !== null && lossB === null) {
56 | return -1;
57 | }
58 | if (lossA === null || lossB === null) {
59 | return 0;
60 | }
61 | if (lossA === lossB) {
62 | return 0;
63 | }
64 | if (lossA <= lossB) {
65 | return -1;
66 | }
67 | return 1;
68 | })
69 | .map((car: CarType) => {
70 | const licencePlateCell = (
71 |
76 | {car.licencePlate}
77 |
78 | );
79 |
80 | const carLossFormatted: number | null = getCarLoss(carsLoss, car);
81 | let carLossColor = '';
82 | if (carLossFormatted !== null) {
83 | if (carLossFormatted < LOSS_VALUE_GOOD_THRESHOLD) {
84 | carLossColor = 'limegreen';
85 | } else if (carLossFormatted < LOSS_VALUE_BAD_THRESHOLD) {
86 | carLossColor = 'orange';
87 | } else {
88 | carLossColor = 'red';
89 | }
90 | }
91 | const lossCell = carsInProgress[car.licencePlate] ? (
92 |
93 |
94 |
95 | ) : (
96 |
97 | {carLossFormatted}
98 |
99 | );
100 |
101 | const fitnessValue: number | null = getCarFitness(carsLoss, car);
102 | const fitnessCell = carsInProgress[car.licencePlate] ? (
103 |
104 |
105 |
106 | ) : (
107 |
108 | {fitnessValue}
109 |
110 | );
111 |
112 | return [
113 | licencePlateCell,
114 | lossCell,
115 | fitnessCell,
116 | ];
117 | });
118 |
119 | return (
120 |
121 |
140 |
141 | );
142 | }
143 |
144 | function getCarLoss(carsLoss: CarsLossType, car: CarType): number | null {
145 | return carsLoss.hasOwnProperty(car.licencePlate) && typeof carsLoss[car.licencePlate] === 'number'
146 | ? formatLossValue(carsLoss[car.licencePlate])
147 | : null;
148 | }
149 |
150 | function getCarFitness(carsLoss: CarsLossType, car: CarType): number | null {
151 | return carsLoss.hasOwnProperty(car.licencePlate) && typeof carsLoss[car.licencePlate] === 'number'
152 | ? formatLossValue(carLossToFitness(carsLoss[car.licencePlate] || 0, FITNESS_ALPHA))
153 | : null;
154 | }
155 |
156 | export default PopulationTable;
157 |
--------------------------------------------------------------------------------
/src/components/evolution/constants/evolution.ts:
--------------------------------------------------------------------------------
1 | export const LOSS_VALUE_GOOD_THRESHOLD = 1;
2 | export const LOSS_VALUE_BAD_THRESHOLD = 2;
3 |
4 | export const FITNESS_ALPHA = 0.5;
5 |
6 | export const BAD_SIMULATION_RETRIES_ENABLED = true;
7 | export const BAD_SIMULATION_BATCH_INDEX_CHECK = 1;
8 | export const BAD_SIMULATION_RETRIES_NUM = 1;
9 | export const BAD_SIMULATION_MIN_LOSS_INCREASE_PERCENTAGE = 110;
10 |
--------------------------------------------------------------------------------
/src/components/evolution/constants/genomes.ts:
--------------------------------------------------------------------------------
1 | import { Genome } from '../../../libs/genetic';
2 | import { DynamicCarsPosition } from '../../world/constants/cars';
3 | import { genomeStringToGenome } from '../utils/evolution';
4 |
5 | export const BEST_GENOMES: Record = {
6 | 'rear': [].map((genomeString: string) => genomeStringToGenome(genomeString)),
7 | 'middle': [
8 | '0 1 0 1 1 1 0 1 1 0 1 1 0 1 0 0 0 1 1 1 1 1 0 1 0 1 1 0 0 0 1 1 0 1 0 0 1 1 1 1 0 1 0 1 0 1 0 0 0 1 0 0 1 1 0 0 0 1 0 1 0 1 1 0 0 1 0 0 0 1 0 0 1 1 1 1 0 1 1 1 0 1 0 0 1 1 1 1 1 1 1 0 1 1 0 0 1 1 1 1 1 1 0 0 0 0 1 1 0 0 0 0 0 0 0 1 0 1 0 1 0 1 1 0 0 1 0 0 0 1 1 1 0 0 1 0 0 0 1 1 1 1 1 0 1 0 0 0 0 1 0 0 0 0 0 0 1 1 0 0 1 0 0 0 0 1 1 0 0 1 1 0 0 0 0 0 1 0 0 1',
9 | '0 0 1 0 1 0 1 1 0 0 1 1 1 1 0 1 1 1 0 0 1 1 0 1 0 0 0 1 1 0 0 1 0 1 1 1 0 1 1 0 1 0 1 0 0 0 0 1 1 0 1 0 0 0 1 1 1 0 0 0 0 1 1 0 0 1 0 0 1 1 1 0 0 1 0 0 0 1 1 1 0 0 1 0 0 1 1 1 0 1 0 1 1 0 1 0 0 1 1 0 1 1 0 0 0 0 1 1 1 0 0 1 0 1 0 1 0 1 1 1 1 1 0 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 0 1 1 0 1 0 0 0 0 0 1 1 0 1 1 0 0 1 0 1 1 1 0 1 0 0 1 1 1 1 1 1 1 1 1 0 1 1 1 0 0 1',
10 | ].map((genomeString: string) => genomeStringToGenome(genomeString)),
11 | 'front': [
12 | '0 1 0 1 1 0 0 0 1 1 0 0 0 1 0 1 1 1 0 0 1 1 1 0 0 0 1 1 1 0 0 1 0 1 1 1 0 0 1 0 0 1 1 1 0 0 0 1 1 1 1 0 0 1 1 0 1 1 0 0 1 0 1 0 0 1 0 1 0 1 1 0 1 1 0 0 0 0 1 1 0 0 0 1 1 0 0 1 0 0 1 1 1 0 0 1 1 0 1 0 0 1 1 0 0 0 1 0 0 1 0 0 0 0 0 0 1 0 1 0 0 1 1 1 0 1 1 1 1 1 1 1 0 0 0 1 1 0 0 0 1 0 0 0 1 0 0 1 0 0 0 0 1 1 1 1 0 0 0 1 1 1 1 1 1 0 1 1 0 1 0 0 1 0 0 0 1 0 0 0',
13 | '0 0 0 1 1 1 1 1 1 0 1 1 1 0 0 1 1 1 1 1 1 1 1 0 1 0 1 0 0 0 1 1 0 0 1 1 0 1 0 1 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 0 0 1 0 1 1 1 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 1 0 1 0 1 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 1 0 1 1 0 0 0 1 0 1 1 1 1 0 1 1 1 0 1 0 1 0 1 1 0 0 1 1 1 0 1 1 0 0 1 0 1 0 1 1 0 1 1 1 0 1 1 0 1 0 1 1 1 0 1 0 0 1 1 0 1',
14 | '0 1 0 1 1 1 1 1 0 1 1 0 0 1 0 1 0 0 0 1 1 0 1 0 1 1 0 1 0 0 1 1 0 1 0 0 0 1 0 1 0 0 0 1 1 1 0 0 0 0 1 0 0 1 0 1 0 0 1 0 1 0 1 1 0 1 1 1 0 1 1 0 0 0 1 1 1 1 0 1 0 0 1 0 0 0 0 0 1 0 0 1 0 1 0 1 0 0 1 1 1 0 1 0 0 1 1 1 0 0 1 0 1 1 1 1 0 0 0 0 0 1 1 0 0 1 0 1 1 1 1 1 0 0 1 0 0 0 1 1 0 0 1 1 0 1 1 0 1 1 0 0 0 0 0 1 1 1 1 0 1 0 1 1 1 1 0 1 1 0 1 0 1 0 0 0 1 0 1 0',
15 | '1 1 0 0 1 1 0 1 1 1 0 0 0 1 0 0 1 0 1 0 1 0 1 0 0 0 1 0 0 0 1 1 1 1 0 0 1 0 1 1 0 1 1 1 1 0 0 1 0 0 1 1 1 0 1 0 0 1 1 0 0 0 1 1 0 0 0 0 1 0 1 1 0 0 1 0 0 0 1 0 0 0 0 0 1 0 1 1 0 1 1 0 1 0 0 1 0 1 1 1 0 0 0 1 0 1 0 1 0 0 1 0 0 0 0 1 1 1 1 0 0 1 1 1 0 1 1 0 1 1 0 1 1 0 0 0 0 1 1 0 0 1 0 0 0 1 0 0 1 1 1 0 0 1 1 0 1 0 0 0 1 0 1 0 0 1 1 0 0 1 0 0 0 0 1 1 0 0 1 0',
16 | // '0 1 1 1 0 1 0 1 1 1 1 0 0 0 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 0 1 0 0 1 0 1 1 1 0 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 1 1 0 0 0 1 1 1 0 1 0 1 0 1 0 1 0 0 1 0 1 1 1 0 0 0 0 1 1 0 1 0 1 0 0 0 0 0 0 0 1 1 1 1 0 0 1 0 1 0 1 0 1 1 0 1 0 1 0 0 1 0 0 0 1 0 0 1 1 0 1 0 0 1 1 0 1 1 1 0 0 1 0 1 1 0 0 0 0 0 1 1 0 0 0 0 1 0 1 1 1 0 1 0 0 1 1 1 1 1 1 1 1 0 0 1 0 0 1 1 1 1 0',
17 | // '1 0 0 0 1 0 1 1 1 1 0 0 1 1 1 0 1 1 0 0 1 0 0 0 0 1 1 1 0 0 0 1 0 1 0 0 1 0 0 1 0 1 0 0 1 1 1 1 0 0 1 1 1 0 1 1 0 0 0 1 0 0 0 0 0 1 1 0 0 1 1 1 1 1 1 0 1 0 1 1 0 0 0 0 1 1 1 1 1 1 0 1 1 0 0 1 0 1 1 1 0 0 1 0 0 0 1 1 1 0 1 0 1 1 1 0 0 0 0 1 0 1 0 1 1 0 1 0 0 0 0 1 1 1 0 0 1 0 1 1 0 0 1 1 0 1 0 0 1 0 1 0 1 1 0 1 0 0 0 1 0 0 1 0 0 0 0 1 0 0 1 0 0 0 1 0 0 1 0 0',
18 | // '1 1 1 0 0 0 1 0 0 0 1 0 0 0 0 0 1 0 0 0 1 1 1 1 0 0 1 1 0 1 1 0 0 1 0 0 0 0 1 1 0 0 0 0 1 0 0 0 1 1 0 0 1 1 0 0 0 0 0 1 1 1 0 0 0 1 1 1 1 1 1 0 0 0 1 0 0 1 0 0 0 0 1 1 1 0 1 1 0 1 1 1 0 1 1 1 0 0 1 1 0 1 0 0 0 1 1 1 0 0 0 0 0 0 0 0 1 0 0 1 0 1 0 1 0 1 1 1 1 0 1 1 1 0 1 0 0 1 0 1 1 0 0 1 1 1 1 1 1 1 1 0 0 1 0 0 1 0 1 0 1 1 0 0 0 1 1 1 1 1 0 0 0 1 1 0 1 1 0 1',
19 | ].map((genomeString: string) => genomeStringToGenome(genomeString)),
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/evolution/utils/evolution.ts:
--------------------------------------------------------------------------------
1 | import { Gene, Generation, Genome } from '../../../libs/genetic';
2 | import {
3 | CarLicencePlateType,
4 | CarsType,
5 | EngineOptionsType,
6 | SensorValuesType, SensorValueType,
7 | WheelOptionsType
8 | } from '../../world/types/car';
9 | import { RectanglePoints } from '../../../types/vectors';
10 | import { CAR_SENSORS_NUM, engineFormula, carLoss, wheelsFormula } from '../../../libs/carGenetic';
11 | import { SENSOR_DISTANCE_FALLBACK } from '../../world/car/constants';
12 | import { read, remove, write } from '../../../utils/storage';
13 | import { PARKING_SPOT_POINTS } from '../../world/constants/parking';
14 |
15 | const GENERATION_STORAGE_KEY = 'generation';
16 | const GENERATION_INDEX_STORAGE_KEY = 'generation-index';
17 | const LOSS_HISTORY_INDEX_STORAGE_KEY = 'loss-history';
18 | const AVG_LOSS_HISTORY_INDEX_STORAGE_KEY = 'avg-loss-history';
19 |
20 | const generateLicencePlate = (
21 | generationIndex: number | null,
22 | genomeIndex: number
23 | ): CarLicencePlateType => {
24 | const generationIdx = generationIndex !== null ? (generationIndex + 1) : '';
25 | const genomeIdx = genomeIndex + 1;
26 | return `CAR-${generationIdx}-${genomeIdx}`;
27 | };
28 |
29 | type GenerationToCarsProps = {
30 | generationIndex: number | null,
31 | generation: Generation,
32 | onLossUpdate?: (licencePlate: CarLicencePlateType, loss: number) => void,
33 | };
34 |
35 | export const generationToCars = (props: GenerationToCarsProps): CarsType => {
36 | const {
37 | generationIndex,
38 | generation,
39 | onLossUpdate = () => {},
40 | } = props;
41 | const cars: CarsType = {};
42 | generation.forEach((genome: Genome, genomeIndex) => {
43 | const licencePlate = generateLicencePlate(generationIndex, genomeIndex);
44 |
45 | const onEngine = (sensors: SensorValuesType): EngineOptionsType => {
46 | const formulaOutput = engineFormula(genome, cleanUpSensors(sensors));
47 | if (formulaOutput === -1) {
48 | return 'backwards';
49 | }
50 | if (formulaOutput === 1) {
51 | return 'forward'
52 | }
53 | return 'neutral';
54 | };
55 |
56 | const onWheel = (sensors: SensorValuesType): WheelOptionsType => {
57 | const formulaOutput = wheelsFormula(genome, cleanUpSensors(sensors));
58 | if (formulaOutput === -1) {
59 | return 'left';
60 | }
61 | if (formulaOutput === 1) {
62 | return 'right'
63 | }
64 | return 'straight';
65 | };
66 |
67 | const onMove = (wheelsPoints: RectanglePoints) => {
68 | const loss = carLoss({
69 | wheelsPosition: wheelsPoints,
70 | parkingLotCorners: PARKING_SPOT_POINTS,
71 | });
72 | onLossUpdate(licencePlate, loss);
73 | };
74 |
75 | cars[licencePlate] = {
76 | licencePlate,
77 | generationIndex: generationIndex !== null ? generationIndex : -1,
78 | sensorsNum: CAR_SENSORS_NUM,
79 | genomeIndex,
80 | onEngine,
81 | onWheel,
82 | onMove,
83 | onHit: () => {},
84 | };
85 | });
86 | return cars;
87 | };
88 |
89 | const cleanUpSensors = (sensors: SensorValuesType): number[] => {
90 | return sensors.map((sensor: SensorValueType) => {
91 | if (sensor === null || sensor === undefined) {
92 | return SENSOR_DISTANCE_FALLBACK;
93 | }
94 | return sensor;
95 | });
96 | };
97 |
98 | export const formatLossValue = (lossValue: number | null | undefined): number | null => {
99 | if (typeof lossValue !== 'number') {
100 | return null;
101 | }
102 | return Math.ceil(lossValue * 100) / 100;
103 | };
104 |
105 | export const generateWorldVersion = (
106 | generationIndex: number | null,
107 | batchIndex: number | null
108 | ): string => {
109 | const generation = generationIndex === null ? -1 : generationIndex;
110 | const batch = batchIndex === null ? -1: batchIndex;
111 | return `world-${generation}-${batch}`;
112 | };
113 |
114 | export const genomeStringToGenome = (genomeString: string): Genome => {
115 | return genomeString
116 | .split(' ')
117 | .map((geneString: string) => {
118 | const gene: Gene = parseInt(geneString, 10) === 1 ? 1 : 0;
119 | return gene;
120 | });
121 | };
122 |
123 | type GenerationDataInStorage = {
124 | generation: Generation | null,
125 | generationIndex: number | null,
126 | lossHistory: number[] | null,
127 | avgLossHistory: number[] | null,
128 | };
129 |
130 | export const loadGenerationFromStorage = (): GenerationDataInStorage => {
131 | const generationIndex: number | null = read(GENERATION_INDEX_STORAGE_KEY)
132 | const generation: Generation | null = read(GENERATION_STORAGE_KEY);
133 | const lossHistory: number[] | null = read(LOSS_HISTORY_INDEX_STORAGE_KEY);
134 | const avgLossHistory: number[] | null = read(AVG_LOSS_HISTORY_INDEX_STORAGE_KEY);
135 | return {
136 | generation,
137 | generationIndex,
138 | lossHistory,
139 | avgLossHistory,
140 | };
141 | };
142 |
143 | export const saveGenerationToStorage = (data: GenerationDataInStorage): boolean => {
144 | const {
145 | generation,
146 | generationIndex,
147 | lossHistory,
148 | avgLossHistory,
149 | } = data;
150 |
151 | if (!generation || !generation.length || !generationIndex || !lossHistory || !avgLossHistory) {
152 | return false;
153 | }
154 |
155 | const keySuccess = write(GENERATION_INDEX_STORAGE_KEY, generationIndex);
156 | const generationSuccess = write(GENERATION_STORAGE_KEY, generation);
157 | const lossHistorySuccess = write(LOSS_HISTORY_INDEX_STORAGE_KEY, lossHistory);
158 | const avgLossHistorySuccess = write(AVG_LOSS_HISTORY_INDEX_STORAGE_KEY, avgLossHistory);
159 |
160 | return keySuccess && generationSuccess && lossHistorySuccess && avgLossHistorySuccess;
161 | };
162 |
163 | export const removeGenerationFromStorage = (): void => {
164 | remove(GENERATION_STORAGE_KEY);
165 | remove(GENERATION_INDEX_STORAGE_KEY);
166 | remove(LOSS_HISTORY_INDEX_STORAGE_KEY);
167 | remove(AVG_LOSS_HISTORY_INDEX_STORAGE_KEY);
168 | };
169 |
--------------------------------------------------------------------------------
/src/components/screens/HomeScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import EvolutionTabs from '../evolution/EvolutionTabs';
4 |
5 | function HomeScreen() {
6 | return (
7 |
8 | );
9 | }
10 |
11 | export default HomeScreen;
12 |
--------------------------------------------------------------------------------
/src/components/shared/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import React, { ErrorInfo } from 'react';
2 | import { Notification, KIND } from 'baseui/notification';
3 |
4 | type Props = {
5 | children: React.ReactNode,
6 | };
7 |
8 | type State = {
9 | hasError: boolean,
10 | };
11 |
12 | class ErrorBoundary extends React.Component {
13 | constructor(props: Props) {
14 | super(props);
15 | this.state = {
16 | hasError: false,
17 | };
18 | }
19 |
20 | static getDerivedStateFromError(): State {
21 | // Update state so the next render will show the fallback UI.
22 | return {
23 | hasError: true,
24 | };
25 | }
26 |
27 | componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
28 | // You can also log the error to an error reporting service.
29 | // eslint-disable-next-line no-console
30 | console.error(error, errorInfo);
31 | this.setState({ hasError: true });
32 | }
33 |
34 | render(): React.ReactNode {
35 | const { children } = this.props;
36 | const { hasError } = this.state;
37 |
38 | if (hasError) {
39 | // You can render any custom fallback UI
40 | return (
41 |
47 | Component has crashed
48 |
49 | );
50 | }
51 |
52 | return children;
53 | }
54 | }
55 |
56 | export default ErrorBoundary;
57 |
--------------------------------------------------------------------------------
/src/components/shared/FadeIn.css:
--------------------------------------------------------------------------------
1 | .component-fade-in {
2 | animation: componentFadeIn ease-out 400ms;
3 | }
4 |
5 | @keyframes componentFadeIn {
6 | 0% {
7 | opacity: 0;
8 | }
9 | 100% {
10 | opacity: 1;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/shared/FadeIn.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import './FadeIn.css';
4 |
5 | type FadeInProps = {
6 | children: React.ReactNode,
7 | };
8 |
9 | function FadeIn(props: FadeInProps) {
10 | const {children} = props;
11 | return (
12 |
13 | {children}
14 |
15 | );
16 | }
17 |
18 | export default FadeIn
19 |
--------------------------------------------------------------------------------
/src/components/shared/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Block } from 'baseui/block';
3 | import { FaGithub, FaTwitter, RiFilePaper2Fill } from 'react-icons/all';
4 | import { IconType } from 'react-icons/lib';
5 |
6 | import { ARTICLE_LINK, GITHUB_LINK, TWITTER_LINK } from '../../constants/links';
7 |
8 | function Footer() {
9 | return (
10 |
17 |
22 |
27 |
32 |
33 | );
34 | }
35 |
36 | type IconLinkProps = {
37 | url: string,
38 | title: string,
39 | Icon: IconType,
40 | };
41 |
42 | function IconLink(props: IconLinkProps) {
43 | const {url, title, Icon} = props;
44 | return (
45 |
46 |
47 |
48 |
49 |
50 | );
51 | }
52 |
53 | export default Footer;
54 |
--------------------------------------------------------------------------------
/src/components/shared/FormElementsRow.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Block } from 'baseui/block';
3 |
4 | type FormElementsRowProps = {
5 | nodes: React.ReactNode[],
6 | buttons?: React.ReactNode,
7 | alignBottom?: boolean,
8 | };
9 |
10 | const marginX = '10px';
11 |
12 | const FormElementsRow = (props: FormElementsRowProps) => {
13 | const {nodes, buttons, alignBottom = false} = props;
14 |
15 | const rows = nodes.map((node: React.ReactNode, nodeIndex: number) => {
16 | const marginLeft = nodeIndex === 0 ? 0 : marginX;
17 | const marginRight = nodeIndex === (nodes.length - 1) && !buttons ? 0 : marginX;
18 |
19 | return (
20 |
29 | {node}
30 |
31 | );
32 | });
33 |
34 | const buttonsRow = buttons ? (
35 |
41 | {buttons}
42 |
43 | ) : null;
44 |
45 | const alignItems = alignBottom ? 'flex-end' : 'flex-start';
46 |
47 | return (
48 |
55 | {rows}
56 | {buttonsRow}
57 |
58 | );
59 | };
60 |
61 | export default FormElementsRow;
62 |
--------------------------------------------------------------------------------
/src/components/shared/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { H5, Paragraph3 } from 'baseui/typography';
3 | import { StyledLink } from 'baseui/link';
4 | import { GiDna2 } from 'react-icons/all';
5 | import { APP_BASE_PATH } from '../../constants/app';
6 |
7 | function Header() {
8 | const onClick = () => {
9 | document.location.href = APP_BASE_PATH;
10 | };
11 |
12 | return (
13 |
14 |
18 |
28 | Self-Parking Car Evolution
29 |
30 |
31 |
32 | Training the car to do self-parking using genetic algorithm
33 |
34 |
35 | );
36 | }
37 |
38 | export default Header;
39 |
--------------------------------------------------------------------------------
/src/components/shared/Hint.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { GoInfo } from 'react-icons/all';
3 | import { Button, SHAPE as BUTTON_SHAPE, KIND as BUTTON_KIND } from 'baseui/button';
4 | import { StatefulTooltip } from 'baseui/tooltip';
5 |
6 | type HintProps = {
7 | hint: string,
8 | };
9 |
10 | const Hint = (props: HintProps) => {
11 | const {hint} = props;
12 |
13 | return (
14 |
18 |
34 |
35 | );
36 | };
37 |
38 | export default Hint;
39 |
--------------------------------------------------------------------------------
/src/components/shared/Layout.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | padding: 0;
3 | margin: 0;
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/shared/Layout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Client as Styletron } from 'styletron-engine-atomic';
3 | import { Provider as StyletronProvider } from 'styletron-react';
4 | import { BaseProvider, LightTheme } from 'baseui';
5 | import { Cell, Grid } from 'baseui/layout-grid';
6 | import { SnackbarProvider } from 'baseui/snackbar';
7 |
8 | import './Layout.css';
9 | import Header from './Header';
10 | import MainNav from './MainNav';
11 | import Footer from './Footer';
12 |
13 | const engine = new Styletron();
14 |
15 | type LayoutProps = {
16 | children: React.ReactNode,
17 | };
18 |
19 | function Layout(props: LayoutProps) {
20 | const { children } = props;
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | |
30 |
31 | {children}
32 | |
33 |
34 |
35 | |
36 |
37 |
38 |
39 |
40 | );
41 | }
42 |
43 | export default Layout;
44 |
--------------------------------------------------------------------------------
/src/components/shared/MainNav.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from 'react';
2 | import { StyledLink } from 'baseui/link';
3 | import { RiFilePaper2Fill, SiGithub } from 'react-icons/all';
4 | import { ARTICLE_LINK, GITHUB_LINK } from '../../constants/links';
5 |
6 | function MainNav() {
7 | const linkStyle: CSSProperties = {
8 | display: 'flex',
9 | alignItems: 'center',
10 | marginRight: '20px',
11 | marginBottom: '10px',
12 | };
13 |
14 | const iconStyle: CSSProperties = {
15 | marginRight: '5px',
16 | };
17 |
18 | return (
19 |
27 | );
28 | }
29 |
30 | export default MainNav;
31 |
--------------------------------------------------------------------------------
/src/components/shared/Row.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Block } from 'baseui/block';
3 |
4 | type RowProps = {
5 | children: React.ReactNode,
6 | };
7 |
8 | const Row = (props: RowProps) => {
9 | const {children} = props;
10 |
11 | return (
12 |
18 | {children}
19 |
20 | );
21 | };
22 |
23 | export default Row;
24 |
--------------------------------------------------------------------------------
/src/components/shared/Timer.css:
--------------------------------------------------------------------------------
1 | .timer-loader,
2 | .timer-loader-reverse {
3 | --clock-color: black;
4 | --clock-width: 1.3rem;
5 | --clock-radius: calc(var(--clock-width) / 2);
6 | --clock-minute-length: calc(var(--clock-width) * 0.45);
7 | --clock-hour-length: calc(var(--clock-width) * 0.35);
8 | --clock-thickness: 0.2rem;
9 |
10 | position: relative;
11 | display: flex;
12 | justify-content: center;
13 | align-items: center;
14 | width: var(--clock-width);
15 | height: var(--clock-width);
16 | border: 3px solid var(--clock-color);
17 | border-radius: 50%;
18 | }
19 |
20 | .timer-loader::before,
21 | .timer-loader::after {
22 | position: absolute;
23 | content: "";
24 | top: calc(var(--clock-radius) * 0.25);
25 | width: var(--clock-thickness);
26 | background: var(--clock-color);
27 | border-radius: 10px;
28 | transform-origin: center calc(100% - calc(var(--clock-thickness) / 2));
29 | animation: timer-spin infinite linear;
30 | }
31 |
32 | .timer-loader-reverse::before,
33 | .timer-loader-reverse::after {
34 | position: absolute;
35 | content: "";
36 | top: calc(var(--clock-radius) * 0.25);
37 | width: var(--clock-thickness);
38 | background: var(--clock-color);
39 | border-radius: 10px;
40 | transform-origin: center calc(100% - calc(var(--clock-thickness) / 2));
41 | animation: timer-spin-reverse infinite linear;
42 | }
43 |
44 | .timer-loader::before {
45 | height: var(--clock-minute-length);
46 | animation-duration: 6s;
47 | }
48 |
49 | .timer-loader::after {
50 | top: calc(var(--clock-radius) * -0.25 + var(--clock-hour-length));
51 | height: var(--clock-hour-length);
52 | animation-duration: 45s;
53 | }
54 |
55 | .timer-loader-reverse::before {
56 | height: var(--clock-minute-length);
57 | animation-duration: 2s;
58 | }
59 |
60 | .timer-loader-reverse::after {
61 | top: calc(var(--clock-radius) * -0.25 + var(--clock-hour-length));
62 | height: var(--clock-hour-length);
63 | animation-duration: 15s;
64 | }
65 |
66 | @keyframes timer-spin-reverse {
67 | to {
68 | transform: rotate(-1turn);
69 | }
70 | }
71 |
72 | @keyframes timer-spin {
73 | to {
74 | transform: rotate(1turn);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/shared/Timer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useRef, useState } from 'react';
2 | import { Label1 } from 'baseui/typography';
3 | import { Block } from 'baseui/block';
4 |
5 | import './Timer.css';
6 |
7 | type TimeMs = number;
8 |
9 | type TimerProps = {
10 | timeout?: TimeMs,
11 | interval?: TimeMs,
12 | version?: string,
13 | }
14 |
15 | function Timer(props: TimerProps) {
16 | const {timeout = null, interval = 1000, version = ''} = props;
17 |
18 | const [timePassed, setTimePassed] = useState(0);
19 | const timePassedRef = useRef(0);
20 |
21 | const timerRef = useRef(null);
22 |
23 | const reversedTime = timeout !== null;
24 |
25 | const onInterval = () => {
26 | timePassedRef.current += interval;
27 | setTimePassed(timePassedRef.current);
28 | };
29 |
30 | // eslint-disable-next-line react-hooks/exhaustive-deps
31 | const onIntervalCallback = useCallback(onInterval, [timeout, interval]);
32 |
33 | useEffect(() => {
34 | if (timerRef.current) {
35 | clearInterval(timerRef.current);
36 | }
37 | timePassedRef.current = 0;
38 | setTimePassed(0);
39 | timerRef.current = setInterval(onIntervalCallback, interval);
40 | return () => {
41 | if (timerRef.current) {
42 | clearInterval(timerRef.current);
43 | }
44 | };
45 | }, [onIntervalCallback, interval, version]);
46 |
47 | const formattedTime = timeout !== null
48 | ? formatTime(timeout - timePassed)
49 | : formatTime(timePassed);
50 |
51 | return (
52 |
53 |
54 |
55 |
56 |
57 | {formattedTime}
58 |
59 |
60 | );
61 | }
62 |
63 | function formatTime(timeMs: TimeMs): string {
64 | let timeSec = Math.max(Math.floor(timeMs / 1000), 0);
65 | let secPrefix = '';
66 | if (timeSec < 60) {
67 | secPrefix = timeSec < 10 ? '0' : '';
68 | return `${secPrefix}${timeSec}s`;
69 | }
70 | const timeMin = Math.floor(timeSec / 60);
71 | timeSec = timeSec % 60;
72 | secPrefix = timeSec < 10 ? '0' : '';
73 | return `${timeMin}m${secPrefix}${timeSec}s`;
74 | }
75 |
76 | export default Timer;
77 |
--------------------------------------------------------------------------------
/src/components/world/World.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Canvas } from '@react-three/fiber';
3 | import { OrbitControls, Stats, Environment, AdaptiveDpr, PerspectiveCamera } from '@react-three/drei';
4 | import { Physics } from '@react-three/cannon';
5 | import * as THREE from 'three';
6 | import { styled, withStyle } from 'baseui';
7 | import { StyledSpinnerNext } from 'baseui/spinner';
8 | import { Block } from 'baseui/block';
9 |
10 | import CarJoystickController from './controllers/CarJoystickController';
11 | import CarKeyboardController from './controllers/CarKeyboardController';
12 | import { WIDER_CAMERA_SCREEN_MAX_WIDTH, WORLD_CONTAINER_HEIGHT } from './constants/world';
13 | import FadeIn from '../shared/FadeIn';
14 | import { getSearchParam } from '../../utils/url';
15 |
16 | type WorldProps = {
17 | children: React.ReactNode,
18 | withJoystickControl?: boolean,
19 | withKeyboardControl?: boolean,
20 | version?: string,
21 | performanceBoost?: boolean,
22 | };
23 |
24 | const worldBackgroundColor = 'lightblue';
25 |
26 | const WorldSpinner = withStyle(StyledSpinnerNext, {
27 | width: '30px',
28 | height: '30px',
29 | borderLeftWidth: '6px',
30 | borderRightWidth: '6px',
31 | borderTopWidth: '6px',
32 | borderBottomWidth: '6px',
33 | borderTopColor: 'black',
34 | });
35 |
36 | const STAT_SEARCH_PARAM_NAME = 'debug';
37 |
38 | function World(props: WorldProps) {
39 | const {
40 | children,
41 | withJoystickControl = false,
42 | withKeyboardControl = false,
43 | version = '0',
44 | performanceBoost = false,
45 | } = props;
46 |
47 | const [withStat] = useState(!!getSearchParam(STAT_SEARCH_PARAM_NAME));
48 |
49 | const stats = withStat ? (
50 |
51 | ) : null;
52 |
53 | const preLoader = (
54 |
70 |
71 |
72 |
73 |
74 | );
75 |
76 | const joystickController = withJoystickControl ? (
77 |
78 | ) : null;
79 |
80 | const keyboardController = withKeyboardControl ? (
81 |
82 | ) : null;
83 |
84 | const cameraFov = window.innerWidth < WIDER_CAMERA_SCREEN_MAX_WIDTH ? 30 : 25;
85 |
86 | const environment = performanceBoost ? null : (
87 |
88 | );
89 |
90 | return (
91 |
92 | {preLoader}
93 |
94 |
131 |
132 | {joystickController}
133 | {keyboardController}
134 | {stats}
135 |
136 | );
137 | }
138 |
139 | const WorldContainer = styled('div', {
140 | height: `${WORLD_CONTAINER_HEIGHT}px`,
141 | boxSizing: 'border-box',
142 | borderStyle: 'dashed',
143 | borderColor: 'rgb(220, 220, 220)',
144 | borderWidth: 0,
145 | });
146 |
147 | export default World;
148 |
--------------------------------------------------------------------------------
/src/components/world/car/CarLabel.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from 'react';
2 | import { Html } from '@react-three/drei';
3 |
4 | type CarLabelProps = {
5 | content: React.ReactNode,
6 | };
7 |
8 | function CarLabel(props: CarLabelProps) {
9 | const { content } = props;
10 |
11 | const labelStyle: CSSProperties = {
12 | backgroundColor: 'rgba(255, 255, 255, 0.7)',
13 | padding: '0px 5px',
14 | borderRadius: '10px',
15 | color: 'black',
16 | fontSize: '10px',
17 | whiteSpace: 'nowrap',
18 | };
19 |
20 | return (
21 |
22 |
23 | {content}
24 |
25 |
26 | )
27 | }
28 |
29 | export default CarLabel;
30 |
--------------------------------------------------------------------------------
/src/components/world/car/Chassis.tsx:
--------------------------------------------------------------------------------
1 | import { BoxProps, useBox } from '@react-three/cannon';
2 | import React, { forwardRef } from 'react';
3 | import * as THREE from 'three';
4 | import { GroupProps } from '@react-three/fiber';
5 |
6 | import { CHASSIS_MASS, CHASSIS_OBJECT_NAME, CHASSIS_SIZE } from './constants';
7 | import { NumVec3 } from '../../../types/vectors';
8 | import ChassisModel from './ChassisModel';
9 | import Sensors from './Sensors';
10 | import CarLabel from './CarLabel';
11 | import { SensorValuesType } from '../types/car';
12 | import ChassisModelSimple from './ChassisModelSimple';
13 |
14 | type ChassisProps = {
15 | sensorsNum: number,
16 | weight?: number,
17 | wireframe?: boolean,
18 | castShadow?: boolean,
19 | receiveShadow?: boolean,
20 | withSensors?: boolean,
21 | visibleSensors?: boolean,
22 | styled?: boolean,
23 | label?: React.ReactNode,
24 | movable?: boolean,
25 | baseColor?: string,
26 | chassisPosition: NumVec3,
27 | bodyProps: BoxProps,
28 | onCollide?: (event: any) => void,
29 | userData?: Record,
30 | collisionFilterGroup?: number,
31 | collisionFilterMask?: number,
32 | onSensors?: (sensors: SensorValuesType) => void,
33 | performanceBoost: boolean,
34 | }
35 |
36 | const Chassis = forwardRef((props, ref) => {
37 | const {
38 | sensorsNum,
39 | wireframe = false,
40 | styled = true,
41 | castShadow = true,
42 | receiveShadow = true,
43 | movable = true,
44 | withSensors = false,
45 | visibleSensors = false,
46 | weight = CHASSIS_MASS,
47 | label = null,
48 | baseColor,
49 | chassisPosition,
50 | bodyProps,
51 | userData = {},
52 | collisionFilterGroup,
53 | collisionFilterMask,
54 | onCollide = () => {},
55 | onSensors = () => {},
56 | performanceBoost,
57 | } = props;
58 |
59 | const boxSize = CHASSIS_SIZE;
60 | useBox(
61 | () => ({
62 | mass: weight,
63 | allowSleep: false,
64 | args: boxSize,
65 | collisionFilterGroup,
66 | collisionFilterMask,
67 | onCollide,
68 | userData,
69 | type: movable ? 'Dynamic' : 'Static',
70 | ...bodyProps,
71 | }),
72 | // @ts-ignore
73 | ref
74 | )
75 |
76 | const groupProps: GroupProps = {
77 | position: chassisPosition,
78 | };
79 |
80 | const sensors = withSensors ? (
81 |
86 | ) : null;
87 |
88 | const carLabel = label ? (
89 |
90 | ) : null;
91 |
92 | const chassisModel = performanceBoost ? (
93 |
99 | ) : (
100 |
108 | );
109 |
110 | return (
111 |
112 |
113 | {chassisModel}
114 |
115 | {sensors}
116 | {carLabel}
117 |
118 | )
119 | })
120 |
121 | export default Chassis;
122 |
--------------------------------------------------------------------------------
/src/components/world/car/ChassisModel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useGLTF } from '@react-three/drei';
3 | import { GroupProps } from '@react-three/fiber';
4 | import { MeshBVH } from 'three-mesh-bvh';
5 |
6 | import { ModelData } from '../types/models';
7 | import { getPlastic, getRubber, getSteel, getGlass } from '../utils/materials';
8 | import { CHASSIS_MODEL_PATH } from './constants';
9 |
10 | // Preview the model: https://sandbox.babylonjs.com/
11 | // @see: https://github.com/pmndrs/drei#usegltf
12 | // useGLTF.preload(CHASSIS_MODEL_PATH);
13 |
14 | type ChassisModelProps = {
15 | bodyProps?: GroupProps,
16 | wireframe?: boolean,
17 | castShadow?: boolean,
18 | receiveShadow?: boolean,
19 | styled?: boolean,
20 | baseColor?: string,
21 | };
22 |
23 | function ChassisModel(props: ChassisModelProps) {
24 | const {
25 | bodyProps = {},
26 | wireframe = false,
27 | styled = true,
28 | castShadow = true,
29 | receiveShadow = true,
30 | baseColor: color,
31 | } = props;
32 |
33 | const { nodes, materials }: ModelData = useGLTF(CHASSIS_MODEL_PATH);
34 |
35 | Object.keys(nodes).forEach((geometryKey) => {
36 | if (geometryKey.startsWith('chassis_')) {
37 | // @ts-ignore
38 | nodes[geometryKey].geometry.boundsTree = new MeshBVH(nodes[geometryKey].geometry);
39 | }
40 | });
41 |
42 | return (
43 |
44 |
50 |
56 |
62 |
68 |
74 |
80 |
86 |
92 |
97 |
103 |
109 |
115 |
120 |
126 |
132 |
138 |
139 | )
140 | }
141 |
142 | export default ChassisModel;
143 |
--------------------------------------------------------------------------------
/src/components/world/car/ChassisModelSimple.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { GroupProps } from '@react-three/fiber';
3 |
4 | import { CHASSIS_HEIGHT, CHASSIS_LENGTH, CHASSIS_WIDTH } from './constants';
5 |
6 | type ChassisModelSimpleProps = {
7 | bodyProps?: GroupProps,
8 | wireframe?: boolean,
9 | castShadow?: boolean,
10 | receiveShadow?: boolean,
11 | styled?: boolean,
12 | baseColor?: string,
13 | };
14 |
15 | function ChassisModelSimple(props: ChassisModelSimpleProps) {
16 | const {
17 | castShadow = true,
18 | receiveShadow = true,
19 | baseColor: color,
20 | } = props;
21 |
22 | const boxArgs: number[] = [
23 | CHASSIS_WIDTH - 0.2,
24 | CHASSIS_HEIGHT - 0.4,
25 | CHASSIS_LENGTH - 0.2,
26 | ];
27 |
28 | return (
29 |
30 | {/* @ts-ignore */}
31 |
32 |
33 |
34 | );
35 | }
36 |
37 | export default ChassisModelSimple;
38 |
--------------------------------------------------------------------------------
/src/components/world/car/SensorRay.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import { Line2 } from 'three/examples/jsm/lines/Line2';
3 | import * as THREE from 'three';
4 | import { useFrame } from '@react-three/fiber';
5 | import throttle from 'lodash/throttle';
6 | import { RootState } from '@react-three/fiber/dist/declarations/src/core/store';
7 | import { Intersection } from 'three/src/core/Raycaster';
8 | import { acceleratedRaycast } from 'three-mesh-bvh';
9 | import { DebouncedFunc } from 'lodash';
10 |
11 | import { NumVec3 } from '../../../types/vectors';
12 | import { SENSOR_DISTANCE } from './constants';
13 | import { INTERSECT_THROTTLE_TIMEOUT, ON_RAY_THROTTLE_TIMEOUT } from '../constants/performance';
14 |
15 | const beamColor = new THREE.Color(0x009900);
16 | const beamWarningColor = new THREE.Color(0xFFFF00);
17 | const beamDangerColor = new THREE.Color(0xFF0000);
18 | const lineWidth = 0.5;
19 |
20 | THREE.Mesh.prototype.raycast = acceleratedRaycast;
21 |
22 | type SensorRayProps = {
23 | index: number,
24 | from: NumVec3,
25 | to: NumVec3,
26 | angleX: number,
27 | obstacles?: THREE.Object3D[],
28 | visible?: boolean,
29 | onRay?: (index: number, distance: number | undefined) => void,
30 | };
31 |
32 | const SensorRay = (props: SensorRayProps) => {
33 | const {
34 | index,
35 | from,
36 | to,
37 | angleX,
38 | obstacles = [],
39 | visible = false,
40 | onRay = (index, distance) => {},
41 | } = props;
42 |
43 | const lineRef = useRef();
44 |
45 | const positionRef = useRef(new THREE.Vector3());
46 | const directionRef = useRef(new THREE.Vector3());
47 | const raycasterRef = useRef(new THREE.Raycaster());
48 |
49 | const intersectObjectsThrottledRef = useRef any> | null>(null);
50 | const onRayCallbackThrottledRef = useRef any> | null>(null);
51 |
52 | const intersectionRef = useRef([]);
53 | raycasterRef.current.near = 0;
54 | raycasterRef.current.far = SENSOR_DISTANCE;
55 |
56 | // @ts-ignore
57 | raycasterRef.current.firstHitOnly = true;
58 |
59 | const intersectObjects = () => {
60 | intersectionRef.current = raycasterRef.current.intersectObjects(obstacles, true);
61 | };
62 |
63 | // if (!intersectObjectsThrottledRef.current) {
64 | // intersectObjectsThrottledRef.current = throttle(intersectObjects, INTERSECT_THROTTLE_TIMEOUT, {
65 | // leading: true,
66 | // trailing: true,
67 | // });
68 | // }
69 | intersectObjectsThrottledRef.current = throttle(intersectObjects, INTERSECT_THROTTLE_TIMEOUT, {
70 | leading: true,
71 | trailing: true,
72 | });
73 |
74 | const onRayCallback = (index: number, distance: number | undefined): void => {
75 | onRay(index, distance);
76 | };
77 |
78 | if (!onRayCallbackThrottledRef.current) {
79 | onRayCallbackThrottledRef.current = throttle(onRayCallback, ON_RAY_THROTTLE_TIMEOUT, {
80 | leading: true,
81 | trailing: true,
82 | });
83 | }
84 |
85 | useFrame((state: RootState, delta: number) => {
86 | if (!lineRef?.current) {
87 | return;
88 | }
89 |
90 | lineRef.current.getWorldPosition(positionRef.current);
91 | lineRef.current.getWorldDirection(directionRef.current);
92 |
93 | raycasterRef.current.set(positionRef.current, directionRef.current);
94 |
95 | if (intersectObjectsThrottledRef.current) {
96 | intersectObjectsThrottledRef.current();
97 | }
98 |
99 | const distance = intersectionRef.current.length
100 | ? intersectionRef.current[0].distance
101 | : undefined;
102 |
103 | if (onRayCallbackThrottledRef.current) {
104 | onRayCallbackThrottledRef.current(index, distance);
105 | }
106 |
107 | if (distance === undefined) {
108 | lineRef.current.material.color = beamColor;
109 | } else if (distance > (SENSOR_DISTANCE - SENSOR_DISTANCE / 4)) {
110 | lineRef.current.material.color = beamWarningColor;
111 | } else {
112 | lineRef.current.material.color = beamDangerColor;
113 | }
114 | });
115 |
116 | const onUnmount = () => {
117 | if (intersectObjectsThrottledRef.current) {
118 | intersectObjectsThrottledRef.current.cancel();
119 | }
120 | if (onRayCallbackThrottledRef.current) {
121 | onRayCallbackThrottledRef.current.cancel();
122 | }
123 | };
124 |
125 | useEffect(() => {
126 | return onUnmount;
127 | }, [])
128 |
129 | useEffect(() => {
130 | if (!lineRef.current) {
131 | return;
132 | }
133 | lineRef.current.rotateY(angleX);
134 | }, [angleX]);
135 |
136 | const lineGeometry = new THREE.BufferGeometry().setFromPoints([
137 | new THREE.Vector3(...from),
138 | new THREE.Vector3(...to),
139 | ]);
140 |
141 | return (
142 |
143 | {/* @ts-ignore */}
144 |
145 |
151 |
152 |
153 | )
154 | };
155 |
156 | export default SensorRay;
157 |
--------------------------------------------------------------------------------
/src/components/world/car/Sensors.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import * as THREE from 'three';
3 | import throttle from 'lodash/throttle';
4 | import { DebouncedFunc } from 'lodash';
5 | import { useThree } from '@react-three/fiber';
6 |
7 | import { CHASSIS_OBJECT_NAME, SENSOR_DISTANCE, SENSOR_HEIGHT } from './constants';
8 | import SensorRay from './SensorRay';
9 | import { CarMetaData, SensorValuesType } from '../types/car';
10 | import { ON_SENSORS_THROTTLE_TIMEOUT } from '../constants/performance';
11 |
12 | type SensorsProps = {
13 | sensorsNum: number,
14 | visibleSensors?: boolean,
15 | onSensors?: (sensors: SensorValuesType) => void,
16 | };
17 |
18 | const Sensors = (props: SensorsProps) => {
19 | const { visibleSensors = false, sensorsNum, onSensors = () => {} } = props;
20 | const obstacles = useRef([]);
21 | const sensorDistances = useRef(new Array(sensorsNum).fill(undefined));
22 | const { scene } = useThree();
23 | const onSensorsCallbackThrottledRef = useRef any> | null>(null);
24 |
25 | const onSensorsCallback = () => {
26 | onSensors(sensorDistances.current);
27 | };
28 |
29 | if (!onSensorsCallbackThrottledRef.current) {
30 | onSensorsCallbackThrottledRef.current = throttle(onSensorsCallback, ON_SENSORS_THROTTLE_TIMEOUT, {
31 | leading: true,
32 | trailing: true,
33 | });
34 | }
35 |
36 | const onRay = (index: number, distance: number | undefined): void => {
37 | sensorDistances.current[index] = typeof distance === 'number'
38 | ? distance
39 | : null;
40 | if (onSensorsCallbackThrottledRef.current) {
41 | onSensorsCallbackThrottledRef.current();
42 | }
43 | };
44 |
45 | // @ts-ignore
46 | obstacles.current = scene.children
47 | .filter((object: THREE.Object3D) => object.type === 'Group')
48 | .map((object: THREE.Object3D) => object.getObjectByName(CHASSIS_OBJECT_NAME))
49 | .filter((object: THREE.Object3D | undefined) => {
50 | if (!object || !object.userData) {
51 | return false;
52 | }
53 | // @ts-ignore
54 | const userData: CarMetaData = object.userData;
55 | return userData?.isSensorObstacle;
56 | });
57 |
58 | const angleStep = 2 * Math.PI / sensorsNum;
59 | const sensorRays = new Array(sensorsNum).fill(null).map((_, index) => {
60 | return (
61 |
71 | );
72 | });
73 |
74 | const onUnmount = () => {
75 | if (onSensorsCallbackThrottledRef.current) {
76 | onSensorsCallbackThrottledRef.current.cancel();
77 | }
78 | };
79 |
80 | useEffect(() => {
81 | return onUnmount;
82 | }, []);
83 |
84 | return (
85 | <>
86 | {sensorRays}
87 | >
88 | )
89 | };
90 |
91 | export default Sensors;
92 |
--------------------------------------------------------------------------------
/src/components/world/car/Wheel.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react';
2 | import { useCylinder, CylinderProps } from '@react-three/cannon';
3 | import * as THREE from 'three';
4 |
5 | import { NumVec3, NumVec4 } from '../../../types/vectors';
6 | import { WHEEL_MASS, WHEEL_OBJECT_NAME, WHEEL_WIDTH } from './constants';
7 | import WheelModel from './WheelModel';
8 | import WheelModelSimple from './WheelModelSimple';
9 |
10 | type WheelProps = {
11 | radius: number,
12 | mass?: number,
13 | width?: number,
14 | segments?: number,
15 | castShadow?: boolean,
16 | receiveShadow?: boolean,
17 | isLeft?: boolean,
18 | styled?: boolean,
19 | wireframe?: boolean,
20 | baseColor?: string,
21 | collisionFilterGroup?: number,
22 | collisionFilterMask?: number,
23 | bodyProps?: CylinderProps,
24 | performanceBoost: boolean,
25 | }
26 |
27 | const Wheel = forwardRef((props, ref) => {
28 | const {
29 | radius,
30 | width = WHEEL_WIDTH,
31 | mass = WHEEL_MASS,
32 | segments = 16,
33 | castShadow = true,
34 | receiveShadow = true,
35 | isLeft = false,
36 | styled = true,
37 | wireframe = false,
38 | bodyProps = {},
39 | baseColor,
40 | performanceBoost
41 | } = props;
42 |
43 | const wheelSize: NumVec4 = [radius, radius, width, segments];
44 |
45 | // The rotation should be applied to the shape (not the body).
46 | const rotation: NumVec3 = [0, 0, ((isLeft ? 1 : -1) * Math.PI) / 2];
47 |
48 | useCylinder(
49 | () => ({
50 | mass,
51 | type: 'Kinematic',
52 | collisionFilterGroup: 0,
53 | args: wheelSize,
54 | ...bodyProps,
55 | }),
56 | // @ts-ignore
57 | ref,
58 | )
59 |
60 | const wheelModel = performanceBoost ? (
61 |
66 | ) : (
67 |
74 | );
75 |
76 | return (
77 |
78 |
79 | {wheelModel}
80 |
81 |
82 | )
83 | })
84 |
85 | export default Wheel;
86 |
--------------------------------------------------------------------------------
/src/components/world/car/WheelModel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useGLTF } from '@react-three/drei';
3 | import { GroupProps } from '@react-three/fiber';
4 |
5 | import { ModelData } from '../types/models';
6 | import { getRubber, getSteel } from '../utils/materials';
7 | import { WHEEL_MODEL_PATH } from './constants';
8 |
9 | // Preview the model.
10 | // @see: https://github.com/pmndrs/drei#usegltf
11 | // useGLTF.preload(WHEEL_MODEL_PATH);
12 |
13 | type WheelModelProps = {
14 | castShadow?: boolean,
15 | receiveShadow?: boolean,
16 | groupProps?: GroupProps,
17 | styled?: boolean,
18 | wireframe?: boolean,
19 | baseColor?: string,
20 | };
21 |
22 | function WheelModel(props: WheelModelProps) {
23 | const {
24 | castShadow = true,
25 | receiveShadow = true,
26 | groupProps = {},
27 | styled = true,
28 | wireframe = false,
29 | baseColor: color,
30 | } = props;
31 |
32 | const { nodes, materials }: ModelData = useGLTF(WHEEL_MODEL_PATH);
33 |
34 | const tire = nodes.wheel_1?.geometry;
35 | const disc = nodes.wheel_2?.geometry;
36 | const cap = nodes.wheel_3?.geometry;
37 |
38 | const tireMaterial = styled
39 | ? materials.Rubber
40 | : getRubber({ wireframe, color: '#000000' });
41 |
42 | // const discMaterial = styled
43 | // ? materials.Steel
44 | // : getSteel({ wireframe });
45 |
46 | const discMaterial = getSteel({ wireframe, color });
47 |
48 | const capMaterial = styled
49 | ? materials.Chrom
50 | : getSteel({ wireframe, color });
51 |
52 | return (
53 |
54 |
60 |
66 |
72 |
73 | )
74 | }
75 |
76 | export default WheelModel;
77 |
--------------------------------------------------------------------------------
/src/components/world/car/WheelModelSimple.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { GroupProps } from '@react-three/fiber';
3 |
4 | import { WHEEL_WIDTH, WHEEL_RADIUS } from './constants';
5 |
6 | type WheelModelSimpleProps = {
7 | castShadow?: boolean,
8 | receiveShadow?: boolean,
9 | groupProps?: GroupProps,
10 | styled?: boolean,
11 | wireframe?: boolean,
12 | baseColor?: string,
13 | };
14 |
15 | function WheelModelSimple(props: WheelModelSimpleProps) {
16 | const {
17 | castShadow = true,
18 | receiveShadow = true,
19 | baseColor: color,
20 | } = props;
21 |
22 | const cylinderArgs: [number, number, number, number] = [
23 | WHEEL_RADIUS,
24 | WHEEL_RADIUS,
25 | WHEEL_WIDTH,
26 | 20,
27 | ];
28 |
29 | return (
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
37 | export default WheelModelSimple;
38 |
--------------------------------------------------------------------------------
/src/components/world/car/constants.ts:
--------------------------------------------------------------------------------
1 | import { NumVec3 } from '../../../types/vectors';
2 | import { getModelPath } from '../utils/models';
3 | import { CAR_SENSORS_NUM } from '../../../libs/carGenetic';
4 |
5 | // Wheels.
6 | export const WHEEL_OBJECT_NAME = 'wheel';
7 | export const WHEEL_MASS = 0.1;
8 | export const WHEEL_RADIUS = 0.3;
9 | export const WHEEL_WIDTH = 0.5;
10 | export const WHEEL_MODEL_PATH = getModelPath('wheel.glb');
11 | export const WHEEL_SUSPENSION_STIFFNESS = 30;
12 | export const WHEEL_SUSPENSION_REST_LENGTH = 0.3;
13 | export const WHEEL_MAX_SUSPENSION_FORCE = 10000;
14 | export const WHEEL_MAX_SUSPENSION_TRAVEL = 0.3;
15 | export const WHEEL_DAMPING_RELAXATION = 2.3;
16 | export const WHEEL_DAMPING_COMPRESSION = 4.4;
17 | export const WHEEL_FRICTION_SLIP = 5;
18 | export const WHEEL_ROLL_INFLUENCE = 0.01;
19 | export const WHEEL_CUSTOM_SLIDING_ROTATION_SPEED = -30;
20 |
21 | // Roughly the cars' visual dimensions.
22 | export const CHASSIS_OBJECT_NAME = 'chassis';
23 | export const CHASSIS_LENGTH = 4;
24 | export const CHASSIS_WIDTH = 1.5;
25 | export const CHASSIS_HEIGHT = 1;
26 | export const CHASSIS_SIZE: NumVec3 = [CHASSIS_WIDTH, CHASSIS_HEIGHT, CHASSIS_LENGTH];
27 | export const CHASSIS_MASS = 3; // kg
28 | export const CHASSIS_BASE_COLOR = '#FFFFFF';
29 | export const CHASSIS_SIMPLIFIED_BASE_COLOR = 'orange';
30 | export const CHASSIS_BASE_TOUCHED_COLOR = '#FF1111';
31 | export const CHASSIS_FRONT_WHEEL_SHIFT = 1.3;
32 | export const CHASSIS_BACK_WHEEL_SHIFT = -1.15;
33 | export const CHASSIS_GROUND_CLEARANCE = -0.04;
34 | export const CHASSIS_WHEEL_WIDTH = 1.2;
35 | export const CHASSIS_MODEL_PATH = getModelPath('beetle.glb');
36 | export const CHASSIS_RELATIVE_POSITION: NumVec3 = [0, -0.6, 0];
37 |
38 | // Sensors.
39 | export const SENSORS_NUM = CAR_SENSORS_NUM;
40 | export const SENSOR_HEIGHT = -0.15;
41 | export const SENSOR_DISTANCE = 4;
42 | export const SENSOR_DISTANCE_FALLBACK = 0;
43 |
44 | // Car.
45 | export const CAR_MAX_STEER_VALUE = 0.6;
46 | export const CAR_MAX_FORCE = 2;
47 | export const CAR_MAX_BREAK_FORCE = 2;
48 |
--------------------------------------------------------------------------------
/src/components/world/cars/DynamicCars.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 |
3 | import Car, { OnCarReadyArgs } from '../car/Car';
4 | import { CarType, EngineOptionsType, SensorValuesType, userCarUUID, WheelOptionsType } from '../types/car';
5 | import { carEvents, off, on } from '../utils/events';
6 | import {
7 | onEngineBackward,
8 | onEngineForward,
9 | onEngineNeutral, onPressBreak, onReleaseBreak,
10 | onWheelsLeft,
11 | onWheelsRight,
12 | onWheelsStraight
13 | } from '../utils/controllers';
14 | import { getRandomColor } from '../../../utils/colors';
15 | import { RectanglePoints } from '../../../types/vectors';
16 | import { CHASSIS_SIMPLIFIED_BASE_COLOR } from '../car/constants';
17 | import { DynamicCarsPosition, DYNAMIC_CARS_POSITION_FRONT } from '../constants/cars';
18 |
19 | type DynamicCarsProps = {
20 | cars: CarType[],
21 | collisionFilterGroup?: number,
22 | collisionFilterMask?: number,
23 | withSensors?: boolean,
24 | withLabels?: boolean,
25 | visibleSensors?: boolean,
26 | controllable?: boolean,
27 | withRandomColors?: boolean,
28 | withRandomStartingPoint?: boolean,
29 | performanceBoost?: boolean,
30 | carsPosition?: DynamicCarsPosition,
31 | };
32 |
33 | function DynamicCars(props: DynamicCarsProps) {
34 | const {
35 | cars,
36 | collisionFilterGroup,
37 | collisionFilterMask,
38 | withSensors = false,
39 | visibleSensors = false,
40 | withLabels = false,
41 | controllable = false,
42 | withRandomColors = false,
43 | withRandomStartingPoint = false,
44 | performanceBoost = false,
45 | carsPosition = DYNAMIC_CARS_POSITION_FRONT,
46 | } = props;
47 | const carsUUIDs = useRef([]);
48 | const carsAPIs = useRef>({});
49 |
50 | const activeCars = cars.map((car) => {
51 | const uuid = car.licencePlate;
52 | carsUUIDs.current.push(uuid);
53 |
54 | const onForward = () => { onEngineForward(carsAPIs.current[uuid].api) };
55 | const onBackward = () => { onEngineBackward(carsAPIs.current[uuid].api) };
56 | const onNeutral = () => { onEngineNeutral(carsAPIs.current[uuid].api) };
57 | const onLeft = () => { onWheelsLeft(carsAPIs.current[uuid].api) };
58 | const onRight = () => { onWheelsRight(carsAPIs.current[uuid].api) };
59 | const onStraight = () => { onWheelsStraight(carsAPIs.current[uuid].api) };
60 | const onBreak = () => { onPressBreak(carsAPIs.current[uuid].api) };
61 | const onBreakRelease = () => { onReleaseBreak(carsAPIs.current[uuid].api) };
62 |
63 | const onCarReady = (args: OnCarReadyArgs) => {
64 | carsAPIs.current[uuid] = args;
65 | if (controllable) {
66 | on(carEvents.engineForward, onForward);
67 | on(carEvents.engineBackward, onBackward);
68 | on(carEvents.engineNeutral, onNeutral);
69 | on(carEvents.wheelsLeft, onLeft);
70 | on(carEvents.wheelsRight, onRight);
71 | on(carEvents.wheelsStraight, onStraight);
72 | on(carEvents.pressBreak, onBreak);
73 | on(carEvents.releaseBreak, onBreakRelease);
74 | }
75 | };
76 |
77 | const onCarDestroy = () => {
78 | if (controllable) {
79 | off(carEvents.engineForward, onForward);
80 | off(carEvents.engineBackward, onBackward);
81 | off(carEvents.engineNeutral, onNeutral);
82 | off(carEvents.wheelsLeft, onLeft);
83 | off(carEvents.wheelsRight, onRight);
84 | off(carEvents.wheelsStraight, onStraight);
85 | off(carEvents.pressBreak, onBreak);
86 | off(carEvents.releaseBreak, onBreakRelease);
87 | }
88 | };
89 |
90 | const onSensors = (sensors: SensorValuesType): void => {
91 | if (car.onEngine) {
92 | const engineOption: EngineOptionsType = car.onEngine(sensors);
93 | switch (engineOption) {
94 | case 'backwards':
95 | onBackward();
96 | break;
97 | case 'neutral':
98 | onNeutral();
99 | break;
100 | case 'forward':
101 | onForward();
102 | break;
103 | }
104 | }
105 | if (car.onWheel) {
106 | const wheelOption: WheelOptionsType = car.onWheel(sensors);
107 | switch (wheelOption) {
108 | case 'left':
109 | onLeft();
110 | break;
111 | case 'straight':
112 | onStraight();
113 | break;
114 | case 'right':
115 | onRight();
116 | break;
117 | }
118 | }
119 | };
120 |
121 | const onMove = (wheelsPositions: RectanglePoints) => {
122 | if (car.onMove) {
123 | car.onMove(wheelsPositions);
124 | }
125 | };
126 |
127 | const zPositions: Record = {
128 | 'rear': withRandomStartingPoint ? -7 - 2 * Math.random() : -7,
129 | 'middle': 0,
130 | 'front': withRandomStartingPoint ? 7 + 2 * Math.random() : 7,
131 | };
132 |
133 | const z = zPositions[carsPosition]
134 | const position = [0, 2, z];
135 | const angularVelocity = [-0.2, 0, 0];
136 |
137 | const styledCar = !withRandomColors;
138 | const carColor = withRandomColors
139 | ? getRandomColor()
140 | : performanceBoost
141 | ? CHASSIS_SIMPLIFIED_BASE_COLOR
142 | : undefined;
143 |
144 | return (
145 |
168 | );
169 | });
170 |
171 | return (
172 | <>
173 | {activeCars}
174 | >
175 | );
176 | }
177 |
178 | export default DynamicCars;
179 |
--------------------------------------------------------------------------------
/src/components/world/cars/StaticCars.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from 'react';
2 |
3 | import Car from '../car/Car';
4 | import { NumVec3 } from '../../../types/vectors';
5 | import { CHASSIS_BASE_COLOR, CHASSIS_BASE_TOUCHED_COLOR, CHASSIS_LENGTH, CHASSIS_WIDTH } from '../car/constants';
6 | import { CarMetaData } from '../types/car';
7 | import { generateStaticCarUUID } from '../utils/uuid';
8 |
9 | type CarBaseColors = Record;
10 |
11 | type StaticCarsProps = {
12 | rows: number,
13 | cols: number,
14 | collisionFilterGroup: number,
15 | collisionFilterMask: number,
16 | skipCells?: number[][],
17 | performanceBoost?: boolean,
18 | };
19 |
20 | function StaticCars(props: StaticCarsProps) {
21 | const {
22 | rows,
23 | cols,
24 | collisionFilterGroup,
25 | collisionFilterMask,
26 | skipCells = [[]],
27 | performanceBoost = false,
28 | } = props;
29 |
30 | const [carBaseColors, setCarBaseColors] = useState({});
31 | const carBaseColorsRef = useRef({});
32 |
33 | const onCollide = (carMetaData: CarMetaData, event: any) => {
34 | const touchedCarUUID = carMetaData.uuid;
35 | if (!touchedCarUUID) {
36 | return;
37 | }
38 | const newCarBaseColors = {
39 | ...carBaseColorsRef.current,
40 | [touchedCarUUID]: CHASSIS_BASE_TOUCHED_COLOR,
41 | };
42 | carBaseColorsRef.current = newCarBaseColors;
43 | setCarBaseColors(newCarBaseColors);
44 | };
45 |
46 | const staticCarPositions: NumVec3[] = [];
47 | for (let row = 0; row < rows; row += 1) {
48 | for (let col = 0; col < cols; col += 1) {
49 | if (skipCells.find(([skipRow, skipCol]) => (skipRow === row && skipCol === col))) {
50 | continue;
51 | }
52 | const marginedLength = 1.4 * CHASSIS_LENGTH;
53 | const marginedWidth = 3.5 * CHASSIS_WIDTH;
54 | const x = -0.5 * marginedWidth + row * marginedWidth;
55 | const z = -2 * marginedLength + col * marginedLength;
56 | staticCarPositions.push([x, 0.6, z]);
57 | }
58 | }
59 |
60 | const staticCars = staticCarPositions.map((position: NumVec3, index: number) => {
61 | const uuid = generateStaticCarUUID(index);
62 | const baseColor = uuid in carBaseColors ? carBaseColors[uuid] : CHASSIS_BASE_COLOR;
63 | return (
64 |
77 | );
78 | });
79 |
80 | return (
81 | <>
82 | {staticCars}
83 | >
84 | );
85 | }
86 |
87 | export default StaticCars;
88 |
--------------------------------------------------------------------------------
/src/components/world/constants/cars.ts:
--------------------------------------------------------------------------------
1 | export type DynamicCarsPosition = 'middle' | 'front' | 'rear';
2 |
3 | export const DYNAMIC_CARS_POSITION_REAR: DynamicCarsPosition = 'rear';
4 | export const DYNAMIC_CARS_POSITION_MIDDLE: DynamicCarsPosition = 'middle';
5 | export const DYNAMIC_CARS_POSITION_FRONT: DynamicCarsPosition = 'front';
6 |
--------------------------------------------------------------------------------
/src/components/world/constants/models.ts:
--------------------------------------------------------------------------------
1 | import { APP_BASE_PATH } from '../../../constants/app';
2 |
3 | export const MODEL_BASE_PATH = `${APP_BASE_PATH}/models`;
4 |
--------------------------------------------------------------------------------
/src/components/world/constants/parking.ts:
--------------------------------------------------------------------------------
1 | import { NumVec3, RectanglePoints } from '../../../types/vectors';
2 | import { CHASSIS_WIDTH, CHASSIS_LENGTH } from '../car/constants';
3 |
4 | // @TODO: Parking lot size should be a configurable from the outside.
5 | // Move this constants to the component parameters.
6 |
7 | // const PARKING_SPOT_POSITION: NumVec3 = [-0.91, 0, -2];
8 | const PARKING_SPOT_POSITION: NumVec3 = [-3.6, 0, -2.1];
9 |
10 | const [x, y, z] = PARKING_SPOT_POSITION;
11 |
12 | const outerW = CHASSIS_WIDTH + 0.3;
13 | const outerL = CHASSIS_LENGTH + 0.3;
14 |
15 | const innerW = 1.2;
16 | const innerL = 2.44;
17 |
18 | const innerX = x + (outerW - innerW) / 2;
19 | const innerY = y;
20 | const innerZ = z + (outerL - innerL) / 2;
21 |
22 | export const PARKING_SPOT_OUTER_CORNERS: [number, number, number][] = [
23 | [x + outerW, y, z + outerL], // Front-left
24 | [x, y, z + outerL], // Front-right
25 | [x, y, z], // Back-right
26 | [x + outerW, y, z], // Back-left
27 | ];
28 |
29 | export const PARKING_SPOT_INNER_CORNERS: [number, number, number][] = [
30 | [innerX + innerW, innerY, innerZ + innerL], // Front-left
31 | [innerX, innerY, innerZ + innerL], // Front-right
32 | [innerX, innerY, innerZ], // Back-right
33 | [innerX + innerW, innerY, innerZ], // Back-left
34 | ];
35 |
36 | export const PARKING_SPOT_POINTS: RectanglePoints = {
37 | fl: [innerX + innerW, innerY, innerZ + innerL],
38 | fr: [innerX, innerY, innerZ + innerL],
39 | br: [innerX, innerY, innerZ],
40 | bl: [innerX + innerW, innerY, innerZ],
41 | };
42 |
--------------------------------------------------------------------------------
/src/components/world/constants/performance.ts:
--------------------------------------------------------------------------------
1 | // Throttle the check of the ray intersection with other objects.
2 | export const INTERSECT_THROTTLE_TIMEOUT = 100;
3 |
4 | // Throttle the onRay sensor events.
5 | export const ON_RAY_THROTTLE_TIMEOUT = 100;
6 |
7 | // Throttle the onSensors sensor events.
8 | export const ON_SENSORS_THROTTLE_TIMEOUT = 100;
9 |
10 | // Throttle the onMove events.
11 | export const ON_MOVE_THROTTLE_TIMEOUT = 100;
12 |
13 | // Throttle the update cat label events.
14 | export const ON_UPDATE_LABEL_THROTTLE_TIMEOUT = 600;
15 |
--------------------------------------------------------------------------------
/src/components/world/constants/world.ts:
--------------------------------------------------------------------------------
1 | export const WORLD_CONTAINER_HEIGHT = 400;
2 | export const WIDER_CAMERA_SCREEN_MAX_WIDTH = 600;
3 |
--------------------------------------------------------------------------------
/src/components/world/controllers/CarJoystickController.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import throttle from 'lodash/throttle';
3 | import ReactNipple from 'react-nipple';
4 |
5 | import { carEvents, trigger } from '../utils/events';
6 |
7 | function CarJoystickController() {
8 | const nippleSize = 100;
9 | const delta = 30;
10 | const throttleTimeout = 250;
11 |
12 | const onMove = (event: any, data: any) => {
13 | const angle = data.angle.degree;
14 | if (angle < (90 - delta) || angle > (270 + delta)) {
15 | trigger(carEvents.wheelsRight);
16 | } else if (angle > (90 + delta) && angle < (270 - delta)) {
17 | trigger(carEvents.wheelsLeft);
18 | }
19 | if (angle > delta && angle < (180 - delta)) {
20 | trigger(carEvents.engineForward);
21 | } else if (angle > (180 + delta) && angle < (360 - delta)) {
22 | trigger(carEvents.engineBackward);
23 | }
24 | };
25 |
26 | const onMoveThrottled = throttle(onMove, throttleTimeout, {
27 | leading: false,
28 | trailing: true,
29 | });
30 |
31 | const onEnd = (event: any, data: any) => {
32 | trigger(carEvents.releaseBreak);
33 | trigger(carEvents.engineNeutral);
34 | trigger(carEvents.wheelsStraight);
35 | };
36 |
37 | const onEndThrottled = throttle(onEnd, throttleTimeout + 10, {
38 | leading: false,
39 | trailing: true,
40 | });
41 |
42 | return
62 | }
63 |
64 | export default CarJoystickController;
65 |
--------------------------------------------------------------------------------
/src/components/world/controllers/CarKeyboardController.tsx:
--------------------------------------------------------------------------------
1 | import { Paragraph4 } from 'baseui/typography';
2 | import React, { useEffect } from 'react';
3 |
4 | import { useKeyPress } from '../../../hooks/useKeyPress';
5 | import { trigger, carEvents } from '../utils/events';
6 | import { Block } from 'baseui/block';
7 | import { WORLD_CONTAINER_HEIGHT } from '../constants/world';
8 |
9 | function CarKeyboardController() {
10 | // const forward = useKeyPress(['w', 'ArrowUp']);
11 | // const backward = useKeyPress(['s', 'ArrowDown']);
12 | // const left = useKeyPress(['a', 'ArrowLeft']);
13 | // const right = useKeyPress(['d', 'ArrowRight']);
14 | // const brake = useKeyPress([' ']);
15 |
16 | const forward = useKeyPress(['w']);
17 | const backward = useKeyPress(['s']);
18 | const left = useKeyPress(['a']);
19 | const right = useKeyPress(['d']);
20 | const brake = useKeyPress([' ']);
21 |
22 | useEffect(() => {
23 | // Left-right.
24 | if (left && !right) {
25 | trigger(carEvents.wheelsLeft);
26 | } else if (right && !left) {
27 | trigger(carEvents.wheelsRight);
28 | } else {
29 | trigger(carEvents.wheelsStraight);
30 | }
31 |
32 | // Front-back.
33 | if (forward && !backward) {
34 | trigger(carEvents.engineForward);
35 | } else if (backward && !forward) {
36 | trigger(carEvents.engineBackward);
37 | } else {
38 | trigger(carEvents.engineNeutral);
39 | }
40 |
41 | // Break.
42 | if (brake) {
43 | trigger(carEvents.pressBreak);
44 | }
45 | if (!brake) {
46 | trigger(carEvents.releaseBreak);
47 | }
48 | }, [forward, backward, left, right, brake]);
49 |
50 | return (
51 |
56 |
57 | {/*WASD
or ↑→↓←
to drive. SPACE
to break.*/}
58 | WASD
keys to drive. SPACE
to break.
59 |
60 |
61 | );
62 | }
63 |
64 | export default CarKeyboardController;
65 |
--------------------------------------------------------------------------------
/src/components/world/parkings/ParkingAutomatic.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Ground from '../surroundings/Ground';
4 | import StaticCars from '../cars/StaticCars';
5 | import DynamicCars from '../cars/DynamicCars';
6 | import { DynamicCarsPosition } from '../constants/cars';
7 | import ParkingSpot from '../surroundings/ParkingSpot';
8 | import { CarType } from '../types/car';
9 |
10 | // Collision groups and masks must be powers of 2.
11 | // @see: https://github.com/schteppe/cannon.js/blob/master/demos/collisionFilter.html
12 | const COLLISION_GROUP_ACTIVE_CARS = 0b0001;
13 | const COLLISION_GROUP_STATIC_OBJECTS = 0b0010;
14 | const COLLISION_MASK_ACTIVE_CARS = COLLISION_GROUP_STATIC_OBJECTS // It can only collide with static objects.
15 | const COLLISION_MASK_STATIC_OBJECTS = COLLISION_GROUP_ACTIVE_CARS // It can only collide with active cars.
16 |
17 | type ParkingAutomaticProps = {
18 | cars: CarType[],
19 | withVisibleSensors?: boolean,
20 | withLabels?: boolean,
21 | performanceBoost?: boolean,
22 | carsPosition?: DynamicCarsPosition,
23 | };
24 |
25 | function ParkingAutomatic(props: ParkingAutomaticProps) {
26 | const {
27 | cars,
28 | withVisibleSensors = false,
29 | withLabels = false,
30 | performanceBoost = false,
31 | carsPosition,
32 | } = props;
33 |
34 | return (
35 | <>
36 |
41 |
42 |
54 |
62 | >
63 | );
64 | }
65 |
66 | export default ParkingAutomatic;
67 |
--------------------------------------------------------------------------------
/src/components/world/parkings/ParkingManual.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Ground from '../surroundings/Ground';
4 | import StaticCars from '../cars/StaticCars';
5 | import DynamicCars from '../cars/DynamicCars';
6 | import { DYNAMIC_CARS_POSITION_MIDDLE } from '../constants/cars';
7 | import ParkingSpot from '../surroundings/ParkingSpot';
8 |
9 | // Collision groups and masks must be powers of 2.
10 | // @see: https://github.com/schteppe/cannon.js/blob/master/demos/collisionFilter.html
11 | const COLLISION_GROUP_ACTIVE_CARS = 0b0001;
12 | const COLLISION_GROUP_STATIC_OBJECTS = 0b0010;
13 | const COLLISION_MASK_ACTIVE_CARS = COLLISION_GROUP_STATIC_OBJECTS // It can only collide with static objects.
14 | const COLLISION_MASK_STATIC_OBJECTS = COLLISION_GROUP_ACTIVE_CARS // It can only collide with active cars.
15 |
16 | type ParkingManualProps = {
17 | withLabels?: boolean,
18 | withSensors?: boolean,
19 | performanceBoost?: boolean,
20 | };
21 |
22 | function ParkingManual(props: ParkingManualProps) {
23 | const {
24 | withLabels = false,
25 | withSensors = false,
26 | performanceBoost = false,
27 | } = props;
28 |
29 | return (
30 | <>
31 |
36 |
37 |
48 |
56 | >
57 | );
58 | }
59 |
60 | export default ParkingManual;
61 |
--------------------------------------------------------------------------------
/src/components/world/surroundings/Ground.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { usePlane, PlaneProps, } from '@react-three/cannon';
3 | import { NumVec2 } from '../../../types/vectors';
4 |
5 | function Ground(props: PlaneProps) {
6 | const args: NumVec2 = [200, 200];
7 | const [ref] = usePlane(() => ({
8 | type: 'Static',
9 | rotation: [-Math.PI / 2, 0, 0],
10 | args,
11 | ...props,
12 | }))
13 | return (
14 |
15 |
16 |
17 |
18 | )
19 | }
20 |
21 | export default Ground;
22 |
--------------------------------------------------------------------------------
/src/components/world/surroundings/ParkingSpot.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Line } from '@react-three/drei';
3 | import { PARKING_SPOT_INNER_CORNERS, PARKING_SPOT_OUTER_CORNERS } from '../constants/parking';
4 |
5 | type ParkingSpotProps = {
6 | color?: string,
7 | };
8 |
9 | // @TODO: Parking lot size should be a configurable from the outside.
10 | // Move this constants to the component parameters.
11 |
12 | const innerLineVisible = false;
13 |
14 | function ParkingSpot(props: ParkingSpotProps) {
15 | const { color = 'yellow' } = props;
16 |
17 | const innerLineComponent = innerLineVisible ? (
18 |
27 | ) : null;
28 |
29 | return (
30 | <>
31 |
40 | {innerLineComponent}
41 | >
42 | );
43 | }
44 |
45 | export default ParkingSpot;
46 |
--------------------------------------------------------------------------------
/src/components/world/types/car.ts:
--------------------------------------------------------------------------------
1 | import { RectanglePoints } from '../../../types/vectors';
2 |
3 | export type CarLicencePlateType = string;
4 |
5 | export type SensorValueType = number | undefined | null;
6 | export type SensorValuesType = SensorValueType[];
7 |
8 | export type EngineOptionsType = 'backwards' | 'neutral' | 'forward';
9 | export type WheelOptionsType = 'left' | 'straight' | 'right';
10 |
11 | export type CarType = {
12 | licencePlate: CarLicencePlateType,
13 | generationIndex: number,
14 | genomeIndex: number,
15 | sensorsNum?: number,
16 | onHit?: () => void,
17 | onEngine?: (sensors: SensorValuesType) => EngineOptionsType,
18 | onWheel?: (sensors: SensorValuesType) => WheelOptionsType,
19 | onMove?: (wheelsPoints: RectanglePoints) => void,
20 | meta?: Record,
21 | };
22 |
23 | type CarPartType = 'chassis' | 'wheel';
24 |
25 | export type userCarUUID = string;
26 |
27 | export type CarMetaData = {
28 | uuid: string,
29 | type: CarPartType,
30 | isSensorObstacle: boolean,
31 | };
32 |
33 | export type CarsType = Record;
34 |
35 | export type RaycastVehiclePublicApi = {
36 | setSteeringValue: (value: number, wheelIndex: number) => void
37 | applyEngineForce: (value: number, wheelIndex: number) => void
38 | setBrake: (brake: number, wheelIndex: number) => void
39 | };
40 |
41 | export type WheelInfoOptions = {
42 | radius?: number
43 | directionLocal?: number[]
44 | suspensionStiffness?: number
45 | suspensionRestLength?: number
46 | maxSuspensionForce?: number
47 | maxSuspensionTravel?: number
48 | dampingRelaxation?: number
49 | dampingCompression?: number
50 | frictionSlip?: number
51 | rollInfluence?: number
52 | axleLocal?: number[]
53 | chassisConnectionPointLocal?: number[]
54 | isFrontWheel?: boolean
55 | useCustomSlidingRotationalSpeed?: boolean
56 | customSlidingRotationalSpeed?: number
57 | };
58 |
--------------------------------------------------------------------------------
/src/components/world/types/models.ts:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 |
3 | interface ExtendedObject3D extends THREE.Object3D {
4 | geometry?: THREE.BufferGeometry,
5 | }
6 |
7 | export type ModelData = {
8 | nodes: {
9 | [name: string]: ExtendedObject3D;
10 | };
11 | materials: {
12 | [name: string]: THREE.Material;
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/src/components/world/utils/controllers.ts:
--------------------------------------------------------------------------------
1 | import { RaycastVehiclePublicApi } from '../types/car';
2 | import { CAR_MAX_BREAK_FORCE, CAR_MAX_FORCE, CAR_MAX_STEER_VALUE } from '../car/constants';
3 |
4 | export const onEngineForward = (carAPI: RaycastVehiclePublicApi, wheelsNum: number = 4): void => {
5 | for (let wheelIdx = 0; wheelIdx < wheelsNum; wheelIdx += 1) {
6 | carAPI.setBrake(0, wheelIdx);
7 | }
8 | carAPI.applyEngineForce(-CAR_MAX_FORCE, 2);
9 | carAPI.applyEngineForce(-CAR_MAX_FORCE, 3);
10 | };
11 |
12 | export const onEngineBackward = (carAPI: RaycastVehiclePublicApi, wheelsNum: number = 4): void => {
13 | for (let wheelIdx = 0; wheelIdx < wheelsNum; wheelIdx += 1) {
14 | carAPI.setBrake(0, wheelIdx);
15 | }
16 | carAPI.applyEngineForce(CAR_MAX_FORCE, 2);
17 | carAPI.applyEngineForce(CAR_MAX_FORCE, 3);
18 | };
19 |
20 | export const onEngineNeutral = (carAPI: RaycastVehiclePublicApi): void => {
21 | carAPI.applyEngineForce(0, 2);
22 | carAPI.applyEngineForce(0, 3);
23 | };
24 |
25 | export const onWheelsLeft = (carAPI: RaycastVehiclePublicApi): void => {
26 | carAPI.setSteeringValue(CAR_MAX_STEER_VALUE, 0);
27 | carAPI.setSteeringValue(CAR_MAX_STEER_VALUE, 1);
28 | };
29 |
30 | export const onWheelsRight = (carAPI: RaycastVehiclePublicApi): void => {
31 | carAPI.setSteeringValue(-CAR_MAX_STEER_VALUE, 0);
32 | carAPI.setSteeringValue(-CAR_MAX_STEER_VALUE, 1);
33 | };
34 |
35 | export const onWheelsStraight = (carAPI: RaycastVehiclePublicApi,): void => {
36 | carAPI.setSteeringValue(0, 0);
37 | carAPI.setSteeringValue(0, 1);
38 | };
39 |
40 | export const onPressBreak = (carAPI: RaycastVehiclePublicApi, wheelsNum: number = 4): void => {
41 | for (let wheelIdx = 0; wheelIdx < wheelsNum; wheelIdx += 1) {
42 | carAPI.setBrake(CAR_MAX_BREAK_FORCE, wheelIdx);
43 | }
44 | };
45 |
46 | export const onReleaseBreak = (carAPI: RaycastVehiclePublicApi, wheelsNum: number = 4): void => {
47 | for (let wheelIdx = 0; wheelIdx < wheelsNum; wheelIdx += 1) {
48 | carAPI.setBrake(0, wheelIdx);
49 | }
50 | };
51 |
--------------------------------------------------------------------------------
/src/components/world/utils/events.ts:
--------------------------------------------------------------------------------
1 | // @see: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent
2 |
3 | type CarEvent = 'engineforward' | 'enginebackward' | 'engineneutral' | 'wheelsleft' | 'wheelsright' | 'wheelsstraight' | 'pressbreak' | 'releasebreak';
4 |
5 | type CarEvents = Record;
6 |
7 | export const carEvents: CarEvents = {
8 | engineForward: 'engineforward',
9 | engineBackward: 'enginebackward',
10 | engineNeutral: 'engineneutral',
11 | wheelsLeft: 'wheelsleft',
12 | wheelsRight: 'wheelsright',
13 | wheelsStraight: 'wheelsstraight',
14 | pressBreak: 'pressbreak',
15 | releaseBreak: 'releasebreak',
16 | };
17 |
18 | export const trigger = (eventType: CarEvent, data: any = {}) => {
19 | const event = new CustomEvent(eventType, { detail: data });
20 | document.dispatchEvent(event);
21 | };
22 |
23 | export const on = (eventType: CarEvent, listener: (evt: Event) => void) => {
24 | document.addEventListener(eventType, listener);
25 | };
26 |
27 | export const off = (eventType: CarEvent, listener: (evt: Event) => void) => {
28 | document.removeEventListener(eventType, listener);
29 | };
30 |
--------------------------------------------------------------------------------
/src/components/world/utils/materials.ts:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 | import { Material } from 'three/src/materials/Material';
3 | import { MeshStandardMaterialParameters } from 'three/src/materials/MeshStandardMaterial';
4 | import { MeshPhysicalMaterialParameters } from 'three';
5 |
6 | export const getSteel = (props: MeshStandardMaterialParameters): Material => {
7 | return new THREE.MeshStandardMaterial({
8 | metalness: 0.9,
9 | roughness: 0.1,
10 | ...props,
11 | });
12 | };
13 |
14 | export const getRubber = (props: MeshStandardMaterialParameters): Material => {
15 | return new THREE.MeshStandardMaterial({
16 | ...props,
17 | });
18 | };
19 |
20 | export const getPlastic = (props: MeshStandardMaterialParameters): Material => {
21 | return new THREE.MeshStandardMaterial({
22 | ...props,
23 | });
24 | };
25 |
26 | export const getGlass = (props: MeshPhysicalMaterialParameters): Material => {
27 | return new THREE.MeshPhysicalMaterial({
28 | metalness: 0.5,
29 | roughness: 0,
30 | transmission: 0.9,
31 | ...props,
32 | transparent: true,
33 | color: '#FFFFFF',
34 | });
35 | };
36 |
--------------------------------------------------------------------------------
/src/components/world/utils/models.ts:
--------------------------------------------------------------------------------
1 | import { MODEL_BASE_PATH } from '../constants/models';
2 |
3 | export const getModelPath = (modelFileName: string): string => {
4 | return `${MODEL_BASE_PATH}/${modelFileName}`;
5 | };
6 |
--------------------------------------------------------------------------------
/src/components/world/utils/uuid.ts:
--------------------------------------------------------------------------------
1 | import { userCarUUID } from '../types/car';
2 |
3 | export const generateStaticCarUUID = (carIndex: number): userCarUUID => {
4 | return `car-static-${carIndex}`;
5 | };
6 |
--------------------------------------------------------------------------------
/src/constants/app.ts:
--------------------------------------------------------------------------------
1 | export const APP_BASE_PATH = '/self-parking-car-evolution';
2 |
--------------------------------------------------------------------------------
/src/constants/links.ts:
--------------------------------------------------------------------------------
1 | export const TWITTER_LINK = 'https://twitter.com/Trekhleb';
2 | export const GITHUB_LINK = 'https://github.com/trekhleb/self-parking-car-evolution';
3 | export const CHECKPOINTS_PATH = 'https://github.com/trekhleb/self-parking-car-evolution/tree/master/src/checkpoints';
4 | export const ARTICLE_LINK = 'https://trekhleb.dev/blog/2021/self-parking-car-evolution/';
5 |
--------------------------------------------------------------------------------
/src/constants/routes.ts:
--------------------------------------------------------------------------------
1 | type RouteID = 'home';
2 |
3 | type Route = {
4 | path: string,
5 | };
6 |
7 | type Routes = Record;
8 |
9 | export const routes: Routes = {
10 | home: {
11 | path: '/',
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/src/hooks/useKeyPress.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from 'react';
2 |
3 | export function useKeyPress(target: string[]): boolean {
4 | const [keyPressed, setKeyPressed] = useState(false)
5 |
6 | const downHandler = ({ key }: KeyboardEvent) => {
7 | if (target.includes(key)) {
8 | setKeyPressed(true);
9 | }
10 | };
11 |
12 | const downHandlerCallback = useCallback(downHandler, [target]);
13 |
14 | const upHandler = ({ key }: KeyboardEvent) => {
15 | if (target.includes(key)) {
16 | setKeyPressed(false);
17 | }
18 | };
19 |
20 | const upHandlerCallback = useCallback(upHandler, [target]);
21 |
22 | useEffect(() => {
23 | window.addEventListener('keydown', downHandlerCallback)
24 | window.addEventListener('keyup', upHandlerCallback)
25 | return () => {
26 | window.removeEventListener('keydown', downHandlerCallback)
27 | window.removeEventListener('keyup', upHandlerCallback)
28 | }
29 | }, [upHandlerCallback, downHandlerCallback]);
30 |
31 | return keyPressed;
32 | }
33 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | ReactDOM.render(
6 |
7 |
8 | ,
9 | document.getElementById('root')
10 | );
11 |
--------------------------------------------------------------------------------
/src/libs/__tests__/carGenetic.test.ts:
--------------------------------------------------------------------------------
1 | import { SENSOR_DISTANCE_FALLBACK } from '../../components/world/car/constants';
2 | import { decodeGenome, engineFormula, FormulaResult, SensorValues } from '../carGenetic';
3 | import { Genome } from '../genetic';
4 | import { genomeStringToGenome } from './../../components/evolution/utils/evolution';
5 |
6 | describe('carGenetic', () => {
7 | it('should try to move forward when the rear car is close', () => {
8 | const rearSensorIndex = 4;
9 |
10 | // Rear (#4) sensor says the car is close.
11 | const na = SENSOR_DISTANCE_FALLBACK;
12 | const sensors: SensorValues = [
13 | na, na, na, na, na, na, na, na,
14 | ];
15 | sensors[rearSensorIndex] = 2.4
16 |
17 | const genome: Genome = genomeStringToGenome(
18 | // 0 1 2 3 4 5
19 | '1 0 1 1 1 1 0 1 0 0 1 1 0 1 1 1 1 0 0 0 1 1 1 1 1 1 0 0 0 0 1 0 1 1 1 0 0 1 1 1 0 1 1 1 1 1 0 1 0 0 1 1 0 1 0 0 1 1 0 0 0 1 1 1 0 1 0 1 0 0 1 0 1 1 1 1 0 0 0 1 1 1 0 0 0 0 0 0 1 1 0 0 1 1 1 0 1 1 1 1 1 0 0 0 1 0 0 1 1 0 1 1 0 1 0 1 0 1 0 0 1 0 0 1 0 1 1 0 1 1 1 1 0 0 0 0 1 1 0 1 0 1 0 0 0 0 0 0 0 0 1 0 0 1 1 1 1 0 1 1 0 1 0 1 0 0 1 0 0 0 0 0 0 1 0 0 0 0 1 1'
20 | );
21 |
22 | const {engineFormulaCoefficients} = decodeGenome(genome);
23 |
24 | expect(engineFormulaCoefficients[rearSensorIndex]).toBe(416);
25 |
26 | const engineMode: FormulaResult = engineFormula(genome, sensors);
27 |
28 | // Move forward, away from the rear car.
29 | expect(engineMode).toBe(1);
30 | });
31 | });
32 |
33 | export {};
34 |
--------------------------------------------------------------------------------
/src/libs/__tests__/genetic.test.ts:
--------------------------------------------------------------------------------
1 | // Tests might be flaky because of a lot of Math.random() usage under the hood.
2 | import {
3 | createGeneration,
4 | FitnessFunction,
5 | Generation,
6 | Genome,
7 | Percentage,
8 | Probability,
9 | select,
10 | } from '../genetic';
11 | import { carLossToFitness, genomeToNumbers } from '../carGenetic';
12 | import { linearPolynomial } from '../math/polynomial';
13 | import { precisionConfigs } from '../math/floats';
14 |
15 | type TestCase = {
16 | only?: boolean,
17 | in: {
18 | targetPolynomial: number[],
19 | epochs: number,
20 | generationSize: number,
21 | mutationProbability: Probability,
22 | longLivingChampionsPercentage: Percentage,
23 | },
24 | out: {
25 | // How big might be the average (for 10 points in space) distance between
26 | // target polynomial value and predicted (genetic) polynomial value.
27 | maxAvgPolynomialResultsDistance?: number,
28 | // How big might be the absolute difference between target polynomial
29 | // coefficient and predicted (genetic) polynomial coefficients.
30 | maxCoefficientsDifference?: number,
31 | // What maximum fitness function value is expected.
32 | minFitness?: number,
33 | },
34 | };
35 |
36 | const testCases: TestCase[] = [
37 | {
38 | in: {
39 | epochs: 100,
40 | generationSize: 100,
41 | mutationProbability: 0,
42 | longLivingChampionsPercentage: 2,
43 | targetPolynomial: [42],
44 | },
45 | out: {
46 | maxCoefficientsDifference: 0.5,
47 | maxAvgPolynomialResultsDistance: 0.1,
48 | minFitness: 0.95,
49 | },
50 | },
51 | {
52 | in: {
53 | epochs: 200,
54 | generationSize: 100,
55 | mutationProbability: 0,
56 | longLivingChampionsPercentage: 2,
57 | targetPolynomial: [0.0042],
58 | },
59 | out: {
60 | // maxCoefficientsDifference: 0.0001,
61 | maxAvgPolynomialResultsDistance: 0.01,
62 | minFitness: 0.95,
63 | },
64 | },
65 | {
66 | in: {
67 | epochs: 300,
68 | generationSize: 200,
69 | mutationProbability: 0.2,
70 | longLivingChampionsPercentage: 2,
71 | targetPolynomial: [42, -3],
72 | },
73 | out: {
74 | maxCoefficientsDifference: 0.5,
75 | maxAvgPolynomialResultsDistance: 0.1,
76 | minFitness: 0.95,
77 | },
78 | },
79 | {
80 | in: {
81 | epochs: 300,
82 | generationSize: 300,
83 | mutationProbability: 0.2,
84 | longLivingChampionsPercentage: 2,
85 | targetPolynomial: [-0.15, 142],
86 | },
87 | out: {
88 | // maxCoefficientsDifference: 1,
89 | // maxAvgPolynomialResultsDistance: 0.1,
90 | minFitness: 0.5,
91 | },
92 | },
93 | {
94 | in: {
95 | epochs: 1000,
96 | generationSize: 500,
97 | mutationProbability: 0.3,
98 | longLivingChampionsPercentage: 3,
99 | targetPolynomial: [
100 | 504, 0.06, -496, 0, -504, 0.008, -0.014, 0.007,
101 | ],
102 | },
103 | out: {
104 | maxCoefficientsDifference: 10,
105 | // maxAvgPolynomialResultsDistance: 0.1,
106 | minFitness: 0.95,
107 | },
108 | },
109 | {
110 | only: true,
111 | in: {
112 | epochs: 1000,
113 | generationSize: 1000,
114 | mutationProbability: 0.2,
115 | longLivingChampionsPercentage: 6,
116 | targetPolynomial: [
117 | 42.4, -3, 0.03, 120.05, 30, -0.01, 0, 170, 362,
118 | 0.01, -10, -396, 0.01, -34.5, -287.5, 0.386, -440, 0,
119 | ],
120 | },
121 | out: {
122 | maxCoefficientsDifference: 0.01,
123 | // maxAvgPolynomialResultsDistance: 0.1,
124 | minFitness: 0.9,
125 | },
126 | },
127 | ];
128 |
129 | describe('genetic', () => {
130 | it('should create a new generation of correct length', () => {
131 | const gen01 = createGeneration({
132 | generationSize: 500,
133 | genomeLength: 10,
134 | });
135 |
136 | expect(gen01.length).toBe(500);
137 |
138 | const gen02 = select(
139 | gen01,
140 | () => Math.random(),
141 | {
142 | mutationProbability: 0.3,
143 | longLivingChampionsPercentage: 3,
144 | },
145 | );
146 |
147 | expect(gen02.length).toBe(500);
148 | });
149 |
150 | let justOneTest = false;
151 |
152 | testCases.forEach((testCase: TestCase, testIndex: number) => {
153 | const { only = false } = testCase;
154 | justOneTest = justOneTest || only;
155 | });
156 |
157 | testCases.forEach((testCase: TestCase, testIndex: number) => {
158 | const {
159 | only = false,
160 | in: {
161 | epochs,
162 | generationSize,
163 | targetPolynomial,
164 | mutationProbability,
165 | longLivingChampionsPercentage,
166 | },
167 | out: {
168 | maxAvgPolynomialResultsDistance,
169 | maxCoefficientsDifference,
170 | minFitness,
171 | },
172 | } = testCase;
173 |
174 | const coefficientsNum: number = targetPolynomial.length;
175 | const genomeLength: number = coefficientsNum * precisionConfigs.custom.totalBitsCount;
176 |
177 | const fitness: FitnessFunction = (genome: Genome): number => {
178 | const genomePolynomial: number[] = genomeToNumbers(genome, precisionConfigs.custom.totalBitsCount);
179 | const avgDelta = avgPolynomialsDelta(genomePolynomial, targetPolynomial);
180 | return carLossToFitness(avgDelta, 0.00001);
181 | };
182 |
183 | if (!justOneTest || only) {
184 | it(`#${testIndex + 1}: should approximate the polynomial: coefficients - ${coefficientsNum}, epochs - ${epochs}, generation size - ${generationSize}, mutation - ${mutationProbability}, champions - ${longLivingChampionsPercentage}`, () => {
185 | // Create the first generation.
186 | const firstGeneration = createGeneration({
187 | generationSize,
188 | genomeLength,
189 | });
190 |
191 | // Let generations live and mate for several epochs.
192 | let epoch = 0;
193 | let latestGeneration: Generation = firstGeneration;
194 |
195 | while (epoch < epochs) {
196 | epoch += 1;
197 | latestGeneration = select(
198 | latestGeneration,
199 | fitness,
200 | {
201 | mutationProbability,
202 | longLivingChampionsPercentage,
203 | },
204 | );
205 | }
206 |
207 | // We may take the first individuum since they are sorted
208 | // by fitness value from best to worst.
209 | const bestGenome = latestGeneration[0];
210 | const genomePolynomial: number[] = genomeToNumbers(bestGenome, precisionConfigs.custom.totalBitsCount);
211 |
212 | // Check if polynomial coefficients are OK.
213 | if (maxCoefficientsDifference !== undefined) {
214 | const failedCoefficientsChecks: number[][] = [];
215 | targetPolynomial.forEach((targetCoefficient: number, i: number) => {
216 | const geneticCoefficient = genomePolynomial[i];
217 | const coefficientDifference = Math.abs(geneticCoefficient - targetCoefficient);
218 | try {
219 | // eslint-disable-next-line jest/no-conditional-expect
220 | expect(maxCoefficientsDifference).toBeGreaterThanOrEqual(coefficientDifference);
221 | } catch(e) {
222 | failedCoefficientsChecks.push([i, geneticCoefficient, targetCoefficient]);
223 | }
224 | });
225 | if (failedCoefficientsChecks.length) {
226 | let errorMessage = `Expect coefficients to be close (< ${maxCoefficientsDifference}):`;
227 | failedCoefficientsChecks.forEach((failedCheck: number[]) => {
228 | const coefficientIndex = failedCheck[0];
229 | const geneticCoefficient = failedCheck[1];
230 | const targetCoefficient = failedCheck[2];
231 | errorMessage += `\n • #${coefficientIndex}: ${geneticCoefficient} → ${targetCoefficient}`;
232 | });
233 | throw new Error(errorMessage);
234 | }
235 | }
236 |
237 | // Check if polynomial value is OK.
238 | if (maxAvgPolynomialResultsDistance !== undefined) {
239 | const avgDistance = avgPolynomialsDelta(genomePolynomial, targetPolynomial);
240 | try {
241 | // eslint-disable-next-line jest/no-conditional-expect
242 | expect(maxAvgPolynomialResultsDistance).toBeGreaterThanOrEqual(avgDistance);
243 | } catch(e) {
244 | throw new Error(`Expect avg polynomial results to be close (< ${maxAvgPolynomialResultsDistance}): ${avgDistance} ≤ ${maxAvgPolynomialResultsDistance}`);
245 | }
246 | }
247 |
248 | // Check if fitness value is OK.
249 | if (minFitness !== undefined) {
250 | const genomeFitness = fitness(bestGenome);
251 | try {
252 | // eslint-disable-next-line jest/no-conditional-expect
253 | expect(minFitness).toBeLessThanOrEqual(genomeFitness);
254 | } catch(e) {
255 | throw new Error(`Expect the fitness value of ${genomeFitness} to be greater than ${minFitness}`);
256 | }
257 | }
258 | });
259 | }
260 | });
261 | });
262 |
263 | const avgPolynomialsDelta = (
264 | polynomialA: number[],
265 | polynomialB: number[],
266 | numPointsToTest: number = 10,
267 | ): number => {
268 | let delta: number = 0;
269 |
270 | const coefficientsNum = polynomialA.length;
271 |
272 | for (let testPointIndex = 0; testPointIndex < numPointsToTest; testPointIndex += 1) {
273 | const variables: number[] = new Array(coefficientsNum - 1)
274 | .fill(null)
275 | .map(() => 100 * Math.random());
276 |
277 | const genomeY: number = linearPolynomial(polynomialA, variables);
278 | const targetY: number = linearPolynomial(polynomialB, variables);
279 |
280 | delta += Math.sqrt((genomeY - targetY) ** 2);
281 | }
282 |
283 | const avgDelta = delta / numPointsToTest;
284 | return avgDelta;
285 | };
286 |
287 | export {};
288 |
--------------------------------------------------------------------------------
/src/libs/carGenetic.ts:
--------------------------------------------------------------------------------
1 | import { RectanglePoints } from '../types/vectors';
2 | import { Gene, Genome } from './genetic';
3 | import { bitsToFloat10, precisionConfigs } from './math/floats';
4 | import { linearPolynomial } from './math/polynomial';
5 | import { sigmoid, sigmoidToCategories } from './math/sigmoid';
6 | import { euclideanDistance } from './math/geometry';
7 |
8 | // Car has 8 distance sensors.
9 | export const CAR_SENSORS_NUM = 8;
10 |
11 | // Additional formula coefficient that is not connected to a sensor.
12 | export const BIAS_UNITS = 1;
13 |
14 | // How many genes we need to encode each numeric parameter for the formulas.
15 | export const GENES_PER_NUMBER = precisionConfigs.custom.totalBitsCount;
16 |
17 | // Based on 8 distance sensors we need to provide two formulas that would define car's behaviour:
18 | // 1. Engine formula (input: 8 sensors; output: -1 (backward), 0 (neutral), +1 (forward))
19 | // 2. Wheels formula (input: 8 sensors; output: -1 (left), 0 (straight), +1 (right))
20 | export const ENGINE_FORMULA_GENES_NUM = (CAR_SENSORS_NUM + BIAS_UNITS) * GENES_PER_NUMBER;
21 | export const WHEELS_FORMULA_GENES_NUM = (CAR_SENSORS_NUM + BIAS_UNITS) * GENES_PER_NUMBER;
22 |
23 | // The length of the binary genome of the car.
24 | export const GENOME_LENGTH = ENGINE_FORMULA_GENES_NUM + WHEELS_FORMULA_GENES_NUM;
25 |
26 | type LossParams = {
27 | wheelsPosition: RectanglePoints,
28 | parkingLotCorners: RectanglePoints,
29 | };
30 |
31 | // Loss function calculates how far the car is from the parking lot
32 | // by comparing the wheels positions with parking lot corners positions.
33 | export const carLoss = (params: LossParams): number => {
34 | const { wheelsPosition, parkingLotCorners } = params;
35 |
36 | const {
37 | fl: flWheel,
38 | fr: frWheel,
39 | br: brWheel,
40 | bl: blWheel,
41 | } = wheelsPosition;
42 |
43 | const {
44 | fl: flCorner,
45 | fr: frCorner,
46 | br: brCorner,
47 | bl: blCorner,
48 | } = parkingLotCorners;
49 |
50 | const flDistance = euclideanDistance(flWheel, flCorner);
51 | const frDistance = euclideanDistance(frWheel, frCorner);
52 | const brDistance = euclideanDistance(brWheel, brCorner);
53 | const blDistance = euclideanDistance(blWheel, blCorner);
54 |
55 | return (flDistance + frDistance + brDistance + blDistance) / 4;
56 | };
57 |
58 | export const carLossToFitness = (loss: number, alpha: number = 1): number => {
59 | return 1 / (alpha * loss + 1);
60 | };
61 |
62 | export type SensorValues = number[];
63 |
64 | export type FormulaCoefficients = number[];
65 |
66 | export type FormulaResult = -1 | 0 | 1;
67 |
68 | type DecodedGenome = {
69 | engineFormulaCoefficients: FormulaCoefficients,
70 | wheelsFormulaCoefficients: FormulaCoefficients,
71 | }
72 |
73 | export const decodeGenome = (genome: Genome): DecodedGenome => {
74 | const engineGenes: Gene[] = genome.slice(0, ENGINE_FORMULA_GENES_NUM);
75 | const wheelsGenes: Gene[] = genome.slice(
76 | ENGINE_FORMULA_GENES_NUM,
77 | ENGINE_FORMULA_GENES_NUM + WHEELS_FORMULA_GENES_NUM,
78 | );
79 |
80 | const engineFormulaCoefficients: FormulaCoefficients = genomeToNumbers(engineGenes, GENES_PER_NUMBER);
81 | const wheelsFormulaCoefficients: FormulaCoefficients = genomeToNumbers(wheelsGenes, GENES_PER_NUMBER);
82 |
83 | return {
84 | engineFormulaCoefficients,
85 | wheelsFormulaCoefficients,
86 | };
87 | };
88 |
89 | export const genomeToNumbers = (genome: Genome, genesPerNumber: number): number[] => {
90 | if (genome.length % genesPerNumber !== 0) {
91 | throw new Error('Wrong number of genes in the numbers genome');
92 | }
93 | const numbers: number[] = [];
94 | for (let numberIndex = 0; numberIndex < genome.length; numberIndex += genesPerNumber) {
95 | const number: number = bitsToFloat10(genome.slice(numberIndex, numberIndex + genesPerNumber));
96 | numbers.push(number);
97 | }
98 | return numbers;
99 | };
100 |
101 | export const engineFormula = (genome: Genome, sensors: SensorValues): FormulaResult => {
102 | const {engineFormulaCoefficients} = decodeGenome(genome);
103 | const rawResult = linearPolynomial(engineFormulaCoefficients, sensors);
104 | const normalizedResult = sigmoid(rawResult);
105 | return sigmoidToCategories(normalizedResult);
106 | };
107 |
108 | export const wheelsFormula = (genome: Genome, sensors: SensorValues): FormulaResult => {
109 | const {wheelsFormulaCoefficients} = decodeGenome(genome);
110 | const rawResult = linearPolynomial(wheelsFormulaCoefficients, sensors);
111 | const normalizedResult = sigmoid(rawResult);
112 | return sigmoidToCategories(normalizedResult);
113 | };
114 |
--------------------------------------------------------------------------------
/src/libs/genetic.ts:
--------------------------------------------------------------------------------
1 | import { weightedRandom } from './math/probability';
2 |
3 | export type Gene = 0 | 1;
4 |
5 | export type Genome = Gene[];
6 |
7 | export type Generation = Genome[];
8 |
9 | export type GenerationParams = {
10 | generationSize: number,
11 | genomeLength: number,
12 | };
13 |
14 | function createGenome(length: number): Genome {
15 | return new Array(length)
16 | .fill(null)
17 | .map(() => (Math.random() < 0.5 ? 0 : 1));
18 | }
19 |
20 | export function createGeneration(params: GenerationParams): Generation {
21 | const { generationSize, genomeLength } = params;
22 | return new Array(generationSize)
23 | .fill(null)
24 | .map(() => createGenome(genomeLength));
25 | }
26 |
27 | // The number between 0 and 1.
28 | export type Probability = number;
29 |
30 | // The number between 0 and 100.
31 | export type Percentage = number;
32 |
33 | // @see: https://en.wikipedia.org/wiki/Mutation_(genetic_algorithm)
34 | function mutate(genome: Genome, mutationProbability: Probability): Genome {
35 | // Conceive children.
36 | for (let geneIndex = 0; geneIndex < genome.length; geneIndex += 1) {
37 | const gene: Gene = genome[geneIndex];
38 | const mutatedGene: Gene = gene === 0 ? 1 : 0;
39 | genome[geneIndex] = Math.random() < mutationProbability ? mutatedGene : gene;
40 | }
41 | return genome;
42 | }
43 |
44 | type SelectionOptions = {
45 | mutationProbability: Probability,
46 | longLivingChampionsPercentage: Percentage,
47 | };
48 |
49 | // Performs Uniform Crossover: each bit is chosen from either parent with equal probability.
50 | // @see: https://en.wikipedia.org/wiki/Crossover_(genetic_algorithm)
51 | function mate(
52 | father: Genome,
53 | mother: Genome,
54 | mutationProbability: Probability,
55 | ): [Genome, Genome] {
56 | if (father.length !== mother.length) {
57 | throw new Error('Cannot mate different species');
58 | }
59 |
60 | const firstChild: Genome = [];
61 | const secondChild: Genome = [];
62 |
63 | // Conceive children.
64 | for (let geneIndex = 0; geneIndex < father.length; geneIndex += 1) {
65 | firstChild.push(
66 | Math.random() < 0.5 ? father[geneIndex] : mother[geneIndex]
67 | );
68 | secondChild.push(
69 | Math.random() < 0.5 ? father[geneIndex] : mother[geneIndex]
70 | );
71 | }
72 |
73 | return [
74 | mutate(firstChild, mutationProbability),
75 | mutate(secondChild, mutationProbability),
76 | ];
77 | }
78 |
79 | export type FitnessFunction = (genome: Genome) => number;
80 |
81 | // @see: https://en.wikipedia.org/wiki/Selection_(genetic_algorithm)
82 | export function select(
83 | generation: Generation,
84 | fitness: FitnessFunction,
85 | options: SelectionOptions,
86 | ) {
87 | const {
88 | mutationProbability,
89 | longLivingChampionsPercentage,
90 | } = options;
91 |
92 | const newGeneration: Generation = [];
93 |
94 | const oldGeneration = [...generation];
95 | // First one - the fittest one.
96 | oldGeneration.sort((genomeA: Genome, genomeB: Genome): number => {
97 | const fitnessA = fitness(genomeA);
98 | const fitnessB = fitness(genomeB);
99 | if (fitnessA < fitnessB) {
100 | return 1;
101 | }
102 | if (fitnessA > fitnessB) {
103 | return -1;
104 | }
105 | return 0;
106 | });
107 |
108 | // Let long-liver champions continue living in the new generation.
109 | const longLiversCount = Math.floor(longLivingChampionsPercentage * oldGeneration.length / 100);
110 | if (longLiversCount) {
111 | oldGeneration.slice(0, longLiversCount).forEach((longLivingGenome: Genome) => {
112 | newGeneration.push(longLivingGenome);
113 | });
114 | }
115 |
116 | // Get the data about he fitness of each individuum.
117 | const fitnessPerOldGenome: number[] = oldGeneration.map((genome: Genome) => fitness(genome));
118 |
119 | // Populate the next generation until it becomes the same size as a old generation.
120 | while (newGeneration.length < generation.length) {
121 | // Select random father and mother from the population.
122 | // The fittest individuums have higher chances to be selected.
123 | let father: Genome | null = null;
124 | let fatherGenomeIndex: number | null = null;
125 | let mother: Genome | null = null;
126 | let matherGenomeIndex: number | null = null;
127 |
128 | // To produce children the father and mother need each other.
129 | // It must be two different individuums.
130 | while (!father || !mother || fatherGenomeIndex === matherGenomeIndex) {
131 | const {
132 | item: randomFather,
133 | index: randomFatherGenomeIndex,
134 | } = weightedRandom(generation, fitnessPerOldGenome);
135 |
136 | const {
137 | item: randomMother,
138 | index: randomMotherGenomeIndex,
139 | } = weightedRandom(generation, fitnessPerOldGenome);
140 |
141 | father = randomFather;
142 | fatherGenomeIndex = randomFatherGenomeIndex;
143 |
144 | mother = randomMother;
145 | matherGenomeIndex = randomMotherGenomeIndex;
146 | }
147 |
148 | // Let father and mother produce two children.
149 | const [firstChild, secondChild] = mate(father, mother, mutationProbability);
150 |
151 | newGeneration.push(firstChild);
152 |
153 | // Depending on the number of long-living champions it is possible that
154 | // there will be the place for only one child, sorry.
155 | if (newGeneration.length < generation.length) {
156 | newGeneration.push(secondChild);
157 | }
158 | }
159 |
160 | return newGeneration;
161 | }
162 |
--------------------------------------------------------------------------------
/src/libs/math/__tests__/floats.test.ts:
--------------------------------------------------------------------------------
1 | import { Bit, Bits, bitsToFloat16, bitsToFloat10 } from '../floats';
2 |
3 | const testCases10Bits: [number, string][] = [
4 | [-504, '1111111111'],
5 | [-496, '1111111110'],
6 | [-0.0146484375, '1000011100'],
7 | [0.0078125, '0000000000'],
8 | [0.008056640625, '0000000001'],
9 | [0.0625, '0001100000'],
10 | [244, '0111011101'],
11 | [256, '0111100000'],
12 | [384, '0111110000'],
13 | [488, '0111111101'],
14 | [496, '0111111110'],
15 | [504, '0111111111'],
16 | ];
17 |
18 | const testCases16Bits: [number, string][] = [
19 | [-65504, '1111101111111111'],
20 | [-10344, '1111000100001101'],
21 | [-27.15625, '1100111011001010'],
22 | [-1, '1011110000000000'],
23 | [-0.09997558, '1010111001100110'],
24 | [0, '0000000000000000'],
25 | [5.9604644775390625e-8, '0000000000000001'],
26 | [0.000004529, '0000000001001100'],
27 | [0.0999755859375, '0010111001100110'],
28 | [0.199951171875, '0011001001100110'],
29 | [0.300048828125, '0011010011001101'],
30 | [1, '0011110000000000'],
31 | [1.5, '0011111000000000'],
32 | [1.75, '0011111100000000'],
33 | [1.875, '0011111110000000'],
34 | [65504, '0111101111111111'],
35 | ];
36 |
37 | describe('floats', () => {
38 | for (let testCaseIndex = 0; testCaseIndex < testCases10Bits.length; testCaseIndex += 1) {
39 | const [decimal, binary] = testCases10Bits[testCaseIndex];
40 |
41 | it(`10 bits: #${testCaseIndex}: should convert ${binary} to ${decimal}`, () => {
42 | const bits: Bits = binary
43 | .split('')
44 | .map((bitString) => parseInt(bitString, 10) === 1 ? 1 : 0);
45 | expect(bitsToFloat10(bits)).toBeCloseTo(decimal, 4);
46 | });
47 | }
48 |
49 | for (let testCaseIndex = 0; testCaseIndex < testCases16Bits.length; testCaseIndex += 1) {
50 | const [decimal, binary] = testCases16Bits[testCaseIndex];
51 |
52 | it(`16 bits: #${testCaseIndex}: should convert ${binary} to ${decimal}`, () => {
53 | const bits: Bits = binary
54 | .split('')
55 | .map((bitString) => parseInt(bitString, 10) === 1 ? 1 : 0);
56 | expect(bitsToFloat16(bits)).toBeCloseTo(decimal, 4);
57 | });
58 | }
59 | });
60 |
61 | export {};
62 |
--------------------------------------------------------------------------------
/src/libs/math/__tests__/geometry.test.ts:
--------------------------------------------------------------------------------
1 | import { euclideanDistance } from '../geometry';
2 | import { NumVec3 } from '../../../types/vectors';
3 |
4 | const testCases: [NumVec3, NumVec3, number][] = [
5 | [[0, 0, 0], [0, 0, 1], 1],
6 | [[0, 0, 0], [0, 0, 5], 5],
7 | [[0, 10, 0], [0, 10, 5], 5],
8 | [[1, 0, 0], [0, 0, 0], 1],
9 | [[4, 0, 0], [0, 0, 0], 4],
10 | [[0, 0, 0], [1, 0, 1], 1.41],
11 | [[1, 0, 1], [1, 0, 1], 0],
12 | [[5, 0, 8], [10, 0, 12], 6.4],
13 | ];
14 |
15 | describe('geometry', () => {
16 | it('should calculate euclidean distance correctly', () => {
17 | for (let testCaseIndex = 0; testCaseIndex < testCases.length; testCaseIndex += 1) {
18 | const from: NumVec3 = testCases[testCaseIndex][0];
19 | const to: NumVec3 = testCases[testCaseIndex][1];
20 | const expectedDistance: number = testCases[testCaseIndex][2];
21 | const calculatedDistance: number = Math.floor(euclideanDistance(from, to) * 100) / 100;
22 | expect(calculatedDistance).toBe(expectedDistance);
23 | }
24 | });
25 | });
26 |
27 | export {};
28 |
--------------------------------------------------------------------------------
/src/libs/math/__tests__/polynomial.test.ts:
--------------------------------------------------------------------------------
1 | import { linearPolynomial } from '../polynomial';
2 |
3 | const testCases: [number[], number[], number][] = [
4 | [[0, 0, 0], [0, 0], 0],
5 | [[0, 0, 0], [1, 2], 0],
6 | [[1, 1, 1], [1, 2], 4],
7 | [[1, 2, 3], [4, 5], 17],
8 | [[1, 2, 3], [0, 0], 3],
9 | ];
10 |
11 | describe('polynomial', () => {
12 | it('should calculate polynomial correctly', () => {
13 | for (let testCaseIndex = 0; testCaseIndex < testCases.length; testCaseIndex += 1) {
14 | const coefficients: number[] = testCases[testCaseIndex][0];
15 | const variables: number[] = testCases[testCaseIndex][1];
16 | const expectedResult: number = testCases[testCaseIndex][2];
17 | const calculatedResult: number = Math.floor(linearPolynomial(coefficients, variables) * 100) / 100;
18 | expect(calculatedResult).toBe(expectedResult);
19 | }
20 | });
21 | });
22 |
23 | export {};
24 |
--------------------------------------------------------------------------------
/src/libs/math/__tests__/probability.test.ts:
--------------------------------------------------------------------------------
1 | import { weightedRandom } from '../probability';
2 |
3 | describe('weightedRandom', () => {
4 | it('should correctly do random selection based on wights', () => {
5 | expect(weightedRandom([1, 2, 3], [10, 0, 0])).toEqual({index: 0, item: 1});
6 | expect(weightedRandom([1, 2, 3], [0, 10, 0])).toEqual({index: 1, item: 2});
7 | expect(weightedRandom([1, 2, 3], [0, 0, 10])).toEqual({index: 2, item: 3});
8 | expect(weightedRandom([1, 2, 3], [0, 10, 10])).not.toEqual({index: 0, item: 1});
9 | expect(weightedRandom([1, 2, 3], [10, 0, 10])).not.toEqual({index: 1, item: 2});
10 |
11 | const counter1: number[] = [];
12 | for (let i = 0; i < 1000; i += 1) {
13 | const randomItem = weightedRandom([0, 1, 2], [10, 30, 60]);
14 | if (!counter1[randomItem.index]) {
15 | counter1[randomItem.index] = 1;
16 | } else {
17 | counter1[randomItem.index] += 1;
18 | }
19 | }
20 |
21 | expect(counter1[0]).toBeGreaterThan(50);
22 | expect(counter1[0]).toBeLessThan(150);
23 |
24 | expect(counter1[1]).toBeGreaterThan(250);
25 | expect(counter1[1]).toBeLessThan(350);
26 |
27 | expect(counter1[2]).toBeGreaterThan(550);
28 | expect(counter1[2]).toBeLessThan(650);
29 | });
30 | });
31 |
32 | export {};
33 |
--------------------------------------------------------------------------------
/src/libs/math/__tests__/sigmoid.test.ts:
--------------------------------------------------------------------------------
1 | import { sigmoid, sigmoidToCategories } from '../sigmoid';
2 |
3 | const sigmoidTestCases: [number, number][] = [
4 | [-100, 0],
5 | [-50, 0],
6 | [-20, 0],
7 | [-10, 0.0000453],
8 | [-1, 0.2689414],
9 | [-0.5, 0.3775406],
10 | [0, 0.5],
11 | [0.5, 0.6224593],
12 | [1, 0.7310585],
13 | [10, 0.9999546],
14 | [20, 0.9999999],
15 | [50, 1],
16 | [100, 1],
17 | ];
18 |
19 | const sigmoidToCategoriesTestCases: [number, number][] = [
20 | [-100, -1],
21 | [-50, -1],
22 | [-20, -1],
23 | [-10, 0],
24 | [-8, 0],
25 | [-5, 0],
26 | [-1, 0],
27 | [-0.5, 0],
28 | [0, 0],
29 | [0.5, 0],
30 | [1, 0],
31 | [5, 0],
32 | [8, 0],
33 | [10, 0],
34 | [20, 1],
35 | [50, 1],
36 | [100, 1],
37 | ];
38 |
39 | describe('sigmoid', () => {
40 | for (let testCaseIndex = 0; testCaseIndex < sigmoidTestCases.length; testCaseIndex += 1) {
41 | const inputNumber: number = sigmoidTestCases[testCaseIndex][0];
42 | const expectedResult: number = sigmoidTestCases[testCaseIndex][1];
43 |
44 | it(`should calculate sigmoid correctly for input of ${inputNumber}`, () => {
45 | const calculatedResult: number = Math.floor(sigmoid(inputNumber) * 10000000) / 10000000;
46 | expect(calculatedResult).toBe(expectedResult);
47 | });
48 | }
49 |
50 |
51 | for (let testCaseIndex = 0; testCaseIndex < sigmoidToCategoriesTestCases.length; testCaseIndex += 1) {
52 | const inputNumber: number = sigmoidToCategoriesTestCases[testCaseIndex][0];
53 | const expectedResult: number = sigmoidToCategoriesTestCases[testCaseIndex][1];
54 |
55 | it(`should calculate sigmoidToCategories correctly for input of ${inputNumber}`, () => {
56 | const calculatedResult: number = sigmoidToCategories(sigmoid(inputNumber));
57 | expect(calculatedResult).toBe(expectedResult);
58 | });
59 | }
60 | });
61 |
62 | export {};
63 |
--------------------------------------------------------------------------------
/src/libs/math/floats.ts:
--------------------------------------------------------------------------------
1 | export type Bit = 0 | 1;
2 |
3 | export type Bits = Bit[];
4 |
5 | export type PrecisionConfig = {
6 | signBitsCount: number,
7 | exponentBitsCount: number,
8 | fractionBitsCount: number,
9 | totalBitsCount: number,
10 | };
11 |
12 | export type PrecisionConfigs = {
13 | custom: PrecisionConfig,
14 | half: PrecisionConfig,
15 | single: PrecisionConfig,
16 | double: PrecisionConfig,
17 | };
18 |
19 | /*
20 | ┌───────────────── sign bit
21 | │ ┌───────────── exponent bits
22 | │ │ ┌───── fraction bits
23 | │ │ │
24 | X XXXXX XXXXXXXXXX
25 |
26 | @see: https://trekhleb.dev/blog/2021/binary-floating-point/
27 | */
28 | export const precisionConfigs: PrecisionConfigs = {
29 | // Custom-made 10-bits precision for faster evolution progress.
30 | custom: {
31 | signBitsCount: 1,
32 | exponentBitsCount: 4,
33 | fractionBitsCount: 5,
34 | totalBitsCount: 10,
35 | },
36 | // @see: https://en.wikipedia.org/wiki/Half-precision_floating-point_format
37 | half: {
38 | signBitsCount: 1,
39 | exponentBitsCount: 5,
40 | fractionBitsCount: 10,
41 | totalBitsCount: 16,
42 | },
43 | // @see: https://en.wikipedia.org/wiki/Single-precision_floating-point_format
44 | single: {
45 | signBitsCount: 1,
46 | exponentBitsCount: 8,
47 | fractionBitsCount: 23,
48 | totalBitsCount: 32,
49 | },
50 | // @see: https://en.wikipedia.org/wiki/Double-precision_floating-point_format
51 | double: {
52 | signBitsCount: 1,
53 | exponentBitsCount: 11,
54 | fractionBitsCount: 52,
55 | totalBitsCount: 64,
56 | },
57 | };
58 |
59 | // Converts the binary representation of the floating point number to decimal float number.
60 | function bitsToFloat(bits: Bits, precisionConfig: PrecisionConfig): number {
61 | const { signBitsCount, exponentBitsCount } = precisionConfig;
62 |
63 | // Figuring out the sign.
64 | const sign = (-1) ** bits[0]; // -1^1 = -1, -1^0 = 1
65 |
66 | // Calculating the exponent value.
67 | const exponentBias = 2 ** (exponentBitsCount - 1) - 1;
68 | const exponentBits = bits.slice(signBitsCount, signBitsCount + exponentBitsCount);
69 | const exponentUnbiased = exponentBits.reduce(
70 | (exponentSoFar: number, currentBit: Bit, bitIndex: number) => {
71 | const bitPowerOfTwo = 2 ** (exponentBitsCount - bitIndex - 1);
72 | return exponentSoFar + currentBit * bitPowerOfTwo;
73 | },
74 | 0,
75 | );
76 | const exponent = exponentUnbiased - exponentBias;
77 |
78 | // Calculating the fraction value.
79 | const fractionBits = bits.slice(signBitsCount + exponentBitsCount);
80 | const fraction = fractionBits.reduce(
81 | (fractionSoFar: number, currentBit: Bit, bitIndex: number) => {
82 | const bitPowerOfTwo = 2 ** -(bitIndex + 1);
83 | return fractionSoFar + currentBit * bitPowerOfTwo;
84 | },
85 | 0,
86 | );
87 |
88 | // Putting all parts together to calculate the final number.
89 | return sign * (2 ** exponent) * (1 + fraction);
90 | }
91 |
92 | // Converts the 16-bit binary representation of the floating point number to decimal float number.
93 | export function bitsToFloat16(bits: Bits): number {
94 | return bitsToFloat(bits, precisionConfigs.half);
95 | }
96 |
97 | // Converts the 8-bit binary representation of the floating point number to decimal float number.
98 | export function bitsToFloat10(bits: Bits): number {
99 | return bitsToFloat(bits, precisionConfigs.custom);
100 | }
101 |
--------------------------------------------------------------------------------
/src/libs/math/geometry.ts:
--------------------------------------------------------------------------------
1 | import { NumVec3 } from '../../types/vectors';
2 |
3 | // Calculates the XZ distance between two points in space.
4 | // The vertical Y distance is not being taken into account.
5 | export const euclideanDistance = (from: NumVec3, to: NumVec3) => {
6 | const fromX = from[0];
7 | const fromZ = from[2];
8 | const toX = to[0];
9 | const toZ = to[2];
10 | return Math.sqrt((fromX - toX) ** 2 + (fromZ - toZ) ** 2);
11 | };
12 |
--------------------------------------------------------------------------------
/src/libs/math/polynomial.ts:
--------------------------------------------------------------------------------
1 | export const linearPolynomial = (coefficients: number[], variables: number[]): number => {
2 | if (coefficients.length !== (variables.length + 1)) {
3 | throw new Error(`Incompatible number polynomial coefficients and variables: ${coefficients.length} and ${variables.length}`);
4 | }
5 | let result = 0;
6 | coefficients.forEach((coefficient: number, coefficientIndex: number) => {
7 | if (coefficientIndex < variables.length) {
8 | result += coefficient * variables[coefficientIndex];
9 | } else {
10 | result += coefficient
11 | }
12 | });
13 | return result;
14 | };
15 |
--------------------------------------------------------------------------------
/src/libs/math/probability.ts:
--------------------------------------------------------------------------------
1 | // Picks the random item based on its weight.
2 | // The items with higher weight will be picked more often.
3 | export const weightedRandom = (items: T[], weights: number[]): { item: T, index: number } => {
4 | if (items.length !== weights.length) {
5 | throw new Error('Items and weights must be of the same size');
6 | }
7 |
8 | // Preparing the cumulative weights array.
9 | // For example:
10 | // - weights = [1, 4, 3]
11 | // - cumulativeWeights = [1, 5, 8]
12 | const cumulativeWeights: number[] = [];
13 | for (let i = 0; i < weights.length; i += 1) {
14 | cumulativeWeights[i] = weights[i] + (cumulativeWeights[i - 1] || 0);
15 | }
16 |
17 | // Getting the random number in a range [0...sum(weights)]
18 | // For example:
19 | // - weights = [1, 4, 3]
20 | // - maxCumulativeWeight = 8
21 | // - range for the random number is [0...8]
22 | const maxCumulativeWeight = cumulativeWeights[cumulativeWeights.length - 1];
23 | const randomNumber = maxCumulativeWeight * Math.random();
24 |
25 | // Picking the random item based on its weight.
26 | // The items with higher weight will be picked more often.
27 | for (let i = 0; i < items.length; i += 1) {
28 | if (cumulativeWeights[i] >= randomNumber) {
29 | return {
30 | item: items[i],
31 | index: i,
32 | };
33 | }
34 | }
35 | return {
36 | item: items[items.length - 1],
37 | index: items.length - 1,
38 | };
39 | };
40 |
--------------------------------------------------------------------------------
/src/libs/math/sigmoid.ts:
--------------------------------------------------------------------------------
1 | export const sigmoid = (x: number): number => {
2 | return 1 / (1 + Math.E ** -x);
3 | };
4 |
5 | export const sigmoidToCategories = (
6 | sigmoidValue: number,
7 | aroundZeroMargin: number = 0.49999, // Value between 0 and 0.5: [0 ... (0.5 - margin) ... 0.5 ... (0.5 + margin) ... 1]
8 | ): -1 | 0 | 1 => {
9 | if (sigmoidValue < (0.5 - aroundZeroMargin)) {
10 | return -1;
11 | }
12 | if (sigmoidValue > (0.5 + aroundZeroMargin)) {
13 | return 1;
14 | }
15 | return 0;
16 | };
17 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare module 'react-nipple';
3 | declare module 'three-mesh-bvh';
4 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/types/vectors.ts:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 |
3 | export type NumVec2 = [number, number];
4 | export type NumVec3 = [number, number, number];
5 | export type NumVec4 = [number, number, number, number];
6 |
7 | export type RectanglePoints = {
8 | fl: NumVec3, // Front-left
9 | fr: NumVec3, // Front-right
10 | bl: NumVec3, // Back-left
11 | br: NumVec3, // Back-right
12 | };
13 |
14 | export type ThreeRectanglePoints = {
15 | fl: THREE.Vector3, // Front-left
16 | fr: THREE.Vector3, // Front-right
17 | bl: THREE.Vector3, // Back-left
18 | br: THREE.Vector3, // Back-right
19 | };
20 |
--------------------------------------------------------------------------------
/src/utils/colors.ts:
--------------------------------------------------------------------------------
1 | import colors from 'nice-color-palettes';
2 |
3 | export const getRandomColor = (): string => {
4 | const flatColors = colors.flat();
5 | const colorIndex = Math.floor(Math.random() * flatColors.length);
6 | return flatColors[colorIndex];
7 | };
8 |
--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------
1 | import { getSearchParam } from './url';
2 |
3 | const LOG_SEARCH_PARAM_NAME = 'debug';
4 |
5 | const logToConsole = !!getSearchParam(LOG_SEARCH_PARAM_NAME);
6 |
7 | type LoggerParams = {
8 | context: string,
9 | };
10 |
11 | type LoggerCallback = (...params: any[]) => void;
12 |
13 | type Logger = {
14 | info: LoggerCallback,
15 | warn: LoggerCallback,
16 | error: LoggerCallback,
17 | };
18 |
19 | export const loggerBuilder = (params: LoggerParams): Logger => {
20 | const { context } = params;
21 |
22 | const info: LoggerCallback = (message, ...optionalParams) => {
23 | if (!logToConsole) {
24 | return;
25 | }
26 | console.log(
27 | `%c${context}`,
28 | 'background: orange; color: white; padding: 0 3px; border-radius: 3px;',
29 | '→',
30 | message,
31 | ...optionalParams
32 | );
33 | };
34 |
35 | const warn: LoggerCallback = (message, ...optionalParams) => {
36 | if (!logToConsole) {
37 | return;
38 | }
39 | console.info(
40 | `%c${context}`,
41 | 'background: red; color: white; padding: 0 3px; border-radius: 3px;',
42 | '→',
43 | message,
44 | ...optionalParams
45 | );
46 | };
47 |
48 | const error: LoggerCallback = (message, ...optionalParams) => {
49 | if (!logToConsole) {
50 | return;
51 | }
52 | console.error(context, message, ...optionalParams);
53 | };
54 |
55 | return { info, warn, error };
56 | };
57 |
--------------------------------------------------------------------------------
/src/utils/storage.ts:
--------------------------------------------------------------------------------
1 | import { loggerBuilder } from './logger';
2 |
3 | export const write = (key: string, data: any): boolean => {
4 | const logger = loggerBuilder({context: 'storage::write'});
5 | try {
6 | const stringifiedData = JSON.stringify(data);
7 | localStorage.setItem(key, stringifiedData);
8 | logger.info(`Wrote data with the key "${key}" to storage successfully`);
9 | } catch (error) {
10 | logger.error('Cannot write data to storage', error);
11 | return false;
12 | }
13 | return true;
14 | };
15 |
16 | export const read = (key: string): any | null => {
17 | const logger = loggerBuilder({context: 'storage::read'});
18 | try {
19 | const stringifiedData: string | null = localStorage.getItem(key);
20 | logger.info(`Read data with the key "${key}" from storage successfully`);
21 | if (!stringifiedData) {
22 | return stringifiedData;
23 | }
24 | return JSON.parse(stringifiedData);
25 | } catch (error) {
26 | logger.error('Cannot read data to storage', error);
27 | return null;
28 | }
29 | };
30 |
31 | export const remove = (key: string): void => {
32 | const logger = loggerBuilder({context: 'storage::remove'});
33 | localStorage.removeItem(key);
34 | logger.info(`Removed data with the key "${key}" from storage`);
35 | };
36 |
--------------------------------------------------------------------------------
/src/utils/url.ts:
--------------------------------------------------------------------------------
1 | export const getSearchParam = (name: string): string | null => {
2 | const searchParams = getSearchParams();
3 | return searchParams.get(name);
4 | };
5 |
6 | export const setSearchParam = (name: string, value: string): void => {
7 | const searchParams = getSearchParams();
8 | searchParams.set(name, value);
9 | const relativeURL = '?' + searchParams.toString() + document.location.hash;
10 | window.history.pushState(null, '', relativeURL);
11 | };
12 |
13 | export const deleteSearchParam = (name: string): void => {
14 | const searchParams = getSearchParams();
15 | searchParams.delete(name);
16 | const relativeURL = '?' + searchParams.toString() + document.location.hash;
17 | window.history.pushState(null, '', relativeURL);
18 | };
19 |
20 | const getSearchParams = (): URLSearchParams => {
21 | const searchQuery = document.location.search.substring(1);
22 | return new URLSearchParams(searchQuery);
23 | };
24 |
25 | export const getStringSearchParam = (name: string, defaultValue: string): string => {
26 | const searchParam: string | null = getSearchParam(name);
27 | if (searchParam === null) {
28 | return defaultValue;
29 | }
30 | return searchParam;
31 | };
32 |
33 | export const getIntSearchParam = (name: string, defaultValue: number): number => {
34 | const searchParam: string | null = getSearchParam(name);
35 | if (searchParam === null) {
36 | return defaultValue;
37 | }
38 | return parseInt(searchParam);
39 | };
40 |
41 | export const getFloatSearchParam = (name: string, defaultValue: number): number => {
42 | const searchParam: string | null = getSearchParam(name);
43 | if (searchParam === null) {
44 | return defaultValue;
45 | }
46 | return parseFloat(searchParam);
47 | };
48 |
49 | export const getBooleanSearchParam = (name: string, defaultValue: boolean): boolean => {
50 | const searchParam: string | null = getSearchParam(name);
51 | if (searchParam === null) {
52 | return defaultValue;
53 | }
54 | return searchParam.toLowerCase() === 'true' ? true : false;
55 | };
56 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------