hifsm 0.1.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 +7 -0
- data/.gitignore +17 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +187 -0
- data/Rakefile +7 -0
- data/hifsm.gemspec +24 -0
- data/lib/hifsm/callbacks.rb +27 -0
- data/lib/hifsm/event.rb +27 -0
- data/lib/hifsm/fsm.rb +58 -0
- data/lib/hifsm/machine.rb +35 -0
- data/lib/hifsm/state.rb +93 -0
- data/lib/hifsm/version.rb +3 -0
- data/lib/hifsm.rb +21 -0
- data/test/minitest_helper.rb +4 -0
- data/test/monster.rb +121 -0
- data/test/setup_tests.rb +2 -0
- data/test/test_any_state_event.rb +28 -0
- data/test/test_basic_fsm.rb +29 -0
- data/test/test_event_guard.rb +28 -0
- data/test/test_monster.rb +44 -0
- metadata +113 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e43ef721bec5c9d473610831836bf1531ae5c782
|
4
|
+
data.tar.gz: c6396a4f8ecd0f81c2963a84685bdae93a1dae3c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6c586dba477b583fd85e1bb4eedac0df1610af363d178569645f51a0aa4820c385edd21709149d3dce2a997f95d847ece2a6cbf3a96b864582b01d7f689b8be5
|
7
|
+
data.tar.gz: 90ffecc59fc1bf1a8af8cf54c2a70cf7c48a52a6a37ae84bf3ce92da72a89102449d94688c63ece647c42987d10fafb60ee063feecd6612decdd9d00a01db52c
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Vladimir Meremyanin
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
# Hierarchical Finite State Machine in Ruby
|
2
|
+
|
3
|
+
This library was created from the desire to have nested states inspired by [rFSM](https://github.com/kmarkus/rFSM).
|
4
|
+
|
5
|
+
It can be used in plain old ruby objects, but works well with `ActiveRecord`s too.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'hifsm'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install hifsm
|
20
|
+
|
21
|
+
I prefer 1.8-style hashes, and since no advanced Ruby magic used it should work in 1.8, but only tested in 2+.
|
22
|
+
|
23
|
+
__This is in early development, so be careful.__
|
24
|
+
|
25
|
+
## Features
|
26
|
+
|
27
|
+
* Easy to use
|
28
|
+
* Any number of state machines per object
|
29
|
+
* Nested states
|
30
|
+
* Parameterised events
|
31
|
+
* Support of both Mealy and Moore machines
|
32
|
+
* Lightweight and non-obtrusive
|
33
|
+
|
34
|
+
## Usage
|
35
|
+
|
36
|
+
Here is how to use it to model a monster in a Quake-like game. It covers most Hifsm features:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
require 'hifsm'
|
40
|
+
|
41
|
+
class Monster
|
42
|
+
@@fsm = Hifsm::FSM.define do
|
43
|
+
state :idle, :initial => true
|
44
|
+
state :attacking do
|
45
|
+
state :acquiring_target, :initial => true do
|
46
|
+
action do
|
47
|
+
# self is the monster instance here
|
48
|
+
plan_attack
|
49
|
+
end
|
50
|
+
end
|
51
|
+
state :pursuing do
|
52
|
+
before_enter do
|
53
|
+
self.roar!
|
54
|
+
end
|
55
|
+
action do
|
56
|
+
step_towards target
|
57
|
+
end
|
58
|
+
end
|
59
|
+
state :fighting do
|
60
|
+
action do
|
61
|
+
hit target
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
event :acquire, :from => :acquiring_target, :to => :pursuing
|
66
|
+
event :reached, :from => :pursuing, :to => :fighting
|
67
|
+
|
68
|
+
action do |tick|
|
69
|
+
debug && puts("#{tick}: Attack!")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
state :coming_back do
|
73
|
+
action do
|
74
|
+
step_towards @home
|
75
|
+
end
|
76
|
+
end
|
77
|
+
state :runaway
|
78
|
+
|
79
|
+
event :sight, :from => [:idle, :coming_back], :to => :runaway, :guard => :low_hp?
|
80
|
+
event :sight, :from => [:idle, :coming_back], :to => :attacking do
|
81
|
+
before do |t|
|
82
|
+
debug && puts("Setting target to #{t}")
|
83
|
+
self.target = t
|
84
|
+
end
|
85
|
+
end
|
86
|
+
event :enemy_dead, :from => :attacking, :to => :coming_back do
|
87
|
+
after do
|
88
|
+
debug && puts("Woohoo!")
|
89
|
+
self.target = nil
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
attr_accessor :target, :low_hp, :debug
|
95
|
+
attr_reader :state
|
96
|
+
|
97
|
+
def initialize
|
98
|
+
@debug = false
|
99
|
+
@home = 'home'
|
100
|
+
@state = @@fsm.new(self) # or @@fsm.new(self, 'attacking.pursuing')
|
101
|
+
@tick = 1
|
102
|
+
@low_hp = false
|
103
|
+
end
|
104
|
+
|
105
|
+
def act!
|
106
|
+
debug && puts("Acting @#{@state}")
|
107
|
+
@state.act!(@tick)
|
108
|
+
@tick = @tick + 1
|
109
|
+
end
|
110
|
+
|
111
|
+
def hit(target)
|
112
|
+
debug && puts("~~> #{target}")
|
113
|
+
end
|
114
|
+
|
115
|
+
def low_hp?
|
116
|
+
@low_hp
|
117
|
+
end
|
118
|
+
|
119
|
+
def plan_attack
|
120
|
+
debug && puts("planning...")
|
121
|
+
acquire
|
122
|
+
end
|
123
|
+
|
124
|
+
def roar!
|
125
|
+
debug && puts("AARGHH!")
|
126
|
+
end
|
127
|
+
|
128
|
+
def step_towards(target)
|
129
|
+
debug && puts("step step #{target}")
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
ogre = Monster.new
|
135
|
+
ogre.debug = true # Console output:
|
136
|
+
ogre.act! # -> Acting @idle
|
137
|
+
ogre.sight 'player' # -> Setting target to player
|
138
|
+
ogre.act! # -> Acting @attacking.acquiring_target
|
139
|
+
# -> planning...
|
140
|
+
# -> AARGHH!
|
141
|
+
# ogre.acquire -> Hifsm::MissingTransition, already acquired in act!
|
142
|
+
ogre.act! # -> Acting @attacking.pursuing
|
143
|
+
# -> step step player
|
144
|
+
ogre.enemy_dead # -> Woohoo!
|
145
|
+
ogre.act! # -> Acting @coming_back
|
146
|
+
|
147
|
+
ogre.sight 'player2' # -> Setting target to player2
|
148
|
+
ogre.acquire # -> AARGHH!
|
149
|
+
ogre.act! # -> Acting @attacking.pursuing
|
150
|
+
# -> step step player2
|
151
|
+
ogre.reached
|
152
|
+
puts ogre.state # -> attacking.fighting
|
153
|
+
ogre.act! # -> ~~> player2
|
154
|
+
5.times { ogre.act! } # -> ...
|
155
|
+
ogre.enemy_dead # -> Woohoo!
|
156
|
+
ogre.act! # -> Acting @coming_back
|
157
|
+
# -> step step home
|
158
|
+
|
159
|
+
```
|
160
|
+
|
161
|
+
## Guards
|
162
|
+
|
163
|
+
Events are tried in order they were defined, if guard callback returns `false` then event is skipped.
|
164
|
+
|
165
|
+
## Callbacks
|
166
|
+
|
167
|
+
On event:
|
168
|
+
|
169
|
+
* event.before
|
170
|
+
* to_state.before_enter
|
171
|
+
* from_state.before_exit
|
172
|
+
* *state changes*
|
173
|
+
* from_state.after_exit
|
174
|
+
* to_state.after_enter
|
175
|
+
* event.after
|
176
|
+
|
177
|
+
If `before...` callback returns Hifsm.cancel then no further processing is done
|
178
|
+
|
179
|
+
On `act!` just calls action block if it was given.
|
180
|
+
|
181
|
+
## Contributing
|
182
|
+
|
183
|
+
1. Fork it
|
184
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
185
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
186
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
187
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/hifsm.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'hifsm/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "hifsm"
|
8
|
+
spec.version = Hifsm::VERSION
|
9
|
+
spec.authors = ["Vladimir Meremyanin"]
|
10
|
+
spec.email = ["vladimir@meremyanin.com"]
|
11
|
+
spec.description = %q{FSM with support for nested states and parameterised events}
|
12
|
+
spec.summary = %q{Hierarchical state machines in Ruby}
|
13
|
+
spec.homepage = "http://github.com/stiff/hifsm"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "minitest"
|
24
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Callbacks
|
2
|
+
|
3
|
+
class <<self
|
4
|
+
def invoke(target, cb, *args)
|
5
|
+
if cb.nil?
|
6
|
+
elsif cb.is_a? Symbol
|
7
|
+
target.send(cb, *args)
|
8
|
+
else
|
9
|
+
target.instance_exec *args, &cb
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@listeners = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def add(&callback)
|
19
|
+
@listeners.push callback
|
20
|
+
end
|
21
|
+
|
22
|
+
def trigger(target, *args)
|
23
|
+
@listeners.each do |callback|
|
24
|
+
Callbacks.invoke target, callback, *args
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/hifsm/event.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Hifsm
|
2
|
+
class Event
|
3
|
+
CALLBACKS = [:before, :after].freeze
|
4
|
+
|
5
|
+
attr_reader :name, :to
|
6
|
+
|
7
|
+
def initialize(name, to, guard)
|
8
|
+
@name = name
|
9
|
+
@guard = guard
|
10
|
+
@to = to
|
11
|
+
|
12
|
+
@callbacks = Hash.new {|h, key| h[key] = Callbacks.new }
|
13
|
+
end
|
14
|
+
|
15
|
+
CALLBACKS.each do |cb|
|
16
|
+
define_method(cb) { |&block| @callbacks[cb].add(&block) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def trigger(target, cb, *args)
|
20
|
+
@callbacks[cb].trigger(target, *args)
|
21
|
+
end
|
22
|
+
|
23
|
+
def guard?(target)
|
24
|
+
!@guard || Callbacks.invoke(target, @guard)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/hifsm/fsm.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module Hifsm
|
2
|
+
class FSM
|
3
|
+
attr_reader :states, :transitions
|
4
|
+
|
5
|
+
class <<self
|
6
|
+
def define(&block)
|
7
|
+
Hifsm::FSM.new(&block)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(parent = nil, &block)
|
12
|
+
@parent = parent
|
13
|
+
@states = {}
|
14
|
+
@initial_state
|
15
|
+
|
16
|
+
instance_eval &block if block
|
17
|
+
end
|
18
|
+
|
19
|
+
def new(target = nil, initial_state = nil)
|
20
|
+
Hifsm::Machine.new(target, self, initial_state)
|
21
|
+
end
|
22
|
+
|
23
|
+
def all_events
|
24
|
+
@states.collect {|name, st| st.events }.flatten.uniq
|
25
|
+
end
|
26
|
+
|
27
|
+
def initial_state!
|
28
|
+
@initial_state || raise(Hifsm::MissingState.new("<initial>"))
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_state!(name)
|
32
|
+
@states[name.to_s] || raise(Hifsm::MissingState.new(name.to_s))
|
33
|
+
end
|
34
|
+
|
35
|
+
def event(name, options, &block)
|
36
|
+
ev = Hifsm::Event.new(name, get_state!(options[:to]), options[:guard])
|
37
|
+
from_states = array_wrap(options[:from])
|
38
|
+
from_states = @states.keys if from_states.empty?
|
39
|
+
from_states.each do |from|
|
40
|
+
st = get_state!(from)
|
41
|
+
st.add_transition(ev)
|
42
|
+
end
|
43
|
+
ev.instance_eval &block if block
|
44
|
+
end
|
45
|
+
|
46
|
+
def state(name, options = {}, &block)
|
47
|
+
st = @states[name.to_s] = Hifsm::State.new(name, @parent)
|
48
|
+
@initial_state = st if options[:initial]
|
49
|
+
st.instance_eval &block if block
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
# like in ActiveSupport
|
54
|
+
def array_wrap(anything)
|
55
|
+
anything.is_a?(Array) ? anything : [anything].compact
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Hifsm
|
2
|
+
class Machine
|
3
|
+
def initialize(target, fsm, initial_state = nil)
|
4
|
+
@target = target || self
|
5
|
+
@fsm = fsm
|
6
|
+
|
7
|
+
@state = fsm.states[initial_state] || fsm.initial_state!
|
8
|
+
|
9
|
+
mach = self
|
10
|
+
fsm.all_events.each do |event_name, event|
|
11
|
+
@target.singleton_class.instance_exec do
|
12
|
+
define_method(event_name) {|*args| mach.fire(event_name, *args) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def act!(*args)
|
18
|
+
@state.act!(@target, *args)
|
19
|
+
end
|
20
|
+
|
21
|
+
def state
|
22
|
+
@state.to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
def fire(event, *args)
|
26
|
+
@state.fire(@target, event, *args) do |new_state|
|
27
|
+
@state = new_state
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
@state.to_s
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/hifsm/state.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
module Hifsm
|
2
|
+
class State
|
3
|
+
CALLBACKS = [:before_enter, :before_exit, :after_enter, :after_exit].freeze
|
4
|
+
|
5
|
+
def initialize(name, parent = nil)
|
6
|
+
@name = name
|
7
|
+
@parent = parent
|
8
|
+
@action
|
9
|
+
|
10
|
+
@callbacks = Hash.new {|h, key| h[key] = Callbacks.new }
|
11
|
+
@transitions = Hash.new {|h, key| h[key] = Array.new }
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_transition(ev)
|
15
|
+
name = ev.name.to_s
|
16
|
+
@transitions[name].push ev
|
17
|
+
end
|
18
|
+
|
19
|
+
def action(&block)
|
20
|
+
@action = block
|
21
|
+
end
|
22
|
+
|
23
|
+
def state(*args, &block)
|
24
|
+
sub_fsm!.state *args, &block
|
25
|
+
end
|
26
|
+
|
27
|
+
def event(*args, &block)
|
28
|
+
sub_fsm!.event *args, &block
|
29
|
+
end
|
30
|
+
|
31
|
+
def events
|
32
|
+
@transitions.keys + (@sub_fsm && @sub_fsm.all_events || [])
|
33
|
+
end
|
34
|
+
|
35
|
+
def fire(target, event_name, *args, &new_state_callback)
|
36
|
+
event_name = event_name.to_s
|
37
|
+
@transitions[event_name].each do |ev|
|
38
|
+
if ev.guard?(target)
|
39
|
+
from_state = self
|
40
|
+
to_state = ev.to
|
41
|
+
if ev.trigger(target, :before, *args) &&
|
42
|
+
to_state.trigger(target, :before_enter, *args) &&
|
43
|
+
from_state.trigger(target, :before_exit, *args)
|
44
|
+
new_state_callback.call(to_state.enter!)
|
45
|
+
from_state.trigger(target, :after_exit, *args)
|
46
|
+
to_state.trigger(target, :after_enter, *args)
|
47
|
+
ev.trigger(target, :after, *args)
|
48
|
+
end
|
49
|
+
return
|
50
|
+
end
|
51
|
+
end
|
52
|
+
if @parent
|
53
|
+
return @parent.fire(target, event_name, *args, &new_state_callback)
|
54
|
+
end
|
55
|
+
raise Hifsm::MissingTransition.new(to_s, event_name)
|
56
|
+
end
|
57
|
+
|
58
|
+
CALLBACKS.each do |cb|
|
59
|
+
define_method(cb) { |&block| @callbacks[cb].add(&block) }
|
60
|
+
end
|
61
|
+
|
62
|
+
def trigger(target, cb, *args)
|
63
|
+
@callbacks[cb].trigger(target, *args)
|
64
|
+
end
|
65
|
+
|
66
|
+
def act!(target, *args)
|
67
|
+
@action && Callbacks.invoke(target, @action, *args)
|
68
|
+
if @sub_fsm
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def enter!
|
73
|
+
if @sub_fsm
|
74
|
+
@sub_fsm.initial_state!
|
75
|
+
else
|
76
|
+
self
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_s
|
81
|
+
if @parent
|
82
|
+
"#{@parent.to_s}.#{@name.to_s}"
|
83
|
+
else
|
84
|
+
@name.to_s
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
def sub_fsm!
|
90
|
+
@sub_fsm ||= Hifsm::FSM.new(self)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/hifsm.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require "hifsm/callbacks"
|
2
|
+
require "hifsm/fsm"
|
3
|
+
require "hifsm/event"
|
4
|
+
require "hifsm/machine"
|
5
|
+
require "hifsm/state"
|
6
|
+
require "hifsm/version"
|
7
|
+
|
8
|
+
module Hifsm
|
9
|
+
class MissingTransition < StandardError
|
10
|
+
def initialize(state, name)
|
11
|
+
super "No transition :#{name} from :#{state}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class MissingState < StandardError
|
16
|
+
def initialize(name)
|
17
|
+
super "No state :#{name} defined"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
data/test/monster.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'hifsm'
|
2
|
+
|
3
|
+
class Monster
|
4
|
+
@@fsm = Hifsm::FSM.define do
|
5
|
+
state :idle, :initial => true
|
6
|
+
state :attacking do
|
7
|
+
state :acquiring_target, :initial => true do
|
8
|
+
action do
|
9
|
+
# self is the monster instance here
|
10
|
+
plan_attack
|
11
|
+
end
|
12
|
+
end
|
13
|
+
state :pursuing do
|
14
|
+
before_enter do
|
15
|
+
self.roar!
|
16
|
+
end
|
17
|
+
action do
|
18
|
+
step_towards target
|
19
|
+
end
|
20
|
+
end
|
21
|
+
state :fighting do
|
22
|
+
action do
|
23
|
+
hit target
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
event :acquire, :from => :acquiring_target, :to => :pursuing
|
28
|
+
event :reached, :from => :pursuing, :to => :fighting
|
29
|
+
|
30
|
+
action do |tick|
|
31
|
+
debug && puts("#{tick}: Attack!")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
state :coming_back do
|
35
|
+
action do
|
36
|
+
step_towards @home
|
37
|
+
end
|
38
|
+
end
|
39
|
+
state :runaway
|
40
|
+
|
41
|
+
event :sight, :from => [:idle, :coming_back], :to => :runaway, :guard => :low_hp?
|
42
|
+
event :sight, :from => [:idle, :coming_back], :to => :attacking do
|
43
|
+
before do |t|
|
44
|
+
debug && puts("Setting target to #{t}")
|
45
|
+
self.target = t
|
46
|
+
end
|
47
|
+
end
|
48
|
+
event :enemy_dead, :from => :attacking, :to => :coming_back do
|
49
|
+
after do
|
50
|
+
debug && puts("Woohoo!")
|
51
|
+
self.target = nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
attr_accessor :target, :low_hp, :debug
|
57
|
+
attr_reader :state
|
58
|
+
|
59
|
+
def initialize
|
60
|
+
@debug = false
|
61
|
+
@home = 'home'
|
62
|
+
@state = @@fsm.new(self) # or @@fsm.new(self, 'attacking.pursuing')
|
63
|
+
@tick = 1
|
64
|
+
@low_hp = false
|
65
|
+
end
|
66
|
+
|
67
|
+
def act!
|
68
|
+
debug && puts("Acting @#{@state}")
|
69
|
+
@state.act!(@tick)
|
70
|
+
@tick = @tick + 1
|
71
|
+
end
|
72
|
+
|
73
|
+
def hit(target)
|
74
|
+
debug && puts("~~> #{target}")
|
75
|
+
end
|
76
|
+
|
77
|
+
def low_hp?
|
78
|
+
@low_hp
|
79
|
+
end
|
80
|
+
|
81
|
+
def plan_attack
|
82
|
+
debug && puts("planning...")
|
83
|
+
acquire
|
84
|
+
end
|
85
|
+
|
86
|
+
def roar!
|
87
|
+
debug && puts("AARGHH!")
|
88
|
+
end
|
89
|
+
|
90
|
+
def step_towards(target)
|
91
|
+
debug && puts("step step #{target}")
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
if $0 == __FILE__
|
97
|
+
ogre = Monster.new
|
98
|
+
ogre.debug = true # Console output:
|
99
|
+
ogre.act! # -> Acting @idle
|
100
|
+
ogre.sight 'player' # -> Setting target to player
|
101
|
+
ogre.act! # -> Acting @attacking.acquiring_target
|
102
|
+
# -> planning...
|
103
|
+
# -> AARGHH!
|
104
|
+
# ogre.acquire -> Hifsm::MissingTransition, already acquired in act!
|
105
|
+
ogre.act! # -> Acting @attacking.pursuing
|
106
|
+
# -> step step player
|
107
|
+
ogre.enemy_dead # -> Woohoo!
|
108
|
+
ogre.act! # -> Acting @coming_back
|
109
|
+
|
110
|
+
ogre.sight 'player2' # -> Setting target to player2
|
111
|
+
ogre.acquire # -> AARGHH!
|
112
|
+
ogre.act! # -> Acting @attacking.pursuing
|
113
|
+
# -> step step player2
|
114
|
+
ogre.reached
|
115
|
+
puts ogre.state # -> attacking.fighting
|
116
|
+
ogre.act! # -> ~~> player2
|
117
|
+
5.times { ogre.act! } # -> ...
|
118
|
+
ogre.enemy_dead # -> Woohoo!
|
119
|
+
ogre.act! # -> Acting @coming_back
|
120
|
+
# -> step step home
|
121
|
+
end
|
data/test/setup_tests.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'setup_tests'
|
2
|
+
|
3
|
+
class TestAnyStateEvent < Minitest::Test
|
4
|
+
def setup
|
5
|
+
@fsm = Hifsm::FSM.define do
|
6
|
+
state :off, :initial => true
|
7
|
+
state :on
|
8
|
+
state :halt
|
9
|
+
|
10
|
+
event :toggle, :from => :off, :to => :on
|
11
|
+
event :toggle, :from => :on, :to => :off
|
12
|
+
event :halt, :to => :halt
|
13
|
+
end
|
14
|
+
@machine = @fsm.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_halt_from_off
|
18
|
+
@machine.halt
|
19
|
+
assert_equal 'halt', @machine.state
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_halt_from_on
|
23
|
+
@machine.toggle
|
24
|
+
@machine.halt
|
25
|
+
assert_equal 'halt', @machine.state
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'setup_tests'
|
2
|
+
|
3
|
+
class TestBasicFSM < Minitest::Test
|
4
|
+
def setup
|
5
|
+
@fsm = Hifsm::FSM.define do
|
6
|
+
state :off, :initial => true
|
7
|
+
state :on
|
8
|
+
|
9
|
+
event :toggle, :from => :off, :to => :on
|
10
|
+
event :toggle, :from => :on, :to => :off
|
11
|
+
end
|
12
|
+
@machine = @fsm.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_initial_state_is_off
|
16
|
+
assert_equal 'off', @machine.state
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_toggle_switches_state_to_on
|
20
|
+
@machine.toggle
|
21
|
+
assert_equal 'on', @machine.state
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_toggle_twice_switches_state_back_to_off
|
25
|
+
@machine.toggle
|
26
|
+
@machine.toggle
|
27
|
+
assert_equal 'off', @machine.state
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'setup_tests'
|
2
|
+
|
3
|
+
class TestEventGuard < Minitest::Test
|
4
|
+
def setup
|
5
|
+
@wall = Struct.new(:stones).new(10)
|
6
|
+
|
7
|
+
@fsm = Hifsm::FSM.define do
|
8
|
+
state :constructed, :initial => true
|
9
|
+
state :broken
|
10
|
+
|
11
|
+
event :break, :from => :constructed, :to => :broken, :guard => proc { stones < 5 }
|
12
|
+
end
|
13
|
+
@machine = @fsm.new(@wall)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_cant_break_wall_10_stones_thick
|
17
|
+
assert_raises(Hifsm::MissingTransition) do
|
18
|
+
@wall.break
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_cant_break_thin_wall
|
23
|
+
@wall.stones = 3
|
24
|
+
@wall.break
|
25
|
+
assert_equal 'broken', @machine.state
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'setup_tests'
|
2
|
+
require 'monster'
|
3
|
+
|
4
|
+
class TestMonster < Minitest::Test
|
5
|
+
def setup
|
6
|
+
@monster = Monster.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_initial_state_is_idle
|
10
|
+
assert_equal 'idle', @monster.state.to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_acting_from_idle_state
|
14
|
+
@monster.act!
|
15
|
+
pass
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_will_attack_player_on_sight_if_alot_hp
|
19
|
+
@monster.sight 'player'
|
20
|
+
assert_equal 'attacking.acquiring_target', @monster.state.to_s
|
21
|
+
assert_equal 'player', @monster.target
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_will_runaway_on_sight_if_low_hp
|
25
|
+
@monster.low_hp = true
|
26
|
+
@monster.sight 'player'
|
27
|
+
assert_equal 'runaway', @monster.state.to_s
|
28
|
+
assert_nil @monster.target
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_will_pursue_player_on_acquire
|
32
|
+
@monster.sight 'player'
|
33
|
+
@monster.acquire
|
34
|
+
assert_equal 'attacking.pursuing', @monster.state.to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_kill_in_middle_of_attack
|
38
|
+
@monster.sight 'player'
|
39
|
+
@monster.acquire
|
40
|
+
@monster.enemy_dead
|
41
|
+
assert_equal 'coming_back', @monster.state.to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hifsm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vladimir Meremyanin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-09-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: FSM with support for nested states and parameterised events
|
56
|
+
email:
|
57
|
+
- vladimir@meremyanin.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- Gemfile
|
64
|
+
- LICENSE.txt
|
65
|
+
- README.md
|
66
|
+
- Rakefile
|
67
|
+
- hifsm.gemspec
|
68
|
+
- lib/hifsm.rb
|
69
|
+
- lib/hifsm/callbacks.rb
|
70
|
+
- lib/hifsm/event.rb
|
71
|
+
- lib/hifsm/fsm.rb
|
72
|
+
- lib/hifsm/machine.rb
|
73
|
+
- lib/hifsm/state.rb
|
74
|
+
- lib/hifsm/version.rb
|
75
|
+
- test/minitest_helper.rb
|
76
|
+
- test/monster.rb
|
77
|
+
- test/setup_tests.rb
|
78
|
+
- test/test_any_state_event.rb
|
79
|
+
- test/test_basic_fsm.rb
|
80
|
+
- test/test_event_guard.rb
|
81
|
+
- test/test_monster.rb
|
82
|
+
homepage: http://github.com/stiff/hifsm
|
83
|
+
licenses:
|
84
|
+
- MIT
|
85
|
+
metadata: {}
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options: []
|
88
|
+
require_paths:
|
89
|
+
- lib
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
requirements: []
|
101
|
+
rubyforge_project:
|
102
|
+
rubygems_version: 2.2.2
|
103
|
+
signing_key:
|
104
|
+
specification_version: 4
|
105
|
+
summary: Hierarchical state machines in Ruby
|
106
|
+
test_files:
|
107
|
+
- test/minitest_helper.rb
|
108
|
+
- test/monster.rb
|
109
|
+
- test/setup_tests.rb
|
110
|
+
- test/test_any_state_event.rb
|
111
|
+
- test/test_basic_fsm.rb
|
112
|
+
- test/test_event_guard.rb
|
113
|
+
- test/test_monster.rb
|