Show code
<think>
Okay, I need to create a self-contained HTML file that simulates traffic on a ring road using the Nagel-Schreckenberg model. Let me recall the rules. The model has cars moving on a single lane, and each step involves acceleration, deceleration to avoid collisions, random slowing, and then moving. The cars should be evenly spaced initially to form a phantom jam. The visualization should show the ring road and color cars based on their speed.
First, I'll structure the HTML. The canvas element must be included. I'll set the width and height attributes, maybe 800x400px. Then, the JavaScript part will handle the simulation and animation.
I need to initialize the canvas context. Let's use a 2D context. The main loop will use requestAnimationFrame for smooth animation.
For the traffic model, the road is a ring, so the positions wrap around. I need an array of cars. Each car has a position and speed. Initially, the cars are equally spaced. Let me think: if the road is length L, then spacing is L / num_cars. Maybe use an array of objects with pos and speed properties.
The Nagel-Schreckenberg rules: each step, for each car:
1. Accelerate if below max speed. Max speed can be set, maybe 3 or 4.
2. Check the distance to the car ahead. Decelerate so that the new speed doesn't make the next position exceed the distance to the next car minus 1 (since positions are integers).
3. With a certain probability (like 20%), the car slows down by 1, but not below 0.
4. Then, move the car by its current speed.
Wait, the order is important. The steps are done in parallel for all cars each step. So, during each simulation step, all updates are based on the previous state, not intermediate changes.
So, I need to process each car in order, perhaps, but store the new positions/speeds in a temporary array or directly update the cars array after calculating all changes in a buffer to avoid interference.
Hmm, maybe first calculate for each car the new speed and position based on the current state (before any updates). Then apply all the changes at once.
The parameters: let's choose max speed (v_max), slowing probability (p), and the road length (N). The number of cars should be less than N. To start with a phantom jam, the cars should be evenly spaced but maybe with some small perturbation? Or just evenly spaced and see the jam form naturally.
Wait, Nagel-Schreckenberg model can form jams even with random noise. Since the initial setup is evenly spaced, when a random deceleration happens, that might trigger the jam.
Let me set up the initial cars. Let's say the road length is, say, 100 units. Number of cars is maybe 30. So each starts spaced by 100/30 ≈ 3.33, but since positions are integers, maybe start at 0,3,6,9 etc.
Wait, positions can be arranged in a circle. So each car's initial position is i * (N / num_cars) for i from 0 to num_cars-1, but rounded to integer? Need to ensure that spacing is correct.
Alternatively, place them at positions 0, N/num_cars, 2*N/num_cars, etc. Since N must be divisible by num_cars? Hmm, maybe pick a N that's a multiple of the number of cars. Or maybe use N=100 and num_cars=30, spacing 100/30 ≈ 3.333, but since it's integers, perhaps initial positions are spaced by 3 (so total cars would be 33), but maybe that's okay.
Alternatively, maybe use N=150 and num_cars=50 for even spacing. Let me decide on some numbers. Let's go with numCars = 30, roadLength = 100. So spacing is 100/30 ≈ 3.33, so positions would be 0,3,6,9,... but then the 30th car would be at 29*3.33 = 96.66, which is over 100. Hmm, maybe better to compute the spacing as (roadLength / numCars) and use that as their initial positions, but with integer positions.
Alternatively, perhaps start by placing the first car at position 0, then each subsequent car is spaced by roadLength/numCars, rounded to the nearest integer. But that might not perfectly fill the road.
Alternatively, use the road length as an integer, and position each car at initial positions starting at 0, then i*(roadLength/numCars) for each i. But since roadLength needs to be divisible by numCars, maybe set roadLength to 30, and numCars=20? Not sure. Maybe it's okay to have positions as floats but the cars move in integers? Wait, in the model, positions and speeds are integers.
Wait, in the Nagel model, the road is a discrete lattice with positions mod N (the length). Cars have integer positions and speeds. So each step, they move forward by their speed. The next position is (current pos + speed) mod N.
The distance to the next car is (next_pos - current_pos) mod N, but actually the distance to the preceding car is the position difference. Wait, the distance to the next car ahead would be the next car's position minus current position, but since it's a ring, if it's negative, add N. Wait, no, for the distance to the car in front, it's (next_car_pos - current_pos) mod N. But in programming terms, you can find the next car and compute the distance as (next_car_pos - current_pos) if positive, else N + (next_car_pos - current_pos). Alternatively, you can sort the cars by position and compute the distances between consecutive cars, then wrap around for the last one to the first.
Hmm, that might be a bit complex. Alternatively, for each car, find the distance to the next car ahead. To do that, maybe sort the cars by position, then the next car for each is next in the array, except the last which wraps to first. Then the distance would be (next_pos - current_pos) if next_pos > current_pos else next_pos + (N - current_pos).
Alternatively, for each car, iterate through all other cars to find the next one ahead. Not efficient but manageable for a small number of cars like 30.
Alternatively, pre-sort the cars by position once per step, then compute the distances between consecutive cars.
Maybe that's better. Let me think of steps:
Each step:
1. Sort the cars by position in increasing order.
2. For each car in the sorted list:
a. Find the next car ahead (next in list or first if last)
b. Compute distance to next car.
c. Update speed based on Nagel rules.
Then after all cars are processed, update their positions.
Wait, but in the Nagel model, the order of processing might matter. Since all changes are based on the current state.
Alternatively, perhaps process each car in order and store the new speed and position in a temporary array, then update the cars.
Wait, the exact steps for each car in each time step are:
- For each car, in parallel (so using previous state):
a. Accelerate: if speed < v_max, then speed +=1.
b. Look ahead to the next car. The distance is (pos_next - pos_current) mod N. The distance is (pos_next - pos_current) if pos_next > pos_current, else N - (pos_current - pos_next). So, the distance to the next car ahead is (next_pos - pos) if next_pos > pos, else N - (pos - next_pos).
Wait, actually, the distance between the current car and the next car ahead is (next_pos - current_pos) mod N. But since it's a ring, it's actually the minimal distance forward. So the distance ahead is (next_pos - current_pos) if next_pos > current_pos, else N - (current_pos - next_pos). But that's the distance. The distance in cells to the next car is (next_pos - current_pos) if next_pos > current_pos, else (next_pos + N - current_pos). Because the road is circular.
The idea is that the next car is the one that's closest in the forward direction.
So for each car, the distance to the next car ahead is (next_car.pos - car.pos) if next_car.pos > car.pos else (next_car.pos + N - car.pos). Wait, but how to find the next car?
Alternatively, after sorting the cars by position, the next car for each is the next in the list, except for the last which wraps around to first. The distance is then (next.pos - car.pos) if next is after, else N - (car.pos - next.pos). Wait, not sure.
Alternatively, for each car, the next car ahead is the first car in the list with a position greater than the current car's position. If none exist, then it's the first car (since it's a ring).
This could be done by sorting the cars and then for each car at index i, the next is i+1, except last which wraps to 0.
Then the distance is (cars[(i+1)%numCars].pos - cars[i].pos).
But that only works if the cars are sorted in order of increasing position. If they are sorted, then the distance between consecutive cars is just next.pos - current.pos, but for the last car, the next is first, so distance is (cars[0].pos + N - cars[last].pos).
Wait, the distance between last car (car at position L) and first car (position F) is F + N - L, because it's a circular road. But in an array sorted by position, the first car's position is smallest, so if the current car is last in the array (highest position), then next is first, so the distance would be first.pos + N - current.pos.
So perhaps in code, after sorting the cars by position, for each car i:
nextCarIndex = (i + 1) % numCars;
nextCar = cars[nextCarIndex];
distance = nextCar.pos - car.pos;
if (distance < 0) distance += N;
Then the distance to the next car is distance.
Wait, but nextCar's position is after the current car only if we sorted correctly. Wait, if the list is sorted in increasing order, the nextCar is the next in the list, so the distance is nextCar.pos minus current.pos. But if it's the last car, next is first, so nextCar.pos is less than current.pos, so distance is nextCar.pos + N - current.pos.
Wait, the calculation above would give the correct distance. So:
distance = (nextCar.pos - car.pos + N) % N;
Wait, not exactly. Let me think:
Suppose N=100.
Car is at 95, next is at 5 (the first car). The distance between them is 10 (5 + 100 -95 = 10). The calculation (5 -95) is -90, adding N gives 10. So yes, distance = (nextCar.pos - car.pos + N) % N.
Wait, but (nextCar.pos - car.pos) could be negative, so adding N and mod N gives the positive distance.
Wait, actually, (nextCar.pos - car.pos) mod N gives the correct distance. Because mod N will give a value between 0 and N-1. However, since the next car is ahead, the distance is positive. Wait but mod N could also give a value between 0 and N-1, but if the next car is behind, the mod would still give the correct distance in the forward direction.
Wait, perhaps the distance is (nextCar.pos - car.pos + N) % N.
Yes, that would give the minimal distance in the forward direction.
So, once cars are sorted by position, for each car, the distance to the next car ahead is (nextCar.pos - car.pos + N) % N.
But since the cars are in order, the next car should be ahead, so the distance is nextCar.pos - car.pos if nextCar is in front, else (nextCar.pos + N - car.pos). So this formula works.
Once we have the distance, the deceleration step: the car will reduce speed if it's higher than the distance minus 1. Because the new position after moving would be current_pos + speed + 1? Wait, the Nagel rules:
After accelerating, the speed is min( speed + 1, v_max )
Then, it looks ahead to the distance to the next car, and speeds down so that:
speed = min( current_speed, (distance_to_next_car - 1) )
Because if the speed is higher than (distance -1), then after moving, the position would be current_pos + speed, which would be beyond the next car's position minus 1 (since the next car is at distance d). So to prevent collisions, speed can't exceed d-1.
Wait, the distance is d. The next car is at d away. So the car's next position should not be >= nextCar's current position. Because nextCar is moving as well.
Wait, actually, in the model, each car's position is updated after all cars have their speed calculated. Because all the speed updates are based on the previous state.
Hmm, this is a bit tricky. Let me recall the Nagel-Schreckenberg model steps:
The algorithm is:
For each time step:
1. Accelerate: if current speed < v_max, then speed +=1
2. Look ahead to the next car's position. The distance is the number of cells to the next car ahead.
Then, if (current_speed > (distance -1)), set speed to (distance -1).
3. Randomization: with probability p, decrease speed by 1 (if >0)
4. Move: position += speed mod N.
Wait, but all these steps are done in parallel. So all cars' new speeds are computed based on their previous positions and speeds.
Therefore, the distance to the next car is computed based on the previous positions (before any moves), and the speed changes are all based on previous data.
So, steps in code:
- Sort the cars by position.
- For each car, compute the next car and distance.
- Then, for each car in parallel (so using temporary arrays):
a. Accelerate (speed +=1, capped at v_max)
b. decelerate: set speed to min(speed, distance -1)
c. randomize: with probability p, speed = max(0, speed -1)
d. record new speed.
- After all cars have their new speed, compute new positions: new_pos = (current_pos + speed) mod N.
Wait, but in code, need to first calculate all the new speeds and new positions based on the original positions.
Therefore, in code steps:
Each step:
1. Sort cars by position.
2. Compute next cars and distances for each.
3. Create an array of new_speeds.
4. For each car:
a. new_speed = car.speed
b. accelerate: new_speed = Math.min(new_speed +1, v_max)
c. decelerate: new_speed = Math.min(new_speed, distance -1) (distance is stored earlier)
d. randomize: if Math.random() < p, new_speed = Math.max(0, new_speed -1)
e. store new_speed in new_speeds array.
5. After all new_speeds are computed, update each car's speed and position.
Wait, but the position is updated as (old position + new_speed) mod N? Wait, no: the new speed determines how much they move. So the new position is (current_pos + new_speed) mod N.
Wait, but all new speeds are computed in parallel, so positions are updated based on those new speeds.
Yes.
So steps in code:
- For each car, after calculating new_speed, set new_pos = (car.pos + new_speed) % N.
Therefore, after calculating all new_speeds, we can loop again to update each car's speed and position.
Alternatively, collect all the new_speeds and new_positions in temporary arrays, then assign them after processing all cars.
Hmm.
Now, the visualization. The ring road can be drawn as a circle or a loop. To draw a circular road, perhaps using canvas's arc method. The cars can be drawn as small rectangles or circles along the circumference.
Alternatively, since it's a single lane, perhaps draw the road as a loop, but represent cars as points along the circumference.
Let me think of the drawing:
The canvas is 800x400. Maybe draw the ring as a circle with a radius of 150, centered at (400, 200). The circumference is 2πr ≈ 942 units. But the road length N can be 100 units, so each unit corresponds to 9.42 pixels along the circumference.
Wait, but maybe it's easier to parameterize the angle for each position. The road is a circle, so each position corresponds to an angle. Let's say the road length N is mapped to the circumference. For example, if N is 100, then each unit is 360 degrees / 100 = 3.6 degrees per unit.
Therefore, for a car at position s on the road (0 <= s < N), its angle is (s / N) * 360 degrees, then converted to radians.
So, the coordinates on the circle can be calculated as:
center_x = canvas.width / 2
center_y = canvas.height / 2
radius = 150 (for example)
angle = ( (s / N) * 2 * Math.PI ) radians
x = center_x + radius * Math.cos(angle)
y = center_y + radius * Math.sin(angle)
So, each car is drawn as a small circle or rectangle at (x,y). The color could be based on their speed.
Alternatively, cars can be drawn as small arcs or lines along the circumference.
The drawing steps:
- Clear the canvas.
- Draw the ring (white or gray).
- For each car, compute its position on the circumference.
- Draw the car as a small circle, color based on speed.
Maybe green to red, depending on speed.
Alternatively, use the current speed to determine color.
Now, code structure:
Initialization:
- Set canvas size.
- Set parameters: N = 100 (road length), numCars = e.g., 20.
- Cars array: each has pos (0..N-1), speed (initially 0 or random?)
Wait, initial positions can be spaced evenly. To start, maybe distribute cars evenly around the ring. Or randomly but not overlapping.
Wait, in code, when initializing, perhaps:
for (let i=0; i<numCars; i++) {
let pos = i * (N / numCars) % N;
let car = { pos: pos % N, speed: 0 };
cars.push(car);
}
But this ensures no overlap initially.
Or random positions with some spacing.
But for simplicity, even distribution.
Variables:
- N = 100 (road length)
- v_max = 5 (maximum speed)
- p = 0.3 (probability of slowing down)
- frame rate: use requestAnimationFrame.
- Draw function:
clear canvas.
draw ring (circle outline).
for each car:
compute angle based on pos.
draw a small circle at (x,y), with color based on speed.
Then, the animation loop:
function animate() {
update() // compute new positions and speeds.
draw();
requestAnimationFrame(animate);
}
The update function:
cars.sort( (a,b)=>a.pos - b.pos );
let nextCars = [];
for (let i=0; i<numCars; i++) {
let nextIndex = (i+1) % numCars;
nextCars[i] = cars[nextIndex];
}
// compute distances for each car.
let distances = [];
for (let i=0; i<numCars; i++) {
let car = cars[i];
let nextCar = nextCars[i];
let distance = (nextCar.pos - car.pos + N) % N;
distances[i] = distance;
}
// compute new speeds.
let newSpeeds = [];
for (let i=0; i<numCars; i++) {
let car = cars[i];
let speed = car.speed;
// accelerate
speed = Math.min(speed +1, v_max);
// decelerate
let distance = distances[i];
speed = Math.min(speed, distance -1);
// randomization
if (Math.random() < p) {
speed = Math.max(0, speed -1);
}
newSpeeds[i] = speed;
}
// now update speeds and positions.
for (let i=0; i<numCars; i++) {
let car = cars[i];
car.speed = newSpeeds[i];
// new position
let newPos = (car.pos + newSpeeds[i]) % N;
car.pos = newPos;
}
Then, after this, the cars will have their new positions and speeds.
Wait, but after updating the positions, the cars are no longer sorted, so in the next frame, we need to re-sort them again.
Therefore, this is correct.
Now, in the drawing code, to get the current position, we can use the car's pos.
Potential issues:
- When cars overtake each other, their positions can cross, so in the next step, the sorted order will change.
But the code correctly sorts them each time.
Now, implementing this in JavaScript.
Additional considerations:
- When the road length N is 100, the circumference can be mapped as 100 units, so each unit corresponds to an angle of (360/100) degrees.
- The canvas coordinates: for a canvas of 800x400, center at (400,200), radius 150.
- Drawing the ring: ctx.beginPath(); ctx.arc(400,200,150,0,2*Math.PI); ctx.stroke();
- Each car is drawn as a small circle: radius 5 or so.
Color mapping: speed ranges from 0 to v_max (e.g., up to 5). Maybe using a gradient.
Alternatively, color = 'hsl(' + (speed / v_max * 120) + ', 50%, 50%)';
So speed 0 is red (0 degrees), speed 5 is green (60 degrees? Wait, hsl 120 is green. Wait, 0 is red, 120 is green, 240 is blue.
Wait, if v_max is 5, then for speed 5, hue=5/5 * 120 = 120 (green). Speed 0 is 0 (red). That could work.
Now, putting it all together.
But let me think about some possible errors:
When computing nextIndex = (i+1)%numCars, but after sorting, the cars are in order, so nextCars[i] is the next in sorted order.
Wait, in the code above, after sorting, nextCars is created as nextCars[i] = cars[ (i+1) % numCars ].
Wait no, in the code:
cars are sorted, and nextCars is an array where nextCars[i] is the next car in the sorted list.
Wait, in code:
nextCars is an array where each element nextCars[i] is cars[ (i+1) % numCars ]
But since the cars are already sorted by pos, the nextCars array correctly points to the next car ahead on the road.
Wait but the next car is the next in the sorted array, so their positions are in order.
Therefore, the distance between car[i] and nextCar[i] is (nextCar.pos - car.pos + N) % N, which is correct.
Another possible error is when computing new positions:
newPos = (car.pos + newSpeed) % N.
Yes, since movement is along the circular track.
Testing this code:
Suppose a car is at position 95, and its speed is 5 (v_max), nextCar is at position 5. distance = (5 -95 +100) % 100 = 10. So distance is 10.
Then, after acceleration (speed was 5 can't increase further), deceleration: speed is min(5, 10-1 =9) so remains 5. Then randomization. So new speed is 5.
Thus, new position is 95 +5 =100 mod100 =0. So the new position is 0. Which is correct, moving around the track.
Another example: two cars at position 0 and 50. N=100. The first car's next is the second, distance is 50. Suppose first car is at speed 0. After accelerating, speed becomes 1. Then decelerate: 1 <=50-1, so stays 1. Then maybe randomization. So moves to 1. The second car has next car as first (since it's a circle). distance is (0 -50 +100) mod100=50. So same as before.
Hmm.
Now, implementing this in code.
Possible code outline:
HTML:
<canvas id="simCanvas" width="800" height="400"></canvas>
JavaScript:
const canvas = document.getElementById('simCanvas');
const ctx = canvas.getContext('2d');
const N = 100; // length of the road
const v_max = 5;
const p_slowdown = 0.3;
const numCars = 20;
let cars = [];
initialize();
function initialize() {
cars = [];
for (let i = 0; i < numCars; i++) {
// distribute cars evenly
let pos = (i * (N / numCars)) % N;
cars.push({ pos: pos, speed: 0 });
}
}
function update() {
// sort cars by position
cars.sort((a, b) => a.pos - b.pos);
let nextCars = [];
let distances = [];
// compute nextCars and distances
for (let i = 0; i < numCars; i++) {
const nextIndex = (i + 1) % numCars;
const nextCar = cars[nextIndex];
nextCars[i] = nextCar;
const distance = (nextCar.pos - cars[i].pos + N) % N;
distances[i] = distance;
}
let newSpeeds = [];
for (let i = 0; i < numCars; i++) {
let currentCar = cars[i];
let currentSpeed = currentCar.speed;
// Acceleration
currentSpeed = Math.min(currentSpeed + 1, v_max);
// Deceleration based on distance
let distance = distances[i];
currentSpeed = Math.min(currentSpeed, distance - 1);
// Random slowdown
if (Math.random() < p_slowdown) {
currentSpeed = Math.max(currentSpeed - 1, 0);
}
newSpeeds[i] = currentSpeed;
}
// Update positions and speeds
for (let i = 0; i < numCars; i++) {
const car = cars[i];
car.speed = newSpeeds[i];
const newPos = (car.pos + newSpeeds[i]) % N;
car.pos = newPos;
}
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw the circular road
ctx.strokeStyle = '#888';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(400, 200, 150, 0, Math.PI * 2);
ctx.stroke();
// Draw each car
ctx.fillStyle = '#000';
cars.forEach(car => {
const { pos } = car;
const angle = (pos / N) * 2 * Math.PI;
const centerX = 400 + 150 * Math.cos(angle - Math.PI / 2);
const centerY = 200 + 150 * Math.sin(angle - Math.PI / 2);
// Color based on speed
const speed = car.speed;
const hue = (speed / v_max) * 120;
ctx.fillStyle = `hsl(${hue}, 50%, 50%)`;
ctx.beginPath();
ctx.arc(centerX, centerY, 5, 0, Math.PI * 2);
ctx.fill();
});
}
function animate() {
update();
draw();
requestAnimationFrame(animate);
}
initialize();
animate();
Wait, but in the drawing code, for position to angle mapping:
The cars' pos is along a linear track of length N=100, but the circle is 360 degrees.
So, pos=0 would be angle 0 (right side?), but when drawing on canvas, the angle in radians.
Wait, need to map pos to the circumference properly.
Assuming that pos 0 is at angle 90 degrees (top of the circle), and increasing clockwise.
Wait, perhaps better to start at angle 0 (right side), going counter-clockwise.
Alternatively, the calculation:
The angle in radians is (pos / N) * 2 * Math.PI.
But to place the car on the circumference:
The coordinates are:
x = center.x + radius * Math.cos(angle)
y = center.y + radius * Math.sin(angle)
But angle starts at 0 (to the right), increasing counter-clockwise.
So, for pos=0, angle = 0: x = 400+150, y=200.
pos=25 would be 90 degrees (top), angle=pi/2 radians.
pos=50: 180 degrees (left), etc.
This is correct.
Wait, but in the code above, the angle is computed as:
angle = (pos / N) * 2 * Math.PI;
Thus, pos=0 is angle 0 (right), pos=N is angle 2pi (same as 0).
Yes.
Thus, drawing is correct.
Testing this code should show cars moving around the track, with colors changing based on speed.
Possible adjustments:
- Adjust the radius or canvas size.
- Adjust the number of cars or parameters.
But this should work.
Potential issues:
- When the cars are sorted by pos, but after moving, their positions can be anywhere, so in the next frame, the sorting is necessary again.
Yes, that's handled correctly in the update function.
Another thing: when two cars are at the same pos? The code should handle that, but initial setup ensures no duplicates.
Overall, this should work.