Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

adding array support in the event's "from" value and passing

in the state change information to callback methods.
  • Loading branch information...
commit 844542bfac93fa31affdc6772ef3f78a7f6a4ce0 1 parent 2ffbf32
Stephen Blankenship authored
5 README
View
@@ -1,5 +0,0 @@
-Description:
-A simple state machine written in CoffeeScript.
-
-Usage:
-TODO
150 README.md
View
@@ -0,0 +1,150 @@
+Description:
+------------
+
+A simple state machine written in CoffeeScript.
+
+Sample Usage:
+------
+
+A "StateMachine" class is provided that can be used as the basis of your state machine implementation.
+The object passed in to the constructor has an expected format that will define the state machine.
+The sample stuff below will use a chess game as a basic example.
+
+Step one will always be to require the state machine:
+
+ {StateMachine} = require 'state_machine'
+
+The StateMachine class' constructor takes in an object that defines the entire state machine.
+Here's what it looks like:
+
+ states:
+ stateName:
+ active: true/false (optional, the 1st state defaults to true)
+ onEnter: enterMethod (called when successfully entering the state)
+ onExit: exitMethod (called when successfully exiting the state)
+ guard: guardMethod (stops the change to this state if returns false)
+ stateName2: etc...
+ events:
+ eventName:
+ from: fromState (should be a defined state, or "any")
+ to: toState (should be a defined state)
+ eventName2: etc...
+ onStateChange: changeMethod (called on any state change)
+
+If you don't need anything fancy on the states, then you can use a basic Array setup:
+
+ game = new StateMachine states: ['whiteToMove', 'blackToMove']
+
+ game.availableStates()
+ # outputs: [ 'whiteToMove', 'blackToMove' ]
+ game.currentState()
+ # outputs: 'whiteToMove'
+
+But, you should really define some *events* that will trigger state changes. Each
+defined event gives you a method you can call to trigger the state change.
+
+ class ChessGame extends StateMachine
+ switchSides: ->
+ # ...
+ console.log "switchSides called."
+
+ game = new ChessGame
+ states:
+ whiteToMove:
+ onEnter: -> this.switchSides()
+ blackToMove:
+ onEnter: -> this.switchSides()
+ events:
+ whiteMoved: {from:'whiteToMove', to:'blackToMove'}
+ blackMoved: {from:'blackToMove', to:'whiteToMove'}
+
+ game.whiteMoved()
+ # outputs: switchSides called.
+
+You can also pass the states definition to the defineStateMachine method. So, a more custom
+and comprehensive implementation may look like:
+
+ class ChessGame extends StateMachine
+ constructor: (@board, @pieces) ->
+ @defineStateMachine
+ states:
+ whiteToMove:
+ # If black was in check, sides can't switch unless they're now not in check
+ guard: (args) -> not (args.from is 'blackInCheck' and this.blackKingInCheck())
+ onEnter: -> this.deliverMessage('white', 'Your move.')
+ blackToMove:
+ guard: (args) -> not (args.from is 'whiteInCheck' and this.whiteKingInCheck())
+ onEnter: -> this.deliverMessage('black', 'Your move.')
+ whiteInCheck:
+ onEnter: -> this.deliverMessage('white', 'Check!')
+ onExit: -> this.deliverMessage('white', 'Check escaped.')
+ blackInCheck:
+ onEnter: -> this.deliverMessage('black', 'Check!')
+ onExit: -> this.deliverMessage('black', 'Check escaped.')
+ whiteCheckmated:
+ onEnter: ->
+ this.deliverMessage('white', 'Checkmate, you lose :-(')
+ this.deliverMessage('black', 'Checkmate, you win!')
+ blackCheckmated:
+ onEnter: ->
+ this.deliverMessage('black', 'Checkmate, you lose :-(')
+ this.deliverMessage('white', 'Checkmate, you win!')
+ events:
+ whiteMoved: { from: 'whiteToMove', to: 'blackToMove' }
+ whiteChecked: { from: ['blackToMove', 'blackInCheck'], to: 'whiteInCheck' }
+ whiteCheckMated: { from: ['blackToMove', 'blackInCheck'], to: 'whiteCheckmated' }
+ blackMoved: { from: 'blackToMove', to: 'whiteToMove' }
+ blackChecked: { from: ['whiteToMove', 'whiteInCheck'], to: 'blackInCheck' }
+ blackCheckMated: { from: ['whitetoMove', 'whiteInCheck'], to: 'blackCheckmated' }
+ onStateChange: (args) -> this.logActivity(args.from, args.to, args.event)
+
+ blackKingInCheck: ->
+ # ...
+
+ whiteKingInCheck: ->
+ # ...
+
+ deliverMessage: (playerColor, message) ->
+ console.log "[Message to #{playerColor}] #{message}"
+
+ logActivity: (from, to, event) ->
+ console.log "Activity: from => #{from}, to => #{to}, event => #{event}"
+
+ ##################################
+
+ game = new ChessGame
+
+ game.whiteMoved()
+ # outputs:
+ # [Message to black] Your move.
+ # Activity: from => whiteToMove, to => blackToMove, event => whiteMoved
+
+ game.blackMoved()
+ # outputs:
+ # [Message to white] Your move.
+ # Activity: from => blackToMove, to => whiteToMove, event => blackMoved
+
+ game.blackChecked()
+ # outputs:
+ # [Message to black] Check!
+ # Activity: from => whiteToMove, to => blackInCheck, event => blackChecked
+
+ game.whiteCheckMated()
+ # outputs:
+ # [Message to black] Check escaped.
+ # [Message to white] Checkmate, you lose :-(
+ # [Message to black] Checkmate, you win!
+ # Activity: from => blackInCheck, to => whiteCheckmated, event => whiteCheckMated
+
+ try
+ game.blackMoved()
+ catch error
+ console.log error
+ # outputs:
+ # Cannot change from state 'blackToMove'; it is not the active state!
+
+
+Note that each callback method (onEnter, onExit, guard, and onStateChange) gets passed an args object that
+has a "from", "to", and "event" key, providing the previous state, new state, and the
+event that triggered the state change.
+
56 lib/state_machine.js
View
@@ -4,8 +4,18 @@
root = typeof exports !== "undefined" && exports !== null ? exports : window;
root.StateMachine = StateMachine = (function() {
function StateMachine(stateMachine) {
- var activeStates, event, eventDef, state, stateDef, states, _fn, _i, _j, _len, _len2, _ref, _ref2;
- this.stateMachine = stateMachine != null ? stateMachine : {};
+ this.stateMachine = stateMachine != null ? stateMachine : {
+ states: {},
+ events: {}
+ };
+ this.defineStateMachine(this.stateMachine);
+ }
+ StateMachine.prototype.defineStateMachine = function(stateMachine) {
+ var activeStates, event, eventDef, state, stateDef, states, _i, _j, _len, _len2, _ref, _ref2, _results;
+ this.stateMachine = stateMachine != null ? stateMachine : {
+ states: {},
+ events: {}
+ };
if (this.stateMachine.states.constructor.toString().indexOf('Array') !== -1) {
states = this.stateMachine.states;
this.stateMachine.states = {};
@@ -47,16 +57,17 @@
}
}
_ref2 = this.stateMachine.events;
- _fn = __bind(function(event, eventDef) {
- return this[event] = function() {
- return this.changeState(eventDef.from, eventDef.to);
- };
- }, this);
+ _results = [];
for (event in _ref2) {
eventDef = _ref2[event];
- _fn(event, eventDef);
+ _results.push(__bind(function(event, eventDef) {
+ return this[event] = function() {
+ return this.changeState(eventDef.from, eventDef.to, event);
+ };
+ }, this)(event, eventDef));
}
- }
+ return _results;
+ };
StateMachine.prototype.currentState = function() {
var state, stateDef;
return ((function() {
@@ -93,8 +104,18 @@
}
return _results;
};
- StateMachine.prototype.changeState = function(from, to) {
- var enterMethod, exitMethod, fromStateDef, guardMethod, toStateDef;
+ StateMachine.prototype.changeState = function(from, to, event) {
+ var args, enterMethod, exitMethod, fromStateDef, guardMethod, toStateDef;
+ if (event == null) {
+ event = null;
+ }
+ if (from.constructor.toString().indexOf('Array') !== -1) {
+ if (from.indexOf(this.currentState()) !== -1) {
+ from = this.currentState();
+ } else {
+ throw "Cannot change from states " + (from.join(' or ')) + "; none are the active state!";
+ }
+ }
fromStateDef = this.stateMachine.states[from];
toStateDef = this.stateMachine.states[to];
if (toStateDef === void 0) {
@@ -113,17 +134,22 @@
fromStateDef = this.stateMachine.states[this.currentState()];
}
exitMethod = fromStateDef.onExit;
- if (guardMethod !== void 0 && guardMethod.call() === false) {
+ args = {
+ from: from,
+ to: to,
+ event: event
+ };
+ if (guardMethod !== void 0 && guardMethod.call(this, args) === false) {
return false;
}
if (exitMethod !== void 0) {
- exitMethod.call();
+ exitMethod.call(this, args);
}
if (enterMethod !== void 0) {
- enterMethod.call();
+ enterMethod.call(this, args);
}
if (this.stateMachine.onStateChange !== void 0) {
- this.stateMachine.onStateChange.call();
+ this.stateMachine.onStateChange.call(this, args);
}
fromStateDef.active = false;
return toStateDef.active = true;
8 package.json
View
@@ -7,12 +7,16 @@
"type": "git",
"url": "git://github.com/stephenb/state_machine.git"
},
+ "main": "./lib/state_machine",
"scripts": {
"test": "cake test"
},
"engines": {
- "node": "~v0.4.7"
+ "node": ">=v0.4.7"
},
"dependencies": {},
- "devDependencies": {}
+ "devDependencies": {
+ "vows": ">=v0.5.8",
+ "coffee-script": ">=v1.1.1"
+ }
}
33 src/state_machine.coffee
View
@@ -9,20 +9,22 @@ root = exports ? window
# guard: guardMethod (stops the change to this state if returns false)
# events:
# eventName:
-# from: fromState (should be a defined state, or "any")
+# from: fromState (should be a defined state, array of defined states, or "any")
# to: toState (should be a defined state)
# onStateChange: changeMethod (called on any state change)
#
root.StateMachine = class StateMachine
- constructor: (@stateMachine = {}) ->
+ constructor: (@stateMachine = {states:{}, events:{}}) ->
+ this.defineStateMachine(@stateMachine)
+
+ defineStateMachine: (@stateMachine = {states:{}, events:{}}) ->
# If array setup was used, translate it into the object setup
if @stateMachine.states.constructor.toString().indexOf('Array') isnt -1
states = @stateMachine.states
@stateMachine.states = {}
for state in states
- @stateMachine.states[state] = { active: (state == states[0]) }
-
+ @stateMachine.states[state] = { active: (state is states[0]) }
# Make sure an active state is properly set
activeStates = (state for own state, stateDef of @stateMachine.states when stateDef.active)
if activeStates.length is 0
@@ -35,12 +37,11 @@ root.StateMachine = class StateMachine
for own state in activeStates
continue if state is activeStates[0]
stateDef.active = false
-
# Define the event methods
for event, eventDef of @stateMachine.events
do(event, eventDef) =>
- this[event] = -> this.changeState(eventDef.from, eventDef.to)
-
+ this[event] = -> this.changeState(eventDef.from, eventDef.to, event)
+
currentState: ->
(state for own state, stateDef of @stateMachine.states when stateDef.active)[0]
@@ -50,7 +51,13 @@ root.StateMachine = class StateMachine
availableEvents: ->
event for own event of @stateMachine.events
- changeState: (from, to) ->
+ changeState: (from, to, event=null) ->
+ if from.constructor.toString().indexOf('Array') isnt -1
+ if from.indexOf(this.currentState()) isnt -1
+ from = this.currentState()
+ else
+ throw "Cannot change from states #{from.join(' or ')}; none are the active state!"
+
fromStateDef = @stateMachine.states[from]
toStateDef = @stateMachine.states[to]
@@ -65,9 +72,11 @@ root.StateMachine = class StateMachine
if from == 'any' then fromStateDef = @stateMachine.states[this.currentState()]
{onExit: exitMethod} = fromStateDef
- return false if guardMethod isnt undefined and guardMethod.call() is false
- exitMethod.call() if exitMethod isnt undefined
- enterMethod.call() if enterMethod isnt undefined
- @stateMachine.onStateChange.call() if @stateMachine.onStateChange isnt undefined
+ args = {from: from, to: to, event: event}
+ return false if guardMethod isnt undefined and guardMethod.call(this, args) is false
+ exitMethod.call(this, args) if exitMethod isnt undefined
+ enterMethod.call(this, args) if enterMethod isnt undefined
+ @stateMachine.onStateChange.call(this, args) if @stateMachine.onStateChange isnt undefined
fromStateDef.active = false
toStateDef.active = true
+
51 test/test_state_machine.coffee
View
@@ -119,12 +119,43 @@ vow.addBatch
'should be setup properly': (topic) ->
assert.isFunction topic.stateMachine.onStateChange
- 'onStateChange should get called state change': (topic) ->
+ 'onStateChange should get called on state change': (topic) ->
try
topic.changeState('state1', 'state2')
catch e
assert.equal e, 'onStateChangeCalled'
-
+
+ 'Callbacks should contain state and event info':
+ topic: ->
+ new StateMachine
+ states:
+ state1:
+ onExit: (args) -> this.returnedArgs = args
+ state2: {}
+ state3:
+ onEnter: (args) -> this.returnedArgs = args
+ state4:
+ guard: (args) -> this.returnedArgs = args
+ events:
+ state2to3: {from: 'state2', to: 'state3'}
+
+ 'onExit': (topic) ->
+ topic.changeState('state1', 'state2')
+ assert.equal topic.returnedArgs.from, 'state1'
+ assert.equal topic.returnedArgs.to, 'state2'
+
+ 'onEnter': (topic) ->
+ topic.state2to3()
+ assert.equal topic.returnedArgs.from, 'state2'
+ assert.equal topic.returnedArgs.to, 'state3'
+ assert.equal topic.returnedArgs.event, 'state2to3'
+
+ 'guard': (topic) ->
+ topic.changeState('state3', 'state4')
+ assert.equal topic.returnedArgs.from, 'state3'
+ assert.equal topic.returnedArgs.to, 'state4'
+ assert.equal topic.returnedArgs.event, null
+
vow.addBatch
'Events':
topic: ->
@@ -134,6 +165,7 @@ vow.addBatch
state1to2: {from:'state1', to:'state2'}
state2to1: {from:'state2', to:'state1'}
anyToState3: {from:'any', to:'state3'}
+ state2or3toState1: {from:['state2', 'state3'], to:'state1'}
'should properly change state': (topic) ->
topic.state1to2()
@@ -147,7 +179,7 @@ vow.addBatch
assert.equal 1, 2 # hrmmm... don't know how to test the error well
catch e
assert.equal e, "Cannot change from state 'state2'; it is not the active state!"
-
+
'should change from any state if "any" is the from key': (topic) ->
assert.equal topic.currentState(), 'state1' # We're in state1
topic.anyToState3()
@@ -157,5 +189,18 @@ vow.addBatch
topic.anyToState3()
assert.equal topic.currentState(), 'state3' # should change to state3
+ 'should support an array in the from key': (topic) ->
+ assert.equal topic.currentState(), 'state3' # We're in state3
+ topic.state2or3toState1()
+ assert.equal topic.currentState(), 'state1'
+ topic.state1to2()
+ assert.equal topic.currentState(), 'state2' # We're in state2
+ topic.state2or3toState1()
+ assert.equal topic.currentState(), 'state1' # We're in state1
+ try
+ topic.state2or3toState1()
+ assert.equal 1, 2 # hrmmm... don't know how to test the error well
+ catch e
+ assert.equal e, "Cannot change from states state2 or state3; none are the active state!"
exports.test_utils = vow
Please sign in to comment.
Something went wrong with that request. Please try again.