-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstop_machine.py
More file actions
103 lines (80 loc) · 3.02 KB
/
stop_machine.py
File metadata and controls
103 lines (80 loc) · 3.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# CANONICAL SOURCE: LalaSkye/constraint-workshop/stop_machine.py
# PINNED COMMIT: 8d04cbc1e8e6576641962d5d3c866b0517ad596e
# Semantics aligned to canonical: RED is terminal, reset() blocked from RED.
"""A deterministic three-state stop controller.
States: GREEN -> AMBER -> RED
RED is terminal. No implicit transitions.
"""
from enum import Enum, unique
@unique
class State(Enum):
"""The three possible states of the stop machine."""
GREEN = "GREEN"
AMBER = "AMBER"
RED = "RED"
# Explicit transition table. Maps (current_state) -> allowed next state.
# RED has no entry because it is terminal.
_TRANSITIONS = {
State.GREEN: State.AMBER,
State.AMBER: State.RED,
}
class TerminalStateError(Exception):
"""Raised when a transition is attempted from a terminal state."""
class InvalidTransitionError(Exception):
"""Raised when a transition targets a state not permitted."""
class StopMachine:
"""A three-state stop controller.
Starts at a given state (default GREEN).
Transitions are explicit and deterministic.
RED is terminal: no further transitions are allowed.
"""
def __init__(self, initial: State = State.GREEN) -> None:
self._state = initial
@property
def state(self) -> State:
"""Return the current state."""
return self._state
@property
def is_terminal(self) -> bool:
"""Return True if the machine is in a terminal state."""
return self._state == State.RED
def advance(self) -> State:
"""Move to the next state in the sequence.
Raises TerminalStateError if already RED.
Returns the new state.
"""
if self.is_terminal:
raise TerminalStateError(
f"Cannot advance: {self._state.value} is terminal."
)
self._state = _TRANSITIONS[self._state]
return self._state
def transition_to(self, target: State) -> State:
"""Transition to a specific target state.
Only the immediate next state in the sequence is allowed.
Raises TerminalStateError if already RED.
Raises InvalidTransitionError if target is not the next state.
Returns the new state.
"""
if self.is_terminal:
raise TerminalStateError(
f"Cannot transition: {self._state.value} is terminal."
)
expected = _TRANSITIONS[self._state]
if target != expected:
raise InvalidTransitionError(
f"Cannot transition from {self._state.value} to "
f"{target.value}. Expected {expected.value}."
)
self._state = target
return self._state
def reset(self) -> State:
"""Reset the machine to GREEN. Forbidden once RED is reached."""
if self.is_terminal:
raise TerminalStateError(
f"Cannot reset: {self._state.value} is terminal."
)
self._state = State.GREEN
return self._state
def __repr__(self) -> str:
return f"StopMachine(state={self._state.value})"