1require 'set'
2
3# Represents a state in the state machine.
4class State
5attr_reader :name
6
7def initialize(name)
8@name = name
9end
10
11def to_s
12@name.to_s
13end
14end
15
16# Represents a transition between two states.
17class Transition
18attr_reader :from_state, :to_state, :event, :guard
19
20def initialize(from_state, to_state, event, guard = nil)
21@from_state = from_state
22@to_state = to_state
23@event = event
24@guard = guard # A lambda or proc that returns true/false
25end
26
27# Checks if this transition is valid given the current context.
28def can_transition?(context)
29@guard.nil? || @guard.call(context)
30end
31end
32
33# The state machine itself.
34class StateMachine
35attr_reader :current_state
36attr_reader :states
37attr_reader :transitions
38
39def initialize(initial_state_name, states_data)
40@states = states_data.each_with_object({}) { |(name, _), obj| obj[name] = State.new(name) }
41@initial_state = @states[initial_state_name]
42@current_state = @initial_state
43@transitions = []
44@context = {}
45end
46
47# Adds a transition to the state machine.
48def add_transition(from_state_name, to_state_name, event, guard = nil)
49from = @states[from_state_name]
50to = @states[to_state_name]
51raise ArgumentError, "Invalid state name" unless from && to
52
53@transitions << Transition.new(from, to, event, guard)
54end
55
56# Sets the context for guard evaluation.
57def set_context(context_hash)
58@context.merge!(context_hash)
59end
60
61# Triggers an event, attempting to transition the state machine.
62# Args:
63# event: The event that occurred.
64# Returns:
65# True if a transition occurred, false otherwise.
66def trigger(event)
67possible_transitions = @transitions.select do |t|
68t.from_state == @current_state && t.event == event
69end
70
71valid_transition = possible_transitions.find do |t|
72t.can_transition?(@context)
73end
74
75if valid_transition
76old_state = @current_state
77@current_state = valid_transition.to_state
78puts "Transitioned from #{old_state} to #{@current_state} on event '#{event}'."
79true
80else
81puts "Could not transition from #{@current_state} on event '#{event}'."
82false
83end
84end
85
86# Checks if a transition is possible for a given event.
87def can_transition?(event)
88@transitions.any? do |t|
89t.from_state == @current_state && t.event == event && t.can_transition?(@context)
90end
91end
92
93def to_s
94"#{@current_state}"
95end
96end
97
98# --- Example Usage ---
99
100# Define states for a simple order processing system
101states = {
102pending: nil,
103processing: nil,
104shipped: nil,
105delivered: nil,
106cancelled: nil
107}
108
109sm = StateMachine.new(:pending, states)
110
111# Define transitions
112sm.add_transition(:pending, :processing, :pay)
113sm.add_transition(:pending, :cancelled, :cancel)
114sm.add_transition(:processing, :shipped, :ship)
115sm.add_transition(:processing, :cancelled, :cancel)
116sm.add_transition(:shipped, :delivered, :receive)
117sm.add_transition(:shipped, :cancelled, :return)
118sm.add_transition(:delivered, :cancelled, :return)
119
120# Add a guard: only allow shipping if payment is confirmed
121sm.add_transition(:processing, :shipped, :ship, ->(ctx) { ctx[:payment_confirmed] })
122
123puts "Initial state: #{sm}"
124
125# Example 1: Successful transitions
126sm.trigger(:pay)
127puts "Current state: #{sm}"
128sm.trigger(:ship) # This will fail without payment confirmation
129
130# Set context for guards
131sm.set_context({ payment_confirmed: true })
132sm.trigger(:ship) # Now this should succeed
133puts "Current state: #{sm}"
134sm.trigger(:receive)
135puts "Current state: #{sm}"
136
137# Example 2: Invalid transitions and cancellation
138sm.set_context({ payment_confirmed: false }) # Reset context
139sm.trigger(:cancel)
140puts "Current state: #{sm}"
141
142# Try to trigger an event from an invalid state
143sm.trigger(:pay)
144puts "Current state: #{sm}"