Two roads, four sets of lamps, and one rule that must never break: two conflicting directions are never green at once. The problem where the design is a state machine, and the timings are data.
Traffic signals are a favourite round because the problem is small and the stakes are obvious. A candidate who writes while True: green(); sleep(30); yellow(); sleep(4) has a demo and nothing else: it can't be tested without waiting, it has nowhere to put a pedestrian button, and an emergency vehicle has no way in. The interviewer wants to see a state machine, timings that live in configuration, safety rules enforced in one place, and a clock that is an argument.
What this round teaches that the earlier ones don't: safety as a design constraint. Three things get the attention. First, the invariant (no conflicting greens) is checked twice: when the plan is built, and by a monitor between the controller and the lamps. Second, an override such as an emergency vehicle must still go through yellow and all-red; it only changes which phase comes next. Third, the failure mode is a designed state: when the controller doesn't trust itself, it goes to flashing red, not to "whatever it was showing".
- State (as enums plus a transition table):
Mode(normal, preempt, flash) andStep(green, yellow, all red) - Observer:
listenershear every change, for a log, a dashboard or a city control room - Command (light): a walk request and an emergency call are small values the controller holds until it can act on them
- Facade (light):
Controlleris the only class a sensor or a technician talks to
How the round runs#
"Design the controller for a four-way intersection: north-south and east-west. Timings should be configurable. Pedestrians can press a button. An ambulance can ask for a green. If the controller fails, the intersection must fail safe. Show a scenario running. 60 minutes."
- 0–8 min — clarify: phases, who conflicts with whom, what a pedestrian button does, what an ambulance may and may not skip, what "fail safe" means
- 8–20 min — the states, and the one invariant; plan and phase as data
- 20–40 min — the controller: steps, the clock, the cycle running
- 40–50 min — pedestrians, then the emergency override
- 50–55 min — the fault and recovery, and a scripted scenario
- 55–60 min — follow-ups: time-of-day plans, sensors, turn arrows
Step 1: Clarify the requirements#
| You ask | They answer | What it changes |
|---|---|---|
| Which movements can run together? | North with south, east with west. Anything else conflicts. | A conflict table, and a phase is a set of approaches |
| Are timings fixed in code? | No. A plan of phases, each with green, yellow and all-red seconds. | A Plan of Phase values, validated on construction |
| What does a pedestrian button do? | Requests a walk at the next green of the parallel road, if the walk window fits. | A request that waits, and a window of walk plus flashing don't walk |
| What can an ambulance do? | Ask for green on its approach. It may not skip yellow or all-red. | A preempt that changes the next phase, never the clearance steps |
| What does "fail safe" mean? | On any fault: every approach flashing red (an all-way stop). | A FLASH mode that nothing else can leave except an explicit recovery |
| How does time move? | Your choice. It must be testable. | advance(now): every call carries the time |
The ambulance is coming from the east, and north-south is green. What happens?
The lights show green for north and south. The east-west road is red. An emergency vehicle asks for a green from the east. The tempting implementation switches the lamps straight to east-west green. What should the controller do, and how long does the ambulance wait?
One strong answer
End the current green now, then run its yellow and all-red in full, then give the east-west phase a held green. Switching straight over would put a green on east-west while north-south traffic is still in the box: exactly the crash the system exists to prevent. With 4 s of yellow and 2 s of all-red, the ambulance waits 6 seconds. The override changes which phase comes next and how long it is held; it never shortens the clearance. The held green ends when the vehicle is clear (or after a maximum, 60 s here), and the cycle resumes from the phase after it.
Step 2: Identify the entities#
Step 3: Class diagram#
%% title: Traffic signal: the classes
classDiagram
class Controller {
+advance(now)
+request_walk(crossing, now) bool
+preempt(approach, now) bool
+clear_preempt(now)
+fault(reason, now)
+recover(now)
+status() str
}
class Plan {
+phases tuple
+walk int
+clear int
+max_preempt int
}
class Phase {
+name str
+greens set
+green int
+yellow int
+all_red int
+crossing str
}
class Intersection {
+lights dict
+show(lights)
+flash()
}
class Crossings {
+request(name) bool
+serve(name, now) str
+cut(clear_by) list
}
class Step {
<<enumeration>>
GREEN
YELLOW
ALL_RED
}
class Mode {
<<enumeration>>
NORMAL
PREEMPT
FLASH
}
class ConflictError
Controller --> Plan
Controller --> Intersection
Controller --> Crossings
Controller --> Step
Controller --> Mode
Plan "1" *-- "many" Phase
Intersection ..> ConflictErrorThe controller is a state machine with two small axes. Step says where it is inside a phase: green, yellow, or all red. Mode says what it is doing overall: following the plan, serving an emergency vehicle, or flashing. A third piece of state, until, is the time at which the current step ends. Everything is driven by advance(now): any transition due at or before now runs, in order, so you can step one second at a time or jump a minute and get the same result.
The lamps go through Intersection.show, which acts as the conflict monitor: it refuses to display two conflicting directions as live. The monitor does not trust the controller, and the controller does not trust itself.
Step 4: Patterns applied#
| Pattern | Where | Why it earns its place |
|---|---|---|
| State (enums + a transition table) | Mode, Step, _finish_step |
Behaviour depends on state, and the states are few. Two enums and one method that maps (step) to the next step are easier to test than a class per state. With many more states (turn arrows, rail crossings), switch to State classes. |
| Observer | listeners |
A log, a dashboard and a remote monitoring system subscribe to the same events. The controller never knows them. |
| Command (light) | walk requests, target |
A request is a small value held until the controller can act on it. A set dedupes it, and target makes the emergency vehicle a plain piece of state. |
| Facade (light) | Controller |
Sensors and technicians call six methods; the monitor, crossings and plan stay behind it. |
Step 5: Code#
The interfaces#
Start with the vocabulary and the plan. A phase is a set of approaches that get green together, and its constructor refuses a set that contains a conflict. A plan is a list of phases plus the pedestrian timings. All of it is data a technician could change without touching the controller.
# signal.py, part 1: approaches, lights and the timing plan: configuration, not code
from __future__ import annotations
import threading
from collections.abc import Callable
from dataclasses import dataclass
from enum import Enum
class Approach(Enum):
N = "N"
E = "E"
S = "S"
W = "W"
CONFLICTS = {
Approach.N: {Approach.E, Approach.W},
Approach.S: {Approach.E, Approach.W},
Approach.E: {Approach.N, Approach.S},
Approach.W: {Approach.N, Approach.S},
}
class Light(Enum):
RED = "RED"
YELLOW = "YELLOW"
GREEN = "GREEN"
FLASHING_RED = "FLASHING_RED"
class Walk(Enum):
DONT_WALK = "DONT_WALK"
WALK = "WALK"
FLASHING_DONT_WALK = "FLASHING_DONT_WALK"
@dataclass(frozen=True)
class Phase:
name: str
greens: frozenset # the approaches that get green together
green: int # seconds
crossing: str # the pedestrian crossing released with this phase
yellow: int = 4
all_red: int = 2
def __post_init__(self):
for a in Approach: # in a fixed order, so the message is the same every run
if a in self.greens and CONFLICTS[a] & self.greens:
raise ValueError(f"phase {self.name}: {a.value} conflicts with another green")
@dataclass(frozen=True)
class Plan:
phases: tuple
walk: int = 7 # seconds of WALK
clear: int = 10 # seconds of flashing don't walk
max_preempt: int = 60 # the longest an emergency vehicle can hold a green
def __post_init__(self):
if not self.phases:
raise ValueError("a plan needs at least one phase")
if len({p.crossing for p in self.phases}) != len(self.phases):
raise ValueError("each phase releases its own crossing")
Listener = Callable[[int, str], None] # (time in seconds, text)// Signal.java, part 1: approaches, lights and the timing plan: configuration, not code
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
enum Approach { N, E, S, W }
class Conflicts {
static boolean between(Approach a, Approach b) { // north-south traffic crosses east-west traffic
boolean ns = a == Approach.N || a == Approach.S;
boolean otherNs = b == Approach.N || b == Approach.S;
return ns != otherNs;
}
}
enum Light { RED, YELLOW, GREEN, FLASHING_RED }
enum Walk { DONT_WALK, WALK, FLASHING_DONT_WALK }
record Phase(String name, Set<Approach> greens, int green, String crossing, int yellow, int allRed) {
Phase {
for (Approach a : Approach.values())
for (Approach b : Approach.values())
if (greens.contains(a) && greens.contains(b) && Conflicts.between(a, b)) throw new IllegalArgumentException("phase " + name + ": " + a + " conflicts with another green");
}
Phase(String name, Set<Approach> greens, int green, String crossing) { this(name, greens, green, crossing, 4, 2); }
}
record Plan(List<Phase> phases, int walk, int clear, int maxPreempt) { // seconds
Plan {
if (phases.isEmpty()) throw new IllegalArgumentException("a plan needs at least one phase");
if (phases.stream().map(Phase::crossing).distinct().count() != phases.size())
throw new IllegalArgumentException("each phase releases its own crossing");
}
Plan(List<Phase> phases) { this(phases, 7, 10, 60); }
}// signal.cpp, part 1: approaches, lights and the timing plan: configuration, not code
#include <algorithm>
#include <functional>
#include <iostream>
#include <iomanip>
#include <map>
#include <mutex>
#include <optional>
#include <set>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
enum class Approach { N, E, S, W };
const std::vector<Approach> ALL = {Approach::N, Approach::E, Approach::S, Approach::W};
std::string name(Approach a) {
switch (a) {
case Approach::N: return "N";
case Approach::E: return "E";
case Approach::S: return "S";
default: return "W";
}
}
bool conflicts(Approach a, Approach b) { // north-south traffic crosses east-west traffic
bool ns = a == Approach::N || a == Approach::S;
bool otherNs = b == Approach::N || b == Approach::S;
return ns != otherNs;
}
enum class Light { Red, Yellow, Green, FlashingRed };
std::string name(Light l) {
switch (l) {
case Light::Red: return "RED";
case Light::Yellow: return "YELLOW";
case Light::Green: return "GREEN";
default: return "FLASHING_RED";
}
}
enum class Walk { DontWalk, Walking, FlashingDontWalk };
std::string name(Walk w) {
switch (w) {
case Walk::DontWalk: return "DONT_WALK";
case Walk::Walking: return "WALK";
default: return "FLASHING_DONT_WALK";
}
}
struct Phase {
std::string name;
std::set<Approach> greens; // the approaches that get green together
int green; // seconds
std::string crossing; // the pedestrian crossing released with this phase
int yellow = 4;
int allRed = 2;
Phase(std::string n, std::set<Approach> g, int secs, std::string c) : name(std::move(n)), greens(std::move(g)), green(secs), crossing(std::move(c)) {
for (Approach a : ALL)
for (Approach b : ALL)
if (greens.count(a) && greens.count(b) && conflicts(a, b))
throw std::invalid_argument("phase " + name + ": " + ::name(a) + " conflicts with another green");
}
};
struct Plan {
std::vector<Phase> phases;
int walk = 7; // seconds of WALK
int clear = 10; // seconds of flashing don't walk
int maxPreempt = 60; // the longest an emergency vehicle can hold a green
explicit Plan(std::vector<Phase> p) : phases(std::move(p)) {
if (phases.empty()) throw std::invalid_argument("a plan needs at least one phase");
std::set<std::string> seen;
for (const auto& ph : phases) seen.insert(ph.crossing);
if (seen.size() != phases.size()) throw std::invalid_argument("each phase releases its own crossing");
}
};
using Listener = std::function<void(int, const std::string&)>; // (time in seconds, text)// signal.js, part 1: approaches, lights and the timing plan: configuration, not code
const APPROACHES = ["N", "E", "S", "W"];
const conflicts = (a, b) => "NS".includes(a) !== "NS".includes(b); // north-south traffic crosses east-west traffic
const Light = Object.freeze({ RED: "RED", YELLOW: "YELLOW", GREEN: "GREEN", FLASHING_RED: "FLASHING_RED" });
const Walk = Object.freeze({ DONT_WALK: "DONT_WALK", WALK: "WALK", FLASHING_DONT_WALK: "FLASHING_DONT_WALK" });
class Phase {
constructor(name, greens, green, crossing, yellow = 4, allRed = 2) {
Object.assign(this, { name, greens: new Set(greens), green, crossing, yellow, allRed });
for (const a of APPROACHES) { // in a fixed order, so the message is the same every run
if (this.greens.has(a) && APPROACHES.some((b) => this.greens.has(b) && conflicts(a, b))) {
throw new Error(`phase ${name}: ${a} conflicts with another green`);
}
}
}
}
class Plan {
constructor(phases, walk = 7, clear = 10, maxPreempt = 60) { // seconds
if (phases.length === 0) throw new Error("a plan needs at least one phase");
if (new Set(phases.map((p) => p.crossing)).size !== phases.length) throw new Error("each phase releases its own crossing");
Object.assign(this, { phases, walk, clear, maxPreempt });
}
}An invalid phase can never exist: Phase("bad", {N, E}, ...) raises at construction, before any lamp is involved. That is the first of two lines of defence.
The intersection: a conflict monitor#
# part 2: the intersection: what the lamps show, behind a conflict monitor
class ConflictError(Exception):
pass
class Intersection:
def __init__(self):
self.lights = {a: Light.RED for a in Approach}
def show(self, lights: dict) -> None:
live = [a for a, l in lights.items() if l in (Light.GREEN, Light.YELLOW)]
for a in live:
for b in live:
if b in CONFLICTS[a]:
raise ConflictError(f"{a.value} and {b.value} would both be live")
self.lights = dict(lights)
def flash(self) -> None: # the fail-safe look; it needs no conflict check: everyone stops
self.lights = {a: Light.FLASHING_RED for a in Approach}
def __str__(self) -> str:
return " ".join(f"{a.value}:{self.lights[a].name}" for a in Approach)// part 2: the intersection: what the lamps show, behind a conflict monitor
class ConflictError extends RuntimeException {
ConflictError(String message) { super(message); }
}
class Intersection {
Map<Approach, Light> lights = new EnumMap<>(Approach.class);
Intersection() { for (Approach a : Approach.values()) lights.put(a, Light.RED); }
void show(Map<Approach, Light> next) {
List<Approach> live = new ArrayList<>();
for (Approach a : Approach.values()) {
Light l = next.get(a);
if (l == Light.GREEN || l == Light.YELLOW) live.add(a);
}
for (Approach a : live)
for (Approach b : live)
if (Conflicts.between(a, b)) throw new ConflictError(a + " and " + b + " would both be live");
lights = new EnumMap<>(next);
}
void flash() { // the fail-safe look; it needs no conflict check: everyone stops
for (Approach a : Approach.values()) lights.put(a, Light.FLASHING_RED);
}
@Override
public String toString() {
StringJoiner j = new StringJoiner(" ");
for (Approach a : Approach.values()) j.add(a + ":" + lights.get(a));
return j.toString();
}
}// part 2: the intersection: what the lamps show, behind a conflict monitor
struct ConflictError : std::runtime_error {
using std::runtime_error::runtime_error;
};
class Intersection {
public:
std::map<Approach, Light> lights;
Intersection() {
for (Approach a : ALL) lights[a] = Light::Red;
}
void show(const std::map<Approach, Light>& next) {
std::vector<Approach> live;
for (Approach a : ALL) {
Light l = next.at(a);
if (l == Light::Green || l == Light::Yellow) live.push_back(a);
}
for (Approach a : live)
for (Approach b : live)
if (conflicts(a, b)) throw ConflictError(name(a) + " and " + name(b) + " would both be live");
lights = next;
}
void flash() { // the fail-safe look; it needs no conflict check: everyone stops
for (Approach a : ALL) lights[a] = Light::FlashingRed;
}
std::string str() const {
std::string s;
for (Approach a : ALL) s += (s.empty() ? "" : " ") + name(a) + ":" + name(lights.at(a));
return s;
}
};// part 2: the intersection: what the lamps show, behind a conflict monitor
class ConflictError extends Error {}
class Intersection {
constructor() {
this.lights = Object.fromEntries(APPROACHES.map((a) => [a, Light.RED]));
}
show(next) {
const live = APPROACHES.filter((a) => next[a] === Light.GREEN || next[a] === Light.YELLOW);
for (const a of live) {
for (const b of live) if (conflicts(a, b)) throw new ConflictError(`${a} and ${b} would both be live`);
}
this.lights = { ...next };
}
flash() { // the fail-safe look; it needs no conflict check: everyone stops
this.lights = Object.fromEntries(APPROACHES.map((a) => [a, Light.FLASHING_RED]));
}
toString() {
return APPROACHES.map((a) => `${a}:${this.lights[a]}`).join(" ");
}
}This is the second line of defence. show is the only way to change the lamps, and it checks the whole picture, not the change. flash skips the check because an all-way flashing red has no conflict: everyone stops.
Pedestrians#
# part 3: pedestrian crossings: a request waits until the walk window fits
class Crossings:
def __init__(self, names: list, walk: int, clear: int):
self.signal = {n: Walk.DONT_WALK for n in names}
self.pending: set = set()
self.timers: list = [] # (time, crossing, next signal), earliest first
self.walk, self.clear = walk, clear
def request(self, name: str) -> bool:
if name not in self.signal:
raise ValueError(f"no such crossing: {name}")
if self.signal[name] is Walk.WALK:
return False # they are crossing right now: the button does nothing
fresh = name not in self.pending
self.pending.add(name) # a set: pressing again changes nothing
return fresh
def can_serve(self, name: str, remaining: int) -> bool: # the whole window must fit in the green
return name in self.pending and self.signal[name] is Walk.DONT_WALK and remaining >= self.walk + self.clear
def serve(self, name: str, now: int) -> str:
self.pending.discard(name)
self.signal[name] = Walk.WALK
self.timers = [(now + self.walk, name, Walk.FLASHING_DONT_WALK), (now + self.walk + self.clear, name, Walk.DONT_WALK)]
return f"crossing {name}: WALK"
def next_due(self):
return min((t for t, _, _ in self.timers), default=None)
def fire(self, t: int) -> list:
out = []
for due, name, sig in [x for x in self.timers if x[0] == t]:
self.signal[name] = sig
out.append(f"crossing {name}: {sig.name}")
self.timers = [x for x in self.timers if x[0] != t]
return out
def cut(self, clear_by: int) -> list: # the green is ending: whoever is crossing gets to finish, then stop
out, timers = [], []
for name, sig in self.signal.items():
if sig is Walk.DONT_WALK:
continue
due = min([t for t, n, _ in self.timers if n == name and _ is Walk.DONT_WALK] + [clear_by])
if sig is Walk.WALK:
self.signal[name] = Walk.FLASHING_DONT_WALK
out.append(f"crossing {name}: FLASHING_DONT_WALK")
timers.append((due, name, Walk.DONT_WALK))
self.timers = timers
return out
def reset(self) -> None:
self.signal = {n: Walk.DONT_WALK for n in self.signal}
self.pending.clear()
self.timers = []// part 3: pedestrian crossings: a request waits until the walk window fits
record Timer(int at, String crossing, Walk next) {}
class Crossings {
final Map<String, Walk> signal = new LinkedHashMap<>();
final Set<String> pending = new HashSet<>();
List<Timer> timers = new ArrayList<>(); // earliest first
final int walk, clear;
Crossings(List<String> names, int walk, int clear) {
for (String n : names) signal.put(n, Walk.DONT_WALK);
this.walk = walk;
this.clear = clear;
}
boolean request(String name) {
if (!signal.containsKey(name)) throw new IllegalArgumentException("no such crossing: " + name);
if (signal.get(name) == Walk.WALK) return false; // they are crossing right now: the button does nothing
return pending.add(name); // a set: pressing again changes nothing
}
boolean canServe(String name, int remaining) { // the whole window must fit in the green
return pending.contains(name) && signal.get(name) == Walk.DONT_WALK && remaining >= walk + clear;
}
String serve(String name, int now) {
pending.remove(name);
signal.put(name, Walk.WALK);
timers = new ArrayList<>(List.of(new Timer(now + walk, name, Walk.FLASHING_DONT_WALK), new Timer(now + walk + clear, name, Walk.DONT_WALK)));
return "crossing " + name + ": WALK";
}
Integer nextDue() {
Integer best = null;
for (Timer t : timers) if (best == null || t.at() < best) best = t.at();
return best;
}
List<String> fire(int t) {
List<String> out = new ArrayList<>();
List<Timer> rest = new ArrayList<>();
for (Timer x : timers) {
if (x.at() == t) {
signal.put(x.crossing(), x.next());
out.add("crossing " + x.crossing() + ": " + x.next());
} else {
rest.add(x);
}
}
timers = rest;
return out;
}
List<String> cut(int clearBy) { // the green is ending: whoever is crossing gets to finish, then stop
List<String> out = new ArrayList<>();
List<Timer> next = new ArrayList<>();
for (var e : signal.entrySet()) {
if (e.getValue() == Walk.DONT_WALK) continue;
int due = clearBy;
for (Timer t : timers) if (t.crossing().equals(e.getKey()) && t.next() == Walk.DONT_WALK) due = Math.min(due, t.at());
if (e.getValue() == Walk.WALK) {
e.setValue(Walk.FLASHING_DONT_WALK);
out.add("crossing " + e.getKey() + ": FLASHING_DONT_WALK");
}
next.add(new Timer(due, e.getKey(), Walk.DONT_WALK));
}
timers = next;
return out;
}
void reset() {
signal.replaceAll((k, v) -> Walk.DONT_WALK);
pending.clear();
timers = new ArrayList<>();
}
}// part 3: pedestrian crossings: a request waits until the walk window fits
struct Timer {
int at;
std::string crossing;
Walk next;
};
class Crossings {
public:
std::vector<std::pair<std::string, Walk>> signal; // in plan order
std::set<std::string> pending;
std::vector<Timer> timers;
int walk, clear;
Crossings(const std::vector<std::string>& names, int w, int c) : walk(w), clear(c) {
for (const auto& n : names) signal.push_back({n, Walk::DontWalk});
}
Walk& at(const std::string& name) {
for (auto& [n, w] : signal) if (n == name) return w;
throw std::invalid_argument("no such crossing: " + name);
}
bool request(const std::string& name) {
if (at(name) == Walk::Walking) return false; // they are crossing right now: the button does nothing
return pending.insert(name).second; // a set: pressing again changes nothing
}
bool canServe(const std::string& name, int remaining) { // the whole window must fit in the green
return pending.count(name) && at(name) == Walk::DontWalk && remaining >= walk + clear;
}
std::string serve(const std::string& name, int now) {
pending.erase(name);
at(name) = Walk::Walking;
timers = {{now + walk, name, Walk::FlashingDontWalk}, {now + walk + clear, name, Walk::DontWalk}};
return "crossing " + name + ": WALK";
}
std::optional<int> nextDue() const {
std::optional<int> best;
for (const auto& t : timers) if (!best || t.at < *best) best = t.at;
return best;
}
std::vector<std::string> fire(int t) {
std::vector<std::string> out;
std::vector<Timer> rest;
for (const auto& x : timers) {
if (x.at == t) {
at(x.crossing) = x.next;
out.push_back("crossing " + x.crossing + ": " + name(x.next));
} else {
rest.push_back(x);
}
}
timers = rest;
return out;
}
std::vector<std::string> cut(int clearBy) { // the green is ending: whoever is crossing gets to finish, then stop
std::vector<std::string> out;
std::vector<Timer> next;
for (auto& [n, sig] : signal) {
if (sig == Walk::DontWalk) continue;
int due = clearBy;
for (const auto& t : timers) if (t.crossing == n && t.next == Walk::DontWalk) due = std::min(due, t.at);
if (sig == Walk::Walking) {
sig = Walk::FlashingDontWalk;
out.push_back("crossing " + n + ": FLASHING_DONT_WALK");
}
next.push_back({due, n, Walk::DontWalk});
}
timers = next;
return out;
}
void reset() {
for (auto& [n, w] : signal) w = Walk::DontWalk;
pending.clear();
timers.clear();
}
};// part 3: pedestrian crossings: a request waits until the walk window fits
class Crossings {
constructor(names, walk, clear) {
this.signal = new Map(names.map((n) => [n, Walk.DONT_WALK]));
this.pending = new Set();
this.timers = []; // { at, crossing, next }
this.walk = walk;
this.clear = clear;
}
request(name) {
if (!this.signal.has(name)) throw new Error(`no such crossing: ${name}`);
if (this.signal.get(name) === Walk.WALK) return false; // they are crossing right now: the button does nothing
const fresh = !this.pending.has(name);
this.pending.add(name); // a set: pressing again changes nothing
return fresh;
}
canServe(name, remaining) { // the whole window must fit in the green
return this.pending.has(name) && this.signal.get(name) === Walk.DONT_WALK && remaining >= this.walk + this.clear;
}
serve(name, now) {
this.pending.delete(name);
this.signal.set(name, Walk.WALK);
this.timers = [
{ at: now + this.walk, crossing: name, next: Walk.FLASHING_DONT_WALK },
{ at: now + this.walk + this.clear, crossing: name, next: Walk.DONT_WALK },
];
return `crossing ${name}: WALK`;
}
nextDue() {
return this.timers.length ? Math.min(...this.timers.map((t) => t.at)) : null;
}
fire(t) {
const out = [];
for (const x of this.timers.filter((x) => x.at === t)) {
this.signal.set(x.crossing, x.next);
out.push(`crossing ${x.crossing}: ${x.next}`);
}
this.timers = this.timers.filter((x) => x.at !== t);
return out;
}
cut(clearBy) { // the green is ending: whoever is crossing gets to finish, then stop
const out = [];
const next = [];
for (const [name, sig] of this.signal) {
if (sig === Walk.DONT_WALK) continue;
const due = Math.min(clearBy, ...this.timers.filter((t) => t.crossing === name && t.next === Walk.DONT_WALK).map((t) => t.at));
if (sig === Walk.WALK) {
this.signal.set(name, Walk.FLASHING_DONT_WALK);
out.push(`crossing ${name}: FLASHING_DONT_WALK`);
}
next.push({ at: due, crossing: name, next: Walk.DONT_WALK });
}
this.timers = next;
return out;
}
reset() {
for (const n of this.signal.keys()) this.signal.set(n, Walk.DONT_WALK);
this.pending.clear();
this.timers = [];
}
}A request is a set entry, so pressing the button ten times asks once. It is served only when the whole window (7 s of walk plus 10 s of flashing don't walk) fits inside the green that remains; otherwise it waits for the next green of that road, and that green is stretched if it would be too short. When a green ends, cut makes anyone still crossing see flashing don't walk, and they get until the end of the yellow and all-red to finish.
The controller#
# part 4: the controller: a state machine over (mode, step), driven by the time you give it
class Step(Enum):
GREEN = 1
YELLOW = 2
ALL_RED = 3
class Mode(Enum):
NORMAL = 1
PREEMPT = 2
FLASH = 3
class Controller:
def __init__(self, plan: Plan, now: int = 0):
self.plan, self.box = plan, Intersection()
self.crossings = Crossings([p.crossing for p in plan.phases], plan.walk, plan.clear)
self.listeners: list = []
self.mode, self.step = Mode.NORMAL, Step.ALL_RED
self.index = len(plan.phases) - 1 # so the first phase to run is phase 0
self.until = now # when the current step ends
self.target = None # the approach an emergency vehicle is coming from
self._out: list = []
self._lock = threading.Lock()
# ---- the public calls: each takes the lock, then tells listeners after releasing it
def advance(self, now: int) -> None:
self._run(lambda: self._advance(now))
def request_walk(self, crossing: str, now: int) -> bool:
return self._run(lambda: self._request_walk(crossing, now))
def preempt(self, approach: Approach, now: int) -> bool:
return self._run(lambda: self._preempt(approach, now))
def clear_preempt(self, now: int) -> None:
self._run(lambda: self._clear_preempt(now))
def fault(self, reason: str, now: int) -> None:
self._run(lambda: (self._advance(now), self._fault(now, reason)))
def recover(self, now: int) -> None:
self._run(lambda: self._recover(now))
def status(self) -> str:
with self._lock:
where = "FLASH" if self.mode is Mode.FLASH else f"{self.mode.name} {self.step.name}"
return f"{where} | {self.box} | " + " ".join(f"{n}:{s.name}" for n, s in self.crossings.signal.items())
def _run(self, fn):
with self._lock:
result = fn()
out, self._out = self._out, []
for t, text in out:
for listener in self.listeners:
listener(t, text)
return result
def _say(self, t: int, *texts: str) -> None:
self._out.extend((t, x) for x in texts)
# ---- the machine
def _advance(self, now: int) -> None:
while self.mode is not Mode.FLASH:
c = self.crossings.next_due()
if c is not None and c <= self.until:
if c > now:
return
self._say(c, *self.crossings.fire(c))
else:
if self.until > now:
return
self._finish_step(self.until)
def _finish_step(self, t: int) -> None:
phase = self.plan.phases[self.index]
if self.step is Step.GREEN:
self._say(t, *self.crossings.cut(t + phase.yellow + phase.all_red))
self._enter(Step.YELLOW, self.index, t, phase.yellow)
elif self.step is Step.YELLOW:
self._enter(Step.ALL_RED, self.index, t, phase.all_red)
else:
self._next_phase(t)
def _next_phase(self, t: int) -> None:
if self.target is not None:
self.mode = Mode.PREEMPT
idx = next(i for i, p in enumerate(self.plan.phases) if self.target in p.greens)
secs = self.plan.max_preempt
else:
idx = (self.index + 1) % len(self.plan.phases)
phase = self.plan.phases[idx]
secs = phase.green
if phase.crossing in self.crossings.pending:
secs = max(secs, self.plan.walk + self.plan.clear) # stretch the green so the walk fits
self._enter(Step.GREEN, idx, t, secs)
if self.mode is not Mode.FLASH:
self._serve_walk(t)
def _serve_walk(self, t: int) -> None:
c = self.plan.phases[self.index].crossing
if self.step is Step.GREEN and self.crossings.can_serve(c, self.until - t):
self._say(t, self.crossings.serve(c, t))
def _enter(self, step: Step, index: int, t: int, secs: int) -> None:
phase = self.plan.phases[index]
lamp = {Step.GREEN: Light.GREEN, Step.YELLOW: Light.YELLOW}.get(step, Light.RED)
lights = {a: (lamp if a in phase.greens and step is not Step.ALL_RED else Light.RED) for a in Approach}
try:
self.box.show(lights)
except ConflictError as e: # the monitor refused: do not show it, go to the safe state
self._fault(t, f"conflict monitor: {e}")
return
self.step, self.index, self.until = step, index, t + secs
what = "all red" if step is Step.ALL_RED else f"{phase.name}: {step.name.lower()}"
self._say(t, f"{what} {secs} s")
def _request_walk(self, crossing: str, now: int) -> bool:
self._advance(now)
if self.mode is Mode.FLASH:
return False # every approach is stopping anyway
if self.crossings.request(crossing):
self._say(now, f"crossing {crossing}: requested")
if self.step is Step.GREEN:
self._serve_walk(now)
return True
def _preempt(self, approach: Approach, now: int) -> bool:
self._advance(now)
if self.mode is Mode.FLASH:
return False
self.target = approach
self._say(now, f"emergency vehicle from {approach.value}")
if self.step is Step.GREEN:
if approach in self.plan.phases[self.index].greens:
self.mode, self.until = Mode.PREEMPT, now + self.plan.max_preempt # hold this green
else:
self.until = now # end this green now: yellow and all red still run in full
self._advance(now)
return True
def _clear_preempt(self, now: int) -> None:
self._advance(now)
if self.target is None:
return
holding = self.mode is Mode.PREEMPT and self.step is Step.GREEN and self.target in self.plan.phases[self.index].greens
self.target = None
if self.mode is Mode.PREEMPT:
self.mode = Mode.NORMAL
if holding:
self.until = now
self._say(now, "emergency vehicle clear")
self._advance(now)
def _fault(self, t: int, reason: str) -> None:
self.mode, self.target = Mode.FLASH, None
self.box.flash()
self.crossings.reset()
self._say(t, f"FAULT {reason}: all approaches flashing red")
def _recover(self, now: int) -> None:
if self.mode is not Mode.FLASH:
return
self.mode, self.index = Mode.NORMAL, len(self.plan.phases) - 1
self._say(now, "recovered: restarting the cycle")
self._enter(Step.ALL_RED, self.index, now, self.plan.phases[self.index].all_red) # all red first, then phase 0
self._advance(now)// part 4: the controller: a state machine over (mode, step), driven by the time you give it
enum Step { GREEN, YELLOW, ALL_RED }
enum Mode { NORMAL, PREEMPT, FLASH }
class Controller {
final Plan plan;
final Intersection box = new Intersection();
final Crossings crossings;
final List<BiConsumer<Integer, String>> listeners = new ArrayList<>();
Mode mode = Mode.NORMAL;
Step step = Step.ALL_RED;
int index;
int until; // when the current step ends
Approach target; // the approach an emergency vehicle is coming from
private List<Map.Entry<Integer, String>> out = new ArrayList<>();
private final ReentrantLock lock = new ReentrantLock();
Controller(Plan plan, int now) {
this.plan = plan;
List<String> names = new ArrayList<>();
for (Phase p : plan.phases()) names.add(p.crossing());
crossings = new Crossings(names, plan.walk(), plan.clear());
index = plan.phases().size() - 1; // so the first phase to run is phase 0
until = now;
}
// ---- the public calls: each takes the lock, then tells listeners after releasing it
void advance(int now) { run(() -> { advanceTo(now); return null; }); }
boolean requestWalk(String crossing, int now) { return run(() -> requestWalkAt(crossing, now)); }
boolean preempt(Approach approach, int now) { return run(() -> preemptAt(approach, now)); }
void clearPreempt(int now) { run(() -> { clearPreemptAt(now); return null; }); }
void fault(String reason, int now) { run(() -> { advanceTo(now); faultAt(now, reason); return null; }); }
void recover(int now) { run(() -> { recoverAt(now); return null; }); }
String status() {
lock.lock();
try {
String where = mode == Mode.FLASH ? "FLASH" : mode + " " + step;
StringJoiner j = new StringJoiner(" ");
for (var e : crossings.signal.entrySet()) j.add(e.getKey() + ":" + e.getValue());
return where + " | " + box + " | " + j;
} finally {
lock.unlock();
}
}
private <T> T run(Supplier<T> fn) {
T result;
List<Map.Entry<Integer, String>> batch;
lock.lock();
try {
result = fn.get();
batch = out;
out = new ArrayList<>();
} finally {
lock.unlock();
}
for (var e : batch) for (var l : listeners) l.accept(e.getKey(), e.getValue());
return result;
}
private void say(int t, List<String> texts) { for (String x : texts) out.add(Map.entry(t, x)); }
private void say(int t, String text) { out.add(Map.entry(t, text)); }
// ---- the machine
private void advanceTo(int now) {
while (mode != Mode.FLASH) {
Integer c = crossings.nextDue();
if (c != null && c <= until) {
if (c > now) return;
say(c, crossings.fire(c));
} else {
if (until > now) return;
finishStep(until);
}
}
}
private void finishStep(int t) {
Phase phase = plan.phases().get(index);
if (step == Step.GREEN) {
say(t, crossings.cut(t + phase.yellow() + phase.allRed()));
enter(Step.YELLOW, index, t, phase.yellow());
} else if (step == Step.YELLOW) {
enter(Step.ALL_RED, index, t, phase.allRed());
} else {
nextPhase(t);
}
}
private void nextPhase(int t) {
int idx;
int secs;
if (target != null) {
mode = Mode.PREEMPT;
idx = 0;
for (int i = 0; i < plan.phases().size(); i++) if (plan.phases().get(i).greens().contains(target)) { idx = i; break; }
secs = plan.maxPreempt();
} else {
idx = (index + 1) % plan.phases().size();
Phase phase = plan.phases().get(idx);
secs = phase.green();
if (crossings.pending.contains(phase.crossing())) secs = Math.max(secs, plan.walk() + plan.clear()); // stretch the green so the walk fits
}
enter(Step.GREEN, idx, t, secs);
if (mode != Mode.FLASH) serveWalk(t);
}
private void serveWalk(int t) {
String c = plan.phases().get(index).crossing();
if (step == Step.GREEN && crossings.canServe(c, until - t)) say(t, crossings.serve(c, t));
}
private void enter(Step s, int idx, int t, int secs) {
Phase phase = plan.phases().get(idx);
Light lamp = s == Step.GREEN ? Light.GREEN : s == Step.YELLOW ? Light.YELLOW : Light.RED;
Map<Approach, Light> lights = new EnumMap<>(Approach.class);
for (Approach a : Approach.values()) lights.put(a, phase.greens().contains(a) && s != Step.ALL_RED ? lamp : Light.RED);
try {
box.show(lights);
} catch (ConflictError e) { // the monitor refused: do not show it, go to the safe state
faultAt(t, "conflict monitor: " + e.getMessage());
return;
}
step = s;
index = idx;
until = t + secs;
String what = s == Step.ALL_RED ? "all red" : phase.name() + ": " + s.name().toLowerCase();
say(t, what + " " + secs + " s");
}
private boolean requestWalkAt(String crossing, int now) {
advanceTo(now);
if (mode == Mode.FLASH) return false; // every approach is stopping anyway
if (crossings.request(crossing)) say(now, "crossing " + crossing + ": requested");
if (step == Step.GREEN) serveWalk(now);
return true;
}
private boolean preemptAt(Approach approach, int now) {
advanceTo(now);
if (mode == Mode.FLASH) return false;
target = approach;
say(now, "emergency vehicle from " + approach);
if (step == Step.GREEN) {
if (plan.phases().get(index).greens().contains(approach)) {
mode = Mode.PREEMPT;
until = now + plan.maxPreempt(); // hold this green
} else {
until = now; // end this green now: yellow and all red still run in full
}
}
advanceTo(now);
return true;
}
private void clearPreemptAt(int now) {
advanceTo(now);
if (target == null) return;
boolean holding = mode == Mode.PREEMPT && step == Step.GREEN && plan.phases().get(index).greens().contains(target);
target = null;
if (mode == Mode.PREEMPT) mode = Mode.NORMAL;
if (holding) until = now;
say(now, "emergency vehicle clear");
advanceTo(now);
}
private void faultAt(int t, String reason) {
mode = Mode.FLASH;
target = null;
box.flash();
crossings.reset();
say(t, "FAULT " + reason + ": all approaches flashing red");
}
private void recoverAt(int now) {
if (mode != Mode.FLASH) return;
mode = Mode.NORMAL;
index = plan.phases().size() - 1;
say(now, "recovered: restarting the cycle");
enter(Step.ALL_RED, index, now, plan.phases().get(index).allRed()); // all red first, then phase 0
advanceTo(now);
}
}// part 4: the controller: a state machine over (mode, step), driven by the time you give it
enum class Step { Green, Yellow, AllRed };
enum class Mode { Normal, Preempt, Flash };
class Controller {
public:
Plan plan;
Intersection box;
Crossings crossings;
std::vector<Listener> listeners;
Controller(Plan p, int now) : plan(std::move(p)), crossings(crossingNames(plan), plan.walk, plan.clear) {
index = static_cast<int>(plan.phases.size()) - 1; // so the first phase to run is phase 0
until = now;
}
// ---- the public calls: each takes the lock, then tells listeners after releasing it
void advance(int now) { run([&] { advanceTo(now); }); }
bool requestWalk(const std::string& crossing, int now) {
bool r = false;
run([&] { r = requestWalkAt(crossing, now); });
return r;
}
bool preempt(Approach approach, int now) {
bool r = false;
run([&] { r = preemptAt(approach, now); });
return r;
}
void clearPreempt(int now) { run([&] { clearPreemptAt(now); }); }
void fault(const std::string& reason, int now) { run([&] { advanceTo(now); faultAt(now, reason); }); }
void recover(int now) { run([&] { recoverAt(now); }); }
std::string status() {
std::lock_guard<std::mutex> g(mu_);
std::string where = mode == Mode::Flash ? "FLASH" : (mode == Mode::Normal ? "NORMAL " : "PREEMPT ") + stepName(step);
std::string walks;
for (const auto& [n, w] : crossings.signal) walks += (walks.empty() ? "" : " ") + n + ":" + name(w);
return where + " | " + box.str() + " | " + walks;
}
private:
Mode mode = Mode::Normal;
Step step = Step::AllRed;
int index = 0;
int until = 0; // when the current step ends
std::optional<Approach> target; // the approach an emergency vehicle is coming from
std::vector<std::pair<int, std::string>> out_;
std::mutex mu_;
static std::vector<std::string> crossingNames(const Plan& p) {
std::vector<std::string> names;
for (const auto& ph : p.phases) names.push_back(ph.crossing);
return names;
}
static std::string stepName(Step s) { return s == Step::Green ? "GREEN" : s == Step::Yellow ? "YELLOW" : "ALL_RED"; }
void run(const std::function<void()>& fn) {
std::vector<std::pair<int, std::string>> batch;
{
std::lock_guard<std::mutex> g(mu_);
fn();
batch.swap(out_);
}
for (const auto& [t, text] : batch) for (const auto& l : listeners) l(t, text);
}
void say(int t, const std::vector<std::string>& texts) { for (const auto& x : texts) out_.push_back({t, x}); }
void say(int t, const std::string& text) { out_.push_back({t, text}); }
// ---- the machine
void advanceTo(int now) {
while (mode != Mode::Flash) {
auto c = crossings.nextDue();
if (c && *c <= until) {
if (*c > now) return;
say(*c, crossings.fire(*c));
} else {
if (until > now) return;
finishStep(until);
}
}
}
void finishStep(int t) {
const Phase& phase = plan.phases[index];
if (step == Step::Green) {
say(t, crossings.cut(t + phase.yellow + phase.allRed));
enter(Step::Yellow, index, t, phase.yellow);
} else if (step == Step::Yellow) {
enter(Step::AllRed, index, t, phase.allRed);
} else {
nextPhase(t);
}
}
void nextPhase(int t) {
int idx = 0, secs = 0;
if (target) {
mode = Mode::Preempt;
for (size_t i = 0; i < plan.phases.size(); i++) if (plan.phases[i].greens.count(*target)) { idx = static_cast<int>(i); break; }
secs = plan.maxPreempt;
} else {
idx = (index + 1) % static_cast<int>(plan.phases.size());
const Phase& phase = plan.phases[idx];
secs = phase.green;
if (crossings.pending.count(phase.crossing)) secs = std::max(secs, plan.walk + plan.clear); // stretch the green so the walk fits
}
enter(Step::Green, idx, t, secs);
if (mode != Mode::Flash) serveWalk(t);
}
void serveWalk(int t) {
const std::string& c = plan.phases[index].crossing;
if (step == Step::Green && crossings.canServe(c, until - t)) say(t, crossings.serve(c, t));
}
void enter(Step s, int idx, int t, int secs) {
const Phase& phase = plan.phases[idx];
Light lamp = s == Step::Green ? Light::Green : s == Step::Yellow ? Light::Yellow : Light::Red;
std::map<Approach, Light> lights;
for (Approach a : ALL) lights[a] = phase.greens.count(a) && s != Step::AllRed ? lamp : Light::Red;
try {
box.show(lights);
} catch (const ConflictError& e) { // the monitor refused: do not show it, go to the safe state
faultAt(t, std::string("conflict monitor: ") + e.what());
return;
}
step = s;
index = idx;
until = t + secs;
std::string lower = s == Step::Green ? "green" : "yellow";
std::string what = s == Step::AllRed ? "all red" : phase.name + ": " + lower;
say(t, what + " " + std::to_string(secs) + " s");
}
bool requestWalkAt(const std::string& crossing, int now) {
advanceTo(now);
if (mode == Mode::Flash) return false; // every approach is stopping anyway
if (crossings.request(crossing)) say(now, "crossing " + crossing + ": requested");
if (step == Step::Green) serveWalk(now);
return true;
}
bool preemptAt(Approach approach, int now) {
advanceTo(now);
if (mode == Mode::Flash) return false;
target = approach;
say(now, "emergency vehicle from " + name(approach));
if (step == Step::Green) {
if (plan.phases[index].greens.count(approach)) {
mode = Mode::Preempt;
until = now + plan.maxPreempt; // hold this green
} else {
until = now; // end this green now: yellow and all red still run in full
}
}
advanceTo(now);
return true;
}
void clearPreemptAt(int now) {
advanceTo(now);
if (!target) return;
bool holding = mode == Mode::Preempt && step == Step::Green && plan.phases[index].greens.count(*target);
target.reset();
if (mode == Mode::Preempt) mode = Mode::Normal;
if (holding) until = now;
say(now, "emergency vehicle clear");
advanceTo(now);
}
void faultAt(int t, const std::string& reason) {
mode = Mode::Flash;
target.reset();
box.flash();
crossings.reset();
say(t, "FAULT " + reason + ": all approaches flashing red");
}
void recoverAt(int now) {
if (mode != Mode::Flash) return;
mode = Mode::Normal;
index = static_cast<int>(plan.phases.size()) - 1;
say(now, "recovered: restarting the cycle");
enter(Step::AllRed, index, now, plan.phases[index].allRed); // all red first, then phase 0
advanceTo(now);
}
};// part 4: the controller: a state machine over (mode, step), driven by the time you give it
// (JavaScript runs one call at a time, so there is no lock; Java and C++ guard each public call with one.)
const Step = Object.freeze({ GREEN: "GREEN", YELLOW: "YELLOW", ALL_RED: "ALL_RED" });
const Mode = Object.freeze({ NORMAL: "NORMAL", PREEMPT: "PREEMPT", FLASH: "FLASH" });
class Controller {
constructor(plan, now = 0) {
this.plan = plan;
this.box = new Intersection();
this.crossings = new Crossings(plan.phases.map((p) => p.crossing), plan.walk, plan.clear);
this.listeners = [];
this.mode = Mode.NORMAL;
this.step = Step.ALL_RED;
this.index = plan.phases.length - 1; // so the first phase to run is phase 0
this.until = now; // when the current step ends
this.target = null; // the approach an emergency vehicle is coming from
this.out = [];
}
// ---- the public calls: each does its work, then tells listeners
advance(now) { return this.run(() => this.advanceTo(now)); }
requestWalk(crossing, now) { return this.run(() => this.requestWalkAt(crossing, now)); }
preempt(approach, now) { return this.run(() => this.preemptAt(approach, now)); }
clearPreempt(now) { return this.run(() => this.clearPreemptAt(now)); }
fault(reason, now) { return this.run(() => { this.advanceTo(now); this.faultAt(now, reason); }); }
recover(now) { return this.run(() => this.recoverAt(now)); }
status() {
const where = this.mode === Mode.FLASH ? "FLASH" : `${this.mode} ${this.step}`;
return `${where} | ${this.box} | ` + [...this.crossings.signal].map(([n, s]) => `${n}:${s}`).join(" ");
}
run(fn) {
const result = fn();
const batch = this.out;
this.out = [];
for (const [t, text] of batch) for (const l of this.listeners) l(t, text);
return result;
}
say(t, ...texts) { for (const x of texts) this.out.push([t, x]); }
// ---- the machine
advanceTo(now) {
while (this.mode !== Mode.FLASH) {
const c = this.crossings.nextDue();
if (c !== null && c <= this.until) {
if (c > now) return;
this.say(c, ...this.crossings.fire(c));
} else {
if (this.until > now) return;
this.finishStep(this.until);
}
}
}
finishStep(t) {
const phase = this.plan.phases[this.index];
if (this.step === Step.GREEN) {
this.say(t, ...this.crossings.cut(t + phase.yellow + phase.allRed));
this.enter(Step.YELLOW, this.index, t, phase.yellow);
} else if (this.step === Step.YELLOW) {
this.enter(Step.ALL_RED, this.index, t, phase.allRed);
} else {
this.nextPhase(t);
}
}
nextPhase(t) {
let idx;
let secs;
if (this.target !== null) {
this.mode = Mode.PREEMPT;
idx = this.plan.phases.findIndex((p) => p.greens.has(this.target));
secs = this.plan.maxPreempt;
} else {
idx = (this.index + 1) % this.plan.phases.length;
const phase = this.plan.phases[idx];
secs = phase.green;
if (this.crossings.pending.has(phase.crossing)) secs = Math.max(secs, this.plan.walk + this.plan.clear); // stretch the green so the walk fits
}
this.enter(Step.GREEN, idx, t, secs);
if (this.mode !== Mode.FLASH) this.serveWalk(t);
}
serveWalk(t) {
const c = this.plan.phases[this.index].crossing;
if (this.step === Step.GREEN && this.crossings.canServe(c, this.until - t)) this.say(t, this.crossings.serve(c, t));
}
enter(step, index, t, secs) {
const phase = this.plan.phases[index];
const lamp = step === Step.GREEN ? Light.GREEN : step === Step.YELLOW ? Light.YELLOW : Light.RED;
const lights = Object.fromEntries(APPROACHES.map((a) => [a, phase.greens.has(a) && step !== Step.ALL_RED ? lamp : Light.RED]));
try {
this.box.show(lights);
} catch (e) { // the monitor refused: do not show it, go to the safe state
if (!(e instanceof ConflictError)) throw e;
this.faultAt(t, `conflict monitor: ${e.message}`);
return;
}
Object.assign(this, { step, index, until: t + secs });
const what = step === Step.ALL_RED ? "all red" : `${phase.name}: ${step.toLowerCase()}`;
this.say(t, `${what} ${secs} s`);
}
requestWalkAt(crossing, now) {
this.advanceTo(now);
if (this.mode === Mode.FLASH) return false; // every approach is stopping anyway
if (this.crossings.request(crossing)) this.say(now, `crossing ${crossing}: requested`);
if (this.step === Step.GREEN) this.serveWalk(now);
return true;
}
preemptAt(approach, now) {
this.advanceTo(now);
if (this.mode === Mode.FLASH) return false;
this.target = approach;
this.say(now, `emergency vehicle from ${approach}`);
if (this.step === Step.GREEN) {
if (this.plan.phases[this.index].greens.has(approach)) {
this.mode = Mode.PREEMPT;
this.until = now + this.plan.maxPreempt; // hold this green
} else {
this.until = now; // end this green now: yellow and all red still run in full
}
}
this.advanceTo(now);
return true;
}
clearPreemptAt(now) {
this.advanceTo(now);
if (this.target === null) return;
const holding = this.mode === Mode.PREEMPT && this.step === Step.GREEN && this.plan.phases[this.index].greens.has(this.target);
this.target = null;
if (this.mode === Mode.PREEMPT) this.mode = Mode.NORMAL;
if (holding) this.until = now;
this.say(now, "emergency vehicle clear");
this.advanceTo(now);
}
faultAt(t, reason) {
this.mode = Mode.FLASH;
this.target = null;
this.box.flash();
this.crossings.reset();
this.say(t, `FAULT ${reason}: all approaches flashing red`);
}
recoverAt(now) {
if (this.mode !== Mode.FLASH) return;
this.mode = Mode.NORMAL;
this.index = this.plan.phases.length - 1;
this.say(now, "recovered: restarting the cycle");
this.enter(Step.ALL_RED, this.index, now, this.plan.phases[this.index].allRed); // all red first, then phase 0
this.advanceTo(now);
}
}Read _advance first: it is the whole machine. It takes whichever is due first, a pedestrian timer or the end of the step, and stops when the next event is in the future. _finish_step is the transition table: green becomes yellow, yellow becomes all red, all red chooses the next phase. _next_phase is the only place that decides who goes next: the target of an emergency vehicle if there is one, otherwise the next phase in the plan. The override is therefore one if in one place, and the yellow and all-red steps are shared by both paths.
Public calls take one lock and notify listeners after releasing it, so a slow log writer can't delay a transition, and two sensors can't interleave halfway through one.
The code#
The five parts above are one program. Here is a scenario on a plan of two phases, North-South green for 30 s and East-West for 20 s, run through a pedestrian request, an ambulance, a fault and a recovery.
# part 5: a signal for a crossroads, over a few minutes
plan = Plan((
Phase("NS", frozenset({Approach.N, Approach.S}), green=30, crossing="ns"),
Phase("EW", frozenset({Approach.E, Approach.W}), green=20, crossing="ew"),
))
try:
Phase("bad", frozenset({Approach.N, Approach.E}), green=10, crossing="x")
except ValueError as e:
print("rejected:", e)
signal = Controller(plan)
signal.listeners.append(lambda t, text: print(f" t={t:>3} {text}"))
signal.advance(0)
print(signal.status())
signal.request_walk("ns", 10)
signal.request_walk("ns", 11)
signal.request_walk("ns", 40)
signal.advance(61)
print("t=61:", signal.status())
signal.advance(62)
signal.preempt(Approach.E, 70)
signal.clear_preempt(95)
signal.advance(105)
try:
signal.box.show({Approach.N: Light.GREEN, Approach.E: Light.GREEN})
except ConflictError as e:
print("monitor:", e)
signal.fault("controller watchdog", 110)
print("t=110:", signal.status())
print("walk request while flashing:", signal.request_walk("ew", 115))
signal.recover(130)
signal.advance(140)
print("t=140:", signal.status())// part 5: a signal for a crossroads, over a few minutes
public class Main {
public static void main(String[] args) {
Plan plan = new Plan(List.of(
new Phase("NS", Set.of(Approach.N, Approach.S), 30, "ns"),
new Phase("EW", Set.of(Approach.E, Approach.W), 20, "ew")));
try {
new Phase("bad", Set.of(Approach.N, Approach.E), 10, "x");
} catch (IllegalArgumentException e) {
System.out.println("rejected: " + e.getMessage());
}
Controller signal = new Controller(plan, 0);
signal.listeners.add((t, text) -> System.out.printf(" t=%3d %s%n", t, text));
signal.advance(0);
System.out.println(signal.status());
signal.requestWalk("ns", 10);
signal.requestWalk("ns", 11);
signal.requestWalk("ns", 40);
signal.advance(61);
System.out.println("t=61: " + signal.status());
signal.advance(62);
signal.preempt(Approach.E, 70);
signal.clearPreempt(95);
signal.advance(105);
try {
signal.box.show(new EnumMap<>(Map.of(Approach.N, Light.GREEN, Approach.E, Light.GREEN, Approach.S, Light.RED, Approach.W, Light.RED)));
} catch (ConflictError e) {
System.out.println("monitor: " + e.getMessage());
}
signal.fault("controller watchdog", 110);
System.out.println("t=110: " + signal.status());
System.out.println("walk request while flashing: " + (signal.requestWalk("ew", 115) ? "True" : "False"));
signal.recover(130);
signal.advance(140);
System.out.println("t=140: " + signal.status());
}
}// part 5: a signal for a crossroads, over a few minutes
int main() {
Plan plan({Phase("NS", {Approach::N, Approach::S}, 30, "ns"), Phase("EW", {Approach::E, Approach::W}, 20, "ew")});
try {
Phase("bad", {Approach::N, Approach::E}, 10, "x");
} catch (const std::invalid_argument& e) {
std::cout << "rejected: " << e.what() << "\n";
}
Controller signal(plan, 0);
signal.listeners.push_back([](int t, const std::string& text) { std::cout << " t=" << std::setw(3) << t << " " << text << "\n"; });
signal.advance(0);
std::cout << signal.status() << "\n";
signal.requestWalk("ns", 10);
signal.requestWalk("ns", 11);
signal.requestWalk("ns", 40);
signal.advance(61);
std::cout << "t=61: " << signal.status() << "\n";
signal.advance(62);
signal.preempt(Approach::E, 70);
signal.clearPreempt(95);
signal.advance(105);
try {
signal.box.show({{Approach::N, Light::Green}, {Approach::E, Light::Green}, {Approach::S, Light::Red}, {Approach::W, Light::Red}});
} catch (const ConflictError& e) {
std::cout << "monitor: " << e.what() << "\n";
}
signal.fault("controller watchdog", 110);
std::cout << "t=110: " << signal.status() << "\n";
std::cout << "walk request while flashing: " << (signal.requestWalk("ew", 115) ? "True" : "False") << "\n";
signal.recover(130);
signal.advance(140);
std::cout << "t=140: " << signal.status() << "\n";
}// part 5: a signal for a crossroads, over a few minutes
const plan = new Plan([new Phase("NS", ["N", "S"], 30, "ns"), new Phase("EW", ["E", "W"], 20, "ew")]);
try {
new Phase("bad", ["N", "E"], 10, "x");
} catch (e) {
console.log("rejected:", e.message);
}
const signal = new Controller(plan);
signal.listeners.push((t, text) => console.log(` t=${String(t).padStart(3)} ${text}`));
signal.advance(0);
console.log(signal.status());
signal.requestWalk("ns", 10);
signal.requestWalk("ns", 11);
signal.requestWalk("ns", 40);
signal.advance(61);
console.log("t=61:", signal.status());
signal.advance(62);
signal.preempt("E", 70);
signal.clearPreempt(95);
signal.advance(105);
try {
signal.box.show({ N: Light.GREEN, E: Light.GREEN, S: Light.RED, W: Light.RED });
} catch (e) {
console.log("monitor:", e.message);
}
signal.fault("controller watchdog", 110);
console.log("t=110:", signal.status());
console.log("walk request while flashing:", signal.requestWalk("ew", 115) ? "True" : "False");
signal.recover(130);
signal.advance(140);
console.log("t=140:", signal.status());rejected: phase bad: N conflicts with another green
t= 0 NS: green 30 s
NORMAL GREEN | N:GREEN E:RED S:GREEN W:RED | ns:DONT_WALK ew:DONT_WALK
t= 10 crossing ns: requested
t= 10 crossing ns: WALK
t= 17 crossing ns: FLASHING_DONT_WALK
t= 27 crossing ns: DONT_WALK
t= 30 NS: yellow 4 s
t= 34 all red 2 s
t= 36 EW: green 20 s
t= 40 crossing ns: requested
t= 56 EW: yellow 4 s
t= 60 all red 2 s
t=61: NORMAL ALL_RED | N:RED E:RED S:RED W:RED | ns:DONT_WALK ew:DONT_WALK
t= 62 NS: green 30 s
t= 62 crossing ns: WALK
t= 69 crossing ns: FLASHING_DONT_WALK
t= 70 emergency vehicle from E
t= 70 NS: yellow 4 s
t= 74 all red 2 s
t= 76 crossing ns: DONT_WALK
t= 76 EW: green 60 s
t= 95 emergency vehicle clear
t= 95 EW: yellow 4 s
t= 99 all red 2 s
t=101 NS: green 30 s
monitor: N and E would both be live
t=110 FAULT controller watchdog: all approaches flashing red
t=110: FLASH | N:FLASHING_RED E:FLASHING_RED S:FLASHING_RED W:FLASHING_RED | ns:DONT_WALK ew:DONT_WALK
walk request while flashing: False
t=130 recovered: restarting the cycle
t=130 all red 2 s
t=132 NS: green 30 s
t=140: NORMAL GREEN | N:GREEN E:RED S:GREEN W:RED | ns:DONT_WALK ew:DONT_WALKFollow it through. The cycle starts at t=0 with North-South green. At t=10 a pedestrian presses the button: 20 s of green remain and the window needs 17, so the walk starts at once, flashes from 17 and ends at 27, three seconds before the yellow. The second press at t=11 does nothing, and the press at t=40 (during East-West green) waits 22 s for the next North-South green at t=62, where the walk begins together with the green. At t=70 an ambulance asks from the east: North-South ends its green early, the pedestrians still crossing get flashing don't walk until t=76, yellow runs 70 to 74 and all red 74 to 76, and East-West gets a held green from t=76: 6 s after the request. At t=95 the ambulance is clear and the cycle resumes at North-South at t=101 after another yellow and all-red. A watchdog fault at t=110 puts everything on flashing red; a button press during the fault is refused. Recovery at t=130 goes through 2 s of all red and then restarts at North-South green at t=132.
Step 6: Concurrency and edge cases#
Sensors, buttons and the clock all call in, on different threads. Every public method takes the controller's single lock, runs to completion, collects its events, and calls the listeners only after releasing the lock. The state machine itself is single-threaded by construction.
- Two conflicting greens in a plan: the phase refuses to exist; the monitor is the second line of defence
- The ambulance request arrives during yellow or all red: the clearance continues, and the all-red step picks the emergency phase
- The ambulance is already on the green road: hold that green; nothing else changes
- The ambulance is cleared before it was served: drop the target; the cycle carries on in order
- A pedestrian presses mid-green with too little time left: wait for the next green of that road, which is stretched to fit the window
- A green ends while someone is still crossing: they get flashing don't walk through the yellow and all red
- A fault during any step: all flashing red at once, pending walks cleared, preempt dropped; recovery starts from all red
advancecalled with a large jump: every due transition runs in order, so the result matches calling it each second
Step 7: Follow-ups and the interview checklist#
- "Different timings at rush hour." A plan is data, so make the controller ask a
PlanSelector(a Strategy) for the plan when a cycle starts. Time-of-day is one selector; sensor-driven green extension is another. - "Vehicle detectors: extend the green while cars keep arriving." Add a
min_greenandmax_greento a phase, and let_finish_stepask "is there demand on the green approaches?" before leaving GREEN. The yellow and all-red steps are untouched. - "A left-turn arrow." A new phase whose greens are a protected movement. The conflict table needs a movement, not an approach, as its key. Say that.
- "Two intersections that must be coordinated (a green wave)." Give each controller an offset from a shared clock: the start time is a parameter, and
advancealready takes the time. Coordination is then configuration, not code. - "An all-red start-up after power returns."
recoveralready does it: all red for a clearance, then phase 0. - "Preempt from two directions at once." Keep the most recent request, or queue them by priority. Say which, and what happens to the first vehicle.
Interview checklist#
Tick these off before you say done.
- I asked which movements conflict, and wrote the conflict table once, in one place.
- I modelled a phase as a set of approaches with its timings, and made timings configuration, not code.
- I rejected an impossible phase at construction, and put a conflict monitor between the controller and the lamps.
- I used a
Step(green, yellow, all red) and aMode(normal, preempt, flash), with one transition method. - I passed the time into every call and processed due transitions in order, with no sleep and no wall clock.
- I served a pedestrian request only when the whole walk window fits, and made a repeated press harmless.
- I made the emergency override change the next phase, not the clearance: yellow and all red still run in full.
- I made the fault state all-way flashing red, and recovery start from all red.
- I held one lock per public call and notified listeners after releasing it.
- I ran a printed scenario that includes a pedestrian, an emergency vehicle and a fault.
Common mistakes#
sleep()between lights: the demo takes a minute and can't be tested- Timings hard-coded in the controller, so a new plan is a code change
- The emergency override jumps straight to green: it skips yellow and all red, and creates the crash the system exists to prevent
- The fault leaves the lamps as they were: a stuck green is not a safe state
- A pedestrian window that can run into the yellow: people in the road as the cross traffic starts
- No second check: the only safety rule is a comment in the controller
- A class per light colour, and a
switchin the controller anyway
Traffic signal
Q1/3 An ambulance asks for green from the east while north-south is green. What does a safe controller do?
Answer & explanation
Answer: B
The override changes which phase comes next, not the clearance. With 4 s of yellow and 2 s of all red the ambulance waits 6 s.
Q2/3 Why is the conflict rule checked both in Phase and in Intersection.show?
Answer & explanation
Answer: C
Defence in depth: configuration is validated when it is created, and the output is validated every time it changes.
Q3/3 What state should the intersection be in after a fault?
Answer & explanation
Answer: C
A stuck green is dangerous and a dark junction is invisible. Flashing red tells every driver to stop and look.
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


