Skip to content

Commit

Permalink
Solves #5. See README.md for documentation of the new feature "dynami…
Browse files Browse the repository at this point in the history
…c transitions"
  • Loading branch information
dschulten committed May 1, 2013
1 parent 6583d68 commit d3cf73a
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 3 deletions.
111 changes: 111 additions & 0 deletions README.md
Expand Up @@ -334,6 +334,117 @@ var fsm = new jsfsa.Automaton( config )
;
```

### DYNAMIC TRANSITIONS WITH GUARDS AND EFFECTS

So far we have seen examples for statically defined transition targets. However, transitions can also be dynamic, i.e. the transition itself can be conditional, transitions can have an effect and it is possible to determine the target state of a transition based on some calculation within the transition. The event handler has access to payload data.

#### GUARDS AND EFFECTS

* transitions can be guarded by a condition and they can have an effect. As an example, this transition might occur in a simplified football game statemachine:

```
goal[valid]/goals++ +----------+
---------------------------> | kickOff |
+----------+
```

When a goal occurs, the effect is that the goals count is increased, but only if the goal is valid. Afterwards the game will be in the kickOff state.

This can be especially useful if there are different effects depending on the event and the condition which brings you to a new state. E.g. you can have one state kickOff which only contains the entry acttions that are common to all kickOffs, rather than distinguishing a kickOffAfterGoal which increases the goal count in its entry action from a kickOffAtBeginning and a kickOffAfterHalftime which don't do that.

For such a guarded transition with effect, define a function as the transition target and return the name of the target state if you want to allow the transition.

```
...
transitions : {
'goal' : function(e, valid) {
if(valid === true) {
goals++;
return 'kickOff';
}
}
}
...
// a valid goal occurs:
sm.doTransition('goal', true)
```

Note that the function returns the target state 'kickOff' if the goal was valid, but it returns undefined if the goal was not valid. In the latter case, the transition is denied and we stay in the current state.

#### INTERNAL TRANSITIONS

* you can also have an internal transition, i.e. you can handle an event without ever leaving a state.

```
+-----------------------------------------------+
| Entering password |
+-----------------------------------------------+
| passwordEntered[invalid]/failed++ |
| |
+-----------------------------------------------+
```

For such an internal transition the event handler function must always return undefined.

```
...
transitions : {
'passwordEntered' : function(e, password) {
if(!passwordValid(password) {
failed++;
}
return undefined;
}
}
...
// wrong password:
sm.doTransition('passwordEntered', 'wr0n5')
```

#### DYNAMIC CHOICES

Finally, this technique also allows you to express a choice, i.e. a transition whose target is determined dynamically. Consider the transitions below which describe what happens if a team scores a goal during a football match in tie state.

```
+-----------+
| Tie |
+-----------+
|
| goal
/ \
__ / \__
| \ / |
[homeTeam] | \ / |[visitingTeam]
/goals.homeTeam++ | | /goals.visitingTeam++
| |
+----------+<--* *-->+----------+
| Lead | | Lead |
| Home | | Visiting |
| Team | | Team |
+----------+ +----------+
```

The goal event can lead to two different transitions. The transition from Tie to LeadHomeTeam occurs only if the goal was scored by the home team. It effectively increments the goals of the home team (and vice versa). For such a choice, you could write:

```
...
transitions : {
'goal' : function(e, scorer) {
goals[scorer]++;
if(scorer === 'homeTeam') {
return 'leadHomeTeam';
} else {
return 'leadVisitingTeam';
}
}
}
...
// The home team scores a goal:
sm.doTransition('goal', 'homeTeam')
```

## Dependencies

### USAGE
Expand Down
69 changes: 68 additions & 1 deletion specs/spec.jsfsa.Automaton.js
Expand Up @@ -304,4 +304,71 @@ describe("jsfsa.Automaton", function(){
expect( spy ).toHaveBeenCalledWith( e );
});
});
});
describe( "transition with action and guard", function(){
beforeEach( function(){
var vendingMachine = jasmine.createSpyObj('vendingMachine',
[ 'calculatePrice', 'accumulate',
'pricePayed', 'returnChange',
'printTicket' ]);
sm.vendingMachine = vendingMachine;
sm.createState('idle', {
transitions : {
'destinationSelected' : 'collectingMoney'
},
isInitial : true
});
sm.createState('collectingMoney', {
listeners: {
entered: function(e, destination) {
vendingMachine.calculatePrice(destination);
}
},
transitions : {
'coinInserted' : function(e, amount) {
vendingMachine.accumulate(amount);
if(vendingMachine.pricePayed()) {
return 'printingTicket';
}
},
'cancel': function(e, amount) {
vendingMachine.returnChange();
}
}
});
sm.createState('printingTicket', {
listeners: {
entered: function() {
vendingMachine.printTicket();
},
exited: function() {
vendingMachine.returnChange();
}
},
transitions : {
'ticketPrinted' : 'idle'
}
});
} );
it( "should execute an action before entering the target state", function(){
var vendingMachine = sm.vendingMachine;
sm.doTransition('destinationSelected', 'Main Station');
expect( vendingMachine.calculatePrice ).toHaveBeenCalledWith('Main Station');
expect( sm.getCurrentState() ).toEqual( sm.getState( 'collectingMoney' ) );

vendingMachine.pricePayed.andReturn(false);
sm.doTransition('coinInserted', 200);
expect( vendingMachine.accumulate ).toHaveBeenCalledWith(200);
expect( sm.getCurrentState() ).toEqual( sm.getState( 'collectingMoney' ) );

vendingMachine.pricePayed.andReturn(true);
sm.doTransition('coinInserted', 50);
expect( vendingMachine.accumulate ).toHaveBeenCalledWith(50);
expect( sm.getCurrentState() ).toEqual( sm.getState( 'printingTicket' ) );

sm.doTransition('ticketPrinted');
expect( vendingMachine.returnChange ).toHaveBeenCalled();
expect( sm.getCurrentState() ).toEqual( sm.getState( 'idle' ) );
});
});
// });
});
15 changes: 13 additions & 2 deletions src/jsfsa.js
Expand Up @@ -938,8 +938,19 @@ var jsfsa;
};

Automaton.prototype._attemptTransition = function (sourceNode, eventFactory) {
var targetNode = this._nodes[sourceNode.getTransition(eventFactory.transition)];
if (!targetNode) {
var targetSpec = sourceNode.getTransition( eventFactory.transition );
var targetName;
if (typeof targetSpec === 'function') {
var args = eventFactory.createArgsArray();
targetName = targetSpec.apply(this, args);
} else {
targetName = targetSpec;
}
var targetNode = null;
if ( targetName ) {
targetNode = this._nodes[ targetName ];
}
if ( !targetNode ) {
//state doesn't exist
this._finishTransition(eventFactory.createArgsArray(StateEvent.TRANSITION_DENIED));
} else {
Expand Down

0 comments on commit d3cf73a

Please sign in to comment.