-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathextras_Stateful.lua
More file actions
291 lines (230 loc) · 10.1 KB
/
Copy pathextras_Stateful.lua
File metadata and controls
291 lines (230 loc) · 10.1 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
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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
require('middleclass')
require('middleclass-extras')
context( 'Stateful', function()
context('A stateful class', function()
local Warrior = class('Warrior'):include(Stateful)
local WarriorIddle, WarriorWalking
context('When adding a new state', function()
test('should not throw errors', function()
assert_not_error(function() WarriorIddle = Warrior:addState('Iddle') end)
function WarriorIddle:speak() return 'iddle' end
end)
test('should return the existing state if it already exists', function()
assert_equal(Warrior:addState('Iddle'), WarriorIddle)
assert_equal(Warrior:addState('Iddle'), Warrior.states.Iddle)
end)
test('should work with superstates', function()
assert_not_error(function()
WarriorWalking = Warrior:addState('Walking', WarriorIddle)
end)
function WarriorWalking:walk() return 'tap tap tap' end
local novita = Warrior:new()
novita:gotoState('Walking')
assert_equal(novita:speak(), 'iddle') -- inherited from Warrioriddle
assert_equal(novita:walk(), 'tap tap tap')
assert_true(subclassOf(WarriorIddle, WarriorWalking))
end)
end)
context('When subclassing', function()
local Vehicle = class('Vehicle'):include(Stateful)
Vehicle:addState('Parked')
function Vehicle.states.Parked:getStatus() return 'stopped' end
local Tank = class('Tank', Vehicle)
test('The subclass should inherit the superclass states', function()
assert_true(subclassOf(Vehicle.states.Parked, Tank.states.Parked))
panzer = Tank:new()
panzer:gotoState('Parked')
assert_equal(panzer:getStatus(), 'stopped')
end)
test('includes() should still work', function()
assert_true( includes(Stateful, Tank) )
assert_false( includes(Branchy, Tank) )
end)
end)
context('When including a mixin', function()
local Class1 = class('Class1'):include(Stateful)
local State1 = Class1:addState('State1')
function State1:foo() return 'state1' end
local Mixin = {
included = function(mixin, theClass) theClass.includesMixin = true end,
foo = function() return 'mixin' end,
states = {
State1 = {
bar = function() return 'bar' end
},
State2 = {
foo = function() return 'state2' end
}
}
}
Class1:include(Mixin)
local obj = Class1:new()
test('should invoke the "included" method when included', function()
assert_true(Class1.includesMixin)
end)
test('should have all its functions (except "included") copied to its target class', function()
assert_equal(Class1:foo(), 'mixin')
assert_equal(Class1.included, nil)
end)
test('should have new states', function()
assert_true(subclassOf(Stateful.State, Class1.states.State2))
obj:gotoState('State2')
assert_equal(obj:foo(), 'state2')
end)
test('existing states should be modified', function()
assert_true(includes(Mixin.states.State1, Class1.states.State1))
obj:gotoState('State1')
assert_equal(obj:foo(), 'state1')
assert_equal(obj:bar(), 'bar')
end)
end)
end)
context('A stateful instance', function()
local Enemy = class('Enemy'):include(Stateful)
function Enemy:getStatus() return 'none' end
local EnemyIddle = Enemy:addState('Iddle')
function EnemyIddle:enterState() self.enteredIddle = true end
function EnemyIddle:getStatus() return 'iddling' end
local Goblin = class('Goblin', Enemy)
local GoblinIddle = Goblin:addState('Iddle')
function GoblinIddle:getStatus() return 'me bored boss' end
function GoblinIddle:exitState() self.exitedIddle = true end
function GoblinIddle:pausedState() self.pausedIddle = true end
function GoblinIddle:poppedState() self.poppedIddle = true end
function GoblinIddle:continuedState() self.continuedIddle = true end
local GoblinAttacking = Goblin:addState('Attacking')
function GoblinAttacking:pushedState() self.pushedAttacking = true end
function GoblinAttacking:enterState() self.enteredAttacking = true end
function GoblinAttacking:shout() return 'gnaaa!' end
function GoblinAttacking:poppedState() self.poppedAttacking = true end
context('When it goes from one state to another', function()
local albert = Enemy:new()
albert:gotoState('Iddle')
local chester = Goblin:new()
chester:gotoState('Iddle')
test('should not throw an error for a valid state name', function()
assert_not_error(function() chester:gotoState('Attacking') end)
assert_equal(chester:shout(), 'gnaaa!')
end)
test('should throw an error for an invalid state name', function()
assert_error(function() albert:gotoState('Sleeping') end)
end)
test('should be able to go to the nil-state', function()
assert_not_error(function()
albert:gotoState(nil)
end)
assert_equal(albert:getStatus(), 'none')
end)
test('enterState callbacks should be called, if existing', function()
assert_true(albert.enteredIddle)
assert_true(chester.enteredIddle)
end)
test('exitState callbacks should be called, if existing', function()
assert_nil(albert.exitedIddle)
assert_true(chester.exitedIddle)
end)
end)
context('When pushing states', function()
local andrew = Enemy:new()
local ian = Goblin:new()
test('should error if the statename is invalid', function()
assert_error(function() andrew:pushState(nil) end)
assert_error(function() andrew:pushState('Sleeping') end)
end)
test('should correctly push valid states, without errors', function()
assert_not_error(function() ian:pushState('Iddle') end)
assert_not_error(function() ian:pushState('Attacking') end)
assert_equal(ian:shout(), 'gnaaa!')
assert_equal(ian:getStatus(), 'me bored boss')
end)
test('pushing the same state several times should not throw errors', function()
assert_not_error(function() andrew:pushState('Iddle') end)
assert_not_error(function() andrew:pushState('Iddle') end)
end)
test('enterState callbacks should be called, if existing', function()
assert_true(andrew.enteredIddle)
assert_true(ian.enteredIddle)
end)
test('pushedState callbacks should be called, if existing', function()
assert_nil(andrew.pushedAttacking)
assert_true(ian.pushedAttacking)
end)
test('pausedState callbacks should be called, if existing', function()
assert_nil(andrew.pausedIddle)
assert_true(ian.pausedIddle)
end)
end)
context('When popping states', function()
local renfield = Goblin:new()
-- reset vlad and renfield before each test
before(function()
renfield = Goblin:new()
end)
test('should be able to pop states by name, calling callbacks', function()
renfield:pushState('Iddle')
renfield:pushState('Attacking')
assert_not_error(function() renfield:popState('Iddle') end)
assert_equal(renfield:getStatus(), 'none')
assert_equal(renfield:shout(), 'gnaaa!')
assert_true(renfield.poppedIddle)
assert_true(renfield.exitedIddle)
end)
test('should be able to pop the top state, with appropiate callbacks', function()
renfield:pushState('Iddle')
renfield:pushState('Attacking')
assert_not_error(function() renfield:popState() end)
assert_equal(renfield:getStatus(), 'me bored boss')
assert_error(function() renfield:shout() end)
assert_true(renfield.poppedAttacking)
assert_true(renfield.continuedIddle)
end)
test('popping the same state several times should not throw errors, and call no callbacks', function()
renfield:pushState('Iddle')
renfield:popState('Iddle')
renfield.poppedIddle = nil
assert_not_error(function() renfield:popState('Iddle') end)
assert_nil(renfield.poppedIddle)
end)
test('popping inexistant states should not throw errors', function()
assert_not_error(function() renfield:popState('Foo') end)
end)
test('popAllStates should work correctly and invoke all required callbacks', function()
renfield:pushState('Iddle')
renfield:pushState('Attacking')
assert_not_error(function() renfield:popAllStates() end)
assert_true(renfield.poppedAttacking)
assert_true(renfield.continuedIddle)
assert_true(renfield.poppedIddle)
assert_true(renfield.exitedIddle)
end)
end)
context('When testing whether it is on one state', function()
local igor = Goblin:new()
test('should return true if the state is on the top of the stack', function()
igor:gotoState('Iddle')
assert_true(igor:isInState('Iddle'))
end)
test('should return true if the state is on the stack and "testStateStack" is true', function()
igor:pushState('Attacking')
assert_true(igor:isInState('Attacking'))
assert_true(igor:isInState('Iddle', true))
end)
test('should return false otherwise', function()
assert_false(igor:isInState(nil))
assert_false(igor:isInState('Foo', true))
end)
end)
context('When getting the current state name', function()
local peppy = Goblin:new()
test('should return nil when on nil-state', function()
assert_nil(peppy:getCurrentStateName())
end)
test('should return the top-of-the-stack statename otherwise', function()
peppy:pushState('Iddle')
assert_equal(peppy:getCurrentStateName(), 'Iddle')
peppy:pushState('Attacking')
assert_equal(peppy:getCurrentStateName(), 'Attacking')
end)
end)
end) -- context 'An Instance'
end)