Skip to content

Instantly share code, notes, and snippets.

@kuanb
Last active August 18, 2021 05:38
Show Gist options
  • Select an option

  • Save kuanb/c5cfd0000cec44f05a037a5cce8397db to your computer and use it in GitHub Desktop.

Select an option

Save kuanb/c5cfd0000cec44f05a037a5cce8397db to your computer and use it in GitHub Desktop.

Revisions

  1. kuanb revised this gist Aug 18, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion index.html
    Original file line number Diff line number Diff line change
    @@ -35,7 +35,7 @@ <h3>Stats</h3>
    <p>Avg Speed: <span id="avgSpeedStatRendered"></span></p>
    </body>
    <script src="https://d3js.org/d3.v6.min.js"></script>
    <script>
    <script type="text/javascript">
    // reference sentinel values
    const speedRange = 1.5;
    const speedLimit = 4;
  2. kuanb revised this gist Aug 18, 2021. 2 changed files with 503 additions and 502 deletions.
    504 changes: 503 additions & 1 deletion index.html
    Original file line number Diff line number Diff line change
    @@ -35,5 +35,507 @@ <h3>Stats</h3>
    <p>Avg Speed: <span id="avgSpeedStatRendered"></span></p>
    </body>
    <script src="https://d3js.org/d3.v6.min.js"></script>
    <script src="https://gist.github.com/kuanb/c5cfd0000cec44f05a037a5cce8397db/raw/6f15287dba5820cf6ed821a4bd269180e27569aa/index.js"></script>
    <script>
    // reference sentinel values
    const speedRange = 1.5;
    const speedLimit = 4;
    const laneCount = 5;
    let VEH_ID_INCREMENTER = 0;
    const TRIGGER_BREAKDOWN_FLAG = {on: false, row: null};
    const IS_LOOPING_FLAG = {on: true}
    const TOOBIG = 9999;

    const DomWidth = window.innerWidth * 0.9;
    const DomHeight = 100;
    const DomSVG = d3.select("body").append("svg").attr("width", DomWidth).attr("height", DomHeight).attr("style", "border:1px solid black");

    function getUserSpeedMultiplier () {
    return Number(document.getElementById("vehicleSpeedsMultiplier").value);
    }

    function triggerBreakDown () {
    TRIGGER_BREAKDOWN_FLAG.on = true;
    TRIGGER_BREAKDOWN_FLAG.row = Math.floor(Math.random() * laneCount);
    }

    function resetBreakDownFlag () {
    TRIGGER_BREAKDOWN_FLAG.on = false;
    TRIGGER_BREAKDOWN_FLAG.row = null;
    TRIGGER_BREAKDOWN_FLAG.inResetState = false;
    }

    function unblockBreakDowns () {
    for (let acali = 0; acali < laneCount; acali++) {
    allCarsAllLanes[acali].forEach(ea => {
    ea.isBrokenDown = false;
    })
    }
    }

    function toggleLooping (toggleCheckbox) {
    IS_LOOPING_FLAG.on = toggleCheckbox.checked;
    }

    function getAddOrRemoveLikelihood () {
    let returnVal = 0;
    document.getElementsByName("carAddRemoveRadio").forEach(function (ea) {
    if (ea.checked) {
    returnVal = Number(ea.value);
    }
    })
    return returnVal;
    }

    class Vehicle {
    constructor(laneNumber, vehId) {
    let requiredSpacingComfortFactor = Math.random();

    // create a unique id for tracking and increment global counter
    this.id = vehId;

    this.lane = laneNumber;
    this.distance = 0;
    this.currentSpeed = 0;
    this.acceleration = 0.2 + Math.random();
    this.braking = (1 + Math.random()) * -1;
    this.hardBraking = 2 * this.braking;
    this.vehicleLength = 20 + (Math.random() * 10);
    this.vehicleWidth = 10;
    this.requiredFrontSpacing = 5 + (requiredSpacingComfortFactor * 55);
    this.color = d3.interpolateCubehelixDefault(requiredSpacingComfortFactor);
    this.isPolite = requiredSpacingComfortFactor > 0.5;
    this.isNervous = this.isPolite && (Math.random() < 0.95);
    this.isAggressive = !this.isPolite && (Math.random() < 0.95);
    this.willPaceCarInFront = this.isPolite && !this.isNervous && (Math.random() < 0.95);
    this.isBrokenDown = false;
    this.isMerging = {state: false, phase: 0, destLane: null, mergeBehindCarId: null};

    // speed based on personality of driver, in part
    if (this.isNervous) {
    this.topSpeed = speedLimit + ((Math.random() * 0.25) * speedRange) - speedRange/2;
    } else if (this.isAggressive) {
    this.topSpeed = speedLimit + ((Math.random() * 0.5 + 0.5) * speedRange) - speedRange/2;
    } else {
    this.topSpeed = speedLimit + ((Math.random() * 0.5 + 0.25) * speedRange) - speedRange/2;
    }
    }

    minOffsetFromDistance () {
    return this.vehicleLength + this.requiredFrontSpacing;
    }

    maximumComfortableDistanceInFront () {
    return this.distance + this.currentSpeed + this.minOffsetFromDistance();
    }

    speedIfAccelerateFully () {
    return Math.min((this.currentSpeed + this.acceleration), this.topSpeed);
    }

    speedIfDecelerating () {
    return Math.max((this.currentSpeed + this.braking), 0);
    }

    speedIfHardBraking () {
    return Math.max((this.currentSpeed + this.hardBraking), 0);
    }

    updateDistanceAfterCurrentSpeedUpdated () {
    if (!this.isBrokenDown) {
    this.distance = this.distance + (this.currentSpeed * getUserSpeedMultiplier());
    }
    }

    accelerateForwardMax () {
    this.currentSpeed = this.speedIfAccelerateFully();
    }

    decelerateForwards () {
    this.currentSpeed = this.speedIfDecelerating();
    }

    hardBrakingForwards () {
    this.currentSpeed = this.speedIfHardBraking();
    }

    setCustomSpeed (customSpeed) {
    this.currentSpeed = customSpeed;
    }

    fullStop () {
    this.currentSpeed = 0;
    }

    breakDown () {
    this.isBrokenDown = true;
    this.currentSpeed = 0;
    }

    resetMergingState () {
    this.lane = this.isMerging.destLane;
    this.isMerging = {state: false, phase: 0, destLane: null, mergeBehindCarId: null};
    this.domElement.style("fill", this.color).style("opacity", 0.5);
    }

    estimatePotentialOffsetDistance (customSpeedInput) {
    return this.distance + this.minOffsetFromDistance() + (customSpeedInput * getUserSpeedMultiplier());
    }

    getMergeSpaceNeeded () {
    // determine how much of a squeeze is ok for merges
    let howMuchSpaceNeededForMerge = {front: 0.5, back: 1.5};
    if (this.isAggressive) {
    howMuchSpaceNeededForMerge = {front: 0.25, back: 0.75};
    }

    howMuchSpaceNeededForMerge.distanceBack = this.distance - (howMuchSpaceNeededForMerge.back * this.minOffsetFromDistance());
    howMuchSpaceNeededForMerge.distanceFront = this.distance + (howMuchSpaceNeededForMerge.front * this.minOffsetFromDistance());
    return howMuchSpaceNeededForMerge;
    }

    considerMerging(carInFront, leftLaneNumber, leftLaneCars, rightLaneNumber, rightLaneCars) {
    let oddsOfMerging = Math.random();
    if (this.isAggressive) {
    oddsOfMerging += 0.2;
    } else if (this.isNervous) {
    oddsOfMerging -= 0.5;
    } else if (this.isPolite){
    oddsOfMerging -= 0.1;
    }

    let shouldMerge = false;
    if (!carInFront) {
    // no need to merge if already in front
    shouldMerge = false;
    } else if (carInFront.distance < this.estimatePotentialOffsetDistance(this.speedIfAccelerateFully()) * 2) {
    if (carInFront.currentSpeed < (this.currentSpeed * 0.9)) {
    shouldMerge = true;
    }
    } else if (carInFront.distance < this.estimatePotentialOffsetDistance(this.speedIfAccelerateFully()) * 5) {
    if (carInFront.currentSpeed < (this.currentSpeed * 0.8)) {
    shouldMerge = true;
    }
    }

    // do not merge off screen
    if (this.distance < (DomWidth * 0.15)) {
    shouldMerge = false;
    } else if (this.distance < (DomWidth > 0.85)) {
    shouldMerge = false;
    }

    // even if you can maybe you won't
    shouldMerge = (shouldMerge && oddsOfMerging > 0.975);

    const howMuchSpaceNeededForMerge = this.getMergeSpaceNeeded();

    // if you can need to pick which lane
    let mergeToLane = null;
    let nextCarInMergeLane = null;

    if (shouldMerge && leftLaneCars) {
    let leftSub = leftLaneCars
    .filter(ea => ea.distance > howMuchSpaceNeededForMerge.distanceBack)
    .filter(ea => ea.distance < howMuchSpaceNeededForMerge.distanceFront);
    if (leftSub.length) {
    mergeToLane = leftLaneNumber;

    let upcomingLeftCars = leftLaneCars.filter(ea => ea.distance >= howMuchSpaceNeededForMerge.distanceFront);
    if (upcomingLeftCars.length) {
    nextCarInMergeLane = upcomingLeftCars[0].id
    }
    }
    }

    if (shouldMerge && rightLaneCars) {
    let rightSub = rightLaneCars
    .filter(ea => ea.distance > howMuchSpaceNeededForMerge.distanceBack)
    .filter(ea => ea.distance < howMuchSpaceNeededForMerge.distanceFront);
    if (rightSub.length) {
    let didUseRight = false;
    if ((mergeToLane != null) && (Math.random() > 0.7)) {
    mergeToLane = rightLaneNumber;
    didUseRight = true;
    } else {
    mergeToLane = rightLaneNumber;
    didUseRight = true;
    }

    if (didUseRight) {
    let upcomingRightCars = rightLaneCars.filter(ea => ea.distance >= howMuchSpaceNeededForMerge.distanceFront);
    if (upcomingRightCars.length) {
    nextCarInMergeLane = upcomingRightCars[0].id
    }
    }
    }
    }


    if ((this.isMerging || shouldMerge) && (mergeToLane != null)) {
    if (!this.isMerging.state) {
    this.isMerging.state = true;
    this.isMerging.mergeBehindCarId = nextCarInMergeLane;
    this.isMerging.destLane = mergeToLane;
    this.isMerging.phase = 1;
    this.domElement.style("fill", "red").style("opacity", 0.85);
    }
    }

    return this.isMerging.state;
    }

    updateVehiclePosition (carInFront, carInFrontInMergeLane) {
    if (!carInFront) {
    this.accelerateForwardMax();
    } else if (this.isBrokenDown) {
    // pass no action if broken down
    } else {
    // handle when there are cars that have looped
    let adjustedDistanceAhead = carInFront.distance;
    if (carInFrontInMergeLane && carInFrontInMergeLane.distance < adjustedDistanceAhead) {
    adjustedDistanceAhead = carInFrontInMergeLane.distance;
    }
    if (IS_LOOPING_FLAG.on && (adjustedDistanceAhead <= this.distance)) {
    adjustedDistanceAhead += DomWidth;
    }

    if (this.estimatePotentialOffsetDistance(this.speedIfAccelerateFully()) < adjustedDistanceAhead) {
    this.accelerateForwardMax();
    } else if (this.estimatePotentialOffsetDistance(this.currentSpeed) < adjustedDistanceAhead) {
    if (!this.isPolite) {
    this.accelerateForwardMax();
    } else if (this.isNervous) {
    this.decelerateForwards();
    } else if (this.willPaceCarInFront) {
    if (carInFrontInMergeLane && carInFrontInMergeLane.currentSpeed < carInFront.currentSpeed) {
    this.setCustomSpeed(carInFrontInMergeLane.currentSpeed);
    } else {
    this.setCustomSpeed(carInFront.currentSpeed);
    }
    }
    // do nothing since no need to update speed if being polite
    } else if (this.estimatePotentialOffsetDistance(this.speedIfDecelerating()) < adjustedDistanceAhead) {
    if (!this.isPolite) {
    this.decelerateForwards();
    } else if (this.isNervous) {
    this.hardBrakingForwards();
    } else {
    // try and move forward at current speed
    }
    } else if (this.estimatePotentialOffsetDistance(this.speedIfHardBraking()) < adjustedDistanceAhead) {
    this.hardBrakingForwards();
    } else {
    this.fullStop();
    }
    }

    this.updateDistanceAfterCurrentSpeedUpdated();
    }

    redrawCarLocation () {
    this.domElement.attr("x", this.distance);

    // only keep merging if moving forward (no sideways sliding)
    if (this.isMerging.state && this.currentSpeed > 0) {
    const shiftAmount = 15 * ((this.isMerging.destLane - this.lane) / 10) * this.isMerging.phase;
    const adjY = shiftAmount + 15 + 15 * this.lane;
    this.domElement.attr("y", adjY);
    }
    }

    addCarToDom () {
    this.domElement = DomSVG
    .append("rect")
    .style("fill", this.color)
    .attr("x", this.distance)
    .attr("y", (15 + 15 * this.lane))
    .attr("opacity", 0.5)
    .attr("height", this.vehicleWidth)
    .attr("width", this.vehicleLength);
    }

    deleteFromDom () {
    this.domElement.remove();
    }
    }

    function getCarById (targetId) {
    for (let acali = 0; acali < laneCount; acali++) {
    let allCars = allCarsAllLanes[acali];
    allCars.forEach(currCar => {
    if (currCar.id == targetId) {
    return currCar;
    }
    });
    }
    }

    let allCarsAllLanes = [];
    for (let i = 0; i < laneCount; i++) {
    allCarsAllLanes.push([])
    }

    const runCycle = setInterval(function() {
    const allAverageSpeeds = [];

    // merge phase - make merge changes
    for (let acali = 0; acali < laneCount; acali++) {
    let allCars = allCarsAllLanes[acali];

    // first thing is to prune out old merge operations
    allCarsAllLanes[acali] = allCarsAllLanes[acali].filter(currCar => {
    // handle orphaned already merged cars
    if ((currCar.lane != acali) && !currCar.isMerging.state) {
    return false;
    }
    return true;

    }).map(currCar => {
    // also increment phase of active mergers
    if (currCar.isMerging.state && currCar.isMerging.destLane == acali) {
    currCar.isMerging.phase = currCar.isMerging.phase + 1;
    }

    if (currCar.isMerging.state && currCar.isMerging.phase > 10) {
    // reset merging state if destination lane has been reached
    currCar.resetMergingState();
    }

    return currCar;
    });

    for (var i = allCars.length - 1; i >= 0; i--) {
    let currCar = allCars[i];

    let nextCar = null;
    if (i == allCars.length - 1) {
    // this is the rightmost car
    nextCar = null;
    } else {
    nextCar = allCars[i+1];
    }

    let leftLaneNumber = null;
    let leftLane = null;
    let rightLaneNumber = null;
    let rightLane = null;

    if (acali > 0) {
    leftLaneNumber = acali - 1;
    leftLane = allCarsAllLanes[leftLaneNumber];
    }
    if (acali < allCarsAllLanes.length - 1) {
    rightLaneNumber = acali + 1;
    rightLane = allCarsAllLanes[rightLaneNumber];

    }

    // consider merging given current conditions for each car
    if (!currCar.isMerging.state && currCar.considerMerging(nextCar, leftLaneNumber, leftLane, rightLaneNumber, rightLane)) {
    let a = allCarsAllLanes[currCar.isMerging.destLane].filter(ea => ea.distance < currCar.distance);
    let b = allCarsAllLanes[currCar.isMerging.destLane].filter(ea => ea.distance > currCar.distance);
    allCarsAllLanes[currCar.isMerging.destLane] = a.concat([currCar]).concat(b);
    }
    }
    }

    for (let acali = 0; acali < laneCount; acali++) {
    let allCars = allCarsAllLanes[acali];

    // there must always be at leat 1 car on the road
    if (allCars.length == 0) {
    const newVehicleToAdd = new Vehicle(acali, VEH_ID_INCREMENTER);
    newVehicleToAdd.addCarToDom();
    VEH_ID_INCREMENTER += 1;
    allCars = [newVehicleToAdd];
    }

    let newCarsList = allCars.filter(ea => ea.distance < DomWidth);
    let carsThatWentOffScreen = allCars.filter(ea => ea.distance >= DomWidth)

    if (IS_LOOPING_FLAG.on) {
    carsThatWentOffScreen = carsThatWentOffScreen.map(ea => {
    ea.distance = Math.min(0, allCars[0].distance - (ea.minOffsetFromDistance()));
    return ea;
    });

    if (carsThatWentOffScreen.length) {
    newCarsList = carsThatWentOffScreen.concat(newCarsList);
    }
    } else {
    carsThatWentOffScreen.forEach(ea => {
    ea.deleteFromDom();
    });
    }

    for (var i = newCarsList.length - 1; i >= 0; i--) {
    let currCar = newCarsList[i];

    let nextCarInMergeLane = null;
    if (currCar.isMerging.state) {
    nextCarInMergeLane = getCarById(currCar.isMerging.mergeBehindCarId);
    }

    // avoid drawing when we are a merge element and not already in lane
    if (currCar.lane == acali) {

    if (TRIGGER_BREAKDOWN_FLAG.on && TRIGGER_BREAKDOWN_FLAG.row == acali) {
    currCar.breakDown();
    resetBreakDownFlag()
    }

    if (i == newCarsList.length - 1) {
    // if the rightmost car, then reference the leftmost (if loop)
    if (IS_LOOPING_FLAG.on) {
    currCar.updateVehiclePosition(newCarsList[0], nextCarInMergeLane);
    } else {
    currCar.updateVehiclePosition(null, nextCarInMergeLane);
    }
    } else if (newCarsList.length <= 1) {
    currCar.updateVehiclePosition(null, nextCarInMergeLane);
    } else {
    const carInFront = newCarsList[i + 1];
    currCar.updateVehiclePosition(carInFront, nextCarInMergeLane);
    }

    // update render location
    currCar.redrawCarLocation();
    }
    }

    const newPotentialCar = new Vehicle(acali, VEH_ID_INCREMENTER);
    const lastCarInLine = newCarsList[0];
    const likelihoodForAddingCarsOrRemoving = getAddOrRemoveLikelihood();
    if (likelihoodForAddingCarsOrRemoving > 0 && (Math.random() > 0.5)) {
    if (lastCarInLine.distance > newPotentialCar.minOffsetFromDistance()) {
    newPotentialCar.addCarToDom();
    VEH_ID_INCREMENTER += 1;
    newCarsList = [newPotentialCar].concat(newCarsList);
    }
    } else if (likelihoodForAddingCarsOrRemoving < 0 && (Math.random() > 0.5)) {
    const removeTheseCars = newCarsList.splice(-1, 1);
    removeTheseCars.forEach(ea => {
    ea.deleteFromDom();
    })
    }

    allAverageSpeeds.extend(newCarsList.map(ea => ea.currentSpeed));
    allCarsAllLanes[acali] = newCarsList;
    }

    let averageCurrentSpeed = 0;
    if (allAverageSpeeds.length) {
    averageCurrentSpeed = allAverageSpeeds.reduce((a,b) => (a + b))/allAverageSpeeds.length;
    }
    document.getElementById("avgSpeedStatRendered").innerText = averageCurrentSpeed.toFixed(2);
    document.getElementById("totalVehicleCount").innerText = allAverageSpeeds.length + " of " + VEH_ID_INCREMENTER + " total created";

    }, 50)

    // utility function/s
    Array.prototype.extend = function (other_array) {
    /* You should include a test to check whether other_array really is an array */
    other_array.forEach(function(v) {this.push(v)}, this);
    }
    </script>
    </html>
    501 changes: 0 additions & 501 deletions index.js
    Original file line number Diff line number Diff line change
    @@ -1,501 +0,0 @@
    // reference sentinel values
    const speedRange = 1.5;
    const speedLimit = 4;
    const laneCount = 5;
    let VEH_ID_INCREMENTER = 0;
    const TRIGGER_BREAKDOWN_FLAG = {on: false, row: null};
    const IS_LOOPING_FLAG = {on: true}
    const TOOBIG = 9999;

    const DomWidth = window.innerWidth * 0.9;
    const DomHeight = 100;
    const DomSVG = d3.select("body").append("svg").attr("width", DomWidth).attr("height", DomHeight).attr("style", "border:1px solid black");

    function getUserSpeedMultiplier () {
    return Number(document.getElementById("vehicleSpeedsMultiplier").value);
    }

    function triggerBreakDown () {
    TRIGGER_BREAKDOWN_FLAG.on = true;
    TRIGGER_BREAKDOWN_FLAG.row = Math.floor(Math.random() * laneCount);
    }

    function resetBreakDownFlag () {
    TRIGGER_BREAKDOWN_FLAG.on = false;
    TRIGGER_BREAKDOWN_FLAG.row = null;
    TRIGGER_BREAKDOWN_FLAG.inResetState = false;
    }

    function unblockBreakDowns () {
    for (let acali = 0; acali < laneCount; acali++) {
    allCarsAllLanes[acali].forEach(ea => {
    ea.isBrokenDown = false;
    })
    }
    }

    function toggleLooping (toggleCheckbox) {
    IS_LOOPING_FLAG.on = toggleCheckbox.checked;
    }

    function getAddOrRemoveLikelihood () {
    let returnVal = 0;
    document.getElementsByName("carAddRemoveRadio").forEach(function (ea) {
    if (ea.checked) {
    returnVal = Number(ea.value);
    }
    })
    return returnVal;
    }

    class Vehicle {
    constructor(laneNumber, vehId) {
    let requiredSpacingComfortFactor = Math.random();

    // create a unique id for tracking and increment global counter
    this.id = vehId;

    this.lane = laneNumber;
    this.distance = 0;
    this.currentSpeed = 0;
    this.acceleration = 0.2 + Math.random();
    this.braking = (1 + Math.random()) * -1;
    this.hardBraking = 2 * this.braking;
    this.vehicleLength = 20 + (Math.random() * 10);
    this.vehicleWidth = 10;
    this.requiredFrontSpacing = 5 + (requiredSpacingComfortFactor * 55);
    this.color = d3.interpolateCubehelixDefault(requiredSpacingComfortFactor);
    this.isPolite = requiredSpacingComfortFactor > 0.5;
    this.isNervous = this.isPolite && (Math.random() < 0.95);
    this.isAggressive = !this.isPolite && (Math.random() < 0.95);
    this.willPaceCarInFront = this.isPolite && !this.isNervous && (Math.random() < 0.95);
    this.isBrokenDown = false;
    this.isMerging = {state: false, phase: 0, destLane: null, mergeBehindCarId: null};

    // speed based on personality of driver, in part
    if (this.isNervous) {
    this.topSpeed = speedLimit + ((Math.random() * 0.25) * speedRange) - speedRange/2;
    } else if (this.isAggressive) {
    this.topSpeed = speedLimit + ((Math.random() * 0.5 + 0.5) * speedRange) - speedRange/2;
    } else {
    this.topSpeed = speedLimit + ((Math.random() * 0.5 + 0.25) * speedRange) - speedRange/2;
    }
    }

    minOffsetFromDistance () {
    return this.vehicleLength + this.requiredFrontSpacing;
    }

    maximumComfortableDistanceInFront () {
    return this.distance + this.currentSpeed + this.minOffsetFromDistance();
    }

    speedIfAccelerateFully () {
    return Math.min((this.currentSpeed + this.acceleration), this.topSpeed);
    }

    speedIfDecelerating () {
    return Math.max((this.currentSpeed + this.braking), 0);
    }

    speedIfHardBraking () {
    return Math.max((this.currentSpeed + this.hardBraking), 0);
    }

    updateDistanceAfterCurrentSpeedUpdated () {
    if (!this.isBrokenDown) {
    this.distance = this.distance + (this.currentSpeed * getUserSpeedMultiplier());
    }
    }

    accelerateForwardMax () {
    this.currentSpeed = this.speedIfAccelerateFully();
    }

    decelerateForwards () {
    this.currentSpeed = this.speedIfDecelerating();
    }

    hardBrakingForwards () {
    this.currentSpeed = this.speedIfHardBraking();
    }

    setCustomSpeed (customSpeed) {
    this.currentSpeed = customSpeed;
    }

    fullStop () {
    this.currentSpeed = 0;
    }

    breakDown () {
    this.isBrokenDown = true;
    this.currentSpeed = 0;
    }

    resetMergingState () {
    this.lane = this.isMerging.destLane;
    this.isMerging = {state: false, phase: 0, destLane: null, mergeBehindCarId: null};
    this.domElement.style("fill", this.color).style("opacity", 0.5);
    }

    estimatePotentialOffsetDistance (customSpeedInput) {
    return this.distance + this.minOffsetFromDistance() + (customSpeedInput * getUserSpeedMultiplier());
    }

    getMergeSpaceNeeded () {
    // determine how much of a squeeze is ok for merges
    let howMuchSpaceNeededForMerge = {front: 0.5, back: 1.5};
    if (this.isAggressive) {
    howMuchSpaceNeededForMerge = {front: 0.25, back: 0.75};
    }

    howMuchSpaceNeededForMerge.distanceBack = this.distance - (howMuchSpaceNeededForMerge.back * this.minOffsetFromDistance());
    howMuchSpaceNeededForMerge.distanceFront = this.distance + (howMuchSpaceNeededForMerge.front * this.minOffsetFromDistance());
    return howMuchSpaceNeededForMerge;
    }

    considerMerging(carInFront, leftLaneNumber, leftLaneCars, rightLaneNumber, rightLaneCars) {
    let oddsOfMerging = Math.random();
    if (this.isAggressive) {
    oddsOfMerging += 0.2;
    } else if (this.isNervous) {
    oddsOfMerging -= 0.5;
    } else if (this.isPolite){
    oddsOfMerging -= 0.1;
    }

    let shouldMerge = false;
    if (!carInFront) {
    // no need to merge if already in front
    shouldMerge = false;
    } else if (carInFront.distance < this.estimatePotentialOffsetDistance(this.speedIfAccelerateFully()) * 2) {
    if (carInFront.currentSpeed < (this.currentSpeed * 0.9)) {
    shouldMerge = true;
    }
    } else if (carInFront.distance < this.estimatePotentialOffsetDistance(this.speedIfAccelerateFully()) * 5) {
    if (carInFront.currentSpeed < (this.currentSpeed * 0.8)) {
    shouldMerge = true;
    }
    }

    // do not merge off screen
    if (this.distance < (DomWidth * 0.15)) {
    shouldMerge = false;
    } else if (this.distance < (DomWidth > 0.85)) {
    shouldMerge = false;
    }

    // even if you can maybe you won't
    shouldMerge = (shouldMerge && oddsOfMerging > 0.975);

    const howMuchSpaceNeededForMerge = this.getMergeSpaceNeeded();

    // if you can need to pick which lane
    let mergeToLane = null;
    let nextCarInMergeLane = null;

    if (shouldMerge && leftLaneCars) {
    let leftSub = leftLaneCars
    .filter(ea => ea.distance > howMuchSpaceNeededForMerge.distanceBack)
    .filter(ea => ea.distance < howMuchSpaceNeededForMerge.distanceFront);
    if (leftSub.length) {
    mergeToLane = leftLaneNumber;

    let upcomingLeftCars = leftLaneCars.filter(ea => ea.distance >= howMuchSpaceNeededForMerge.distanceFront);
    if (upcomingLeftCars.length) {
    nextCarInMergeLane = upcomingLeftCars[0].id
    }
    }
    }

    if (shouldMerge && rightLaneCars) {
    let rightSub = rightLaneCars
    .filter(ea => ea.distance > howMuchSpaceNeededForMerge.distanceBack)
    .filter(ea => ea.distance < howMuchSpaceNeededForMerge.distanceFront);
    if (rightSub.length) {
    let didUseRight = false;
    if ((mergeToLane != null) && (Math.random() > 0.7)) {
    mergeToLane = rightLaneNumber;
    didUseRight = true;
    } else {
    mergeToLane = rightLaneNumber;
    didUseRight = true;
    }

    if (didUseRight) {
    let upcomingRightCars = rightLaneCars.filter(ea => ea.distance >= howMuchSpaceNeededForMerge.distanceFront);
    if (upcomingRightCars.length) {
    nextCarInMergeLane = upcomingRightCars[0].id
    }
    }
    }
    }


    if ((this.isMerging || shouldMerge) && (mergeToLane != null)) {
    if (!this.isMerging.state) {
    this.isMerging.state = true;
    this.isMerging.mergeBehindCarId = nextCarInMergeLane;
    this.isMerging.destLane = mergeToLane;
    this.isMerging.phase = 1;
    this.domElement.style("fill", "red").style("opacity", 0.85);
    }
    }

    return this.isMerging.state;
    }

    updateVehiclePosition (carInFront, carInFrontInMergeLane) {
    if (!carInFront) {
    this.accelerateForwardMax();
    } else if (this.isBrokenDown) {
    // pass no action if broken down
    } else {
    // handle when there are cars that have looped
    let adjustedDistanceAhead = carInFront.distance;
    if (carInFrontInMergeLane && carInFrontInMergeLane.distance < adjustedDistanceAhead) {
    adjustedDistanceAhead = carInFrontInMergeLane.distance;
    }
    if (IS_LOOPING_FLAG.on && (adjustedDistanceAhead <= this.distance)) {
    adjustedDistanceAhead += DomWidth;
    }

    if (this.estimatePotentialOffsetDistance(this.speedIfAccelerateFully()) < adjustedDistanceAhead) {
    this.accelerateForwardMax();
    } else if (this.estimatePotentialOffsetDistance(this.currentSpeed) < adjustedDistanceAhead) {
    if (!this.isPolite) {
    this.accelerateForwardMax();
    } else if (this.isNervous) {
    this.decelerateForwards();
    } else if (this.willPaceCarInFront) {
    if (carInFrontInMergeLane && carInFrontInMergeLane.currentSpeed < carInFront.currentSpeed) {
    this.setCustomSpeed(carInFrontInMergeLane.currentSpeed);
    } else {
    this.setCustomSpeed(carInFront.currentSpeed);
    }
    }
    // do nothing since no need to update speed if being polite
    } else if (this.estimatePotentialOffsetDistance(this.speedIfDecelerating()) < adjustedDistanceAhead) {
    if (!this.isPolite) {
    this.decelerateForwards();
    } else if (this.isNervous) {
    this.hardBrakingForwards();
    } else {
    // try and move forward at current speed
    }
    } else if (this.estimatePotentialOffsetDistance(this.speedIfHardBraking()) < adjustedDistanceAhead) {
    this.hardBrakingForwards();
    } else {
    this.fullStop();
    }
    }

    this.updateDistanceAfterCurrentSpeedUpdated();
    }

    redrawCarLocation () {
    this.domElement.attr("x", this.distance);

    // only keep merging if moving forward (no sideways sliding)
    if (this.isMerging.state && this.currentSpeed > 0) {
    const shiftAmount = 15 * ((this.isMerging.destLane - this.lane) / 10) * this.isMerging.phase;
    const adjY = shiftAmount + 15 + 15 * this.lane;
    this.domElement.attr("y", adjY);
    }
    }

    addCarToDom () {
    this.domElement = DomSVG
    .append("rect")
    .style("fill", this.color)
    .attr("x", this.distance)
    .attr("y", (15 + 15 * this.lane))
    .attr("opacity", 0.5)
    .attr("height", this.vehicleWidth)
    .attr("width", this.vehicleLength);
    }

    deleteFromDom () {
    this.domElement.remove();
    }
    }

    function getCarById (targetId) {
    for (let acali = 0; acali < laneCount; acali++) {
    let allCars = allCarsAllLanes[acali];
    allCars.forEach(currCar => {
    if (currCar.id == targetId) {
    return currCar;
    }
    });
    }
    }

    let allCarsAllLanes = [];
    for (let i = 0; i < laneCount; i++) {
    allCarsAllLanes.push([])
    }

    const runCycle = setInterval(function() {
    const allAverageSpeeds = [];

    // merge phase - make merge changes
    for (let acali = 0; acali < laneCount; acali++) {
    let allCars = allCarsAllLanes[acali];

    // first thing is to prune out old merge operations
    allCarsAllLanes[acali] = allCarsAllLanes[acali].filter(currCar => {
    // handle orphaned already merged cars
    if ((currCar.lane != acali) && !currCar.isMerging.state) {
    return false;
    }
    return true;

    }).map(currCar => {
    // also increment phase of active mergers
    if (currCar.isMerging.state && currCar.isMerging.destLane == acali) {
    currCar.isMerging.phase = currCar.isMerging.phase + 1;
    }

    if (currCar.isMerging.state && currCar.isMerging.phase > 10) {
    // reset merging state if destination lane has been reached
    currCar.resetMergingState();
    }

    return currCar;
    });

    for (var i = allCars.length - 1; i >= 0; i--) {
    let currCar = allCars[i];

    let nextCar = null;
    if (i == allCars.length - 1) {
    // this is the rightmost car
    nextCar = null;
    } else {
    nextCar = allCars[i+1];
    }

    let leftLaneNumber = null;
    let leftLane = null;
    let rightLaneNumber = null;
    let rightLane = null;

    if (acali > 0) {
    leftLaneNumber = acali - 1;
    leftLane = allCarsAllLanes[leftLaneNumber];
    }
    if (acali < allCarsAllLanes.length - 1) {
    rightLaneNumber = acali + 1;
    rightLane = allCarsAllLanes[rightLaneNumber];

    }

    // consider merging given current conditions for each car
    if (!currCar.isMerging.state && currCar.considerMerging(nextCar, leftLaneNumber, leftLane, rightLaneNumber, rightLane)) {
    let a = allCarsAllLanes[currCar.isMerging.destLane].filter(ea => ea.distance < currCar.distance);
    let b = allCarsAllLanes[currCar.isMerging.destLane].filter(ea => ea.distance > currCar.distance);
    allCarsAllLanes[currCar.isMerging.destLane] = a.concat([currCar]).concat(b);
    }
    }
    }

    for (let acali = 0; acali < laneCount; acali++) {
    let allCars = allCarsAllLanes[acali];

    // there must always be at leat 1 car on the road
    if (allCars.length == 0) {
    const newVehicleToAdd = new Vehicle(acali, VEH_ID_INCREMENTER);
    newVehicleToAdd.addCarToDom();
    VEH_ID_INCREMENTER += 1;
    allCars = [newVehicleToAdd];
    }

    let newCarsList = allCars.filter(ea => ea.distance < DomWidth);
    let carsThatWentOffScreen = allCars.filter(ea => ea.distance >= DomWidth)

    if (IS_LOOPING_FLAG.on) {
    carsThatWentOffScreen = carsThatWentOffScreen.map(ea => {
    ea.distance = Math.min(0, allCars[0].distance - (ea.minOffsetFromDistance()));
    return ea;
    });

    if (carsThatWentOffScreen.length) {
    newCarsList = carsThatWentOffScreen.concat(newCarsList);
    }
    } else {
    carsThatWentOffScreen.forEach(ea => {
    ea.deleteFromDom();
    });
    }

    for (var i = newCarsList.length - 1; i >= 0; i--) {
    let currCar = newCarsList[i];

    let nextCarInMergeLane = null;
    if (currCar.isMerging.state) {
    nextCarInMergeLane = getCarById(currCar.isMerging.mergeBehindCarId);
    }

    // avoid drawing when we are a merge element and not already in lane
    if (currCar.lane == acali) {

    if (TRIGGER_BREAKDOWN_FLAG.on && TRIGGER_BREAKDOWN_FLAG.row == acali) {
    currCar.breakDown();
    resetBreakDownFlag()
    }

    if (i == newCarsList.length - 1) {
    // if the rightmost car, then reference the leftmost (if loop)
    if (IS_LOOPING_FLAG.on) {
    currCar.updateVehiclePosition(newCarsList[0], nextCarInMergeLane);
    } else {
    currCar.updateVehiclePosition(null, nextCarInMergeLane);
    }
    } else if (newCarsList.length <= 1) {
    currCar.updateVehiclePosition(null, nextCarInMergeLane);
    } else {
    const carInFront = newCarsList[i + 1];
    currCar.updateVehiclePosition(carInFront, nextCarInMergeLane);
    }

    // update render location
    currCar.redrawCarLocation();
    }
    }

    const newPotentialCar = new Vehicle(acali, VEH_ID_INCREMENTER);
    const lastCarInLine = newCarsList[0];
    const likelihoodForAddingCarsOrRemoving = getAddOrRemoveLikelihood();
    if (likelihoodForAddingCarsOrRemoving > 0 && (Math.random() > 0.5)) {
    if (lastCarInLine.distance > newPotentialCar.minOffsetFromDistance()) {
    newPotentialCar.addCarToDom();
    VEH_ID_INCREMENTER += 1;
    newCarsList = [newPotentialCar].concat(newCarsList);
    }
    } else if (likelihoodForAddingCarsOrRemoving < 0 && (Math.random() > 0.5)) {
    const removeTheseCars = newCarsList.splice(-1, 1);
    removeTheseCars.forEach(ea => {
    ea.deleteFromDom();
    })
    }

    allAverageSpeeds.extend(newCarsList.map(ea => ea.currentSpeed));
    allCarsAllLanes[acali] = newCarsList;
    }

    let averageCurrentSpeed = 0;
    if (allAverageSpeeds.length) {
    averageCurrentSpeed = allAverageSpeeds.reduce((a,b) => (a + b))/allAverageSpeeds.length;
    }
    document.getElementById("avgSpeedStatRendered").innerText = averageCurrentSpeed.toFixed(2);
    document.getElementById("totalVehicleCount").innerText = allAverageSpeeds.length + " of " + VEH_ID_INCREMENTER + " total created";

    }, 50)

    // utility function/s
    Array.prototype.extend = function (other_array) {
    /* You should include a test to check whether other_array really is an array */
    other_array.forEach(function(v) {this.push(v)}, this);
    }
  3. kuanb revised this gist Aug 18, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion index.html
    Original file line number Diff line number Diff line change
    @@ -35,5 +35,5 @@ <h3>Stats</h3>
    <p>Avg Speed: <span id="avgSpeedStatRendered"></span></p>
    </body>
    <script src="https://d3js.org/d3.v6.min.js"></script>
    <script src="index.js"></script>
    <script src="https://gist.github.com/kuanb/c5cfd0000cec44f05a037a5cce8397db/raw/6f15287dba5820cf6ed821a4bd269180e27569aa/index.js"></script>
    </html>
  4. kuanb renamed this gist Aug 18, 2021. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  5. kuanb created this gist Aug 18, 2021.
    501 changes: 501 additions & 0 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,501 @@
    // reference sentinel values
    const speedRange = 1.5;
    const speedLimit = 4;
    const laneCount = 5;
    let VEH_ID_INCREMENTER = 0;
    const TRIGGER_BREAKDOWN_FLAG = {on: false, row: null};
    const IS_LOOPING_FLAG = {on: true}
    const TOOBIG = 9999;

    const DomWidth = window.innerWidth * 0.9;
    const DomHeight = 100;
    const DomSVG = d3.select("body").append("svg").attr("width", DomWidth).attr("height", DomHeight).attr("style", "border:1px solid black");

    function getUserSpeedMultiplier () {
    return Number(document.getElementById("vehicleSpeedsMultiplier").value);
    }

    function triggerBreakDown () {
    TRIGGER_BREAKDOWN_FLAG.on = true;
    TRIGGER_BREAKDOWN_FLAG.row = Math.floor(Math.random() * laneCount);
    }

    function resetBreakDownFlag () {
    TRIGGER_BREAKDOWN_FLAG.on = false;
    TRIGGER_BREAKDOWN_FLAG.row = null;
    TRIGGER_BREAKDOWN_FLAG.inResetState = false;
    }

    function unblockBreakDowns () {
    for (let acali = 0; acali < laneCount; acali++) {
    allCarsAllLanes[acali].forEach(ea => {
    ea.isBrokenDown = false;
    })
    }
    }

    function toggleLooping (toggleCheckbox) {
    IS_LOOPING_FLAG.on = toggleCheckbox.checked;
    }

    function getAddOrRemoveLikelihood () {
    let returnVal = 0;
    document.getElementsByName("carAddRemoveRadio").forEach(function (ea) {
    if (ea.checked) {
    returnVal = Number(ea.value);
    }
    })
    return returnVal;
    }

    class Vehicle {
    constructor(laneNumber, vehId) {
    let requiredSpacingComfortFactor = Math.random();

    // create a unique id for tracking and increment global counter
    this.id = vehId;

    this.lane = laneNumber;
    this.distance = 0;
    this.currentSpeed = 0;
    this.acceleration = 0.2 + Math.random();
    this.braking = (1 + Math.random()) * -1;
    this.hardBraking = 2 * this.braking;
    this.vehicleLength = 20 + (Math.random() * 10);
    this.vehicleWidth = 10;
    this.requiredFrontSpacing = 5 + (requiredSpacingComfortFactor * 55);
    this.color = d3.interpolateCubehelixDefault(requiredSpacingComfortFactor);
    this.isPolite = requiredSpacingComfortFactor > 0.5;
    this.isNervous = this.isPolite && (Math.random() < 0.95);
    this.isAggressive = !this.isPolite && (Math.random() < 0.95);
    this.willPaceCarInFront = this.isPolite && !this.isNervous && (Math.random() < 0.95);
    this.isBrokenDown = false;
    this.isMerging = {state: false, phase: 0, destLane: null, mergeBehindCarId: null};

    // speed based on personality of driver, in part
    if (this.isNervous) {
    this.topSpeed = speedLimit + ((Math.random() * 0.25) * speedRange) - speedRange/2;
    } else if (this.isAggressive) {
    this.topSpeed = speedLimit + ((Math.random() * 0.5 + 0.5) * speedRange) - speedRange/2;
    } else {
    this.topSpeed = speedLimit + ((Math.random() * 0.5 + 0.25) * speedRange) - speedRange/2;
    }
    }

    minOffsetFromDistance () {
    return this.vehicleLength + this.requiredFrontSpacing;
    }

    maximumComfortableDistanceInFront () {
    return this.distance + this.currentSpeed + this.minOffsetFromDistance();
    }

    speedIfAccelerateFully () {
    return Math.min((this.currentSpeed + this.acceleration), this.topSpeed);
    }

    speedIfDecelerating () {
    return Math.max((this.currentSpeed + this.braking), 0);
    }

    speedIfHardBraking () {
    return Math.max((this.currentSpeed + this.hardBraking), 0);
    }

    updateDistanceAfterCurrentSpeedUpdated () {
    if (!this.isBrokenDown) {
    this.distance = this.distance + (this.currentSpeed * getUserSpeedMultiplier());
    }
    }

    accelerateForwardMax () {
    this.currentSpeed = this.speedIfAccelerateFully();
    }

    decelerateForwards () {
    this.currentSpeed = this.speedIfDecelerating();
    }

    hardBrakingForwards () {
    this.currentSpeed = this.speedIfHardBraking();
    }

    setCustomSpeed (customSpeed) {
    this.currentSpeed = customSpeed;
    }

    fullStop () {
    this.currentSpeed = 0;
    }

    breakDown () {
    this.isBrokenDown = true;
    this.currentSpeed = 0;
    }

    resetMergingState () {
    this.lane = this.isMerging.destLane;
    this.isMerging = {state: false, phase: 0, destLane: null, mergeBehindCarId: null};
    this.domElement.style("fill", this.color).style("opacity", 0.5);
    }

    estimatePotentialOffsetDistance (customSpeedInput) {
    return this.distance + this.minOffsetFromDistance() + (customSpeedInput * getUserSpeedMultiplier());
    }

    getMergeSpaceNeeded () {
    // determine how much of a squeeze is ok for merges
    let howMuchSpaceNeededForMerge = {front: 0.5, back: 1.5};
    if (this.isAggressive) {
    howMuchSpaceNeededForMerge = {front: 0.25, back: 0.75};
    }

    howMuchSpaceNeededForMerge.distanceBack = this.distance - (howMuchSpaceNeededForMerge.back * this.minOffsetFromDistance());
    howMuchSpaceNeededForMerge.distanceFront = this.distance + (howMuchSpaceNeededForMerge.front * this.minOffsetFromDistance());
    return howMuchSpaceNeededForMerge;
    }

    considerMerging(carInFront, leftLaneNumber, leftLaneCars, rightLaneNumber, rightLaneCars) {
    let oddsOfMerging = Math.random();
    if (this.isAggressive) {
    oddsOfMerging += 0.2;
    } else if (this.isNervous) {
    oddsOfMerging -= 0.5;
    } else if (this.isPolite){
    oddsOfMerging -= 0.1;
    }

    let shouldMerge = false;
    if (!carInFront) {
    // no need to merge if already in front
    shouldMerge = false;
    } else if (carInFront.distance < this.estimatePotentialOffsetDistance(this.speedIfAccelerateFully()) * 2) {
    if (carInFront.currentSpeed < (this.currentSpeed * 0.9)) {
    shouldMerge = true;
    }
    } else if (carInFront.distance < this.estimatePotentialOffsetDistance(this.speedIfAccelerateFully()) * 5) {
    if (carInFront.currentSpeed < (this.currentSpeed * 0.8)) {
    shouldMerge = true;
    }
    }

    // do not merge off screen
    if (this.distance < (DomWidth * 0.15)) {
    shouldMerge = false;
    } else if (this.distance < (DomWidth > 0.85)) {
    shouldMerge = false;
    }

    // even if you can maybe you won't
    shouldMerge = (shouldMerge && oddsOfMerging > 0.975);

    const howMuchSpaceNeededForMerge = this.getMergeSpaceNeeded();

    // if you can need to pick which lane
    let mergeToLane = null;
    let nextCarInMergeLane = null;

    if (shouldMerge && leftLaneCars) {
    let leftSub = leftLaneCars
    .filter(ea => ea.distance > howMuchSpaceNeededForMerge.distanceBack)
    .filter(ea => ea.distance < howMuchSpaceNeededForMerge.distanceFront);
    if (leftSub.length) {
    mergeToLane = leftLaneNumber;

    let upcomingLeftCars = leftLaneCars.filter(ea => ea.distance >= howMuchSpaceNeededForMerge.distanceFront);
    if (upcomingLeftCars.length) {
    nextCarInMergeLane = upcomingLeftCars[0].id
    }
    }
    }

    if (shouldMerge && rightLaneCars) {
    let rightSub = rightLaneCars
    .filter(ea => ea.distance > howMuchSpaceNeededForMerge.distanceBack)
    .filter(ea => ea.distance < howMuchSpaceNeededForMerge.distanceFront);
    if (rightSub.length) {
    let didUseRight = false;
    if ((mergeToLane != null) && (Math.random() > 0.7)) {
    mergeToLane = rightLaneNumber;
    didUseRight = true;
    } else {
    mergeToLane = rightLaneNumber;
    didUseRight = true;
    }

    if (didUseRight) {
    let upcomingRightCars = rightLaneCars.filter(ea => ea.distance >= howMuchSpaceNeededForMerge.distanceFront);
    if (upcomingRightCars.length) {
    nextCarInMergeLane = upcomingRightCars[0].id
    }
    }
    }
    }


    if ((this.isMerging || shouldMerge) && (mergeToLane != null)) {
    if (!this.isMerging.state) {
    this.isMerging.state = true;
    this.isMerging.mergeBehindCarId = nextCarInMergeLane;
    this.isMerging.destLane = mergeToLane;
    this.isMerging.phase = 1;
    this.domElement.style("fill", "red").style("opacity", 0.85);
    }
    }

    return this.isMerging.state;
    }

    updateVehiclePosition (carInFront, carInFrontInMergeLane) {
    if (!carInFront) {
    this.accelerateForwardMax();
    } else if (this.isBrokenDown) {
    // pass no action if broken down
    } else {
    // handle when there are cars that have looped
    let adjustedDistanceAhead = carInFront.distance;
    if (carInFrontInMergeLane && carInFrontInMergeLane.distance < adjustedDistanceAhead) {
    adjustedDistanceAhead = carInFrontInMergeLane.distance;
    }
    if (IS_LOOPING_FLAG.on && (adjustedDistanceAhead <= this.distance)) {
    adjustedDistanceAhead += DomWidth;
    }

    if (this.estimatePotentialOffsetDistance(this.speedIfAccelerateFully()) < adjustedDistanceAhead) {
    this.accelerateForwardMax();
    } else if (this.estimatePotentialOffsetDistance(this.currentSpeed) < adjustedDistanceAhead) {
    if (!this.isPolite) {
    this.accelerateForwardMax();
    } else if (this.isNervous) {
    this.decelerateForwards();
    } else if (this.willPaceCarInFront) {
    if (carInFrontInMergeLane && carInFrontInMergeLane.currentSpeed < carInFront.currentSpeed) {
    this.setCustomSpeed(carInFrontInMergeLane.currentSpeed);
    } else {
    this.setCustomSpeed(carInFront.currentSpeed);
    }
    }
    // do nothing since no need to update speed if being polite
    } else if (this.estimatePotentialOffsetDistance(this.speedIfDecelerating()) < adjustedDistanceAhead) {
    if (!this.isPolite) {
    this.decelerateForwards();
    } else if (this.isNervous) {
    this.hardBrakingForwards();
    } else {
    // try and move forward at current speed
    }
    } else if (this.estimatePotentialOffsetDistance(this.speedIfHardBraking()) < adjustedDistanceAhead) {
    this.hardBrakingForwards();
    } else {
    this.fullStop();
    }
    }

    this.updateDistanceAfterCurrentSpeedUpdated();
    }

    redrawCarLocation () {
    this.domElement.attr("x", this.distance);

    // only keep merging if moving forward (no sideways sliding)
    if (this.isMerging.state && this.currentSpeed > 0) {
    const shiftAmount = 15 * ((this.isMerging.destLane - this.lane) / 10) * this.isMerging.phase;
    const adjY = shiftAmount + 15 + 15 * this.lane;
    this.domElement.attr("y", adjY);
    }
    }

    addCarToDom () {
    this.domElement = DomSVG
    .append("rect")
    .style("fill", this.color)
    .attr("x", this.distance)
    .attr("y", (15 + 15 * this.lane))
    .attr("opacity", 0.5)
    .attr("height", this.vehicleWidth)
    .attr("width", this.vehicleLength);
    }

    deleteFromDom () {
    this.domElement.remove();
    }
    }

    function getCarById (targetId) {
    for (let acali = 0; acali < laneCount; acali++) {
    let allCars = allCarsAllLanes[acali];
    allCars.forEach(currCar => {
    if (currCar.id == targetId) {
    return currCar;
    }
    });
    }
    }

    let allCarsAllLanes = [];
    for (let i = 0; i < laneCount; i++) {
    allCarsAllLanes.push([])
    }

    const runCycle = setInterval(function() {
    const allAverageSpeeds = [];

    // merge phase - make merge changes
    for (let acali = 0; acali < laneCount; acali++) {
    let allCars = allCarsAllLanes[acali];

    // first thing is to prune out old merge operations
    allCarsAllLanes[acali] = allCarsAllLanes[acali].filter(currCar => {
    // handle orphaned already merged cars
    if ((currCar.lane != acali) && !currCar.isMerging.state) {
    return false;
    }
    return true;

    }).map(currCar => {
    // also increment phase of active mergers
    if (currCar.isMerging.state && currCar.isMerging.destLane == acali) {
    currCar.isMerging.phase = currCar.isMerging.phase + 1;
    }

    if (currCar.isMerging.state && currCar.isMerging.phase > 10) {
    // reset merging state if destination lane has been reached
    currCar.resetMergingState();
    }

    return currCar;
    });

    for (var i = allCars.length - 1; i >= 0; i--) {
    let currCar = allCars[i];

    let nextCar = null;
    if (i == allCars.length - 1) {
    // this is the rightmost car
    nextCar = null;
    } else {
    nextCar = allCars[i+1];
    }

    let leftLaneNumber = null;
    let leftLane = null;
    let rightLaneNumber = null;
    let rightLane = null;

    if (acali > 0) {
    leftLaneNumber = acali - 1;
    leftLane = allCarsAllLanes[leftLaneNumber];
    }
    if (acali < allCarsAllLanes.length - 1) {
    rightLaneNumber = acali + 1;
    rightLane = allCarsAllLanes[rightLaneNumber];

    }

    // consider merging given current conditions for each car
    if (!currCar.isMerging.state && currCar.considerMerging(nextCar, leftLaneNumber, leftLane, rightLaneNumber, rightLane)) {
    let a = allCarsAllLanes[currCar.isMerging.destLane].filter(ea => ea.distance < currCar.distance);
    let b = allCarsAllLanes[currCar.isMerging.destLane].filter(ea => ea.distance > currCar.distance);
    allCarsAllLanes[currCar.isMerging.destLane] = a.concat([currCar]).concat(b);
    }
    }
    }

    for (let acali = 0; acali < laneCount; acali++) {
    let allCars = allCarsAllLanes[acali];

    // there must always be at leat 1 car on the road
    if (allCars.length == 0) {
    const newVehicleToAdd = new Vehicle(acali, VEH_ID_INCREMENTER);
    newVehicleToAdd.addCarToDom();
    VEH_ID_INCREMENTER += 1;
    allCars = [newVehicleToAdd];
    }

    let newCarsList = allCars.filter(ea => ea.distance < DomWidth);
    let carsThatWentOffScreen = allCars.filter(ea => ea.distance >= DomWidth)

    if (IS_LOOPING_FLAG.on) {
    carsThatWentOffScreen = carsThatWentOffScreen.map(ea => {
    ea.distance = Math.min(0, allCars[0].distance - (ea.minOffsetFromDistance()));
    return ea;
    });

    if (carsThatWentOffScreen.length) {
    newCarsList = carsThatWentOffScreen.concat(newCarsList);
    }
    } else {
    carsThatWentOffScreen.forEach(ea => {
    ea.deleteFromDom();
    });
    }

    for (var i = newCarsList.length - 1; i >= 0; i--) {
    let currCar = newCarsList[i];

    let nextCarInMergeLane = null;
    if (currCar.isMerging.state) {
    nextCarInMergeLane = getCarById(currCar.isMerging.mergeBehindCarId);
    }

    // avoid drawing when we are a merge element and not already in lane
    if (currCar.lane == acali) {

    if (TRIGGER_BREAKDOWN_FLAG.on && TRIGGER_BREAKDOWN_FLAG.row == acali) {
    currCar.breakDown();
    resetBreakDownFlag()
    }

    if (i == newCarsList.length - 1) {
    // if the rightmost car, then reference the leftmost (if loop)
    if (IS_LOOPING_FLAG.on) {
    currCar.updateVehiclePosition(newCarsList[0], nextCarInMergeLane);
    } else {
    currCar.updateVehiclePosition(null, nextCarInMergeLane);
    }
    } else if (newCarsList.length <= 1) {
    currCar.updateVehiclePosition(null, nextCarInMergeLane);
    } else {
    const carInFront = newCarsList[i + 1];
    currCar.updateVehiclePosition(carInFront, nextCarInMergeLane);
    }

    // update render location
    currCar.redrawCarLocation();
    }
    }

    const newPotentialCar = new Vehicle(acali, VEH_ID_INCREMENTER);
    const lastCarInLine = newCarsList[0];
    const likelihoodForAddingCarsOrRemoving = getAddOrRemoveLikelihood();
    if (likelihoodForAddingCarsOrRemoving > 0 && (Math.random() > 0.5)) {
    if (lastCarInLine.distance > newPotentialCar.minOffsetFromDistance()) {
    newPotentialCar.addCarToDom();
    VEH_ID_INCREMENTER += 1;
    newCarsList = [newPotentialCar].concat(newCarsList);
    }
    } else if (likelihoodForAddingCarsOrRemoving < 0 && (Math.random() > 0.5)) {
    const removeTheseCars = newCarsList.splice(-1, 1);
    removeTheseCars.forEach(ea => {
    ea.deleteFromDom();
    })
    }

    allAverageSpeeds.extend(newCarsList.map(ea => ea.currentSpeed));
    allCarsAllLanes[acali] = newCarsList;
    }

    let averageCurrentSpeed = 0;
    if (allAverageSpeeds.length) {
    averageCurrentSpeed = allAverageSpeeds.reduce((a,b) => (a + b))/allAverageSpeeds.length;
    }
    document.getElementById("avgSpeedStatRendered").innerText = averageCurrentSpeed.toFixed(2);
    document.getElementById("totalVehicleCount").innerText = allAverageSpeeds.length + " of " + VEH_ID_INCREMENTER + " total created";

    }, 50)

    // utility function/s
    Array.prototype.extend = function (other_array) {
    /* You should include a test to check whether other_array really is an array */
    other_array.forEach(function(v) {this.push(v)}, this);
    }
    39 changes: 39 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,39 @@
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Traffic Simulation</title>
    </head>
    <body>
    <h3>Controls</h3>
    <p>
    <p>Vehicle Generation State</p>
    <input type="radio" id="removeCars" name="carAddRemoveRadio" value="-1">
    <label for="removeCars">Remove cars</label><br>
    <input type="radio" id="doNothing" name="carAddRemoveRadio" value="0" checked>
    <label for="doNothing">Do nothing</label><br>
    <input type="radio" id="addCars" name="carAddRemoveRadio" value="1">
    <label for="addCars">Add cars</label>
    <p>
    </p>
    Speed Multiplier
    <input type="range" min="1" max="10" value="2" class="slider" id="vehicleSpeedsMultiplier">
    </p>
    </p>
    <input type="checkbox" id="loopOnOff" name="loopOnOff" onclick="toggleLooping(this)" checked>
    <label for="loopOnOff"> Vehicles Loop Route</label>
    </p>
    </p>
    <button id="triggerBreakDown" onclick="triggerBreakDown()">Trigger Break Down</button>
    <button id="unblockBreakDowns" onclick="unblockBreakDowns()">Unblock Break Downs</button>
    <button id="stopRunCycle" onclick="clearInterval(runCycle)">Stop Run Cycle</button>
    </p>
    <h3>Stats</h3>
    <p>Total Vehicle Count: <span id="totalVehicleCount"></span></p>
    <p>Avg Speed: <span id="avgSpeedStatRendered"></span></p>
    </body>
    <script src="https://d3js.org/d3.v6.min.js"></script>
    <script src="index.js"></script>
    </html>