Skip to main content

Challenge #5: Traffic Light Simulator [COMPLETED]

Created
Active
Viewed 3k times
4 entries
18

Congratulations to Kamran for the best response to this challenge!

We were impressed by your simulation and also the inclusion of variable weather elements.

This Challenge is now closed but entries can still be posted and votes can still be cast.

Original Post:

We’re excited to have a new series of Challenges for the Stack Overflow community. We’ll be posting one challenge every day this week. This challenge comes from Stack Overflow user @andam. If you’d like to submit a challenge for consideration, please head over to the Sandbox.

This challenge is to create a Traffic Light Simulator.

The Challenge

Traffic systems are everywhere, and they can get chaotic! The challenge is to simulate a traffic intersection where cars spawn randomly at each side of a traffic light.

Requirements:

  • Cars spawn randomly at each traffic light side.

  • Traffic rules should be respected (e.g., don’t block turning lanes, obey light cycles).

Bonus:

  • Add weather conditions (rain, fog, etc.) that affect driving behavior.

  • Cars can have unique traits like drifting while turning, honking when the light turns green, etc.

  • Include pedestrians crossing.

  • Implement car crashes or collisions for chaotic fun.

Skills Needed: HTML, CSS, JavaScript (with optional canvas animations or game libraries).

How does the actual contest work?

You have two weeks from the date this challenge is posted to submit your entry. During this period, other entries are only visible once you have submitted your own. After that, anyone can view and vote on others’ entries. After you submit, or during the voting period, please vote on whichever entries seem the most interesting or noteworthy. Our goal is to encourage creative approaches, and this challenge does not have specific performance criteria.

User entries with the highest vote score will be recognized. Please note that any votes received as part of this challenge do not count towards site reputation.

To keep the voting fair, we’ve hidden the vote counts until the end of the challenge. The scores will be unhidden on October 6, and we’ll announce the winners soon after that.

September 15: Challenge goes live

September 29: All entries visible to everyone. Vote scores are hidden to reduce voting bias.

October 6: Challenge ends. Vote counts and winners are announced.

How to Submit:

Enter your completed entry in the text box below.

Your submission should include:

  • An explanation of your approach

  • The code you have written

  • Instructions for how others can run your code to observe how it works. Hosting your code on an online code editor often works well for this.

  • Anything you learned or any interesting challenges you faced while coding!

Your entry is not permitted to be written by AI.

4 entries
Sorted by:
79786087
-2

I have taken help of AI to understand the question as first i thought I have to make a clean traffict light simulator where no collision occur.

Here Is my work so far:

https://jsfiddle.net/rh41cks7/

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Neon Diagonal Traffic Demo</title>
  <style>
    body {
      margin: 0;
      background: #191c23;
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      user-select: none;
      color: #e0e3ff;
      font-family: "Segoe UI", sans-serif;
    }

    h1 {
      font-size: 1.45rem;
      letter-spacing: 2px;
      margin: 32px 0 10px 0;
      color: #9afff3;
    }

    #intersection {
      position: relative;
      width: 420px;
      height: 420px;
      background: #222736;
      border-radius: 35px;
      box-shadow: 0 4px 32px #111a, 0 0 0 5px #29ffe966 inset;
      overflow: hidden;
    }

    /* Diagonal main & cross roads */
    .road {
      position: absolute;
      background: #313b4e;
      border-radius: 32px;
      box-shadow: 0 0 10px #344;
    }
    .road.main {
      width: 520px;
      height: 44px;
      left: -50px;
      top: 188px;
      transform: rotate(27deg);
    }
    .road.cross {
      width: 400px;
      height: 32px;
      left: 10px;
      top: 131px;
      transform: rotate(-63deg);
    }

    /* Lane lines */
    .lanes {
      position: absolute;
      pointer-events: none;
    }
    .lanes.main {
      width: 370px;
      height: 2px;
      left: 25px;
      top: 209px;
      background: repeating-linear-gradient(
        to right,
        #22ffee 0 20px,
        transparent 20px 40px
      );
      transform: rotate(27deg);
      opacity: 0.8;
    }
    .lanes.cross {
      width: 300px;
      height: 1.5px;
      left: 70px;
      top: 164px;
      background: repeating-linear-gradient(
        to right,
        #f8d552 0 20px,
        transparent 20px 35px
      );
      transform: rotate(-63deg);
      opacity: 0.7;
    }

    /* Neon Signals */
    .signal {
      position: absolute;
      width: 22px;
      height: 65px;
      border-radius: 10px;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: space-between;
      background: #161c29;
      box-shadow: 0 0 12px #3227ff55;
      z-index: 4;
    }
    .signal .bulb {
      width: 16px;
      height: 16px;
      border-radius: 50%;
      margin: 7px 0 3px 0;
      background: #232231;
      box-shadow: 0 0 1px #fff2;
      transition: background 0.27s;
    }
    .signal .bulb.green.on {
      background: #67f96d;
      box-shadow: 0 0 8px #29ff63;
    }
    .signal .bulb.red.on {
      background: #f76b6b;
      box-shadow: 0 0 8px #ff2323;
    }
    .signal .bulb.yellow.on {
      background: #f8d552;
      box-shadow: 0 0 10px #ffe15a;
    }

    /* Signal Positioning */
    .signal.s0 { left: 378px; top: 106px; }
    .signal.s1 { left: 27px; top: 34px; }
    .signal.s2 { left: 11px; top: 355px; }
    .signal.s3 { left: 340px; top: 355px; }

    /* Car */
    .car {
79771856
-10
import random
conditions = ["pedestrians","high speed","foggy","night","clear",]   #list of possible scenarios

number = random.randint(0,4)   #choosing of a number

situation = conditions[number]   #linking the number with the scenario

if number in [0,1]:   #attributong the outcome to the scenario
    print(f"the conditions are too dangerous to pass: {situation}, stop (red light)")
elif number in [2,3]:
    print(f"the conditions are dangerous, proceed with caution: {situation}, slow down (yellow light)")
elif number == 4:
    print(f"the conditions are great, you can proceed: {situation}, go ahead (green light)")
79771566
-4
// script.js

const canvas = document.getElementById('simCanvas');

const ctx = canvas.getContext('2d');

let trafficLight = \['green', 'yellow', 'red'\];

let lightIndex = 0;

let lightTimer = 0;

let cars = \[\];

function spawnCar() {

  // Sides: 'north', 'south', 'east', 'west'

  const side = \['north', 'south', 'east', 'west'\]\[Math.floor(Math.random() \* 4)\];

  const car = { x: side === 'east' ? 590 : side === 'west' ? 10 : 300, 

                y: side === 'north' ? 10 : side === 'south' ? 390 : 200, 

                side: side,

                color: \['blue', 'red', 'yellow', 'green'\]\[Math.floor(Math.random() \* 4)\],

                waiting: true };

  cars.push(car);

}

function drawIntersection() {

  ctx.clearRect(0, 0, 600, 400);

  ctx.fillStyle = '#222';

  ctx.fillRect(250, 0, 100, 400);

  ctx.fillRect(0, 150, 600, 100);

  // Draw cars

  cars.forEach(car =\> {

    ctx.fillStyle = car.color;

    ctx.fillRect(car.x-10, car.y-10, 20, 20);

  });

}

function updateCars() {

  cars.forEach(car =\> {

    // Move logic based on traffic light and lane

    if (trafficLight\[lightIndex\] === 'green' && car.waiting) {

      car.waiting = false;

    }

    if (!car.waiting) {

      // Move car forward based on side

      if (car.side === 'north') car.y += 2;

      if (car.side === 'south') car.y -= 2;

      if (car.side === 'east') car.x -= 2;

      if (car.side === 'west') car.x += 2;

    }

  });

  // Remove cars outside canvas

  cars = cars.filter(car =\> car.x \> 0 && car.x \< 600 && car.y \> 0 && car.y \< 400);

}

function drawTrafficLight() {

  const colors = \['green', 'yellow', 'red'\];

  for (let i = 0; i \< colors.length; i++) {

    ctx.beginPath();

    ctx.arc(300, 200 + (i \* 40), 15, 0, 2 \* Math.PI);

    ctx.fillStyle = lightIndex === i ? colors\[i\] : '#444';

    ctx.fill();

  }

}

function loop() {

  lightTimer++;

  if (lightTimer \> 100) {

    lightIndex = (lightIndex + 1) % 3;

    lightTimer = 0;

  }

  if (Math.random() \< 0.03) spawnCar();

  updateCars();

  drawIntersection();

  drawTrafficLight();

  requestAnimationFrame(loop);

}

loop();
79771515
8

Codepen link: https://codepen.io/kamran-khalid-v9/pen/XJXJQZw

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Code Challenge #5: Traffic Light Simulator</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0
        }

        body {
            font-family: system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial;
            background: #fff;
            color: #111;
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 18px
        }

        header {
            margin-bottom: 12px
        }

        h1 {
            font-size: 20px;
            font-weight: 600;
            color: #111;
            margin-bottom: 6px
        }

        .controls {
            display: flex;
            gap: 8px;
            margin-bottom: 10px
        }

        button {
            padding: 8px 10px;
            border: 1px solid #ccc;
            background: #f2f2f2;
            border-radius: 6px;
            cursor: pointer
        }

        button:active {
            transform: translateY(1px)
        }

        .stats {
            display: flex;
            gap: 18px;
            margin-bottom: 12px
        }

        .stat {
            font-size: 13px
        }

        .intersection-container {
            width: 560px;
            height: 560px
        }

        .intersection {
            position: relative;
            width: 100%;
            height: 100%;
            background: #efefef;
            border: 1px solid #ddd;
            border-radius: 6px;
            overflow: hidden
        }

        /* road (centered) */
        .road-horizontal {
            position: absolute;
            left: 0;
            right: 0;
            height: 120px;
            top: 220px;
            background: #dcdcdc;
            z-index: 2
        }

        .road-vertical {
            position: absolute;
            top: 0;
            bottom: 0;
            width: 120px;
            left: 220px;
            background: #dcdcdc;
            z-index: 2
        }

        .divider-h {
            position: absolute;
            left: 0;
            right: 0;
            height: 0;
            top: 280px;
            border-top: 2px solid #bdbdbd;
            pointer-events: none;
            z-index: 3
        }

        .divider-v {
            position: absolute;
            top: 0;
            bottom: 0;
            width: 0;
            left: 280px;
            border-left: 2px solid #bdbdbd;
            pointer-events: none;
            z-index: 3
        }

        /* car basics */
        .car {
            position: absolute;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 12px;
            color: #111;
            border-radius: 4px;
            /* box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.06); */
            will-change: transform;
            z-index: 40
        }

        .car.horizontal {
            width: 36px;
            height: 22px
        }

        .car.vertical {
            width: 22px;
            height: 36px
        }

        /* traffic lights */
        .light {
            position: absolute;
            width: 18px;
            height: 50px;
            background: #fff;
            border: 1px solid #ccc;
            border-radius: 4px;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 4px;
            padding: 6px;
            box-shadow: 0 0 0 rgba(0, 0, 0, 0);
            z-index: 60
        }

        .lamp {
            width: 10px;
            height: 10px;
            border-radius: 50%;
            background: #ddd
        }

        .on-red {
            background: #c0392b;
            box-shadow: 0 0 6px rgba(192, 57, 43, 0.6)
        }

        .on-yellow {
            background: #d4a017;
            box-shadow: 0 0 6px rgba(212, 160, 23, 0.6)
        }

        .on-green {
            background: #2e8b57;
            box-shadow: 0 0 6px rgba(46, 139, 87, 0.6)
        }

        /* light positions (simple) */
        .light-north {
            left: 302px;
            top: 160px
        }

        .light-south {
            left: 268px;
            top: 420px;
            transform: rotate(180deg)
        }

        .light-east {
            left: 410px;
            top: 298px;
            transform: rotate(90deg)
        }

        .light-west {
            left: 120px;
            top: 262px;
            transform: rotate(270deg)
        }

        /* weather */
        .weather-effect {
            position: absolute;
            pointer-events: none;
            z-index: 70
        }

        .rain-drop {
            position: absolute;
            width: 2px;
            height: 18px;
            background: linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.8));
            animation: rain linear infinite
        }

        .fog-effect {
            position: absolute;
            width: 100%;
            height: 100%;
            background: rgba(200, 200, 200, 0.35);
            pointer-events: none;
            z-index: 72
        }

        @keyframes rain {
            0% {
                transform: translateY(-140px);
                opacity: 0
            }

            20% {
                opacity: 0.6
            }

            100% {
                transform: translateY(720px);
                opacity: 0
            }
        }

        .crash {
            position: absolute;
            font-size: 20px;
            pointer-events: none;
            z-index: 90
        }

        footer {
            margin-top: 12px;
            font-size: 12px;
            color: #666
        }
    </style>
</head>

<body>
    <header>
        <h1>Traffic Light Simulator</h1>
    </header>

    <div class="controls">
        <button id="start">Start</button>
        <button id="stop">Stop</button>
        <button id="reset">Reset</button>
        <button id="weather">Weather: Clear</button>
    </div>

    <div class="stats">
        <div class="stat">Cars: <span id="carCount">0</span></div>
        <div class="stat">Crashes: <span id="crashCount">0</span></div>
        <div class="stat">Weather: <span id="weatherState">Clear</span></div>
    </div>

    <div class="intersection-container">
        <div class="intersection" id="intersection">
            <div class="road-horizontal"></div>
            <div class="road-vertical"></div>
            <div class="divider-h"></div>
            <div class="divider-v"></div>

            <!-- traffic lights -->
            <div class="light light-north" data-dir="north">
                <div class="lamp red"></div>
                <div class="lamp yellow"></div>
                <div class="lamp green"></div>
            </div>
            <div class="light light-south" data-dir="south">
                <div class="lamp red"></div>
                <div class="lamp yellow"></div>
                <div class="lamp green"></div>
            </div>
            <div class="light light-east" data-dir="east">
                <div class="lamp red"></div>
                <div class="lamp yellow"></div>
                <div class="lamp green"></div>
            </div>
            <div class="light light-west" data-dir="west">
                <div class="lamp red"></div>
                <div class="lamp yellow"></div>
                <div class="lamp green"></div>
            </div>
        </div>
    </div>


    <script>
        // ---
        // Traffic Light Simulator (ES6/OOP)
        // Author: Kamran Khalid (2025)
        //
        // This file is a playground for traffic logic, DOM tricks, and OOP in JS.
        // If you see weird stuff, it's probably me experimenting. Suggestions welcome!
        //
        // Main classes: Simulation, Car, Lights
        //
        // TODO: Add more fun features (e.g., cyclists, night mode, better crash animation)
        // ---

        // Utility functions for randomness, etc. (could be expanded)
        class Utils {
            static randInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
            // Sometimes I forget to use these helpers everywhere!
        }

        // Handles traffic light logic and DOM updates
        // The timings are a bit arbitrary, tweak as you like!
        class Lights {
            constructor(root) {
                this.root = root;
                this.state = 0;
                this.timer = 0;
                this.durations = [3500, 1000, 3500, 1000];
                this.map = {
                    north: root.querySelector('.light-north'),
                    south: root.querySelector('.light-south'),
                    east: root.querySelector('.light-east'),
                    west: root.querySelector('.light-west')
                };
                this.updateDOM();
            }
            update(dt) {
                this.timer += dt;
                if (this.timer >= this.durations[this.state]) {
                    this.timer = 0;
                    this.state = (this.state + 1) % 4;
                    this.updateDOM();
                }
            }
            updateDOM() {
                const clear = (el) => el.querySelectorAll('.lamp').forEach(l => l.className = 'lamp');
                Object.values(this.map).forEach(clear);
                const set = (el, r, y, g) => {
                    if (r) el.querySelector('.lamp:nth-child(1)').classList.add('on-red');
                    if (y) el.querySelector('.lamp:nth-child(2)').classList.add('on-yellow');
                    if (g) el.querySelector('.lamp:nth-child(3)').classList.add('on-green');
                };
                if (this.state === 0) { set(this.map.north, false, false, true); set(this.map.south, false, false, true); set(this.map.east, true, false, false); set(this.map.west, true, false, false); }
                else if (this.state === 1) { set(this.map.north, false, true, false); set(this.map.south, false, true, false); set(this.map.east, true, false, false); set(this.map.west, true, false, false); }
                else if (this.state === 2) { set(this.map.east, false, false, true); set(this.map.west, false, false, true); set(this.map.north, true, false, false); set(this.map.south, true, false, false); }
                else { set(this.map.east, false, true, false); set(this.map.west, false, true, false); set(this.map.north, true, false, false); set(this.map.south, true, false, false); }
            }
            isGreenFor(dir) { return (dir === 'north' || dir === 'south') ? this.state === 0 : this.state === 2; }
        }

        // Car entity: moves, crashes, vertical for north/south
        // North/south cars are rotated for realism. East/west are not.
        class Car {
            constructor({ x, y, dir, icon, speed, laneId }) {
                this.x = x; this.y = y; this.dir = dir; this.icon = icon || '🚗'; this.speed = speed || 80; this.laneId = laneId; this.crashed = false;
                this.orientation = (dir === 'north' || dir === 'south') ? 'vertical' : 'horizontal';
                this.width = this.orientation === 'horizontal' ? 36 : 22;
                this.height = this.orientation === 'horizontal' ? 22 : 36;
                this.el = document.createElement('div');
                this.el.className = `car ${this.orientation}`;
                this.el.textContent = this.icon;
                this.setTransform();
            }
            setTransform() { this.el.style.transform = `translate(${this.x}px, ${this.y}px)`; }
            update(dt, lights, cars) {
                if (this.crashed) return;
                const inIntersection = (this.x > 240 && this.x < 360 && this.y > 240 && this.y < 360);
                let blocked = false;
                for (const o of cars) {
                    if (o === this || o.crashed) continue;
                    if (o.dir === this.dir && o.laneId === this.laneId) {
                        if (this.dir === 'north' && o.x === this.x && o.y < this.y && Math.abs(o.y - this.y) < 44) blocked = true;
                        if (this.dir === 'south' && o.x === this.x && o.y > this.y && Math.abs(o.y - this.y) < 44) blocked = true;
                        if (this.dir === 'east' && o.y === this.y && o.x > this.x && Math.abs(o.x - this.x) < 44) blocked = true;
                        if (this.dir === 'west' && o.y === this.y && o.x < this.x && Math.abs(o.x - this.x) < 44) blocked = true;
                    }
                }
                let canProceed = true;
                if (inIntersection) canProceed = lights.isGreenFor(this.dir);
                else {
                    if (this.dir === 'north' && this.y < 360 && this.y > 240) canProceed = lights.isGreenFor('north');
                    if (this.dir === 'south' && this.y > 240 && this.y < 360) canProceed = lights.isGreenFor('south');
                    if (this.dir === 'east' && this.x > 240 && this.x < 360) canProceed = lights.isGreenFor('east');
                    if (this.dir === 'west' && this.x < 360 && this.x > 240) canProceed = lights.isGreenFor('west');
                }
                if (!blocked && canProceed) {
                    const dist = (this.speed * dt) / 1000;
                    if (this.dir === 'north') this.y -= dist;
                    else if (this.dir === 'south') this.y += dist;
                    else if (this.dir === 'east') this.x += dist;
                    else if (this.dir === 'west') this.x -= dist;
                    this.setTransform();
                }
            }
            crash() { this.crashed = true; this.el.textContent = '💥'; }
            outOfBounds() { return (this.x < -100 || this.x > 700 || this.y < -100 || this.y > 700); }
            bounds() { return { x: this.x, y: this.y, w: this.width, h: this.height }; }
        }

        // Simulation: ties everything together, handles UI, spawns, updates, etc.
        // This is the "main loop". If you want to add features, start here!
        class Simulation {
            constructor(root) {
                this.root = root;
                this.lights = new Lights(root);
                this.cars = []; this.running = false; this.last = 0; this.crashes = 0;
                this.center = 280;
                this.northLanesX = [this.center + 18, this.center + 48];
                this.southLanesX = [this.center - 48, this.center - 18];
                this.eastLanesY = [this.center - 48, this.center - 18];
                this.westLanesY = [this.center + 18, this.center + 48];
                this.spawnYNorth = 620; this.spawnYSouth = -60; this.spawnXEast = -60; this.spawnXWest = 620;
                this.carCounter = document.getElementById('carCount');
                this.crashCounter = document.getElementById('crashCount');
                this.weatherBtn = document.getElementById('weather');
                this.weatherState = document.getElementById('weatherState');
                this.weather = 'Clear';
                this.spawnTimer = 0;

                document.getElementById('start').addEventListener('click', () => this.start());
                document.getElementById('stop').addEventListener('click', () => this.stop());
                document.getElementById('reset').addEventListener('click', () => this.reset());
                this.weatherBtn.addEventListener('click', () => { this.toggleWeather(); });

                // initial render (no effects)
                this.renderWeather();
            }

            toggleWeather() {
                this.weather = (this.weather === 'Clear') ? 'Rain' : (this.weather === 'Rain' ? 'Fog' : 'Clear');
                this.weatherBtn.textContent = `Weather: ${this.weather}`;
                this.renderWeather();
            }

            renderWeather() {
                // remove old weather effects
                this.root.querySelectorAll('.weather-effect').forEach(n => n.remove());
                if (this.weather === 'Rain') {
                    // create drops
                    const count = 40;
                    for (let i = 0; i < count; i++) {
                        const d = document.createElement('div');
                        d.className = 'rain-drop weather-effect';
                        d.style.left = `${Math.random() * 560}px`;
                        d.style.top = `${-Math.random() * 200}px`;
                        d.style.opacity = (0.4 + Math.random() * 0.6);
                        d.style.animationDuration = `${0.6 + Math.random() * 0.9}s`;
                        d.style.animationDelay = `${Math.random() * 1.5}s`;
                        this.root.appendChild(d);
                    }
                } else if (this.weather === 'Fog') {
                    const f = document.createElement('div');
                    f.className = 'fog-effect weather-effect';
                    this.root.appendChild(f);
                }
                // Clear => nothing to do
            }

            start() { if (this.running) return; this.running = true; this.last = performance.now(); requestAnimationFrame(t => this.loop(t)); }
            stop() { this.running = false; }

            loop(now) {
                if (!this.running) return;
                const dt = now - this.last; this.last = now;
                this.update(dt);
                requestAnimationFrame(t => this.loop(t));
            }

            update(dt) {
                this.lights.update(dt);
                this.spawnTimer += dt;
                if (this.spawnTimer > 700) { this.spawnTimer = 0; if (Math.random() > 0.35) this.spawnCar(); }
                for (const c of this.cars) c.update(dt, this.lights, this.cars);
                this.detectCollisions();
                for (let i = this.cars.length - 1; i >= 0; i--) { if (this.cars[i].outOfBounds()) { this.cars[i].el.remove(); this.cars.splice(i, 1); } }
                this.carCounter.textContent = this.cars.length;
                this.crashCounter.textContent = this.crashes;
            }

            spawnCar() {
                // Only spawn at valid lanes, avoid blocking turning lanes
                // Only side lanes are used, not the center. North/south cars are vertical.
                const approaches = ['north', 'south', 'east', 'west'];
                const dir = approaches[Utils.randInt(0, approaches.length - 1)];
                let x, y, laneId;
                if (dir === 'north') { laneId = Utils.randInt(0, this.northLanesX.length - 1); x = this.northLanesX[laneId]; y = this.spawnYNorth; }
                else if (dir === 'south') { laneId = Utils.randInt(0, this.southLanesX.length - 1); x = this.southLanesX[laneId]; y = this.spawnYSouth; }
                else if (dir === 'east') { laneId = Utils.randInt(0, this.eastLanesY.length - 1); y = this.eastLanesY[laneId]; x = this.spawnXEast; }
                else { laneId = Utils.randInt(0, this.westLanesY.length - 1); y = this.westLanesY[laneId]; x = this.spawnXWest; }

                // avoid immediate overlap at spawn
                for (const c of this.cars) {
                    if (c.dir === dir && c.laneId === laneId) {
                        const dist = Math.hypot(c.x - x, c.y - y);
                        if (dist < 48) return;
                    }
                }

                // All cars are 🚗 for now. Add more icons for fun!
                const car = new Car({ x, y, dir, icon: '🚗', speed: 60 + Math.random() * 40, laneId });
                this.root.appendChild(car.el);
                this.cars.push(car);
            }

            detectCollisions() {
                for (let i = 0; i < this.cars.length; i++) {
                    for (let j = i + 1; j < this.cars.length; j++) {
                        const a = this.cars[i].bounds(), b = this.cars[j].bounds();
                        if (a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y) {
                            this.cars[i].crash(); this.cars[j].crash(); this.showCrash((a.x + b.x) / 2, (a.y + b.y) / 2);
                            this.crashes++;
                            setTimeout(() => { this.cars = this.cars.filter(c => { if (c.crashed) { c.el.remove(); return false; } return true; }); }, 350);
                        }
                    }
                }
            }

            showCrash(x, y) {
                const el = document.createElement('div');
                el.className = 'crash';
                el.style.left = `${x}px`; el.style.top = `${y}px`;
                el.textContent = '💥';
                this.root.appendChild(el);
                setTimeout(() => el.remove(), 400);
            }

            reset() {
                this.stop();
                this.cars.forEach(c => c.el.remove());
                this.cars = []; this.crashes = 0; this.carCounter.textContent = '0'; this.crashCounter.textContent = '0';
                this.lights.state = 0; this.lights.timer = 0; this.lights.updateDOM();
                this.weather = 'Clear'; this.weatherBtn.textContent = 'Weather: Clear'; this.renderWeather();
            }
        }

        const root = document.getElementById('intersection');
        const sim = new Simulation(root);
        sim.lights.updateDOM();
    </script>
</body>

</html>