hifsm 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e43ef721bec5c9d473610831836bf1531ae5c782
4
- data.tar.gz: c6396a4f8ecd0f81c2963a84685bdae93a1dae3c
3
+ metadata.gz: 25b5e3332cdc03d7cdc7efee6fa52b83a2a85881
4
+ data.tar.gz: 52f1ffd9779265883292b018ed76ee480032cbf5
5
5
  SHA512:
6
- metadata.gz: 6c586dba477b583fd85e1bb4eedac0df1610af363d178569645f51a0aa4820c385edd21709149d3dce2a997f95d847ece2a6cbf3a96b864582b01d7f689b8be5
7
- data.tar.gz: 90ffecc59fc1bf1a8af8cf54c2a70cf7c48a52a6a37ae84bf3ce92da72a89102449d94688c63ece647c42987d10fafb60ee063feecd6612decdd9d00a01db52c
6
+ metadata.gz: ecec06c7780e4ab19a6c98fa6191d5a3aeea238ba5c790d0b9a3a02a87b68838ec36c166bbdec15f8d75f9d125d0197f6b79d1833f2e1bcb73f96b110df702e2
7
+ data.tar.gz: 4e113ea01e29154d6cf2b62a08243a0b7a4054a9c254f59dffc468874f94b843e94c7586de327d0743dfda2c2f98c0e6f775c6168a968011ec3ef62f9d3d20c7
data/README.md CHANGED
@@ -39,7 +39,7 @@ Here is how to use it to model a monster in a Quake-like game. It covers most Hi
39
39
  require 'hifsm'
40
40
 
41
41
  class Monster
42
- @@fsm = Hifsm::FSM.define do
42
+ @@fsm = Hifsm::FSM.new do
43
43
  state :idle, :initial => true
44
44
  state :attacking do
45
45
  state :acquiring_target, :initial => true do
@@ -97,7 +97,7 @@ class Monster
97
97
  def initialize
98
98
  @debug = false
99
99
  @home = 'home'
100
- @state = @@fsm.new(self) # or @@fsm.new(self, 'attacking.pursuing')
100
+ @state = @@fsm.instantiate(self) # or @@fsm.instantiate(self, 'attacking.pursuing')
101
101
  @tick = 1
102
102
  @low_hp = false
103
103
  end
@@ -155,6 +155,9 @@ ogre.act! # -> ~~> player2
155
155
  ogre.enemy_dead # -> Woohoo!
156
156
  ogre.act! # -> Acting @coming_back
157
157
  # -> step step home
158
+ ogre.low_hp = true
159
+ ogre.sight 'player3'
160
+ ogre.act! # -> Acting @runaway
158
161
 
159
162
  ```
160
163
 
@@ -174,10 +177,18 @@ On event:
174
177
  * to_state.after_enter
175
178
  * event.after
176
179
 
177
- If `before...` callback returns Hifsm.cancel then no further processing is done
180
+ If any of `before...` callbacks returns `false` then no further processing is done, no exceptions raised, machine state is not changed.
178
181
 
179
182
  On `act!` just calls action block if it was given.
180
183
 
184
+ ## Testing
185
+
186
+ Only 'public' API is unit-tested, internal implementation may be freely changed, so don't rely on it.
187
+
188
+ To run tests use `bundle exec rake test`
189
+
190
+ Try also `bundle exec ruby test/monster.rb`
191
+
181
192
  ## Contributing
182
193
 
183
194
  1. Fork it
@@ -4,9 +4,13 @@ class Callbacks
4
4
  def invoke(target, cb, *args)
5
5
  if cb.nil?
6
6
  elsif cb.is_a? Symbol
7
- target.send(cb, *args)
7
+ if target.method(cb).arity > 0
8
+ target.send(cb, *args)
9
+ else
10
+ target.send(cb)
11
+ end
8
12
  else
9
- target.instance_exec *args, &cb
13
+ target.instance_exec(*args, &cb)
10
14
  end
11
15
  end
12
16
  end
@@ -15,12 +19,12 @@ class Callbacks
15
19
  @listeners = []
16
20
  end
17
21
 
18
- def add(&callback)
19
- @listeners.push callback
22
+ def add(symbol = nil, &callback)
23
+ @listeners.push symbol || callback
20
24
  end
21
25
 
22
26
  def trigger(target, *args)
23
- @listeners.each do |callback|
27
+ @listeners.map do |callback|
24
28
  Callbacks.invoke target, callback, *args
25
29
  end
26
30
  end
data/lib/hifsm/event.rb CHANGED
@@ -1,15 +1,17 @@
1
1
  module Hifsm
2
2
  class Event
3
- CALLBACKS = [:before, :after].freeze
3
+ CALLBACKS = [:before, :after, :guard].freeze
4
4
 
5
5
  attr_reader :name, :to
6
6
 
7
- def initialize(name, to, guard)
7
+ def initialize(name, to, guards)
8
8
  @name = name
9
- @guard = guard
10
9
  @to = to
11
-
12
10
  @callbacks = Hash.new {|h, key| h[key] = Callbacks.new }
11
+
12
+ guards.each do |g|
13
+ @callbacks[:guard].add g
14
+ end
13
15
  end
14
16
 
15
17
  CALLBACKS.each do |cb|
@@ -20,8 +22,8 @@ module Hifsm
20
22
  @callbacks[cb].trigger(target, *args)
21
23
  end
22
24
 
23
- def guard?(target)
24
- !@guard || Callbacks.invoke(target, @guard)
25
+ def guard?(target, *args)
26
+ trigger(target, :guard, *args).all?
25
27
  end
26
28
  end
27
29
  end
data/lib/hifsm/fsm.rb CHANGED
@@ -2,22 +2,16 @@ module Hifsm
2
2
  class FSM
3
3
  attr_reader :states, :transitions
4
4
 
5
- class <<self
6
- def define(&block)
7
- Hifsm::FSM.new(&block)
8
- end
9
- end
10
-
11
5
  def initialize(parent = nil, &block)
12
6
  @parent = parent
13
7
  @states = {}
14
- @initial_state
8
+ @initial_state = nil
15
9
 
16
- instance_eval &block if block
10
+ instance_eval(&block) if block
17
11
  end
18
12
 
19
- def new(target = nil, initial_state = nil)
20
- Hifsm::Machine.new(target, self, initial_state)
13
+ def instantiate(target = nil, initial_state = nil)
14
+ Hifsm::Machine.new(self, target, initial_state)
21
15
  end
22
16
 
23
17
  def all_events
@@ -29,24 +23,30 @@ module Hifsm
29
23
  end
30
24
 
31
25
  def get_state!(name)
32
- @states[name.to_s] || raise(Hifsm::MissingState.new(name.to_s))
26
+ top_level_state, rest = name.to_s.split('.', 2)
27
+ st = @states[top_level_state] || raise(Hifsm::MissingState.new(name.to_s))
28
+ if rest
29
+ st.get_substate!(rest)
30
+ else
31
+ st
32
+ end
33
33
  end
34
34
 
35
35
  def event(name, options, &block)
36
- ev = Hifsm::Event.new(name, get_state!(options[:to]), options[:guard])
36
+ ev = Hifsm::Event.new(name, get_state!(options[:to]), array_wrap(options[:guard]))
37
37
  from_states = array_wrap(options[:from])
38
38
  from_states = @states.keys if from_states.empty?
39
39
  from_states.each do |from|
40
40
  st = get_state!(from)
41
41
  st.add_transition(ev)
42
42
  end
43
- ev.instance_eval &block if block
43
+ ev.instance_eval(&block) if block
44
44
  end
45
45
 
46
46
  def state(name, options = {}, &block)
47
47
  st = @states[name.to_s] = Hifsm::State.new(name, @parent)
48
48
  @initial_state = st if options[:initial]
49
- st.instance_eval &block if block
49
+ st.instance_eval(&block) if block
50
50
  end
51
51
 
52
52
  private
data/lib/hifsm/machine.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  module Hifsm
2
2
  class Machine
3
- def initialize(target, fsm, initial_state = nil)
3
+ def initialize(fsm, target, initial_state = nil)
4
4
  @target = target || self
5
5
  @fsm = fsm
6
6
 
7
- @state = fsm.states[initial_state] || fsm.initial_state!
7
+ @state = (initial_state && fsm.get_state!(initial_state) || fsm.initial_state!).enter!
8
8
 
9
9
  mach = self
10
10
  fsm.all_events.each do |event_name, event|
data/lib/hifsm/state.rb CHANGED
@@ -5,7 +5,8 @@ module Hifsm
5
5
  def initialize(name, parent = nil)
6
6
  @name = name
7
7
  @parent = parent
8
- @action
8
+ @action = nil
9
+ @sub_fsm = nil
9
10
 
10
11
  @callbacks = Hash.new {|h, key| h[key] = Callbacks.new }
11
12
  @transitions = Hash.new {|h, key| h[key] = Array.new }
@@ -21,11 +22,11 @@ module Hifsm
21
22
  end
22
23
 
23
24
  def state(*args, &block)
24
- sub_fsm!.state *args, &block
25
+ sub_fsm!.state(*args, &block)
25
26
  end
26
27
 
27
28
  def event(*args, &block)
28
- sub_fsm!.event *args, &block
29
+ sub_fsm!.event(*args, &block)
29
30
  end
30
31
 
31
32
  def events
@@ -35,7 +36,7 @@ module Hifsm
35
36
  def fire(target, event_name, *args, &new_state_callback)
36
37
  event_name = event_name.to_s
37
38
  @transitions[event_name].each do |ev|
38
- if ev.guard?(target)
39
+ if ev.guard?(target, *args)
39
40
  from_state = self
40
41
  to_state = ev.to
41
42
  if ev.trigger(target, :before, *args) &&
@@ -46,7 +47,7 @@ module Hifsm
46
47
  to_state.trigger(target, :after_enter, *args)
47
48
  ev.trigger(target, :after, *args)
48
49
  end
49
- return
50
+ return target
50
51
  end
51
52
  end
52
53
  if @parent
@@ -64,9 +65,8 @@ module Hifsm
64
65
  end
65
66
 
66
67
  def act!(target, *args)
68
+ @parent.act!(target, *args) if @parent
67
69
  @action && Callbacks.invoke(target, @action, *args)
68
- if @sub_fsm
69
- end
70
70
  end
71
71
 
72
72
  def enter!
@@ -77,6 +77,11 @@ module Hifsm
77
77
  end
78
78
  end
79
79
 
80
+ def get_substate!(name)
81
+ raise Hifsm::MissingState.new(name.to_s) unless @sub_fsm
82
+ @sub_fsm.get_state!(name)
83
+ end
84
+
80
85
  def to_s
81
86
  if @parent
82
87
  "#{@parent.to_s}.#{@name.to_s}"
data/lib/hifsm/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Hifsm
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/test/monster.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'hifsm'
2
2
 
3
3
  class Monster
4
- @@fsm = Hifsm::FSM.define do
4
+ @@fsm = Hifsm::FSM.new do
5
5
  state :idle, :initial => true
6
6
  state :attacking do
7
7
  state :acquiring_target, :initial => true do
@@ -59,7 +59,7 @@ class Monster
59
59
  def initialize
60
60
  @debug = false
61
61
  @home = 'home'
62
- @state = @@fsm.new(self) # or @@fsm.new(self, 'attacking.pursuing')
62
+ @state = @@fsm.instantiate(self) # or @@fsm.instantiate(self, 'attacking.pursuing')
63
63
  @tick = 1
64
64
  @low_hp = false
65
65
  end
@@ -118,4 +118,7 @@ if $0 == __FILE__
118
118
  ogre.enemy_dead # -> Woohoo!
119
119
  ogre.act! # -> Acting @coming_back
120
120
  # -> step step home
121
+ ogre.low_hp = true
122
+ ogre.sight 'player3'
123
+ ogre.act! # -> Acting @runaway
121
124
  end
@@ -2,7 +2,7 @@ require 'setup_tests'
2
2
 
3
3
  class TestAnyStateEvent < Minitest::Test
4
4
  def setup
5
- @fsm = Hifsm::FSM.define do
5
+ @fsm = Hifsm::FSM.new do
6
6
  state :off, :initial => true
7
7
  state :on
8
8
  state :halt
@@ -11,7 +11,7 @@ class TestAnyStateEvent < Minitest::Test
11
11
  event :toggle, :from => :on, :to => :off
12
12
  event :halt, :to => :halt
13
13
  end
14
- @machine = @fsm.new
14
+ @machine = @fsm.instantiate
15
15
  end
16
16
 
17
17
  def test_halt_from_off
@@ -2,14 +2,14 @@ require 'setup_tests'
2
2
 
3
3
  class TestBasicFSM < Minitest::Test
4
4
  def setup
5
- @fsm = Hifsm::FSM.define do
5
+ @fsm = Hifsm::FSM.new do
6
6
  state :off, :initial => true
7
7
  state :on
8
8
 
9
9
  event :toggle, :from => :off, :to => :on
10
10
  event :toggle, :from => :on, :to => :off
11
11
  end
12
- @machine = @fsm.new
12
+ @machine = @fsm.instantiate
13
13
  end
14
14
 
15
15
  def test_initial_state_is_off
@@ -1,27 +1,30 @@
1
1
  require 'setup_tests'
2
2
 
3
3
  class TestEventGuard < Minitest::Test
4
- def setup
5
- @wall = Struct.new(:stones).new(10)
4
+ Wall = Struct.new(:stones)
6
5
 
7
- @fsm = Hifsm::FSM.define do
6
+ def build_wall(thickness)
7
+ fsm = Hifsm::FSM.new do
8
8
  state :constructed, :initial => true
9
9
  state :broken
10
10
 
11
11
  event :break, :from => :constructed, :to => :broken, :guard => proc { stones < 5 }
12
12
  end
13
- @machine = @fsm.new(@wall)
13
+ wall = Wall.new(thickness)
14
+ @machine = fsm.instantiate(wall)
15
+ wall
14
16
  end
15
17
 
16
18
  def test_cant_break_wall_10_stones_thick
19
+ wall = build_wall 10
17
20
  assert_raises(Hifsm::MissingTransition) do
18
- @wall.break
21
+ wall.break
19
22
  end
20
23
  end
21
24
 
22
25
  def test_cant_break_thin_wall
23
- @wall.stones = 3
24
- @wall.break
26
+ wall = build_wall 3
27
+ wall.break
25
28
  assert_equal 'broken', @machine.state
26
29
  end
27
30
 
@@ -0,0 +1,58 @@
1
+ require 'setup_tests'
2
+
3
+ class TestHierarchical < Minitest::Test
4
+ def setup
5
+ @fsm = Hifsm::FSM.new do
6
+ async = proc do
7
+ state :pending, :initial => true
8
+ state :sync
9
+
10
+ event :sync, :from => :pending, :to => :sync
11
+ end
12
+
13
+ state :off, :initial => true, &async
14
+ state :on, &async
15
+
16
+ event :toggle, :from => 'off.sync', :to => :on
17
+ event :toggle, :from => 'on.sync', :to => 'off.sync'
18
+ end
19
+ end
20
+
21
+ def test_initial_state_is_off_pending_by_default
22
+ machine = @fsm.instantiate
23
+ assert_equal 'off.pending', machine.state
24
+ end
25
+
26
+ def test_explicit_initial_state
27
+ machine2 = @fsm.instantiate(nil, 'on.sync')
28
+ assert_equal 'on.sync', machine2.state
29
+ machine2.toggle
30
+ pass # assert_nothing_raised
31
+ end
32
+
33
+ def test_toggle_raises_an_error_in_pending_state
34
+ machine = @fsm.instantiate
35
+ assert_raises(Hifsm::MissingTransition) do
36
+ machine.toggle
37
+ end
38
+ end
39
+
40
+ def test_sync
41
+ machine = @fsm.instantiate
42
+ machine.sync
43
+ assert_equal 'off.sync', machine.state
44
+ end
45
+
46
+ def test_toggle_from_off_sync_to_on_pending
47
+ machine = @fsm.instantiate
48
+ machine.sync
49
+ machine.toggle
50
+ assert_equal 'on.pending', machine.state
51
+ end
52
+
53
+ def test_toggle_from_on_sync_to_off_sync
54
+ machine2 = @fsm.instantiate(nil, 'on.sync')
55
+ machine2.toggle
56
+ assert_equal 'off.sync', machine2.state
57
+ end
58
+ end
@@ -0,0 +1,41 @@
1
+ require 'setup_tests'
2
+
3
+ class TestIflessFactorial < Minitest::Test
4
+ Value = Struct.new(:value)
5
+
6
+ def setup
7
+ @fsm = Hifsm::FSM.new do
8
+ state :idle, :initial => true
9
+ state :counting
10
+
11
+ event :count, :to => :idle do
12
+ guard { |x| x == 0 }
13
+ after { |x| self.value = 1 }
14
+ end
15
+ event :count, :to => :counting do
16
+ after do |x|
17
+ count(x - 1)
18
+ self.value *= x
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ def factorial(n)
25
+ val = Value.new
26
+ @fsm.instantiate(val)
27
+ val.count(n).value
28
+ end
29
+
30
+ def test_factorial_0
31
+ assert_equal 1, factorial(0)
32
+ end
33
+
34
+ def test_factorial_5
35
+ assert_equal 120, factorial(5)
36
+ end
37
+
38
+ def test_factorial_100
39
+ assert_equal 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000, factorial(100)
40
+ end
41
+ end
@@ -0,0 +1,34 @@
1
+ require 'setup_tests'
2
+
3
+ class TestManyStates < Minitest::Test
4
+ def inefficient_factorial(n)
5
+ fsm = Hifsm::FSM.new do
6
+ state :value0 do
7
+ action { 1 }
8
+ end
9
+ (1..n).each do |x|
10
+ state "value#{x}" do
11
+ action do
12
+ x * prev.act!
13
+ end
14
+ end
15
+ event :prev, :from => "value#{x}", :to => "value#{x - 1}"
16
+ end
17
+ end
18
+ machine = fsm.instantiate(nil, "value#{n}")
19
+ machine.act!
20
+ end
21
+
22
+ def test_factorial_0
23
+ assert_equal 1, inefficient_factorial(0)
24
+ end
25
+
26
+ def test_factorial_5
27
+ assert_equal 120, inefficient_factorial(5)
28
+ end
29
+
30
+ def test_factorial_100
31
+ assert_equal 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000, inefficient_factorial(100)
32
+ end
33
+
34
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hifsm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Meremyanin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-09-01 00:00:00.000000000 Z
11
+ date: 2014-09-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -78,6 +78,9 @@ files:
78
78
  - test/test_any_state_event.rb
79
79
  - test/test_basic_fsm.rb
80
80
  - test/test_event_guard.rb
81
+ - test/test_hierarchical.rb
82
+ - test/test_ifless_factorial.rb
83
+ - test/test_many_states.rb
81
84
  - test/test_monster.rb
82
85
  homepage: http://github.com/stiff/hifsm
83
86
  licenses:
@@ -110,4 +113,7 @@ test_files:
110
113
  - test/test_any_state_event.rb
111
114
  - test/test_basic_fsm.rb
112
115
  - test/test_event_guard.rb
116
+ - test/test_hierarchical.rb
117
+ - test/test_ifless_factorial.rb
118
+ - test/test_many_states.rb
113
119
  - test/test_monster.rb