diff --git a/middleclass/MiddleClass.lua b/middleclass/MiddleClass.lua index b4e7e5e..6ff5477 100644 --- a/middleclass/MiddleClass.lua +++ b/middleclass/MiddleClass.lua @@ -118,8 +118,8 @@ function includes(module, aClass) return includes(module, aClass.superclass) end --- Creates a new class named 'name'. It uses baseClass as the parent (Object if none specified) -function class(name, baseClass) +-- Creates a new class named 'name'. Uses Object if no baseClass is specified. Additional parameters for compatibility +function class(name, baseClass, ...) baseClass = baseClass or Object - return baseClass:subclass(name) + return baseClass:subclass(name, ...) end diff --git a/middleclass/MindState.lua b/middleclass/MindState.lua index ff233c2..81b03e5 100644 --- a/middleclass/MindState.lua +++ b/middleclass/MindState.lua @@ -52,12 +52,9 @@ local _ignoredMethods = { states=1, initialize=1, gotoState=1, pushState=1, popState=1, popAllStates=1, isInState=1, enterState=1, exitState=1, pushedState=1, poppedState=1, pausedState=1, continuedState=1, - addState=1, subclass=1, includes=1, destroy=1, getCurrentStateName=1 + addState=1, subclass=1, include=1, destroy=1, getCurrentStateName=1 } -local _prevSubclass = StatefulObject.subclass -- previous way of creating subclasses (used to redefine subclass itself) - - ------------------------------------ -- STATE CLASS ------------------------------------ @@ -65,11 +62,17 @@ local _prevSubclass = StatefulObject.subclass -- previous way of creating subcla -- The State class; is the father of all State objects State = class('State', Object) -function State.subclass(theClass, name, theStatefulClass) +-- subclass takes an extra parameter: theRootClass the class where the state is being added +-- It is used for method lookup +function State.subclass(theClass, name, theRootClass) + assert(type(name) == 'string', "Must provide a name for the new state") + assert(subclassOf(StatefulObject, theRootClass), "Must provide a stateful object subclass") + local theSubClass = Object.subclass(theClass, name) - local superDict = (theClass==State and theClass.__classDict or theStatefulClass.superclass.__classDict) + local superDict = (theClass==State and theRootClass.superclass.__classDict or theClass.__classDict ) theSubClass.subclass = State.subclass + -- Modify super so it points to either the SuperState or RootClass if we are subclassing State local mt = getmetatable(theSubClass) mt.__newindex = function(_, methodName, method) if type(method) == 'function' then @@ -253,11 +256,11 @@ end ]] function StatefulObject.addState(theClass, stateName, superState) superState = superState or State - --print(theClass.name, stateName, superState.name) + assert(subclassOf(StatefulObject, theClass), "Use class:addState instead of class.addState") assert(type(stateName)=="string", "stateName must be a string") - local prevState = theClass.states[stateName] + local prevState = rawget(theClass.states, stateName) if prevState~=nil then return prevState end -- states are just regular classes. If superState is nil, this uses Object as superClass @@ -274,11 +277,11 @@ end function StatefulObject.subclass(theClass, name) assert(theClass==StatefulObject or subclassOf(StatefulObject, theClass), "Use class:subclass instead of class.subclass") - local theSubClass = _prevSubclass(theClass, name) --for now, theClass is just a regular subclass + local theSubClass = Object.subclass(theClass, name) --for now, theClass is just a regular subclass --the states of the subclass are subclasses of the superclass' states theSubClass.states = {} - for stateName,state in pairs(theClass.states) do + for stateName,state in pairs(theClass.states) do theSubClass:addState(stateName, state) end @@ -307,19 +310,19 @@ end then each member of that module.states is included on the StatefulObject class. If module.states has a state that doesn't exist on StatefulObject, a new state will be created. ]] -function StatefulObject.includes(theClass, module, ...) +function StatefulObject.include(theClass, module, ...) assert(subclassOf(StatefulObject, theClass), "Use class:includes instead of class.includes") for methodName,method in pairs(module) do if methodName ~="included" and methodName ~= "states" then theClass[methodName] = method end end - if type(module.included)=="function" then module.included(theClass, ...) end + if type(module.included)=="function" then module:included(theClass, ...) end if type(module.states)=="table" then for stateName,moduleState in pairs(module.states) do local state = theClass.states[stateName] if state == nil then state = theClass:addState(stateName) end - state:includes(moduleState, ...) + state:include(moduleState, ...) end end end diff --git a/spec/StatefulObject_spec.lua b/spec/StatefulObject_spec.lua index af45add..0c2c8a3 100644 --- a/spec/StatefulObject_spec.lua +++ b/spec/StatefulObject_spec.lua @@ -3,29 +3,117 @@ require('MindState') context( 'StatefulObject', function() - local Enemy = class('Enemy', StatefulObject) - function Enemy:getStatus() return 'none' end - local EnemyIddle = Enemy:addState('Iddle') - function EnemyIddle:enterState() self.enteredIddle = true end - function EnemyIddle:getStatus() return 'iddling' end + context('A State', function() + test('it should require 3 parameters when subclassed', function() + assert_error(function() State:subclass() end) + assert_error(function() State:subclass('meh') end) + end) + + test('Super calls should work correctly', function() + local SuperClass = class('SuperClass', StatefulObject) + function SuperClass:foo() return 'foo' end + + local RootClass = class('RootClass', SuperClass) - 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 + local State1 = RootClass:addState('State1') + function State1:foo() return(super.foo(self) .. 'state1') end - context('An instance', function() + local State2 = RootClass:addState('State2', State1) + function State2:foo() return(super.foo(self) .. 'state2') end + + local obj = RootClass:new() + + obj:gotoState('State1') + assert_equal(obj:foo(), 'foostate1') + + obj:gotoState('State2') + assert_equal(obj:foo(), 'foostate1state2') + end) + + end) + + context('A stateful class', function() + local Warrior = class('Warrior', StatefulObject) + local WarriorIddle, WarriorWalking + + context('When adding a new state', function() + + test('it should not throw errors', function() + assert_not_error(function() WarriorIddle = Warrior:addState('Iddle') end) + function WarriorIddle:speak() return 'iddle' end + end) + + test('it returns an existing state if it already exists', function() + assert_equal(Warrior:addState('Iddle'), WarriorIddle) + assert_equal(Warrior:addState('Iddle'), Warrior.states.Iddle) + end) + + test('it 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', StatefulObject) + 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) + end) + + context('When instantiating', function() + -- pending + end) + + end) + + context('A stateful instance', function() + + local Enemy = class('Enemy', StatefulObject) + 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() @@ -179,13 +267,10 @@ context( 'StatefulObject', function() end) -- context 'An Instance' - context('A mixin on a stateful object', function() - -- pending - end) - - context('A State', function() + context('A mixin included on a stateful object', function() -- pending end) + end)