Created
May 24, 2018 18:34
-
-
Save thesephist/f99d8400893edd2f1780c1ec8df62ecb to your computer and use it in GitHub Desktop.
Highway Lane Changes Simulation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is pretty crude. I didn't really care though since it was | |
// mostly something fueled by curiosity after a long road trip | |
// and wasn't intended to capture things like collisions / AI control of speeds. | |
// All code in this file is licensed under the MIT License. | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<title>Traffic Simulation</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<style> | |
body { | |
font-family: sans-serif; | |
color: #fff; | |
text-align: center; | |
font-size: 14px; | |
} | |
.highway { | |
display: flex; | |
flex-direction: row; | |
} | |
.lane { | |
background: #bbb; | |
margin-right: 10px; | |
position: relative; | |
height: 901px; | |
width: 16px; | |
} | |
.car { | |
height: 16px; | |
width: 16px; | |
background: red; | |
transform: translateY(-5px); | |
position: absolute; | |
left: 0; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="highway"></div> | |
<script src="main.js"></script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is pretty crude. I didn't really care though since it was | |
// mostly something fueled by curiosity after a long road trip | |
// and wasn't intended to capture things like collisions / AI control of speeds. | |
// All code in this file is licensed under the MIT License. | |
const NUM_CARS_PER_LANE = 7; | |
const NUM_LANES = 4; | |
const AVG_SPEED = 5; // pixels per tick | |
const SPEED_STD = 3.5; | |
const ROAD_LENGTH = 900; | |
const FOLLOWING_DISTANCE = 20; // pixels | |
let carIDIter = 0; | |
let CONGESTION = []; | |
const carid = () => { | |
return carIDIter ++; | |
} | |
class Car { | |
constructor(lane) { | |
this.speed = AVG_SPEED; | |
this.lane = lane; | |
this.id = carid(); | |
} | |
getPosition() { | |
return this.lane.getCarPosition(this); | |
} | |
getSpeed() { | |
const lastSpeed = this.speed; | |
let iter = Math.random() > .5 ? 1 : -1; | |
if (lastSpeed > AVG_SPEED + SPEED_STD) { | |
iter = -1; | |
} else if (lastSpeed < AVG_SPEED - SPEED_STD) { | |
iter = 1; | |
} | |
this.speed += iter; | |
return lastSpeed; | |
} | |
getFrontDistance() { | |
return this.lane.getCarFrontDistance(this); | |
} | |
getRearDistance() { | |
return this.lane.getCarRearDistance(this); | |
} | |
getDistanceTo(car) { | |
return this.lane.getCarDistance(ahead, this); | |
} | |
moveToLane(lane) { | |
const pos = this.getPosition(); | |
const homeLane = this.lane; | |
const destLane = lane; | |
homeLane.removeCar(this); | |
destLane.addCar(this, pos); | |
this.lane = destLane; | |
} | |
} | |
class Lane { | |
constructor(multilane) { | |
this.multilane = multilane; | |
this.cars = new Map(); | |
for (let i = 0; i < NUM_CARS_PER_LANE; i ++) { | |
this.cars.set(new Car(this), (i + 1) * (ROAD_LENGTH / NUM_CARS_PER_LANE)); | |
} | |
} | |
removeCar(car) { | |
this.cars.delete(car); | |
} | |
addCar(car, position) { | |
this.cars.set(car, position); | |
} | |
getCars() { | |
return [...this.cars.keys()]; | |
} | |
getCarPosition(car) { | |
return this.cars.get(car); | |
} | |
getCarDistance(ahead, behind) { | |
return this.cars.get(behind) - this.cars.get(ahead); | |
} | |
getCarFrontDistance(car) { | |
const cars = this.getCars(); | |
const carIdx = cars.indexOf(car); | |
if (carIdx === 0) { | |
return Infinity; | |
} else { | |
return this.getCarDistance(cars[carIdx - 1], car); | |
} | |
} | |
getCarRearDistance(car) { | |
const cars = this.getCars(); | |
const carIdx = cars.indexOf(car); | |
if (carIdx === NUM_CARS_PER_LANE - 1) { | |
return Infinity; | |
} else { | |
return this.getCarDistance(car, cars[carIdx + 1]); | |
} | |
} | |
hasSpaceFor(car) { | |
const carPos = car.getPosition(); | |
const frontDistances = []; | |
const rearDistances = []; | |
for (const pos of this.cars.values()) { | |
frontDistances.push(carPos - pos); | |
rearDistances.push(pos - carPos); | |
} | |
const minFrontDistance = Math.min(...frontDistances.filter(n => n > 0)); | |
const minRearDistance = Math.min(...rearDistances.filter(n => n > 0)); | |
return minFrontDistance > FOLLOWING_DISTANCE | |
&& minRearDistance > FOLLOWING_DISTANCE; | |
} | |
getLeftLane() { | |
return this.multilane.getAdjacentLeft(this); | |
} | |
getRightLane() { | |
return this.multilane.getAdjacentRight(this); | |
} | |
tick() { | |
const cars = this.getCars(); | |
// move the car positions | |
for (const car of cars) { | |
const newPos = this.getCarPosition(car) + car.getSpeed(); | |
this.cars.set(car, newPos); | |
} | |
// take care of lane switches | |
const pendingMoves = new Map(); | |
for (const car of cars) { | |
if (car.getFrontDistance() < FOLLOWING_DISTANCE) { | |
const ll = this.getLeftLane(); | |
const rl = this.getRightLane(); | |
if (ll && ll.hasSpaceFor(car)) { | |
pendingMoves.set(car, ll); | |
} else if (rl && rl.hasSpaceFor(car)) { | |
pendingMoves.set(car, rl); | |
} else { | |
CONGESTION.push(car.id); | |
} | |
} | |
} | |
for (const [car, lane] of pendingMoves.entries()) { | |
car.moveToLane(lane); | |
} | |
// move the camera position | |
for (const car of this.getCars()) { // recmputing getCars() to account for lane changes | |
this.cars.set(car, this.getCarPosition(car) - AVG_SPEED); | |
} | |
} | |
} | |
class Multilane { | |
constructor() { | |
this.lanes = []; | |
for (let i = 0; i < NUM_LANES; i ++) { | |
this.lanes.push(new Lane(this)); | |
} | |
} | |
getLanes() { | |
return this.lanes; | |
} | |
getAdjacentLeft(lane) { | |
const laneIdx = this.lanes.indexOf(lane); | |
if (laneIdx > 0) { | |
return this.lanes[laneIdx - 1]; | |
} else { | |
return null; | |
} | |
} | |
getAdjacentRight(lane) { | |
const laneIdx = this.lanes.indexOf(lane); | |
if (laneIdx < this.lanes.length - 1) { | |
return this.lanes[laneIdx + 1]; | |
} else { | |
return null; | |
} | |
} | |
getCars() { | |
let cars = []; | |
for (const lane of this.lanes) { | |
cars = cars.concat(lane.getCars()); | |
} | |
return cars; | |
} | |
/** | |
* Move all lanes forward a tick. | |
*/ | |
tick() { | |
for (const lane of this.lanes) { | |
lane.tick(); | |
} | |
} | |
} | |
// main script | |
const CAR_TPLSTR = ` | |
<div class="car"></div> | |
`; | |
const LANE_TPLSTR = ` | |
<div class="lane"></div> | |
`; | |
const CAR_TPL = document.createElement('template'); | |
CAR_TPL.innerHTML = CAR_TPLSTR; | |
const LANE_TPL = document.createElement('template'); | |
LANE_TPL.innerHTML = LANE_TPLSTR; | |
const carDiv = car => { | |
const div = CAR_TPL.cloneNode(true).content.firstElementChild; | |
div.textContent = car.id; | |
div.style.top = `${car.getPosition()}px`; | |
return div; | |
} | |
const laneDiv = lane => { | |
const div = LANE_TPL.cloneNode(true).content.firstElementChild; | |
for (const car of lane.getCars()) { | |
div.appendChild(carDiv(car)); | |
} | |
return div; | |
} | |
const main = () => { | |
// create a multilane setup | |
const ml = new Multilane(); | |
const mlCars = ml.getCars(); | |
const $h = document.querySelector('.highway'); | |
// simulate | |
const frame = () => { | |
ml.tick(); | |
$h.innerHTML = ''; | |
for (const lane of ml.getLanes()) { | |
$h.appendChild(laneDiv(lane)); | |
} | |
if (CONGESTION.length) console.log(CONGESTION.join(' ')); | |
CONGESTION = []; | |
} | |
frame(); | |
setInterval(frame, 300); | |
} | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment