diff --git a/Rakefile b/Rakefile index feb90b2..79a53c7 100644 --- a/Rakefile +++ b/Rakefile @@ -8,6 +8,6 @@ base_dir = Dir.pwd task :default => :test task :test do - lua_load_path = "#{base_dir}/MiddleClass.lua" - sh "tsc -f --load=\"#{lua_load_path}\" test/*.lua" + lua_path_command = "(function() package.path = '#{base_dir}/lib/?.lua;' .. package.path end)()" + sh "tsc -f --before=\"#{lua_path_command}\" test/*.lua" end diff --git a/lib/MiddleClass.lua b/lib/MiddleClass.lua new file mode 100644 index 0000000..df1f8f8 --- /dev/null +++ b/lib/MiddleClass.lua @@ -0,0 +1,125 @@ +----------------------------------------------------------------------------------- +-- MiddleClass.lua +-- Enrique García ( enrique.garcia.cota [AT] gmail [DOT] com ) - 19 Oct 2009 +-- Based on YaciCode, from Julien Patte and LuaObject, from Sébastien Rocca-Serra +----------------------------------------------------------------------------------- + +local _classes = setmetatable({}, {__mode = "k"}) -- weak table storing references to all declared _classes and their included modules + +Object = { name = "Object" } -- The 'Object' class + +_classes[Object] = { modules={} } -- adds Object to the list of _classes + + -- creates a new instance +Object.new = function(theClass, ...) + assert(_classes[theClass]~=nil, "Use class:new instead of class.new") + + local instance = setmetatable({ class = theClass }, theClass.__classDict) -- the class dictionary is the instance's metatable + instance:initialize(...) + return instance +end + +-- creates a subclass +Object.subclass = function(theClass, name) + assert(_classes[theClass]~=nil, "Use class:subclass instead of class.subclass") + assert( type(name)=="string", "You must provide a name(string) for your class") + + local theSubclass = { name = name, superclass = theClass, __classDict = {} } + local classDict = theSubclass.__classDict + + -- classDict is the instances' metatable. It "points to himself" so they start looking for methods there. + classDict.__index = classDict + + local mt = {__index = theClass.__classDict} + + -- making metamethods "be looked up" as well as regular methods (lua prevents this by default) + for _,m in ipairs({ + '__add', '__sub', '__mul', '__div', '__mod', '__pow', '__unm', '__concat', + '__len', '__eq', '__lt', '__le', '__call', '__gc', '__tostring', '__newindex' + }) do + rawset(mt, m, function(...) return theClass.__classDict[m](...) end) + end + setmetatable(classDict, mt ) + + -- control how the new methods are inserted on the subclass, and how they are looked up + setmetatable(theSubclass, { + __index = function(_,methodName) + local localMethod = classDict[methodName] -- this allows using classDic as a class method AND instance method dict + if localMethod ~= nil then return localMethod end + return theClass[methodName] + end, + -- FIXME add support for __index method here + __newindex = function(_, methodName, method) -- when adding new methods, include a "super" function + if type(method) == 'function' then + local fenv = getfenv(method) + local newenv = setmetatable( {super = theClass.__classDict}, {__index = fenv, __newindex = fenv} ) + setfenv( method, newenv ) + end + rawset(classDict, methodName, method) + end, + __tostring = function() return ("class ".. name) end, + __call = function(_, ...) return theSubclass:new(...) end + }) + -- instance methods go after the setmetatable, so we can use "super" + theSubclass.initialize = function(instance,...) super.initialize(instance) end + + _classes[theSubclass]={ modules={} } --registers the new class on the list of _classes + + theClass:subclassed(theSubclass) -- hook method. By default it does nothing + + return theSubclass +end + +-- Mixin extension function - simulates very basically ruby's include(module) +-- module is a lua table of functions. The functions will be copied to the class +-- if present in the module, the included() method will be called +Object.include = function(theClass, module, ... ) + assert(_classes[theClass]~=nil, "Use class:includes instead of class.includes") + for methodName,method in pairs(module) do + if methodName ~="included" then theClass[methodName] = method end + end + if type(module.included)=="function" then module:included(theClass, ... ) end + _classes[theClass].modules[module] = true +end + +-- built-in methods +Object.__classDict = { + initialize = function(instance, ...) end, -- empty method + destroy = function(instance) end, -- empty method + __tostring = function(instance) return ("instance of ".. instance.class.name) end, + subclassed = function(theClass, other) end -- empty method +} +Object.__classDict.__index = Object.__classDict -- instances of Object need this + +-- This allows doing tostring(obj) and Object() instead of Object:new() +setmetatable(Object, { __index = Object.__classDict, __newindex = Object.__classDict, + __tostring = function() return ("class Object") end, + __call = Object.new +}) + +-- Returns true if aClass is a subclass of other, false otherwise +function subclassOf(other, aClass) + if _classes[aClass]==nil or _classes[other]==nil then return false end + if aClass.superclass==nil then return false end -- aClass is Object, or a non-class + return aClass.superclass == other or subclassOf(other, aClass.superclass) +end + +-- Returns true if obj is an instance of aClass (or one of its subclasses) false otherwise +function instanceOf(aClass, obj) + if _classes[aClass]==nil or type(obj)~='table' or _classes[obj.class]==nil then return false end + if obj.class==aClass then return true end + return subclassOf(aClass, obj.class) +end + +-- Returns true if the a module has already been included on a class (or a superclass of that class) +function included(module, aClass) + if _classes[aClass]==nil or _classes[aClass].modules==nil then return false end + if _classes[aClass].modules[module] then return true end + return included(module, aClass.superclass) +end + +-- Creates a new class named 'name'. It uses baseClass as the parent (Object if none specified) +function class(name, baseClass) + baseClass = baseClass or Object + return baseClass:subclass(name) +end diff --git a/lib/MindState.lua b/lib/MindState.lua new file mode 100644 index 0000000..06392de --- /dev/null +++ b/lib/MindState.lua @@ -0,0 +1,299 @@ +----------------------------------------------------------------------------------- +-- MindState.lua +-- Enrique García ( enrique.garcia.cota [AT] gmail [DOT] com ) - 19 Oct 2009 +-- Based on Unrealscript's stateful objects +----------------------------------------------------------------------------------- + +assert(Object~=nil and class~=nil, 'MiddleClass not detected. Please require it before using MindState') + +--[[ StatefulObject declaration + * Stateful classes have a list of states (accesible through class.states). + * When a method is invoked on an instance of such classes, it is first looked up on the class current state (accesible through class.currentState) + * If a method is not found on the current state, or if current state is nil, the method is looked up on the class itself + * It is possible to change states by doing class:gotoState(stateName) +]] +StatefulObject = class('StatefulObject') + +StatefulObject.states = {} -- the root state list + +------------------------------------ +-- PRIVATE ATTRIBUTES AND METHODS +------------------------------------ +local _private = setmetatable({}, {__mode = "k"}) -- weak table storing private references + +-- helper function used to call state callbacks (enterState, exitState, etc) +local _invokeCallback = function(self, state, callbackName, ... ) + if state==nil then return end + local callback = state[callbackName] + if(type(callback)=='function') then callback(self, ...) end +end + +local _getStack=function(self) + local stack = _private[self].stateStack + assert(stack~=nil, "Could not find the stack for the object. Make sure you invoked super.initialize(self) on the constructor.") + return stack +end + +-- These methods will not be overriden by the states. +local _ignoredMethods = { + states=1, initialize=1, + gotoState=1, pushState=1, popState=1, popAllStates=1, getCurrentState=1, isInState=1, + enterState=1, exitState=1, pushedState=1, poppedState=1, pausedState=1, continuedState=1, + addState=1, subclass=1, includes=1, destroy=1 +} + +local _prevSubclass = StatefulObject.subclass -- previous way of creating subclasses (used to redefine subclass itself) + + +-- The State class; is the father of all State objects +local State = class('State', Object) + +function State.subclass(theClass, name, theStatefulClass) + local theSubClass = Object.subclass(theClass, name) + local superDict = (theClass==State and theClass.__classDict or theStatefulClass.superclass.__classDict) + theSubClass.subclass = State.subclass + + local mt = getmetatable(theSubClass) + mt.__newindex = function(_, methodName, method) + if type(method) == 'function' then + local fenv = getfenv(method) + local newenv = setmetatable( {super = superDict}, {__index = fenv, __newindex = fenv} ) + setfenv( method, newenv ) + end + rawset(theSubClass.__classDict, methodName, method) + end + + return theSubClass +end + +------------------------------------ +-- INSTANCE METHODS +------------------------------------ + +--[[ constructor + If your states need initialization, they can receive parameters via the initParameters parameter + initParameters is a table with parameters used for initializing the states. These are needed mostly if + your states have a custom superclass that needs parameters on their initialize() function. +]] +function StatefulObject:initialize(initParameters) + super.initialize(self) + initParameters = initParameters or {} --initialize to empty table if nil + + _private[self] = { + states = {}, + stateStack = {} + } + + for stateName,stateClass in pairs(self.class.states) do + local state = stateClass:new(unpack(initParameters[stateName] or {})) + state.name = stateName + _private[self].states[stateName] = state + end +end + +--[[ Changes the current state. + If the current state has a method called onExitState, it will be called, with the instance as a parameter. + If the "next" state exists and has a method called onExitState, it will be called, with the instance as a parameter. + use gotoState(nil) for setting states to nothing + This method invokes the exitState and enterState functions if they exist on the current state + Second parameter is optional. If true, the stack will be conserved. Otherwise, it will be popped. +]] +function StatefulObject:gotoState(newStateName, keepStack) + assert(_private[self].states~=nil, "Attribute 'states' not detected. check that you called instance:gotoState and not instance.gotoState, and that you invoked super.initialize(self) in the constructor.") + + local prevState = self:getCurrentState() + + -- If we're trying to go to a state in which we already are, return (do nothing) + if(prevState~=nil and prevState.name == newStateName) then return end + + local nextState + if(newStateName~=nil) then + nextState = _private[self].states[newStateName] + assert(nextState~=nil, "State '" .. newStateName .. "' not found") + end + + -- Either empty completely the stack, or just call the exitstate callback on current state + if(keepStack~=true) then + self:popAllStates() + else + _invokeCallback(self, prevState, 'exitState', newStateName ) + end + + -- replace the top of the stack with the new state + local stack = _getStack(self) + stack[math.max(#stack,1)] = nextState + + -- Invoke enterState on the new state. 2nd parameter is the name of the previous state, or nil + _invokeCallback(self, nextState, 'enterState', prevState~=nil and prevState.name or nil) + +end + +--[[ Changes the current state, by pushing a new state on the stack. + If the pushed state is already on the stack, this function does nothing. + Invokes 'pausedState' on the previous state, if existing + The new state is pushed on the top of the stack and then + Invokes 'pushedState' and 'enterState' on the new state, if existing +]] +function StatefulObject:pushState(newStateName) + assert(type(newStateName)=='string', "newStateName must be a string.") + assert(_private[self].states~=nil, "Attribute 'states' not detected. check that you called instance:pushState and not instance.pushState, and that you invoked super.initialize(self) in the constructor.") + + local nextState = _private[self].states[newStateName] + assert(nextState~=nil, "State '" .. newStateName .. "' not found") + + -- If we attempt to push a state and the state is already on return (do nothing) + local stack = _getStack(self) + for _,state in ipairs(stack) do + if(state.name == newStateName) then return end + end + + -- Invoke pausedState on the previous state + _invokeCallback(self, self:getCurrentState(), 'pausedState') + + -- Do the push + table.insert(stack, nextState) + + -- Invoke pushedState & enterState on the next state + _invokeCallback(self, nextState, 'pushedState') + _invokeCallback(self, nextState, 'enterState') + + return nextState +end + +--[[ Removes a state from the state stack + If a state name is given, it will attempt to remove it from the stack. If not found on the stack it will do nothing. + If no state name is give, this pops the top state from the stack, if any. Otherwise it does nothing. + Callbacks will be called when needed. +]] +function StatefulObject:popState(stateName) + assert(_private[self].states~=nil, "Attribute 'states' not detected. check that you called instance:popState and not instance.popState, and that you invoked super.initialize(self) in the constructor.") + + -- Invoke exitstate & poppedState on the state being popped out + local prevState = self:getCurrentState() + _invokeCallback(self, prevState, 'exitState') + _invokeCallback(self, prevState, 'poppedState') + + -- Do the pop + local stack = _getStack(self) + table.remove(stack, #stack) + + -- Invoke continuedState on the new state + local newState = self:getCurrentState() + _invokeCallback(self, newState, 'continuedState') + + return newState +end + +--[[ Empties the state stack + This function will invoke all the popState, exitState callbacks on all the states as they pop out. +]] +function StatefulObject:popAllStates() + local state = self:popState() + while(state~=nil) do state = self:popState() end +end + +--[[ Returns the current state (top of the stack only) + The current state's name can be obtained doing object:getCurrentState().name +]] +function StatefulObject:getCurrentState() + local stack = _getStack(self) + if #stack == 0 then return nil end + return(stack[#stack]) +end + +--[[ + Returns true if the object is in the state named 'stateName' + If second(optional) parameter is true, this method returns true if the state is on the stack instead +]] +function StatefulObject:isInState(stateName, testStateStack) + local stack = _getStack(self) + + if(testStateStack==true) then + for _,state in ipairs(stack) do + if(state.name == stateName) then return true end + end + else --testStateStack==false + local state = stack[#stack] + if(state~=nil and state.name == stateName) then return true end + end + + return false +end + +------------------------------------ +-- CLASS METHODS +------------------------------------ + +--[[ Adds a new state to the "states" class member. + superState is optional. If nil, State will be the parent class of the new state + returns the newly created state +]] +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(theClass.states[stateName]==nil, "The class " .. theClass.name .. " already has a state called '" .. stateName) + assert(type(stateName)=="string", "stateName must be a string") + -- states are just regular classes. If superState is nil, this uses Object as superClass + local state = superState:subclass(stateName, theClass) + theClass.states[stateName] = state + return state +end + +--[[ Redefinition of Object:subclass + Subclasses inherit all the states of their superclases, in a special way: + If class A has a state called Sleeping and B = A.subClass('B'), then B.states.Sleeping is a subclass of A.states.Sleeping + returns the newly created stateful class +]] +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 + + --the states of the subclass are subclasses of the superclass' states + theSubClass.states = {} + for stateName,state in pairs(theClass.states) do + theSubClass:addState(stateName, state) + end + + --look for instance methods on the state stack before looking them up on the class' dictionary + local classDict = theSubClass.__classDict + classDict.__index = function(instance, methodName) + -- If the method isn't on the 'ignoredMethods' list, look through the stack to see if it is defined + if(_ignoredMethods[methodName]~=1) then + local stack = _private[instance].stateStack + local method + for i = #stack,1,-1 do -- reversal loop + method = stack[i][methodName] + if(method~=nil) then return method end + end + end + --if ignored or not found, look on the class method + return classDict[methodName] + end + + return theSubClass +end + + +--[[ Include override for stateful classes. + This is exactly like MiddleClass' include function, except that it module has a property called "states" + 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, ...) + 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.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, ...) + end + end +end diff --git a/test/Object_test.lua b/test/Object_test.lua index bf3c41e..e81a6d2 100644 --- a/test/Object_test.lua +++ b/test/Object_test.lua @@ -1,4 +1,5 @@ --- Test base classes (classes that depend directly from Object) +require('MiddleClass') + context( 'Object', function() context( 'When creating a subclass of Object', function() diff --git a/test/class_test.lua b/test/class_test.lua index 9e6da87..e5b3a0e 100644 --- a/test/class_test.lua +++ b/test/class_test.lua @@ -1,3 +1,5 @@ +require('MiddleClass') + context( 'class', function() context( 'When creating a class', function() diff --git a/test/instanceOf_test.lua b/test/instanceOf_test.lua index db96991..7e71e8f 100644 --- a/test/instanceOf_test.lua +++ b/test/instanceOf_test.lua @@ -1,3 +1,5 @@ +require('MiddleClass') + context( 'instanceOf', function() context( 'Primitives', function() diff --git a/test/subclassOf_test.lua b/test/subclassOf_test.lua index 56bd506..6fb9b67 100644 --- a/test/subclassOf_test.lua +++ b/test/subclassOf_test.lua @@ -1,3 +1,5 @@ +require('MiddleClass') + context( 'subclassOf', function() context( 'Primitives', function()