From 6999a8532e145a1d34ead1c642072d77cc2c709f Mon Sep 17 00:00:00 2001 From: Thibault Neveu Date: Wed, 6 Jun 2018 10:04:06 +0100 Subject: [PATCH] WIP: js to ts --- README.md | 57 ++++++- demo/app.js | 64 +++++++ demo/app.yaml | 32 ++++ demo/package.json | 14 ++ demo/webapp/index.html | 10 ++ package.json | 22 +++ src/asset_manager.ts | 313 +++++++++++++++++++++++++++++++++++ src/basic_motion_engine.ts | 163 ++++++++++++++++++ src/bot_motion_engine.ts | 244 +++++++++++++++++++++++++++ src/car.ts | 236 ++++++++++++++++++++++++++ src/control_motion_engine.ts | 178 ++++++++++++++++++++ src/global.ts | 109 ++++++++++++ src/index.ts | 2 + src/level.ts | 215 ++++++++++++++++++++++++ src/metacar.ts | 121 ++++++++++++++ src/motion_engine.ts | 104 ++++++++++++ src/utils.ts | 168 +++++++++++++++++++ tsconfig.json | 30 ++++ webpack.config.js | 35 ++++ 19 files changed, 2116 insertions(+), 1 deletion(-) create mode 100644 demo/app.js create mode 100644 demo/app.yaml create mode 100644 demo/package.json create mode 100644 demo/webapp/index.html create mode 100644 package.json create mode 100644 src/asset_manager.ts create mode 100644 src/basic_motion_engine.ts create mode 100644 src/bot_motion_engine.ts create mode 100644 src/car.ts create mode 100644 src/control_motion_engine.ts create mode 100644 src/global.ts create mode 100644 src/index.ts create mode 100644 src/level.ts create mode 100644 src/metacar.ts create mode 100644 src/motion_engine.ts create mode 100644 src/utils.ts create mode 100644 tsconfig.json create mode 100644 webpack.config.js diff --git a/README.md b/README.md index 6f54258..585ccad 100644 --- a/README.md +++ b/README.md @@ -1 +1,56 @@ -# metacar \ No newline at end of file +# Quickstart for Node.js in the App Engine flexible environment + +This is the sample application for the +[Quickstart for Node.js in the App Engine flexible environment][tutorial] +tutorial found in the [Google App Engine Node.js flexible environment][appengine] +documentation. + +* [Setup](#setup) +* [Running locally](#running-locally) +* [Deploying to App Engine](#deploying-to-app-engine) +* [Running the tests](#running-the-tests) + +## Setup + +Before you can run or deploy the sample, you need to do the following: + +1. Refer to the [appengine/README.md][readme] file for instructions on + running and deploying. +1. Install dependencies: + + With `npm`: + + npm install + + or with `yarn`: + + yarn install + +## Running locally + +With `npm`: + + npm start + +or with `yarn`: + + yarn start + +## Deploying to App Engine + +With `npm`: + + npm run deploy + +or with `yarn`: + + yarn run deploy + +## Running the tests + +See [Contributing][contributing]. + +[appengine]: https://cloud.google.com/appengine/docs/flexible/nodejs +[tutorial]: https://cloud.google.com/appengine/docs/flexible/nodejs/quickstart +[readme]: ../../README.md +[contributing]: https://github.com/GoogleCloudPlatform/nodejs-docs-samples/blob/master/CONTRIBUTING.md diff --git a/demo/app.js b/demo/app.js new file mode 100644 index 0000000..f8cde0d --- /dev/null +++ b/demo/app.js @@ -0,0 +1,64 @@ +'use strict'; + +var fs = require('fs'); +// [START app] +const express = require('express'); +var path = require("path"); + + +function fromDir(startPath,filter){ + + //console.log('Starting from dir '+startPath+'/'); + + if (!fs.existsSync(startPath)){ + console.log("no dir ",startPath); + return []; + } + + var files_list = []; + var files=fs.readdirSync(startPath); + for(var i=0;i=0) { + files_list.push(filename); + }; + }; + return files_list; +} + +const app = express(); + +app.use("/dist", express.static(path.join(__dirname, "dist/"))); +app.use("/public", express.static(path.join(__dirname, "webapp/public/"))); + + +function get_path(file){ + return path.join(path.join(__dirname, "webapp/"), file); +} + +app.get('/', (req, res) => { + res.sendFile(get_path("index.html")); +}); + +const files = fromDir(path.join(__dirname, "webapp/"),'.html'); +files.forEach(file => { + let route = file.split("/"); + route = route[route.length - 1]; + + console.log("Open route:", route, file); + app.get('/'+route, (req, res) => { + res.sendFile(file); + }); +}); + +// Start the server +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`App listening on port ${PORT}`); + console.log('Press Ctrl+C to quit.'); +}); +// [END app] diff --git a/demo/app.yaml b/demo/app.yaml new file mode 100644 index 0000000..ddd5726 --- /dev/null +++ b/demo/app.yaml @@ -0,0 +1,32 @@ +# Copyright 2017, Google, Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app_yaml] +runtime: nodejs +env: flex + +skip_files: +- yarn.lock + +# This sample incurs costs to run on the App Engine flexible environment. +# The settings below are to reduce costs during testing and are not appropriate +# for production use. For more information, see: +# https://cloud.google.com/appengine/docs/flexible/nodejs/configuring-your-app-with-app-yaml +manual_scaling: + instances: 1 +resources: + cpu: 1 + memory_gb: 0.5 + disk_size_gb: 10 + +# [END app_yaml] diff --git a/demo/package.json b/demo/package.json new file mode 100644 index 0000000..f8eec2a --- /dev/null +++ b/demo/package.json @@ -0,0 +1,14 @@ +{ + "name": "metacar-demo", + "version": "0.0.1", + "description": "", + "main": "app.js", + "scripts": { + "start": "node app.js" + }, + "author": "Thibault Neveu", + "license": "ISC", + "dependencies": { + "express": "^4.16.3" + } +} diff --git a/demo/webapp/index.html b/demo/webapp/index.html new file mode 100644 index 0000000..ef6037b --- /dev/null +++ b/demo/webapp/index.html @@ -0,0 +1,10 @@ + + + + + Hello World + + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..15f9a03 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "metacar", + "version": "0.0.1", + "main": "index.js", + "author": "Thibault Neveu", + "license": "MIT", + "devDependencies": { + "ts-loader": "^4.3.1", + "typescript": "^2.9.1", + "webpack": "^4.10.2", + "webpack-cli": "^3.0.2" + }, + "dependencies": { + "@types/pixi.js": "^4.7.5", + "express": "^4.16.3", + "pixi.js": "^4.8.0" + }, + "scripts": { + "build": "./node_modules/.bin/webpack-cli --mode production", + "watch": "./node_modules/.bin/webpack-cli --watch --mode development" + } +} diff --git a/src/asset_manager.ts b/src/asset_manager.ts new file mode 100644 index 0000000..61df32c --- /dev/null +++ b/src/asset_manager.ts @@ -0,0 +1,313 @@ +/* + @AssetManger class + Used to create all assets on the map (Trees, roads, cars...) +*/ + +import {Level, LevelInfo} from "./level"; +import * as U from "./utils"; + +import { + ROADSIZE, Graphics, Sprite, ASSETS, MAP, CAR_IMG +} from "./global"; + +import {CarOptions, Car} from "./car"; +import {BasicMotionEngine} from "./basic_motion_engine"; +import {ControlMotionEngine} from "./control_motion_engine"; +import {BotMotionEngine} from "./bot_motion_engine"; + +export interface AssetInfo { + readonly mx?: number; + readonly my?: number; + readonly type?: any; + readonly x?: number; + readonly y?: number; +}; + +export interface RoadSprite extends PIXI.Sprite { + // The road is not an obstacle (should be false) + obstacle?: boolean; + // Id of the road + mapId?: number; + // Orientation of the road + orientation?: number; + // Method call to set a car on this road + setCarPosition?: any; + // Array used to stored all the car currently on this road + // This method take a list of id (id of each car) + cars?: number[]; + // (x, y position) Relatif to the map + mx?: number; + my?: number; +} + +export interface SimpleSprite extends PIXI.Sprite { + obstacle?: boolean; + // name of the asset (not the name of the image) + type?: string; + mapId?: number; +} + +export class AssetManger { + + private level: Level; + // Used to list the differents MotionEngine possible to add on the car + private motion: any; + // List of all items on the map + private assets: SimpleSprite[] = []; + + constructor(level: Level) { + this.level = level; + this.motion = { + "BasicMotionEngine": BasicMotionEngine, + "ControlMotionEngine": ControlMotionEngine + } + } + + createRoadSide(info: AssetInfo, textures: any){ + /* + @textures: (Pixi textures) + */ + let x = (info.mx) * ROADSIZE; + let y = (info.my) * ROADSIZE; + + let area = new Graphics(); + area.beginFill(0xe8e8e8); + area.drawRoundedRect(0, 0, ROADSIZE+30, ROADSIZE+30, undefined); + area.x = x-15; + area.y = y-15; + area.endFill(); + + this.level.addChild(area); + } + + createRoad(info: AssetInfo, textures: any){ + /* + Method use to add a new road on the map + ↕, ↱ or ↔, ↰, ↲, ↳ + @textures: (Pixi textures) + */ + let road: RoadSprite = new Sprite(textures[ASSETS.ROADS[info.type].image]); + + // Set the position of this road + road.x = (info.mx) * ROADSIZE; + road.y = (info.my) * ROADSIZE; + + road.obstacle = false; + road.mapId = MAP.ROAD; + road.orientation = ASSETS.ROADS[info.type].orientation; + road.setCarPosition = (car: any, line: number) => this.setCarOnRoad(road, car, line); + road.cars = []; + road.mx = info.mx; // Map x + road.my = info.my; // Map y + + this.level.addRoad(road); + } + + setCarOnRoad(road: RoadSprite, car: any, line: number){ + /* + @road: (Obejct) road to position the car on + @car: (Object) car object to position + @line is Optional. 0 By default. + */ + if (line == undefined) + line = 0; + if (road.cars.length >= 1 && this.level.findCarById(road.cars[0]).core.line == line){ + line = line == 0 ? 1:0; + } + if (road.cars.length >= 2){ + car.is_valid = false; + return; + } + car.line = line; + + // Set the car on the road + car.x = road.x; + car.y = road.y; + car.x += ROADSIZE / 2; + car.y += ROADSIZE / 2; + let x_margin = 0 + let y_margin = (ROADSIZE*1/4); + // transform to map x coordinate + // transform to map y coordinate + road.orientation = (Math.floor(road.orientation / car.rotation_step)*car.rotation_step); + let theta = -road.orientation*(Math.PI); + let x_m = (Math.cos(theta) * x_margin) - (y_margin * Math.sin(theta)); + let y_m = (Math.cos(theta) * y_margin) + (x_margin * Math.sin(theta)); + + let line_side_factor = line == 0 ? 1:-1; + let line_side_factor_t = line == 0 ? 0:Math.PI; + car.x += line_side_factor*Math.round(x_m); + car.y += line_side_factor*Math.round(y_m); + + // (x, y position) Relatif to the map + car.mx = Math.floor(car.x / ROADSIZE); + car.my = Math.floor(car.y / ROADSIZE); + // Car rotation + car.rotation = (theta+line_side_factor_t); + // Set the new road of the car + car.checkAndsetNewRoad(road); + } + + createAsset(img: string, info: AssetInfo, textures: any, type: string){ + /* + Create a simple asset on the map + @type: (String) name of the asset (image) + @info: (Object) x,y position of the asset + @textures: (Pixi textures) + @type (String) name of the asset (not the name of the image) + */ + let asset: SimpleSprite = new Sprite(textures[img]); + // The road is not an obstacle + asset.obstacle = false; + // Set the id of the road) + asset.type = type; + asset.mapId = 0; + asset.x = info.x; + asset.y = info.y; + + this.level.addChild(asset); + this.assets.push(asset); + } + + createMap(map: (string|number)[][], info: LevelInfo, textures: PIXI.Texture, roadside:boolean=true){ + /* + Method used to create the map with all assets (except the cars) + @map (2dim Array) + @info (Object) Level's json. + @textures: (Pixi textures) + */ + if (roadside){ + // Draw the roads side + for (let my = 0; my < map.length; my++) { + for (let mx = 0; mx < map[my].length; mx++) { + if (ASSETS.ROADS[map[my][mx]]){ + this.createRoadSide({mx, my, type: map[my][mx]}, textures); + } + } + } + } + // Draw the roads + for (let my = 0; my < map.length; my++) { + for (let mx = 0; mx < map[my].length; mx++) { + if (ASSETS.ROADS[map[my][mx]]){ + this.createRoad({mx, my, type: map[my][mx]}, textures); + } + } + } + // Draw the others items + for (let key in ASSETS){ + if (key != "ROADS" && info[key]){ + for (let a=0; a < info[key].length; a++){ + this.createAsset(ASSETS[key].image, info[key][a], textures, key); + } + } + } + } + + createCars(map: (string|number)[][], info: LevelInfo, textures: PIXI.Texture){ + /* + Method used to create all the cars + @map (2dim Array) + @info (Object) Level's json. + @textures: (Pixi textures) + */ + // Go through all the bot cars on the map + for (let c in info.cars){ + let options: CarOptions = {lidar: false, lidarInfo: {pts: 2, width: 0.5, height: 1, pos: 1}}; + if (info.cars[c].auto){ + options.lidar = true; + options.motionEngine = new BotMotionEngine(this.level); + } + let n_car = new Car(this.level, info.cars[c], textures, options); + // Append the car to the canvas + this.level.addCar(n_car); + } + } + + createAgent(map: (string|number)[][], info: LevelInfo, textures: PIXI.Texture){ + /* + Method used to create the agent's car. + @map (2dim Array) + @info (Object) Level's json. + @textures: (Pixi textures) + */ + let agent = new Car(this.level, info.agent, textures, { + image: CAR_IMG.AGENT, + lidar: true, + motionEngine: new this.motion[info.agent.motion.type](this.level, info.agent.motion.options) + }); + + this.level.addChild(agent.lidar) + this.level.addCar(agent); + agent.core.agent = true; + + return agent; + } + + exportMap(width: number, height: number, file_name: string){ + /* + Export the map + @width (Integer) Width of the new map + @height (Integer) Height of the new map + @file_name (String) name of the file to export + */ + var file = {"cars": []}; + let map = []; + for (var y = 0; y < height; y++) { + let line = []; + for (var x = 0; x < width; x++) { + line.push(0); + } + map.push(line); + } + for (var e = 0; e < this.level.envs.length; e++) { + let elem = this.level.envs[e]; + if (!elem.isremove){ + // Add the cars + if (elem.mapId == MAP.CAR && !elem.agent && elem.is_valid){ + file.cars.push({ + "mx": elem.mx, + "my": elem.my, + "line": elem.line + }); + } + else if (elem.mapId == MAP.ROAD){ + map[elem.my][elem.mx] = elem.arrow; + } + } + } + for (var e = 0; e < this.assets.length; e++) { + let elem = this.assets[e]; + if (!elem.isremove){ + if (elem.type != "car"){ + if (!file[elem.type]) + file[elem.type] = []; + file[elem.type].push({ + "x": elem.x, + "y": elem.y + }); + } + } + } + // Set the map + file.map = map; + // If the agent exist + if (this.level.agent){ + file.agent = { + "mx": this.level.agent.core.mx, + "my": this.level.agent.core.my, + "line": this.level.agent.core.line, + "motion": { + "type": "BasicMotionEngine", + "options":{ + "rotation_step": 0.5, + "actions": ["UP", "LEFT", "RIGHT", "DOWN", "WAIT"] + } + } + }; + } + console.log(file); + file = JSON.stringify(file, null, 4); + U.saveAs(file, file_name); + } +} diff --git a/src/basic_motion_engine.ts b/src/basic_motion_engine.ts new file mode 100644 index 0000000..e3875e6 --- /dev/null +++ b/src/basic_motion_engine.ts @@ -0,0 +1,163 @@ +import { MotionEngine, MotionOption } from "./motion_engine"; +import {Level} from "./level"; + +import { + MAP, ROADSIZE +} from "./global"; + +import * as U from "./utils"; + +export class BasicMotionEngine extends MotionEngine { + /* + Basic Motion Engine + In this configuration the possible action of the environement are + either left, right, up, down or wait. + */ + private rotationStep: number; + private actions: string[]; + + constructor(level: Level, options: MotionOption) { + super(level); + this.rotationStep = options.rotationStep; + this.actions = options.actions; + } + + setUp(car: any, lidar: any){ + /* + Setup the motion engine + @car: (Vehicle Object) + @lidar: (Lidar Object) + */ + this.car = car; + this.lidar = lidar; + // Init the lidar state + this.state = []; + for (let y = 0; y < lidar.pts; y++) { + let line = []; + for (let x = 0; x < lidar.pts; x++) { + line.push(MAP.DEFAULT); + } + this.state.push(line); + } + // Set up keyboard interaction + this.setUpKeyboard(); + // Setup up velocity to 0 + this.car.v = 0; + + this.detectInteractions(); + } + + setUpKeyboard(){ + /* + Setup possible keyboard interactions + */ + let left = U.keyboard(37); + let up = U.keyboard(38); + let right = U.keyboard(39); + let down = U.keyboard(40); + + // Left and Right + if (this.actions.indexOf("LEFT") != -1) + left.press = () => { this.turnLeft(); }; + if (this.actions.indexOf("RIGHT") != -1) + right.press = () => { this.turnRight(); }; + // Move forward + if (this.actions.indexOf("UP") != -1){ + up.press = () => { this.moveForward(); }; + up.release = () => { + this.car.v = 0; + }; + } + if (this.actions.indexOf("DOWN") != -1){ + // Move backward + down.press = () => { this.moveBackward();}; + down.release = () => { + this.car.v = 0; + }; + } + } + + turnLeft(){ + /* + Turn left + */ + this.car.rotation -= this.rotationStep*Math.PI; + this.lidar.rotation = this.car.rotation; + } + + turnRight(){ + /* + Turn right + */ + this.car.rotation += this.rotationStep*Math.PI; + this.lidar.rotation = this.car.rotation; + } + + moveForward(){ + /* + Move forward + */ + this.car.v = 1; + } + + moveBackward(){ + /* + Move backward + */ + this.car.v = -1; + } + + actionStep(delta: number, action: number){ + /* + Step into the environement with one action + @delta (Float) time since the last update + @action: (Integer) The action to take (can be null if no action) + */ + if (this.actions[action] == "LEFT") { this.turnLeft();} + if (this.actions[action] == "RIGHT") { this.turnRight();} + if (this.actions[action] == "UP") { this.moveForward();} + if (this.actions[action] == "DOWN") { this.moveBackward();} + + let {agent_col, on_road} = this.step(delta); + this.car.v = 0; + return {agent_col, on_road}; + } + + actionSpace(){ + /* + Return an array with all possibles actions + Ex: [0, 1, 2] + */ + return Array.apply(null, {length: this.actions.length}).map(Number.call, Number); + } + + step(delta: number){ + /* + Step into the environement + @delta (Float) time since the last update + */ + // Update the x and y position according to the velocity + this.car.x += this.car.v * Math.cos(this.car.rotation)*delta; + this.car.y += this.car.v * Math.sin(this.car.rotation)*delta; + // Update the x and x position according to the map + this.car.mx = Math.floor(this.car.x / ROADSIZE); + this.car.my = Math.floor(this.car.y / ROADSIZE); + + // Set the new road of the car (if changed) + this.car.checkAndsetNewRoad(); + + // Be sure to keep the lidar position at the same position than the car + this.lidar.x = this.car.x; + this.lidar.y = this.car.y; + this.lidar.rotation = this.car.rotation; + + // Detection the new collision with the environement + let {agent_col, on_road} = this.detectInteractions(); + + if (agent_col.length > 0){ // Stop the vehicle if a collision is detected + this.car.v = 0; + this.car.vy = 0; + } + return {agent_col, on_road}; + } +} diff --git a/src/bot_motion_engine.ts b/src/bot_motion_engine.ts new file mode 100644 index 0000000..13fd653 --- /dev/null +++ b/src/bot_motion_engine.ts @@ -0,0 +1,244 @@ +import { MotionEngine } from "./motion_engine"; +import {Level} from "./level"; + +import { + MAP, ROADSIZE +} from "./global"; + +export class BotMotionEngine extends MotionEngine { + /* + Motion Engine for the bot cars (not for the agent) + This motion Engin let the cars move by themself. + The roation of the cars is either left, right or forward. + */ + + private mapSizeY: number; + private mapSizeX: number; + private rotationStep: number; + private map: (string|number)[][]; + private rotationToNextCase: any; + + constructor(level: Level) { + super(level); + + this.map = this. level.getMap(); + + this.mapSizeY = this.map.length; + this.mapSizeX = this.map[0].length; + this.rotationStep = 0.5; + + this.rotationToNextCase = {}; + this.rotationToNextCase[0] = {"mx": 1, "my": 0}; + this.rotationToNextCase[0.5] = {"mx": 0, "my": 1}; + this.rotationToNextCase[1.0] = {"mx": -1, "my": 0}; + this.rotationToNextCase[1.5] = {"mx": 0, "my": -1}; + this.rotationToNextCase[-0.5] = {"mx": 0, "my": -1}; + this.rotationToNextCase[-1.0] = {"mx": -1, "my": 0}; + this.rotationToNextCase[-1.5] = {"mx": 0, "my": 1}; + // this.rotationToNextCase[null] = {"mx": 0, "my": 0}; + } + + setUp(car: any, lidar: any){ + /* + Setup the motion engine + @car: (Vehicle Object) + @lidar: (Lidar Object) + */ + this.car = car; + this.lidar = lidar; + // Init the lidar state + this.state = []; + for (let y = 0; y < lidar.pts; y++) { + let line = []; + for (let x = 0; x < lidar.pts; x++) { + line.push(MAP.DEFAULT); + } + this.state.push(line); + } + // Setup up velocity to 0 + this.car.v = 0; + // Set up the lidar state by detecting interactions + this.detectInteractions(); + } + + turnLeft(){ + /* + Turn left + */ + this.car.rotation -= this.rotationStep*Math.PI; + this.lidar.rotation = this.car.rotation; + } + + turnRight(){ + /* + Turn right + */ + this.car.rotation += this.rotationStep*Math.PI; + this.lidar.rotation = this.car.rotation; + } + + moveUp(){ + this.car.v = 1; + } + + moveDown(){ + /* + Move forward + */ + this.car.v = -1; + } + + isRoad(nx: number, ny: number){ + /* + Method used to check is there a road is present at this potion + on the map + */ + if (ny < 0 || ny >= this.mapSizeY || nx < 0 || nx >= this.mapSizeX || this.level.map[ny][nx] == 0){ + return false; + } + return true; + } + + autoRotation(){ + /* + Auto roation of the bot car + */ + let right_dist = ROADSIZE + (ROADSIZE/3); + let left_dist = ROADSIZE - (ROADSIZE/3.5); + let same_dist = ROADSIZE; + // If a next road is already define, we just check if this is the + // good moment to turn + if (this.car.next_road){ + let y_dist = Math.abs(((this.car.next_road.outroad.y + ROADSIZE/2) - this.car.y) * this.car.next_road.outroad_np.my); + let x_dist = Math.abs(((this.car.next_road.outroad.x + ROADSIZE/2) - this.car.x) * this.car.next_road.outroad_np.mx); + + if (this.car.next_road.rotation_type == 0){ // Turn right + if (y_dist < right_dist && x_dist < right_dist){ + this.car.have_turned = true; + this.car.rotation = this.car.rotation += Math.PI/2; + this.car.rotation = this.car.rotation % (2*Math.PI); + this.car.next_road = undefined; + return; + } + } + + if (this.car.next_road.rotation_type == 1){ // Turn left + if (y_dist < left_dist && x_dist < left_dist){ + this.car.have_turned = true; + this.car.rotation = this.car.rotation -= Math.PI/2; + this.car.rotation = this.car.rotation % (2*Math.PI); + this.car.next_road = undefined; + return; + } + } + + if (this.car.next_road.rotation_type == 2){ // Rotation + if (y_dist < same_dist && x_dist < same_dist){ + this.car.have_turned = true; + this.car.rotation = this.car.rotation -= Math.PI; + this.car.rotation = this.car.rotation % (2*Math.PI); + + if (y_dist != 0 && this.car.x < this.car.next_road.outroad.x + ROADSIZE/2){ + this.car.x = this.car.next_road.outroad.x + ROADSIZE/2 + ROADSIZE/4; + this.car.y = this.car.next_road.outroad.y; + } + else if (y_dist != 0 && this.car.x > this.car.next_road.outroad.x + ROADSIZE/2){ + this.car.x = this.car.next_road.outroad.x + ROADSIZE - ROADSIZE/2 - ROADSIZE/4; + this.car.y = this.car.next_road.outroad.y; + } + else if (x_dist != 0 && this.car.y < this.car.next_road.outroad.y + ROADSIZE/2){ + this.car.y = this.car.next_road.outroad.y + ROADSIZE/2 + ROADSIZE/4; + this.car.x = this.car.next_road.outroad.x; + } + else if (x_dist != 0 && this.car.y > this.car.next_road.outroad.y + ROADSIZE/2){ + this.car.y = this.car.next_road.outroad.y + ROADSIZE - ROADSIZE/2 - ROADSIZE/4; + this.car.x = this.car.next_road.outroad.x; + } + + this.car.next_road = undefined; + return; + } + } + + } + else{ + let key = this.car.rotation / Math.PI; + let key_r = ((this.car.rotation / Math.PI) + 0.5) % 2; + let key_l = ((this.car.rotation / Math.PI) - 0.5) % 2; + + let is_next_pos = this.rotationToNextCase[key]; + let is_next_pos_r = this.rotationToNextCase[key_r]; + let is_next_pos_l = this.rotationToNextCase[key_l]; + + let turn = false; + let nx = this.car.mx + is_next_pos.mx; + let ny = this.car.my + is_next_pos.my; + if (!this.isRoad(nx, ny)){ + turn = true; + } + else if (!this.car.have_turned && !this.car.turn_random){ + this.car.turn_random = Math.random(); + } + else if(!this.car.have_turned && this.car.turn_random < 0.33 && this.isRoad(this.car.mx + is_next_pos_r.mx, this.car.my + is_next_pos_r.my)){ + turn = true; + } + else if(!this.car.have_turned && this.car.turn_random > 0.66 && this.isRoad(this.car.mx + is_next_pos_l.mx, this.car.my + is_next_pos_l.my)){ + turn = true; + } + if (turn){ + let possibles_rotations = [(key+0.5) % 2., (key-0.5) % 2., null]; + for (let p = 0; p < possibles_rotations.length; p++){ + let next_pos = this.rotationToNextCase[possibles_rotations[p]]; + let px = this.car.mx + next_pos.mx; + let py = this.car.my + next_pos.my; + if (this.isRoad(px, py)){ + this.car.next_road = { + "outroad": {y : ny*ROADSIZE, x: nx*ROADSIZE}, + "outroad_np": is_next_pos, + "rotation_type": p, + }; + return; + } + } + } + } + } + + step(delta: number){ + /* + Step into the environement + @delta (Float) time since the last update + */ + // Possible collision detected + if (this.state.toString().indexOf(","+MAP.CAR.toString()) != -1){ + this.car.v = 0; + } + else{ // No collision, move forward + this.car.v = 0.8; + this.autoRotation(); + } + + // Update the x and y position according to the velocity + this.car.x += this.car.v * Math.cos(this.car.rotation)*delta; + this.car.y += this.car.v * Math.sin(this.car.rotation)*delta; + // Update the x and x position according to the map + this.car.mx = Math.floor(this.car.x / ROADSIZE); + this.car.my = Math.floor(this.car.y / ROADSIZE); + + // Set the new road of the car (if changed) + this.car.checkAndsetNewRoad(); + + // Be sure to keep the lidar position at the same position than the car + this.lidar.x = this.car.x; + this.lidar.y = this.car.y; + this.lidar.rotation = this.car.rotation; + + // Detection the new collision with the environement + let {agent_col, on_road} = this.detectInteractions(); + + if (agent_col.length > 0){ + this.car.v = 0; + } + return {agent_col, on_road}; + } +} diff --git a/src/car.ts b/src/car.ts new file mode 100644 index 0000000..6563c07 --- /dev/null +++ b/src/car.ts @@ -0,0 +1,236 @@ +/* + Car class + Class used to create a new car on the map + This class create the agent and the associated sensor (lidar) +*/ + +import {Level, LevelInfo} from "./level"; + +import { + CAR_IMG, Sprite, MAP, ROADSIZE, Container, Graphics +} from "./global"; +import { RoadSprite } from "./asset_manager"; + + +var Global_carId = 0; + +export interface LidarInfoI { + pts?: number; + width?: number; + height?: number; + pos?: number; +} + +export interface CarOptions { + image?: string; + lidar?: boolean; + lidarInfo?: LidarInfoI; + motionEngine?: any; +} + +export interface CarInfo{ + readonly mx: number; + readonly my: number; + readonly line: number; +} + +export interface CarSprite extends PIXI.Sprite { + road?: RoadSprite; + carId?: number; + obstacle?: boolean; + mapId?: number; + lidar?: any; + rotationStep?: any; + mx?: number; + my?: number; + checkAndsetNewRoad?: any; + line?: number; + haveTurned?: boolean; + agent?: boolean; +} + +export class Car { + + public level: Level; + public core: CarSprite; + public lidar: any; + + private info: CarInfo; + private motion: any; + private turnedRandom: any; + + constructor(level: Level, info: CarInfo, textures: any, options:CarOptions={}) { + /* + @level (Level class) + @info: (CarInfo) Info from the json file about the car + @textures: (Pixi: loader.resources) Used ot load the agent texture + @options + image (String) Default CAR_IMG.DEFAULT + lidar (False) Default: false + lidar_info: (Object) Object with the informations about the lidar + motionEngine (Class) Motion engine to use for this car + */ + this.level = level; + options.image = options.image || CAR_IMG.DEFAULT; + options.lidar = options.lidar || false; + options.lidarInfo = options.lidarInfo || {pts: 5, width: 4, height: 5, pos: 0}; + // Create the sprite of the car + this.core = new Sprite(textures[options.image]); + this.info = info; // Store the original information about the car + // Change the scale and the anchor of the sprite + this.core.scale.x = 0.8; + this.core.scale.y = 0.8; + this.core.anchor.x = 0.5; + this.core.anchor.y = 0.5; + this.core.road = undefined; + // Id of this car + this.core.carId = Global_carId; + Global_carId += 1; + + // THis car is not an agent by default + this.core.agent = false; + // A car is an obstacle + this.core.obstacle = true; + // Set the map id + this.core.mapId = MAP.CAR; + + // Create the lidar of the car if required + if (options.lidar){ + this.createLidar(options.lidarInfo); + this.core.lidar = this.lidar; + } + + // Setup the motionEngine of the car if needded + // TODO: Remove useless setUp + if (options.motionEngine){ + this.motion = options.motionEngine; + this.motion.setUp(this.core, this.lidar); + this.core.rotationStep = 0.5; + } + else{ + this.core.rotationStep = 0.5; + } + + // Set the position of the car + this.core.mx = this.info.mx; + this.core.my = this.info.my; + this.core.x = (this.info.mx) * ROADSIZE; + this.core.y = (this.info.my) * ROADSIZE; + + // Usefull method to set the new road position of the car + // and keep the list of cars on each road up to date + this.core.checkAndsetNewRoad = (road: RoadSprite) => this.checkAndsetNewRoad(road); + + // If the car is on a road, we set the position of the car on + // the road properly + let road = this.level.getRoad(this.core.my, this.core.mx); + this.core.line = 0; // By defaut + if (road){ // If the car is on a road + road.setCarPosition(this.core, this.info.line); + } + } + + checkAndsetNewRoad(n_road?: RoadSprite){ + /* + Check if the car is on a new road + If the car is on a new road, we keep the list up to date. + @n_road (Road object) The new road + */ + let current_road = this.core.road; + if (!n_road){ // If the road is not define + n_road = this.level.getRoad(this.core.my, this.core.mx);; + } + if (n_road != current_road){ + if (current_road){ // The car is not on this road anymore + current_road.cars.splice(current_road.cars.indexOf(this.core.carId), 1); + } + // Add the car to the road list + this.core.road = n_road; + // TOdo: CHECK something is strange here + this.core.haveTurned = false; + this.turnedRandom = undefined; + if (n_road) + n_road.cars.push(this.core.carId); + } + } + + reset(){ + /* + Method used to restore the position of the car + to the original position (as set into the json file) + */ + this.core.mx = this.info.mx; + this.core.my = this.info.my; + let road = this.level.getRoad(this.core.my, this.core.mx); + if (road){ // If the car is on a road + road.setCarPosition(this.core, this.info.line); + } + } + + getState(){ + /* + Get the current state of the car + The state is the current value of each point + of the lidar. + */ + return this.motion.state.map(function(arr: any) { return arr.slice(); }); + } + + step(delta: number, action:number=null){ + /* + Take one step into the environement + @delta (Float) time since the last update + @action: (Integer) The action to take (can be null if no action) + */ + if (action == null){ + var {agent_col, on_road} = this.motion.step(delta); + } + else{ + var {agent_col, on_road} = this.motion.actionStep(delta, action); + } + return {agent_col, on_road}; + } + + createLidar(lidarOptions: LidarInfoI){ + /* + Create the lidar sensor of the car + @lidarOptions (Object) option to create the lidar + @pts (Integer) Number of point required + @width: Width of the lidar (in proportion to the car) + @height: Height of the lidar (in proportion to the car) + */ + this.lidar = new Container(); + + // Area of the lidar + let area = new Graphics(); + area.alpha = 0.5; + area.beginFill(0x515151); + area.drawRect(0, 0, this.core.width*lidarOptions.width, this.core.height*lidarOptions.height); + area.endFill(); + + // Create all the points of the lidar + let x_step = area.width/lidarOptions.pts; + let y_step = area.height/lidarOptions.pts; + for (let xs = lidarOptions.pts - 1; xs >= 0; xs--) { + for (let ys = 0; ys < lidarOptions.pts; ys++) { + + let x = (x_step/4) + xs * x_step; + let y = (y_step/4) + ys * y_step; + + let pt = new Graphics(); + pt.beginFill(0xffffff); + pt.drawRect(x, y, 5, 5); + pt.endFill(); + this.lidar.addChild(pt); + } + } + + this.lidar.pts = lidarOptions.pts; + this.lidar.addChild(area); + this.lidar.x = this.core.x; + this.lidar.y = this.core.y; + this.lidar.pivot.y = this.lidar.height/2; + this.lidar.pivot.x = this.core.width/2 - (lidarOptions.pos*this.core.width); + this.lidar.rotation = this.core.rotation; + } +} diff --git a/src/control_motion_engine.ts b/src/control_motion_engine.ts new file mode 100644 index 0000000..58fa940 --- /dev/null +++ b/src/control_motion_engine.ts @@ -0,0 +1,178 @@ +import { MotionEngine } from "./motion_engine"; +import {Level} from "./level"; + +import { + MAP, ROADSIZE +} from "./global"; + +import * as U from "./utils"; + +export class ControlMotionEngine extends MotionEngine { + + private actions: (string|number)[]; + + constructor(level: Level) { + /* + @level (Level class) + @options (Object) Option to build the motion engine + @rotation_step: Between 0 and 2. (rotation_step*Pi) + @actions: List of possible actions (Left, Right, Up, Down, Wait) + */ + super(level); + + this.level = level; + this.actions = ["SteeringAngle", "Throttle"] + } + + setUp(car: any, lidar: any){ + /* + Setup the motion engine + @car: (Vehicle Object) + @lidar: (Lidar Object) + */ + this.car = car; + this.lidar = lidar; + // Init the lidar state + this.state = []; + for (let y = 0; y < lidar.pts; y++) { + let line = []; + for (let x = 0; x < lidar.pts; x++) { + line.push(MAP.DEFAULT); + } + this.state.push(line); + } + // Set up keyboard interaction + this.setUpKeyboard(); + // Setup up velocity to 0 + this.car.v = 0; + this.car.a = 0; // Acceleration + this.car.yaw_rate = 0; + + this.detectInteractions(); + } + + setUpKeyboard(){ + /* + Setup possible keyboard interactions + */ + let left = U.keyboard(37); + let up = U.keyboard(38); + let right = U.keyboard(39); + let down = U.keyboard(40); + + left.press = () => { + this.turn(-0.5); + }; + left.release = () => { + this.turn(0); + }; + + right.press = () => { + this.turn(0.5); + }; + right.release = () => { + this.turn(0); + }; + + // Move forward + up.press = () => { this.move(1); }; + up.release = () => { + this.car.a = 0; + }; + + // Move backward + down.press = () => { this.move(-1);}; + down.release = () => { + this.car.a = 0; + }; + } + + turn(value: number){ + /* + Turn + @value: yaw_rate value Between -1 and 1 + */ + this.car.yaw_rate = value; + } + + move(throttle: number){ + /* + Move forward + @throttle throttle value Between -1 and 1 + */ + this.car.a = throttle; + } + + actionStep(delta: number, actions: number[]){ + /* + Step into the environement with one action + @delta (Float) time since the last update + @action: (Array of Float) The throttle and the steering angle + */ + // TODO: CHeck why parseInt ??? + this.car.a = Math.min(Math.max(parseInt(actions[0].toString()), -1), 1); + this.car.yaw_rate = Math.min(Math.max(parseInt(actions[1].toString()), -1), 1); + let {agent_col, on_road} = this.step(delta); + this.car.a = 0; + this.car.yaw_rate = 0; + return {agent_col, on_road}; + } + + actionSpace(){ + /* + Return an array with all possibles actions + Ex: [0, 1, 2] + */ + return Array.apply(null, {length: this.actions.length}).map(Number.call, Number); + } + + step(delta: number){ + /* + Step into the environement + @delta (Float) time since the last update + */ + // Update the x and y position according to the velocity + if (this.car.v > 0) + this.car.v = Math.max(0, this.car.v - 0.01); + else if (this.car.v < 0) + this.car.v = Math.min(0, this.car.v + 0.01); + + if (this.car.a > 0) + this.car.v = Math.min(2., this.car.v + this.car.a*0.02); + else + this.car.v = Math.max(-2., this.car.v + this.car.a*0.02); + if (this.car.yaw_rate == 0){ + this.car.x += this.car.v * Math.cos(this.car.rotation)*delta; + this.car.y += this.car.v * Math.sin(this.car.rotation)*delta; + } + else{ + // Rewrite the yaw rate + let yr = this.car.yaw_rate*0.01*(this.car.v+0.001)*Math.PI; + + this.car.x += (this.car.v/yr)*(Math.sin(this.car.rotation+(yr*delta)) - Math.sin(this.car.rotation)); + this.car.y += (this.car.v/yr)*(Math.cos(this.car.rotation) - Math.cos(this.car.rotation+(yr*delta))); + + this.car.rotation += yr*delta; + } + // Update the x and x position according to the map + this.car.mx = Math.floor(this.car.x / ROADSIZE); + this.car.my = Math.floor(this.car.y / ROADSIZE); + + // Set the new road of the car (if changed) + this.car.checkAndsetNewRoad(); + + // Be sure to keep the lidar position at the same position than the car + this.lidar.x = this.car.x; + this.lidar.y = this.car.y; + this.lidar.rotation = this.car.rotation; + + // Detection the new collision with the environement + let {agent_col, on_road} = this.detectInteractions(); + + if (agent_col.length > 0){ // Stop the vehicle if a collision is detected + this.car.v = 0; + this.car.vy = 0; + } + return {agent_col, on_road}; + } +} diff --git a/src/global.ts b/src/global.ts new file mode 100644 index 0000000..f4edba2 --- /dev/null +++ b/src/global.ts @@ -0,0 +1,109 @@ +/* + Global variables +*/ + +import * as PIXI from 'pixi.js' + +// PIXI Aliases +export const Application = PIXI.Application; +export const Loader = PIXI.loader; +export const Resources = PIXI.loader.resources; +export const Sprite = PIXI.Sprite; +export const Graphics = PIXI.Graphics; +export const Container = PIXI.Container; + +// Textures files +export const JSON_TEXTURES = "textures/textures.json"; +// Level foler +export const LEVEL_FOLDER = "/level/"; + +// Width and height of each road asset +export const ROADSIZE = 60; + +export interface CARIMGI{ + DEFAULT: string; + AGENT: string; +} + +export const CAR_IMG: CARIMGI = { + DEFAULT: "car.png", // Name of the image to represent a simple car + AGENT: "car_agent.png" // Name of the image to represent an agent +} + +export const MAP = { + ROAD: -1, + DEFAULT: 0, + CAR: 1, + AGENT: 1, +}; + +/* +* Asset configurations +*/ +export const ASSETS: any = { + ROADS: { + "↕": { + "image": "road_u.png", + "orientation": 1.5 + }, + "↔": { + "image": "road_r.png", + "orientation": 0. + }, + "↱": { + "image": "road_rotate_ld.png", + "orientation": 1.25 + }, + "↰": { + "image": "road_rotate_ul.png", + "orientation": 0.75 + }, + "↲": { + "image": "road_rotate_rl.png", + "orientation": 0.25 + }, + "↳": { + "image": "road_rotate_dl.png", + "orientation": 1.75 + }, + "↟": { + "image": "road_up.png", + "orientation": 1.5 + }, + "↠": { + "image": "road_rp.png", + "orientation": 0. + }, + "↤": { + "image": "road_r2.png", + "orientation": 0. + }, + "↦": { + "image": "road_l2.png", + "orientation": 1. + }, + "↥": { + "image": "road_d2.png", + "orientation": 0.5 + }, + "↧": { + "image": "road_u2.png", + "orientation": 1.5 + }, + }, + "house":{ + "image": "house.png" + }, + "house2":{ + "image": "house2.png" + }, + "house3":{ + "image": "house3.png" + }, + "bench":{ + "image": "bench.png" + }, + "tree":{ + "image": "tree.png" + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..1cdfc07 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +import {MetaCar} from "./metacar"; +export default MetaCar; \ No newline at end of file diff --git a/src/level.ts b/src/level.ts new file mode 100644 index 0000000..0e84bb8 --- /dev/null +++ b/src/level.ts @@ -0,0 +1,215 @@ +/* + @Level class + This is the core of game, the class is used create all the differents + services in the game (assets, map, agents...). +*/ + +import * as U from "./utils"; +import { + LEVEL_FOLDER, ROADSIZE, Loader, JSON_TEXTURES +} from "./global"; +import {AssetManger, RoadSprite, SimpleSprite, AssetInfo, } from "./asset_manager"; + +import {Car} from "./car"; + +export interface LevelInfo { + map: (string|number)[][]; + agent: any; + [index: string]: any; +} + +export interface Roads { + [key: string]: RoadSprite +} + +export class Level { + + private app: any = null; // The pixi app. + private name: string = null; // Name fo the level + private info: LevelInfo = null; // Information of the loaded level + private envs: any[] = [] // Used to list all elements the car can crash with + private map: (string|number)[][] = null; // Use the roads positions of the level + private agent: any = null; // (@Car class) for the agent + // Object to store all the roads assets + // Each road is accesible as follow this.roads[[my, mx]] + // with my and mx the (x, y) position relative to the map (not pixel). + private roads: Roads; + private cars: any; // Array used to store all the cars assets + private loop: any; // Loop method called for each render + private am: AssetManger; // The AssetManger is used to set most of the elements on the map + private canvasId: string; // Id of the target canvas + + constructor(levelName: string, canvasId: string) { + /* + @levelName: Name of the level to load (.json) + */ + this.name = levelName; + this.canvasId = canvasId; + this.am = new AssetManger(this); + } + + load(loop: any){ + /* + Load the map from the file given in the constructor of the class + @loop (Method) This method will be call for each render + */ + this.loop = loop; + return new Promise((resolve, reject) => { + // Load the level + U.loadJSON(LEVEL_FOLDER + this.name, (response: string) => { + // Parse JSON string into object + this.info = JSON.parse(response); + this.map = this.info.map; // Store the map to let it accessible faster later on. + this.createLevel(this.info).then(() => resolve()); + }); + }); + } + + createLevel(info: LevelInfo){ + /* + Create the level + @info Information about the level + */ + //Create the Pixi Application + this.app = new PIXI.Application({ + width: this.map[0].length * ROADSIZE, + height: this.map.length * ROADSIZE, + backgroundColor: 0x80bf3e + } + ); + // Append the app to the body + document.getElementById(this.canvasId).appendChild(this.app.view); + + return new Promise((resolve, reject) => { + Loader.add(["textures/textures.json", "textures/textures.png"]).load(() => { + this.setup(info); // Set up the level (Add assets) + resolve(); + }); + }); + } + + setup(info: LevelInfo){ + /* + Setup all the element of the map + */ + // Load the textures file + let textures = Loader.resources[JSON_TEXTURES].textures; + // Load all elements of the car + this.am.createMap(this.map, info, textures); + this.am.createCars(this.map, info, textures); + if (info.agent) + this.agent = this.am.createAgent(this.map, info, textures); + // Set up the main loop + this.app.ticker.add((delta: number) => this.loop(delta)); + } + + render(){ + /* + If the rendering has been stopped, this Method + restart the rendering. + */ + this.app.ticker.start(); + } + + reset(){ + /* + Reset the game. + For the moment, only the agent position is reset. + TODO: Reset the positions of the others car too to avoid + useless collisions. + */ + this.agent.reset(); + } + + setReward(agent_col: any, on_road: any, action: any){ + /* + TODO: Let's the reward define in the agent class + */ + let reward = -0.1; + if (action == 0 || this.agent.core.vx == 1) + reward += 0.5; + if (agent_col.length > 0){ + reward = -10; + } + else if (!on_road){ + reward = -10; + } + return reward; + } + + step(delta: number, action:number=null){ + /* + Process one step into the environement + @delta (Float) time since the last update + @action: (Integer) The action to take (can be null if no action) + */ + // Go through all cars to move each one + for (var c = 0; c < this.cars.length; c++) { + if (this.cars[c].lidar && !this.cars[c].core.agent) // If this car can move + this.cars[c].step(delta); + } + // Move the agent + if (this.agent){ + let {agent_col, on_road} = this.agent.step(delta, action); + // Get the reward + let reward = this.setReward(agent_col, on_road, action); + return reward; + } + return 0; + } + + stopRender(){ + /** + * Stop to render the canvas + */ + this.app.ticker.stop(); + } + + addChild(child: any){ + /** + * Add child to the app + */ + this.app.stage.addChild(child); + } + + addRoad(road: RoadSprite){ + /* + Add road using the mx and my positions + */ + this.roads[[road.my.toString(), road.mx.toString()].toString()] = road; + this.envs.push(road); + this.app.stage.addChild(road); + } + + addCar(car: Car){ + /** + * Add car to the level + */ + this.cars.push(car); + this.app.stage.addChild(car.core); + this.envs.push(car.core); + } + + getRoad(my: number, mx: number){ + return this.roads[[my.toString(), mx.toString()].toString()]; + } + + findCarById(id: number){ + /* + Find car by @id + */ + return this.cars.find((e: any) => {return e.car_id == id}); + } + + getEnvs(): any[] { + /** + * Return the list of envs + * / + */ + return this.envs; + } + + getMap(): (string|number)[][] { + return this.map; + } +} diff --git a/src/metacar.ts b/src/metacar.ts new file mode 100644 index 0000000..971317b --- /dev/null +++ b/src/metacar.ts @@ -0,0 +1,121 @@ +/* + Main class of the project +*/ + +import {Level} from "./level"; + +export class MetaCar { + + private isPlaying: boolean; + private agent: any; + private level: Level; + + constructor() { + } + + load(level: string, agent: any){ + /* + Load the environement + @level (String) Name of the json level to load + @agent (Agent class) + */ + this.isPlaying = false; + this.agent = agent; + this.level = new Level(level); + this.level.load((delta: number) => this.loop(delta)); + + document.getElementById("train").addEventListener("click", () => { + this.level.app.ticker.stop(); // .add(delta => this.loop(delta)); + this.agent.train(this); + }); + document.getElementById("stop").addEventListener("click", () => { + this.isPlaying = false; + this.agent.stop(); + this.level.render(); + }); + document.getElementById("reset").addEventListener("click", () => { + this.level.reset(); + }); + document.getElementById("play").addEventListener("click", () => { + this.isPlaying = true + }); + document.getElementById("saveAgent").addEventListener("click", () => { + this.agent.save(this); + }); + document.getElementById("dumpFile").addEventListener("change", (e) => { + //readDump(e, (content) => this.agent.restore(this, content)); + }); + } + + render(){ + /* + Render the environement + again + */ + this.level.render(); + } + + save(content: any, file_name: string){ + /* + Save the agent + */ + saveAs(content, file_name); + } + + actionSpace(){ + /* + Get the possible action to do in the environement + Ex: [0, 1, 2] + */ + return this.level.agent.motion.actionSpace(); + } + + getState(){ + /* + Get the state of this environement + */ + return this.level.agent.getState(); + } + + step(action: number){ + /* + Step into the environement + @action (Integer) + */ + return this.level.step(1, action); + } + + reset(){ + /* + Reset the agent position + */ + this.level.reset(); + } + + randomRoadPosition(){ + /* + This position + */ + this.level.agent.last_position = []; + let keys = Object.keys(this.level.roads); + keys.sort(function() {return Math.random()-0.5;}); + for (let k in keys){ + let road = this.level.roads[keys[k]]; + if (road.cars.length == 0){ + road.setCarPosition(this.level.agent.core); + break; + } + } + } + + loop(delta: number){ + if (this.isPlaying){ + this.agent.play(this); + } + else { + this.level.step(delta); + } + document.getElementById("rewardDisplay").innerHTML = this.level.last_reward; + } + +} diff --git a/src/motion_engine.ts b/src/motion_engine.ts new file mode 100644 index 0000000..351574e --- /dev/null +++ b/src/motion_engine.ts @@ -0,0 +1,104 @@ +/* + Motion engine + Parent of all motion engines classes +*/ + +import {Level} from "./level"; + +import { + MAP +} from "./global"; + +export interface MotionOption{ + readonly rotationStep?: number; + readonly actions: string[]; +} + +export class MotionEngine { + + protected level: Level; + protected car: any; + protected lidar: any; + protected state: any; + + constructor(level: Level) { + this.level = level; + } + + protected boxesIntersect(a: any, b: any) { + /* + Chack if a the two elements intersect each others + @a (Pixi sprite) + @b (Pixi sprite) + */ + var ab = a.getBounds(); + var bb = b.getBounds(); + return ab.x + ab.width > bb.x && ab.x < bb.x + bb.width && ab.y + ab.height > bb.y && ab.y < bb.y + bb.height; + } + + protected detectInteractions(){ + /* + Detect the interaction with the environement + - Car collisions + - Lidar collisions + - Road position (Is the vehicle on the road) + */ + let envs = this.level.getEnvs(); + + let agent_col = []; + let lidar_collisions = []; + let on_road = false; + + for (let i = 0; i < envs.length; i++) { + if (envs[i] != this.car && envs[i].obstacle && this.boxesIntersect(envs[i], this.car)){ + agent_col.push(envs[i]); + } + else if (envs[i].map_id == MAP.ROAD && this.boxesIntersect(envs[i], this.car)){ + on_road = true; + } + if (this.boxesIntersect(envs[i], this.lidar)){ + lidar_collisions.push(envs[i]); + } + } + + this.setState(lidar_collisions); + return {agent_col, on_road}; + } + + protected setState(lidar_collisions: any){ + /* + Set up the current state of the agent + @lidar_collisions List with all possible lidar collisions + */ + let state = []; + let f = 0.1; + let pt_id = 0; + + for (var i = 0; i < this.lidar.children.length; i++) { + this.lidar.children[i].alpha = 0.1; + if (this.lidar.children[i].pt){ // If this is a lidar point + + let pt_y = Math.round(pt_id/this.lidar.pts); + let pt_x = Math.round(pt_id%this.lidar.pts); + this.state[pt_y][pt_x] = MAP.DEFAULT; + + for (var a = 0; a < lidar_collisions.length; a++) { + if (lidar_collisions[a] != this.car){ + let touch = this.boxesIntersect(lidar_collisions[a], this.lidar.children[i]); + if (touch && lidar_collisions[a].obstacle){ + // If this is an obstacle + this.lidar.children[i].alpha = 1.; + } + if (touch && (lidar_collisions[a].map_id > this.state[pt_y][pt_x] || (lidar_collisions[a].map_id == MAP.ROAD && this.state[pt_y][pt_x]==MAP.DEFAULT))){ + // Add this interaction to the state + // If this interaction is more important than the one befor + // we kept the more important one + this.state[pt_y][pt_x] = lidar_collisions[a].map_id; + } + } + } + pt_id += 1; + } + } + } +}; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..c263f75 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,168 @@ +/* + File with some usefull methods +*/ +export function loadJSON(url: string, callback: any) { + /* + Utils method to load a json on the server + @url: Url to the json file to load + @callback: Method to call when the json file is loaded + */ + var xobj = new XMLHttpRequest(); + xobj.overrideMimeType("application/json"); + xobj.open('GET', url, true); // Replace 'my_data' with the path to your file + xobj.onreadystatechange = function () { + if (xobj.readyState == 4 && xobj.status == 200) { + // Required use of an anonymous callback as .open will NOT return a value but simply returns undefined in asynchronous mode + callback(xobj.responseText); + } + }; + xobj.send(null); +} + +export function mod(n: number, m:number) { + return ((n % m) + m) % m; +} + +export function argMax(array: number[]) { + /* + Return the argmax of an array. + */ + if (array.every((v) => v == -Infinity)){ + return Math.floor(Math.random()*array.length); + } + return array.map((x, i) => [x, i]).reduce((r, a) => (a[0] > r[0] ? a : r))[1]; +} + +export function mean(array: number){ + /** + * Return the mean of the array + */ + if (array.length == 0) + return null; + var sum = array.reduce(function(a, b) { return a + b; }); + var avg = sum / array.length; + return avg; +} + +function saveAs(content: string, name: string) { + var a = document.createElement("a"); + //a.style = "display: none"; + document.body.appendChild(a); + + var blob = new Blob([content], { type: 'application/octet-binary' }), + tmpURL = window.URL.createObjectURL(blob); + + a.href = tmpURL; + a.download = name; + a.click(); + + window.URL.revokeObjectURL(tmpURL); + a.href = ""; +} + +/* +function readBuffer(buffer) { + var reader = new BinaryReader(buffer) + + console.log(reader); + + var validation = reader.getUint8() + + if (validation !== BinaryWriter.validationByte) { + throw "validation byte doesn't match." + } + + var tocLength = reader.getUint32(); + var tocString = reader.getString(tocLength); + var toc = JSON.parse(tocString); + var contents = []; + + for (var i = 0; i < toc.length; i++) { + switch (toc[i].t) { + case 's': contents.push(reader.getString(toc[i].l)); break + case 't': contents.push(new (typeof window !== 'undefined' ? window : global)[toc[i].i](reader.getBuffer(toc[i].l))); break + case 'b': contents.push(reader.getBuffer(toc[i].l)); break + } + + if (toc[i]['o'] === 1) { + contents[i] = JSON.parse(contents[i]); + } + } + + return contents +} +*/ + +/* +function readDump(e, callback) { + var input = e.target; + var file = input.files[0]; + + var reader = new FileReader(); + reader.readAsText(file, "UTF-8"); + reader.onload = (evt) => { + callback(evt.target.result); + } +} +*/ + +export function shuffleArray(array: number[]) { + /** + * Shuffle an @array of numbers + */ + for (var i = array.length - 1; i > 0; i--) { + var j = Math.floor(Math.random() * (i + 1)); + var temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } + return array; +} + +/* +function randomChoice(p) { + let rnd = p.reduce( (a, b) => a + b ) * Math.random(); + return p.findIndex( a => (rnd -= a) < 0 ); +} +*/ + +export interface KeyBoard{ + [key: string]: any; +} + +export function keyboard(keyCode: any ) { + let key: KeyBoard = {}; + key.code = keyCode; + key.isDown = false; + key.isUp = true; + key.press = undefined; + key.release = undefined; + + + key.downHandler = (event: any) => { + if (event.keyCode === key.code) { + if (key.isUp && key.press) key.press(); + key.isDown = true; + key.isUp = false; + } + event.preventDefault(); + }; + + key.upHandler = (event: any) => { + if (event.keyCode === key.code) { + if (key.isDown && key.release) key.release(); + key.isDown = false; + key.isUp = true; + } + event.preventDefault(); + }; + + //Attach event listeners + window.addEventListener( + "keydown", key.downHandler.bind(key), false + ); + window.addEventListener( + "keyup", key.upHandler.bind(key), false + ); + return key; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..cb7eb6b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "module": "ES2015", + "moduleResolution": "node", + "noImplicitAny": true, + "sourceMap": true, + "removeComments": true, + "preserveConstEnums": true, + "declaration": true, + "target": "es5", + "lib": ["es2015", "dom"], + "outDir": "./dist-es6", + "noUnusedLocals": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedParameters": false, + "pretty": true, + "noFallthroughCasesInSwitch": true, + "allowUnreachableCode": false, + "experimentalDecorators": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "demo" + ] +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..2b334da --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,35 @@ +const path = require('path'); + +var config = { + entry: './src/index', + module: { + rules: [ + { + test: /\.ts$/, + use: 'ts-loader', + }, + ], + }, + resolve: { + extensions: [ + '.ts', + ], + } +}; + +var packageConfig = Object.assign({}, config, { + output: { + filename: 'metacar.min.js', + path: path.resolve(__dirname, './dist'), + } +}); +var demoConfig = Object.assign({}, config,{ + output: { + filename: 'metacar.min.js', + path: path.resolve(__dirname, './demo/dist'), + } +}); + +module.exports = [ + packageConfig, demoConfig, +];