Skip to content

Instantly share code, notes, and snippets.

@thesephist
Created May 24, 2018 18:34
Show Gist options
  • Save thesephist/f99d8400893edd2f1780c1ec8df62ecb to your computer and use it in GitHub Desktop.
Save thesephist/f99d8400893edd2f1780c1ec8df62ecb to your computer and use it in GitHub Desktop.
Highway Lane Changes Simulation
// 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 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