]> git.mxchange.org Git - simgear.git/commitdiff
State-machine structure, initial work.
authorJames Turner <zakalawe@mac.com>
Sun, 20 Jan 2013 13:57:20 +0000 (14:57 +0100)
committerJames Turner <zakalawe@mac.com>
Tue, 22 Jan 2013 17:08:44 +0000 (18:08 +0100)
simgear/structure/CMakeLists.txt
simgear/structure/SGBinding.cxx
simgear/structure/SGBinding.hxx
simgear/structure/StateMachine.cxx [new file with mode: 0644]
simgear/structure/StateMachine.hxx [new file with mode: 0644]
simgear/structure/state_machine_test.cxx [new file with mode: 0644]

index ebe6be5d0a769341ee37e189a0cb60cdc5c2d9e5..0c5dc4f6326c1848861000289cd24b89cf56a44d 100644 (file)
@@ -23,6 +23,7 @@ set(HEADERS
     exception.hxx
     intern.hxx
     subsystem_mgr.hxx
+    StateMachine.hxx
     )
 
 set(SOURCES
@@ -36,7 +37,15 @@ set(SOURCES
     commands.cxx
     event_mgr.cxx
     exception.cxx
-    subsystem_mgr.cxx
+    subsystem_mgr.cxx 
+    StateMachine.cxx
     )
 
 simgear_component(structure structure "${SOURCES}" "${HEADERS}")
+
+if(ENABLE_TESTS)
+add_executable(test_state_machine state_machine_test.cxx)
+target_link_libraries(test_state_machine SimGearCore)
+add_test(test_state_machine ${EXECUTABLE_OUTPUT_PATH}/test_state_machine)
+
+endif(ENABLE_TESTS)
index 8ca19034daeddff8737c3c48b53609034c64affd..db501d4073de354b7740d07497a67edb2c25a3b3 100644 (file)
@@ -11,6 +11,7 @@
 #  include <simgear_config.h>
 #endif
 
+#include <boost/foreach.hpp>
 #include <simgear/compiler.h>
 #include "SGBinding.hxx"
 
@@ -23,6 +24,14 @@ SGBinding::SGBinding()
 {
 }
 
+SGBinding::SGBinding(const std::string& commandName)
+    : _command(0),
+    _arg(0),
+    _setting(0)
+{
+    _command_name = commandName;
+}
+
 SGBinding::SGBinding(const SGPropertyNode* node, SGPropertyNode* root)
   : _command(0),
     _arg(0),
@@ -98,3 +107,10 @@ SGBinding::fire (double setting) const
     fire();
   }
 }
+
+void fireBindingList(const SGBindingList& aBindings)
+{
+    BOOST_FOREACH(SGBinding_ptr b, aBindings) {
+        b->fire();
+    }
+}
index 25e1601efc4e970bace30afa993b9ebc0fdcd289..23f38820ce2bc754980d77966682711a2adb1055 100644 (file)
@@ -38,6 +38,12 @@ public:
    */
   SGBinding ();
 
+    /**
+     * Convenience constructor.
+     *
+     * @param node The binding will be built from this node.
+     */
+    SGBinding(const std::string& commandName);
 
   /**
    * Convenience constructor.
@@ -119,7 +125,15 @@ private:
   mutable SGPropertyNode_ptr _setting;
 };
 
-typedef std::vector<SGSharedPtr<SGBinding> > SGBindingList;
+typedef SGSharedPtr<SGBinding> SGBinding_ptr;
+
+typedef std::vector<SGBinding_ptr > SGBindingList;
 typedef std::map<unsigned,SGBindingList> SGBindingMap;
 
+/**
+ * fire every binding in a list, in sequence
+ */
+void fireBindingList(const SGBindingList& aBindings);
+
 #endif
diff --git a/simgear/structure/StateMachine.cxx b/simgear/structure/StateMachine.cxx
new file mode 100644 (file)
index 0000000..76a00b2
--- /dev/null
@@ -0,0 +1,456 @@
+/* -*-c++-*-
+ *
+ * Copyright (C) 2013 James Turner
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#  include <simgear_config.h>
+#endif
+     
+#include "StateMachine.hxx"
+
+#include <cassert>
+#include <set>
+#include <boost/foreach.hpp>
+
+#include <simgear/debug/logstream.hxx>
+#include <simgear/structure/SGBinding.hxx>
+#include <simgear/props/condition.hxx>
+#include <simgear/timing/timestamp.hxx>
+#include <simgear/structure/exception.hxx>
+                    
+namespace simgear
+{
+
+typedef std::vector<StateMachine::State_ptr> StatePtrVec;
+
+static void readBindingList(SGPropertyNode* desc, const std::string& name, 
+    SGPropertyNode* root, SGBindingList& result)
+{
+    BOOST_FOREACH(SGPropertyNode* b, desc->getChildren(name)) {
+        SGBinding* bind = new SGBinding;
+        bind->read(b, root);
+        result.push_back(bind);
+    }
+}
+
+///////////////////////////////////////////////////////////////////////////
+
+class StateMachine::State::StatePrivate
+{
+public:
+    std::string _name;
+    SGBindingList _updateBindings, 
+        _entryBindings, 
+        _exitBindings;
+};
+
+///////////////////////////////////////////////////////////////////////////
+
+class StateMachine::Transition::TransitionPrivate
+{
+public:
+    std::string _name;
+    SGBindingList _bindings;
+    std::set<State*> _sourceStates; ///< weak refs to source states
+    State* _target;
+    SGSharedPtr<SGCondition> _condition;
+};
+    
+///////////////////////////////////////////////////////////////////////////
+
+class StateMachine::StateMachinePrivate : public SGPropertyChangeListener
+{
+public:
+    StateMachinePrivate(StateMachine* p) : _p(p) { }
+    
+    void computeEligibleTransitions()
+    {
+        _eligible.clear();
+        BOOST_FOREACH(Transition_ptr t, _transitions) {
+            if (t->applicableForState(_currentState)) {
+                _eligible.push_back(t.ptr());
+            }
+        }
+    }
+    
+    StateMachine* _p;
+    State_ptr _currentState;
+    StatePtrVec _states;
+    std::vector<Transition_ptr> _transitions;
+    std::vector<Transition*> _eligible;
+    SGTimeStamp _timeInState;
+    
+    bool _listenerLockout; ///< block our listener when self-updating props
+    virtual void valueChanged(SGPropertyNode* changed)
+    {
+        if (_listenerLockout) {
+            return;
+        }
+        
+        if (changed == _currentStateIndex) {
+            State_ptr s = _p->stateByIndex(changed->getIntValue());
+            _p->changeToState(s);
+        } else if (changed == _currentStateName) {
+            _p->changeToStateName(changed->getStringValue());
+        }
+    }
+    
+// exposed properties
+    SGPropertyNode_ptr _root;
+    SGPropertyNode_ptr _currentStateIndex;
+    SGPropertyNode_ptr _currentStateName;
+    SGPropertyNode_ptr _timeInStateProp;
+};
+
+///////////////////////////////////////////////////////////////////////////
+
+StateMachine::State::State(const std::string& aName) :
+    d(new StatePrivate)
+{
+    d->_name = aName;
+}
+
+StateMachine::State::~State()
+{
+}
+
+std::string StateMachine::State::name() const
+{
+    return d->_name;
+}
+
+void StateMachine::State::update()
+{
+    fireBindingList(d->_updateBindings);
+}
+
+void StateMachine::State::fireEntryBindings()
+{
+    fireBindingList(d->_entryBindings);
+}   
+
+void StateMachine::State::fireExitBindings()
+{
+    fireBindingList(d->_exitBindings);
+}   
+
+void StateMachine::State::addUpdateBinding(SGBinding* aBinding)
+{
+    d->_updateBindings.push_back(aBinding);
+}
+
+void StateMachine::State::addEntryBinding(SGBinding* aBinding)
+{
+    d->_entryBindings.push_back(aBinding);
+}
+
+void StateMachine::State::addExitBinding(SGBinding* aBinding)
+{
+    d->_exitBindings.push_back(aBinding);
+}
+
+///////////////////////////////////////////////////////////////////////////
+
+StateMachine::Transition::Transition(const std::string& aName, State* aTarget) :
+    d(new TransitionPrivate)
+{
+    assert(aTarget);
+    d->_name = aName;
+    d->_target = aTarget;
+}
+
+StateMachine::Transition::~Transition()
+{
+}
+
+StateMachine::State* StateMachine::Transition::target() const
+{
+    return d->_target;
+}    
+
+void StateMachine::Transition::addSourceState(State* aSource)
+{
+    if (aSource == d->_target) { // should this be disallowed outright?
+        SG_LOG(SG_GENERAL, SG_WARN, d->_name << ": adding target state as source");
+    }
+    
+    d->_sourceStates.insert(aSource);
+}    
+
+bool StateMachine::Transition::applicableForState(State* aCurrent) const
+{
+    return d->_sourceStates.count(aCurrent);
+} 
+    
+bool StateMachine::Transition::evaluate() const
+{
+    return d->_condition->test();
+}    
+
+void StateMachine::Transition::fireBindings()
+{
+    fireBindingList(d->_bindings);
+}   
+
+std::string StateMachine::Transition::name() const
+{
+    return d->_name;
+}
+
+void StateMachine::Transition::setTriggerCondition(SGCondition* aCondition)
+{
+    d->_condition = aCondition;
+}
+
+void StateMachine::Transition::addBinding(SGBinding* aBinding)
+{
+    d->_bindings.push_back(aBinding);
+}
+    
+///////////////////////////////////////////////////////////////////////////
+
+StateMachine::StateMachine() :
+    d(new StateMachinePrivate(this))
+{
+    d->_root = new SGPropertyNode();
+}
+
+StateMachine::~StateMachine()
+{
+    
+}
+
+void StateMachine::init()
+{
+    
+    
+    d->_currentStateIndex = d->_root->getChild("current-index", 0, true);
+    d->_currentStateIndex->setIntValue(0);
+    
+    d->_currentStateName = d->_root->getChild("current-name", 0, true);
+    d->_currentStateName->setStringValue("");
+    
+    d->_currentStateIndex->addChangeListener(d.get());
+    d->_currentStateName->addChangeListener(d.get());
+    
+    d->_timeInStateProp = d->_root->getChild("elapsed-time-msec", 0, true);
+    d->_timeInStateProp->setIntValue(0);
+    
+    // TODO go to default state if found
+    d->computeEligibleTransitions();
+    
+}
+
+void StateMachine::shutdown()
+{
+    d->_currentStateIndex->removeChangeListener(d.get());
+    d->_currentStateName->removeChangeListener(d.get());
+    
+}
+
+void StateMachine::innerChangeState(State_ptr aState, Transition_ptr aTrans)
+{
+    d->_currentState->fireExitBindings();
+        
+// fire bindings before we change the state, hmmmm    
+    if (aTrans) {
+        aTrans->fireBindings();
+    }
+    
+    // update our private state and properties
+    d->_listenerLockout = true;
+    d->_currentState = aState;
+    d->_timeInState.stamp();
+    d->_currentStateName->setStringValue(d->_currentState->name());
+    d->_currentStateIndex->setIntValue(indexOfState(aState));
+    d->_timeInStateProp->setIntValue(0);
+    d->_listenerLockout = false;
+    
+    // fire bindings
+    d->_currentState->fireEntryBindings();
+    d->_currentState->update();
+        
+    d->computeEligibleTransitions();
+}
+
+void StateMachine::changeToState(State_ptr aState, bool aOnlyIfDifferent)
+{
+    assert(aState != NULL);
+    if (std::find(d->_states.begin(), d->_states.end(), aState) == d->_states.end()) {
+        throw sg_exception("Requested change to state not in machine");
+    }
+    
+    if (aOnlyIfDifferent && (aState == d->_currentState)) {
+        return;
+    }
+    
+    innerChangeState(aState, NULL);
+}
+
+void StateMachine::changeToStateName(const std::string& aName, bool aOnlyIfDifferent)
+{
+    State_ptr st = findStateByName(aName);
+    if (!st) {
+        throw sg_range_exception("unknown state:" + aName);
+    }
+    
+    changeToState(st, aOnlyIfDifferent);
+}
+
+StateMachine::State_ptr StateMachine::state() const
+{
+    return d->_currentState;
+}
+    
+SGPropertyNode* StateMachine::root()
+{
+    return d->_root;
+}
+
+void StateMachine::update(double aDt)
+{
+    // do this first, for triggers which depend on time in current state
+    // (spring-loaded transitions)
+    d->_timeInStateProp->setIntValue(d->_timeInState.elapsedMSec());
+    
+    Transition_ptr trigger;
+    
+    BOOST_FOREACH(Transition* trans, d->_eligible) {
+        if (trans->evaluate()) {
+            if (trigger != Transition_ptr()) {
+                SG_LOG(SG_GENERAL, SG_WARN, "ambiguous transitions! " 
+                    << trans->name() << " or "  << trigger->name());
+            }
+            
+            trigger = trans;
+        }
+    }
+    
+    if (trigger != Transition_ptr()) {
+        SG_LOG(SG_GENERAL, SG_DEBUG, "firing transition:" << trigger->name());
+        innerChangeState(trigger->target(), trigger);
+    }
+    
+    d->_currentState->update();
+}    
+
+StateMachine::State_ptr StateMachine::findStateByName(const std::string& aName) const
+{
+    BOOST_FOREACH(State_ptr sp, d->_states) {
+        if (sp->name() == aName) {
+            return sp;
+        }
+    }
+    
+    SG_LOG(SG_GENERAL, SG_WARN, "unknown state:" << aName);
+    return State_ptr();
+}
+
+StateMachine::State_ptr StateMachine::stateByIndex(unsigned int aIndex) const
+{
+    if (aIndex >= d->_states.size()) {
+        throw sg_range_exception("invalid state index, out of bounds");
+    }
+    
+    return d->_states[aIndex];
+}
+    
+int StateMachine::indexOfState(State_ptr aState) const
+{
+    StatePtrVec::const_iterator it = std::find(d->_states.begin(), d->_states.end(), aState);
+    if (it == d->_states.end()) {
+        return -1;
+    }
+    
+    return it - d->_states.begin();
+}
+
+StateMachine::State_ptr StateMachine::createState(const std::string& aName)
+{
+    if (findStateByName(aName) != NULL) {
+        throw sg_range_exception("duplicate state name");
+    }
+    
+    State_ptr st = new State(aName);
+    addState(st);
+    return st;
+}
+
+StateMachine::Transition_ptr
+StateMachine::createTransition(const std::string& aName, State_ptr aTarget)
+{
+    Transition_ptr t = new Transition(aName, aTarget);
+    addTransition(t);
+    return t;
+}
+
+StateMachine* StateMachine::createFromPlist(SGPropertyNode* desc, SGPropertyNode* root)
+{
+    StateMachine* sm = new StateMachine;
+    
+    
+    BOOST_FOREACH(SGPropertyNode* stateDesc, desc->getChildren("state")) {
+        std::string nm = stateDesc->getStringValue("name");
+        State_ptr st(new State(nm));
+        
+        readBindingList(stateDesc, "enter", root, st->d->_updateBindings);
+        readBindingList(stateDesc, "exit", root, st->d->_entryBindings);
+        readBindingList(stateDesc, "update", root, st->d->_exitBindings);
+        
+        sm->addState(st);
+    } // of states iteration
+    
+    BOOST_FOREACH(SGPropertyNode* tDesc, desc->getChildren("transition")) {
+        std::string nm = tDesc->getStringValue("name");
+        State_ptr target = sm->findStateByName(tDesc->getStringValue("target"));
+        
+        SGCondition* cond = sgReadCondition(root, tDesc->getChild("condition"));
+        
+        Transition_ptr t(new Transition(nm, target));
+        t->setTriggerCondition(cond);
+        
+        BOOST_FOREACH(SGPropertyNode* src, desc->getChildren("source")) {
+            State_ptr srcState = sm->findStateByName(src->getStringValue());
+            t->addSourceState(srcState);
+        }
+        
+        readBindingList(tDesc, "binding", root, t->d->_bindings);
+        
+        sm->addTransition(t);
+    } // of states iteration
+    
+    return sm;
+}
+
+void StateMachine::addState(State_ptr aState)
+{
+    bool wasEmpty = d->_states.empty();
+    d->_states.push_back(aState);
+    if (wasEmpty) {
+        d->_currentState = aState;
+    }
+}
+
+void StateMachine::addTransition(Transition_ptr aTrans)
+{
+    d->_transitions.push_back(aTrans);
+}
+
+} // of namespace simgear
diff --git a/simgear/structure/StateMachine.hxx b/simgear/structure/StateMachine.hxx
new file mode 100644 (file)
index 0000000..34fb84e
--- /dev/null
@@ -0,0 +1,166 @@
+/* -*-c++-*-
+ *
+ * Copyright (C) 2013 James Turner
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ *
+ */
+
+#ifndef SIMGEAR_STATE_MACHINE_H
+#define SIMGEAR_STATE_MACHINE_H
+
+#include <memory>
+
+#include <simgear/structure/SGReferenced.hxx>
+#include <simgear/structure/SGSharedPtr.hxx>
+     
+// forward decls
+class SGPropertyNode;
+class SGBinding;
+class SGCondition;
+
+namespace simgear
+{
+
+class StateMachine : public SGReferenced
+{
+public:
+    StateMachine();
+    virtual ~StateMachine();
+    
+    class State : public SGReferenced
+    {
+    public:    
+        virtual ~State();
+        
+        std::string name() const;
+        
+        void addUpdateBinding(SGBinding* aBinding);
+        void addEntryBinding(SGBinding* aBinding);
+        void addExitBinding(SGBinding* aBinding);
+        
+    private:  
+        friend class StateMachine;
+        
+        State(const std::string& name);
+        
+        void fireExitBindings();
+        void fireEntryBindings();
+        
+        void update();
+        
+        class StatePrivate;
+        std::auto_ptr<StatePrivate> d;
+    };
+    
+    class Transition : public SGReferenced
+    {
+    public:
+        virtual ~Transition();
+        
+        std::string name() const;
+        
+        /**
+         * The state we end in, after this transition fires
+         */
+        State* target() const;
+        
+        /**
+         * Add a state in which this transition is eligible to fire
+         */
+        void addSourceState(State* aSource);
+        
+        /**
+         * Specify the transition trigger condition. Takes ownership
+         */
+        void setTriggerCondition(SGCondition* aCondition);
+        
+        
+        void addBinding(SGBinding* aBinding);
+    private:
+        friend class StateMachine;
+        
+        Transition(const std::string& aName, State* aTarget);
+        
+        /**
+         * predicate to determine if this transition can fire given a
+         * current state.
+         */
+        bool applicableForState(State* aCurrent) const;
+        
+        /**
+        * test if the transition should fire, based on current state
+        */
+        bool evaluate() const;
+         
+        void fireBindings();
+    
+        class TransitionPrivate;
+        std::auto_ptr<TransitionPrivate> d;
+    };
+    
+    typedef SGSharedPtr<State> State_ptr;
+    typedef SGSharedPtr<Transition> Transition_ptr;
+    
+    /**
+     * create a state machine from a property list description
+     */
+    static StateMachine* createFromPlist(SGPropertyNode* desc, SGPropertyNode* root);
+    
+    SGPropertyNode* root();
+    
+    void init();
+    void shutdown();
+    
+    void update(double dt);
+    
+    State_ptr state() const;
+    
+    /**
+     * public API to force a change to a particular state.
+     * @param aOnlyIfDifferent - only make a transition if the new state is
+     *   different from the current state. Otherwise, the existing state will
+     * be exited and re-entered.
+     */
+    void changeToState(State_ptr aState, bool aOnlyIfDifferent=true);
+    
+     /// wrapper to change state by looking up a name
+    void changeToStateName(const std::string& aName, bool aOnlyIfDifferent=true);
+    
+    State_ptr findStateByName(const std::string& aName) const;
+    
+    State_ptr stateByIndex(unsigned int aIndex) const;
+    
+    int indexOfState(State_ptr aState) const;
+    
+    // programatic creation
+    State_ptr createState(const std::string& aName);
+    Transition_ptr createTransition(const std::string& aName, State_ptr aTarget);
+private:
+    void addState(State_ptr aState);
+    void addTransition(Transition_ptr aTrans);
+    
+    void innerChangeState(State_ptr aState, Transition_ptr aTrans);
+    
+    class StateMachinePrivate;
+    std::auto_ptr<StateMachinePrivate> d;
+};
+
+typedef SGSharedPtr<StateMachine> StateMachine_ptr;
+
+} // of simgear namespace
+
+#endif // of SIMGEAR_STATE_MACHINE_H
diff --git a/simgear/structure/state_machine_test.cxx b/simgear/structure/state_machine_test.cxx
new file mode 100644 (file)
index 0000000..a22ae63
--- /dev/null
@@ -0,0 +1,226 @@
+#ifdef HAVE_CONFIG_H
+#  include <simgear_config.h>
+#endif
+
+#ifdef NDEBUG
+// Always enable DEBUG mode in test application, otherwise "assert" test
+// statements have no effect and don't actually test anything (catch 17 ;-) ).
+#undef NDEBUG
+#endif
+
+#include <simgear/compiler.h>
+
+#include <iostream>
+#include <cassert>
+#include <cstdlib>
+#include <cstring>
+
+#include "StateMachine.hxx"
+
+#include <simgear/structure/SGBinding.hxx>
+#include <simgear/structure/exception.hxx>
+#include <simgear/props/condition.hxx>
+#include <simgear/props/props.hxx>
+#include <simgear/props/props_io.hxx>
+#include <simgear/structure/commands.hxx>
+
+using std::string;
+using std::cout;
+using std::cerr;
+using std::endl;
+
+// SGCondition subclass we can trivially manipulate from test code.
+class DummyCondition : public SGCondition
+{
+public:
+    DummyCondition(): _state(false) { }
+    
+    virtual bool test() const
+    {
+        return _state;
+    }
+    
+    bool _state;
+};
+
+static int dummy_cmd_state = 0;
+
+bool dummyCommand(const SGPropertyNode* arg)
+{
+    ++dummy_cmd_state;
+    return true;
+}
+
+#define COMPARE(a, b) \
+    if ((a) != (b))  { \
+        cerr << "failed:" << #a << " != " << #b << endl; \
+        cerr << "\tgot:'" << a << "'" << endl; \
+        exit(1); \
+    }
+
+#define VERIFY(a) \
+    if (!(a))  { \
+        cerr << "failed:" << #a << endl; \
+        exit(1); \
+    }
+    
+using namespace simgear;
+
+#define BUILD_MACHINE_1() \
+    StateMachine_ptr sm(new StateMachine); \
+    StateMachine::State_ptr stateA = sm->createState("a"); \
+    StateMachine::State_ptr stateB = sm->createState("b"); \
+    StateMachine::State_ptr stateC = sm->createState("c"); \
+    \
+    DummyCondition* trigger1 = new DummyCondition; \
+    StateMachine::Transition_ptr t1 = sm->createTransition(">b", stateB); \
+    t1->addSourceState(stateA); \
+    t1->setTriggerCondition(trigger1); \
+    \
+    DummyCondition* trigger2 = new DummyCondition; \
+    StateMachine::Transition_ptr t2 = sm->createTransition(">c", stateC); \
+    t2->addSourceState(stateB); \
+    t2->setTriggerCondition(trigger2); \
+    \
+    DummyCondition* trigger3 = new DummyCondition; \
+    StateMachine::Transition_ptr t3 = sm->createTransition(">a", stateA); \
+    t3->addSourceState(stateC); \
+    t3->addSourceState(stateB); \
+    t3->setTriggerCondition(trigger3); \
+    sm->init();
+
+void testBasic()
+{
+    BUILD_MACHINE_1();
+////////////////////////////////////////////
+    COMPARE(sm->state()->name(), "a");
+    
+    COMPARE(sm->indexOfState(stateA), 0);
+    COMPARE(sm->findStateByName("c"), stateC);
+    
+    sm->changeToState(stateC);
+    COMPARE(sm->state(), stateC);
+    
+    trigger3->_state = true;
+    sm->update(1.0);
+    COMPARE(sm->state()->name(), "a");
+    trigger3->_state = false;
+    
+    trigger1->_state = true;
+    sm->update(1.0);
+    trigger1->_state = false;
+    COMPARE(sm->state()->name(), "b");
+    
+    trigger3->_state = true;
+    sm->update(1.0);
+    COMPARE(sm->state()->name(), "a");
+    trigger3->_state = false;
+    
+    trigger1->_state = true;
+    sm->update(1.0);
+    trigger1->_state = false;
+    COMPARE(sm->state()->name(), "b");
+    
+    trigger2->_state = true;
+    sm->update(1.0);
+    trigger2->_state = false;
+    COMPARE(sm->state()->name(), "c");
+    
+//////////////////////////////////////////
+    COMPARE(sm->root()->getIntValue("current-index"), 2);
+    COMPARE(sm->root()->getStringValue("current-name"), string("c"));
+    
+    sm->root()->setStringValue("current-name", "b");
+    COMPARE(sm->state()->name(), "b");
+
+////////////////////////////////////////
+    COMPARE(sm->findStateByName("foo"), NULL);
+    COMPARE(sm->indexOfState(StateMachine::State_ptr()), -1);
+    
+    COMPARE(sm->stateByIndex(1), stateB);
+    
+    try {
+        sm->stateByIndex(44);
+        VERIFY(false && "should have raised an exception");
+    } catch (sg_exception& e){
+        // expected!
+    }
+}
+
+void testBindings()
+{    
+    SGCommandMgr* cmdMgr = SGCommandMgr::instance();
+    cmdMgr->addCommand("dummy", dummyCommand);
+    BUILD_MACHINE_1();
+    
+    t2->addBinding(new SGBinding("dummy"));
+    
+    stateA->addEntryBinding(new SGBinding("dummy"));
+    stateA->addExitBinding(new SGBinding("dummy"));
+    stateC->addEntryBinding(new SGBinding("dummy"));
+    
+////////////////////////
+    COMPARE(sm->state()->name(), "a");
+    trigger1->_state = true;
+    sm->update(1.0);
+    trigger1->_state = false;
+    COMPARE(sm->state()->name(), "b");
+    COMPARE(dummy_cmd_state, 1); // exit state A
+    
+    trigger2->_state = true;
+    sm->update(1.0);
+    trigger2->_state = false;
+    COMPARE(dummy_cmd_state, 3); // fire transition 2, enter state C
+    
+    dummy_cmd_state = 0;
+    sm->changeToState(stateA);
+    COMPARE(dummy_cmd_state, 1); // enter state A
+    trigger1->_state = true;
+    sm->update(1.0);
+    trigger1->_state = false;
+    COMPARE(dummy_cmd_state, 2); // exit state A
+    
+////////////////////////
+    t3->addBinding(new SGBinding("dummy"));
+    t3->addBinding(new SGBinding("dummy"));
+    t3->addBinding(new SGBinding("dummy"));
+    
+    sm->changeToStateName("b");
+    dummy_cmd_state = 0;
+    trigger3->_state = true;
+    sm->update(1.0);
+    trigger3->_state = false;
+    COMPARE(dummy_cmd_state, 4); // three transition bindings, enter A
+}
+
+void testParse()
+{
+    const char* xml = "<?xml version=\"1.0\"?>"
+        "<PropertyList>"
+            "<state>"
+                "<name>one</name>"
+            "</state>"
+            "<state>"
+                "<name>two</name>"
+            "</state>"
+        "</PropertyList>";
+    
+    SGPropertyNode* desc = new SGPropertyNode;
+    readProperties(xml, strlen(xml), desc);
+    
+    SGPropertyNode_ptr root(new SGPropertyNode);
+    StateMachine_ptr sm = StateMachine::createFromPlist(desc, root);
+    
+    VERIFY(sm->findStateByName("one") != NULL);
+    VERIFY(sm->findStateByName("two") != NULL);
+}
+
+int main(int argc, char* argv[])
+{
+    testBasic();
+    testBindings();
+    testParse();
+    cout << __FILE__ << ": All tests passed" << endl;
+    return EXIT_SUCCESS;
+}
+