diff --git a/docs/do want.md b/docs/do want.md index 8a5fafed..3dac420b 100644 --- a/docs/do want.md +++ b/docs/do want.md @@ -3,10 +3,10 @@ - [x] action names (edge names are unique, action names are unique-to-source,) - [ ] the probability of an edge, - [ ] being the most probable edge, - - [ ] which states are "complete" (that is, that an input sequence can be considered satisfactorily terminal), - - [ ] whether a machine is + - [x] which states are "complete" (that is, that an input sequence can be considered satisfactorily terminal), + - [x] whether a machine is - [x] complete, - - [ ] final (not is_changing, complete, and terminal), + - [x] final (not is_changing, complete, and terminal), - [ ] edges as force-only (eg to "off" in the traffic light) - [ ] actions as force-only (also eg to "off") - [ ] the ability to list @@ -64,6 +64,12 @@ - [ ] data change, - [ ] init, - [ ] non-matching event +- [ ] timers (todo needs fleshing out) +- [ ] language support? + - [ ] promise support? + - [ ] generator support? + - [ ] observable support? + - [ ] async await support? - [x] the ability to generate - [x] flowchart representations - [x] as DOT strings diff --git a/package.json b/package.json index 48339f04..0bc31f93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jssm", - "version": "0.15.0", + "version": "0.16.0", "description": "A Javascript state machine with a simple API. Well tested, and typed with Flowtype. MIT License.", "main": "dist/jssm.es5.browserified.js", "scripts": { diff --git a/src/js/jssm-tests.js b/src/js/jssm-tests.js index bc1e1a2b..5eaa4d56 100644 --- a/src/js/jssm-tests.js +++ b/src/js/jssm-tests.js @@ -44,6 +44,68 @@ describe('Simple stop light', async it => { +describe('Stochastic weather', async it => { + + const weather = new jssm.machine({ + + initial_state: 'breezy', + + transitions:[ + + { from: 'breezy', to: 'breezy', probability: 0.4 }, + { from: 'breezy', to: 'sunny', probability: 0.3 }, + { from: 'breezy', to: 'cloudy', probability: 0.15 }, + { from: 'breezy', to: 'windy', probability: 0.1 }, + { from: 'breezy', to: 'rain', probability: 0.05 }, + + { from: 'sunny', to: 'sunny', probability: 0.5 }, + { from: 'sunny', to: 'hot', probability: 0.15 }, + { from: 'sunny', to: 'breezy', probability: 0.15 }, + { from: 'sunny', to: 'cloudy', probability: 0.15 }, + { from: 'sunny', to: 'rain', probability: 0.05 }, + + { from: 'hot', to: 'hot', probability: 0.75 }, + { from: 'hot', to: 'breezy', probability: 0.05 }, + { from: 'hot', to: 'sunny', probability: 0.2 }, + + { from: 'cloudy', to: 'cloudy', probability: 0.6 }, + { from: 'cloudy', to: 'sunny', probability: 0.2 }, + { from: 'cloudy', to: 'rain', probability: 0.15 }, + { from: 'cloudy', to: 'breezy', probability: 0.05 }, + + { from: 'windy', to: 'windy', probability: 0.3 }, + { from: 'windy', to: 'gale', probability: 0.1 }, + { from: 'windy', to: 'breezy', probability: 0.4 }, + { from: 'windy', to: 'rain', probability: 0.15 }, + { from: 'windy', to: 'sunny', probability: 0.05 }, + + { from: 'gale', to: 'gale', probability: 0.65 }, + { from: 'gale', to: 'windy', probability: 0.25 }, + { from: 'gale', to: 'torrent', probability: 0.05 }, + { from: 'gale', to: 'hot', probability: 0.05 }, + + { from: 'rain', to: 'rain', probability: 0.3 }, + { from: 'rain', to: 'torrent', probability: 0.05 }, + { from: 'rain', to: 'windy', probability: 0.1 }, + { from: 'rain', to: 'breezy', probability: 0.15 }, + { from: 'rain', to: 'sunny', probability: 0.1 }, + { from: 'rain', to: 'cloudy', probability: 0.3 }, + + { from: 'torrent', to: 'torrent', probability: 0.65 }, + { from: 'torrent', to: 'rain', probability: 0.25 }, + { from: 'torrent', to: 'cloudy', probability: 0.05 }, + { from: 'torrent', to: 'gale', probability: 0.05 } + + ] + + }); + +}); + + + + + describe('Complex stop light', async it => { const light2 = new jssm.machine({ diff --git a/src/js/jssm-types.js b/src/js/jssm-types.js index 806a44d5..33a8d7fe 100644 --- a/src/js/jssm-types.js +++ b/src/js/jssm-types.js @@ -77,13 +77,13 @@ type JssmGenericMachine = { type JssmTransition = { - from : NT, - to : NT, - name? : string, - action? : string, - check? : JssmTransitionPermitterMaybeArray, // validate this edge's transition; usually about data - likelihood? : number, // for stoch modelling, would like to constrain to [0..1], dunno how - usual? : '' // most common exit, for graphing; likelihood overrides + from : NT, + to : NT, + name? : string, + action? : string, + check? : JssmTransitionPermitterMaybeArray, // validate this edge's transition; usually about data + probability? : number, // for stoch modelling, would like to constrain to [0..1], dunno how + usual? : '' // most common exit, for graphing; likelihood overrides }; type JssmTransitions = Array< JssmTransition >; @@ -114,6 +114,8 @@ type JssmGenericConfig = { allow_force? : false, actions? : JssmPermittedOpt, + simplify_bidi? : boolean, + auto_api? : boolean | string; // boolean false means don't; boolean true means do; string means do-with-this-prefix }; diff --git a/src/js/jssm.js b/src/js/jssm.js index dacf6d04..a75dff64 100644 --- a/src/js/jssm.js +++ b/src/js/jssm.js @@ -151,10 +151,14 @@ todo comeback return this._state; } - in_flux() : boolean { + is_changing() : boolean { return true; // todo whargarbl } + is_final() : boolean { + return ( (!this.is_changing()) && (this.is_terminal()) && (this.is_complete()) ); + } + machine_state() : JssmMachineInternalState { @@ -308,6 +312,8 @@ todo comeback // can leave machine in inconsistent state. generally do not use force_transition(newState : mNT, newData? : mDT) : boolean { + // todo whargarbl implement hooks + // todo whargarbl implement data stuff if (this.valid_force_transition(newState, newData)) { this._state = newState; return true; @@ -340,17 +346,51 @@ todo comeback const nodes = l_states.map( (s:any) => `${node_of(s)} [label="${s}"];`).join(' '); - const edges = this.states().map( (s:any) => + const strike = []; + const edges = this.states().map( (s:any) => this.exits_for(s).map( (ex:any) => { - const edge = this.edge(s, ex), - label = edge? (edge.name || undefined) : undefined; - return `${node_of(s)}->${node_of(ex)} [${label? `label="${(label:any)}"`:''} len=2];`; + + if ( strike.find(row => (row[0] === s) && (row[1] == ex) ) ) { + return ''; // already did the pair + } + + const edge = this.edge(s, ex), + pair = this.edge(ex, s), + double = pair && (s !== ex), + +// label = edge ? ([edge.name?`${(edge.name:any)}`:undefined,`${(edge.probability:any)}`] +// .filter(not_undef => !!not_undef) +// .join('\n') || undefined +// ) : undefined, + + if_obj_field = (obj, field) => obj? obj[field] : undefined, + + label = edge ? (`label="${ (edge.name : any)}";` || '') : '', + headlabel = pair ? (`headlabel="${(pair.probability : any)}";` || '') : '', + taillabel = edge ? (`taillabel="${(edge.probability : any)}";` || '') : '', + + labelInline = [ + [edge, 'name', 'label'], + [pair, 'probability', 'headlabel'], + [edge, 'probability', 'taillabel'] + ] + .map( r => ({ which: r[2], whether: if_obj_field(r[0], r[1]) }) ) + .filter( present => present.whether ) + .map( r => `${r.which}="${(r.whether : any)}"`) + .join(' '), + + edgeInline = edge ? (double? 'dir=both;color="#777777:#555555"' : 'color="#555555"') : ''; + + if (pair) { strike.push([ex, s]); } + + return `${node_of(s)}->${node_of(ex)} [${labelInline}${edgeInline}];`; + }).join(' ') ).join(' '); - return `digraph G {\n fontname="helvetica neue";\n style=filled;\n bgcolor=lightgrey;\n node [shape=box; style=filled; fillcolor=white; fontname="helvetica neue"];\n edge [len=2; fontname="helvetica neue"];\n\n ${nodes}\n\n ${edges}\n}`; + return `digraph G {\n fontname="helvetica neue";\n style=filled;\n bgcolor=lightgrey;\n node [shape=box; style=filled; fillcolor=white; fontname="helvetica neue"];\n edge [fontsize=9;fontname="helvetica neue"];\n\n ${nodes}\n\n ${edges}\n}`; }