Step 7 of 7
Challenge: a table-driven state machine
Protocols, UI flows, order pipelines and game rounds are state machines: the thing is always in exactly one state, and events move it to another state, but only along legal paths. An order can go from Paid to Shipped, never from Shipped back to Cart.
Encoding the legal transitions in a table, instead of nested ifs scattered through the code, makes them easy to review (the table is the specification), test and extend.
#include <iostream>
#include <optional>
enum class Door { Closed, Open, Locked };
enum class Action { Open, Close, Lock, Unlock };
struct Transition {
Door from;
Action on;
Door to;
};
constexpr Transition rules[] = {
{Door::Closed, Action::Open, Door::Open},
{Door::Open, Action::Close, Door::Closed},
{Door::Closed, Action::Lock, Door::Locked},
{Door::Locked, Action::Unlock, Door::Closed},
};
std::optional<Door> next(Door d, Action a) {
for (const auto& r : rules) {
if (r.from == d && r.on == a) return r.to;
}
return std::nullopt; // not in the table: illegal
}
const char* name(Door d) {
switch (d) {
case Door::Closed: return "closed";
case Door::Open: return "open";
case Door::Locked: return "locked";
}
return "?";
}
int main() {
Door d = Door::Closed;
for (Action a : {Action::Lock, Action::Open, Action::Unlock, Action::Open}) {
if (auto n = next(d, a)) {
d = *n;
std::cout << "-> " << name(d) << "\n";
} else {
std::cout << "rejected, still " << name(d) << "\n";
}
}
}
-> locked
rejected, still locked
-> closed
-> open
How it works
- Each row of
rulesis one legal move: from this state, on this event, go to that state. Anything not listed is illegal. nextsearches the table and returns the new state, orstd::nulloptfor an illegal event. With a handful of rules, a linear search is perfectly fast.- The caller changes state only when
nextsucceeds, so an illegal event leaves everything as it was. - Adding a new rule is one more line in the table. No logic changes.
Your task
Write the six transitions from the table below as a constexpr Transition array, and next as above. In Match::handle, call next(state_, e); on success, update state_, increment accepted_ and return true; otherwise return false without changing anything.
Your turn: implement a match lifecycle with these transitions:
| From | Event | To |
|---|---|---|
| Idle | Start | Warmup |
| Warmup | GoLive | Live |
| Live | Pause | Paused |
| Paused | Resume | Live |
| Live | End | Over |
| Paused | End | Over |
Write std::optional<State> next(State s, Event e) using a table, and class Match whose bool handle(Event e) applies a legal event (returning true) or rejects an illegal one (returning false, state unchanged). state() returns the current state and history() the number of accepted events.