hifsm 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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