/
init.lua
191 lines (143 loc) · 4.68 KB
/
init.lua
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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
local Option = require(script.Parent.Option)
type Event = {[string]: string | () -> string?}
type FlowMap = {[string]: Event}
type LockLayers = {[string]: number}
local function Assert(eval, genMsg: () -> string, level: number?)
if not eval then
error(genMsg(), level)
end
return eval
end
local function FreshLockLayers(flowMap: FlowMap): LockLayers
local lockLayers = {}
for eventName in flowMap do
lockLayers[eventName] = 0
end
return lockLayers
end
--[=[
@class StateMachine
An immutable class for handling the state of things, where the design is a copy of Rust's sm crate, but with a few additions and changes.
```lua
-- Returns a function that constructs an object of the StateMachine class based on the control flow map provided.
local Lock = StateMachine {
TurnKey = {
Locked = 'Unlocked',
Unlocked = 'Locked',
},
Break = {
Locked = 'Broken',
Unlocked = 'Broken',
},
}
-- Starts the machine on the "Locked" state.
local lock = Lock('Locked')
lock = lock:transition('TurnKey')
assert(lock:State(), 'Unlocked')
assert(lock:Trigger():Unwrap(), 'TurnKey')
```
]=]
local StateMachine = {}
StateMachine.__index = StateMachine
function StateMachine.new(flowMap: FlowMap, initialState: string, _trigger: string?, _lockLayers: LockLayers?)
local self = setmetatable({}, StateMachine)
self._FlowMap = flowMap
self._State = initialState
self._Trigger = _trigger
self._LockLayers = _lockLayers or FreshLockLayers(flowMap)
return table.freeze(self)
end
--[=[
@method transition
@param eventName string
@return StateMachine
@within StateMachine
Returns a new StateMachine with the post-transition state.
]=]
function StateMachine:transition(eventName: string)
local event: Event = self._FlowMap[eventName]
local newState = Assert(event[self._State], function()
return ('The %q event cannot be triggered when on the %q state'):format(eventName, self._State)
end, 3)
Assert(not self:IsLocked(eventName), function()
return ('The %q event is locked'):format(eventName)
end, 3)
return if newState == self._State then
self
else
StateMachine.new(self._FlowMap, newState, eventName, self._LockLayers)
end
--[=[
@method lock
@param eventName string
@return StateMachine
@within StateMachine
Returns a new StateMachine where the event can no longer be triggered.
:::note
Locking and unlocking is layer-based, which means that locking twice results in 2 layers, thus, to actually
unlock the event, you now have to unlock it 2 times.
:::
]=]
function StateMachine:lock(eventName: string)
local newLockLayers = table.clone(self._LockLayers)
newLockLayers[eventName] += 1
return StateMachine.new(self._FlowMap, self._State, self._Trigger, newLockLayers)
end
--[=[
@method unlock
@param eventName string
@return StateMachine
@within StateMachine
Returns a new StateMachine where the event can be triggered, but only if no lock layer remains.
]=]
function StateMachine:unlock(eventName: string)
if self._LockLayers[eventName] == 0 then return end
local newLockLayers = table.clone(self._LockLayers)
newLockLayers[eventName] -= 1
return StateMachine.new(self._FlowMap, self._State, self._Trigger, newLockLayers)
end
--[=[
@method IsLocked
@param eventName string
@return boolean
@within StateMachine
Returns true if there are 1 or more layers of lock for the event.
]=]
function StateMachine:IsLocked(eventName: string): boolean
return self._LockLayers[eventName] ~= 0
end
--[=[
@method State
@return string
@within StateMachine
Returns the state of the machine.
]=]
function StateMachine:State(): string
return self._State
end
--[=[
@method Trigger
@return Option<string>
@within StateMachine
Returns the last triggered event wrapped in an option or option.None if no event has been triggerd yet.
Option's API: https://sleitnick.github.io/RbxUtil/api/Option/
]=]
function StateMachine:Trigger()
return Option.Wrap(self._Trigger)
end
--[=[
@method Can
@param eventName string
@return boolean
@within StateMachine
Returns true if the event can be triggered based on the machine's current state.
]=]
function StateMachine:Can(eventName: string): boolean
local event: Event = self._FlowMap[eventName]
return event[self._State] ~= nil and not self:IsLocked(eventName)
end
return function(flowMap: FlowMap)
return function(initialState: string)
return StateMachine.new(flowMap, initialState)
end
end