baby_bots 0.0.3 → 0.0.4
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.
- data/lib/baby_bots/baby_bot.rb +47 -16
- data/lib/baby_bots/state.rb +34 -7
- data/lib/baby_bots/version.rb +2 -1
- data/spec/baby_bot_spec.rb +32 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/state_spec.rb +80 -0
- metadata +8 -2
data/lib/baby_bots/baby_bot.rb
CHANGED
@@ -1,42 +1,63 @@
|
|
1
|
+
# A tiny finite-state automata library.
|
2
|
+
#
|
3
|
+
# Author:: Justin Hamilton (mailto:justinanthonyhamilton@gmail.com)
|
4
|
+
# Copyright :: BSD2 (see LICENSE for more details)
|
5
|
+
|
1
6
|
module BabyBots
|
2
7
|
|
8
|
+
# Error to handle attempts to transition to a state that does not exist.
|
3
9
|
class NoSuchStateException < Exception
|
4
10
|
end
|
5
11
|
|
12
|
+
# Error to handle attempts to use a transition that does not exist.
|
6
13
|
class NoSuchTransitionException < Exception
|
7
14
|
end
|
8
15
|
|
16
|
+
# A tiny finite-state automata class.
|
9
17
|
class BabyBot
|
10
18
|
attr_accessor :curr, :states, :start
|
11
19
|
|
20
|
+
# Accepts an optional hash of states.
|
12
21
|
def initialize(states={})
|
22
|
+
# Hash of state names to state objects
|
13
23
|
@states = states
|
24
|
+
# Initial state
|
14
25
|
@start = nil
|
26
|
+
# Current state
|
15
27
|
@curr = nil
|
16
28
|
end
|
17
29
|
|
18
|
-
#
|
30
|
+
# Adds a new state to the finite-state automata, also accepts
|
31
|
+
# an optional start flag, which will set the supplied state as the initial
|
32
|
+
# state. Note that this machine may only have one start state. Additionally,
|
33
|
+
# adding the first start state will set @curr to be this state, after this
|
34
|
+
# has been done @curr will remain on whatever state it is currently residing
|
35
|
+
# on, and must be reset using the reset method.
|
19
36
|
def add_state(state, start=nil)
|
20
|
-
# key on state names to the actual state
|
37
|
+
# key on state names to the actual state object
|
21
38
|
@states[state.state] = state
|
39
|
+
|
40
|
+
# if this is a start state
|
22
41
|
if start
|
23
42
|
@start = state
|
24
|
-
|
43
|
+
|
44
|
+
# only set the current state to the start state if @curr is nil.
|
45
|
+
if @curr.nil? then @curr = @start end
|
25
46
|
end
|
26
47
|
end
|
27
48
|
|
49
|
+
# Build up this machine's states with a given state table. The format of
|
50
|
+
# this table is assumed to be {state_name => {event1 => transition1, ...}}.
|
51
|
+
# build assumes that the first state provided is the start state, although
|
52
|
+
# the optional no_first parameter may be set to override this.
|
53
|
+
def build(table, no_first=false)
|
54
|
+
first_state = !no_first
|
28
55
|
|
29
|
-
|
30
|
-
# structure, and adds these states to the state machine, assumes first state
|
31
|
-
# is the starting state
|
32
|
-
def build(table)
|
33
|
-
first_state = true
|
34
|
-
|
35
|
-
# iterate through the provided {state => {event => transition, ...}, ...}
|
56
|
+
# iterate through each provided state table entries
|
36
57
|
table.each do |state, state_table|
|
37
58
|
temp_state = State.new(state)
|
38
|
-
|
39
|
-
# iterate through
|
59
|
+
|
60
|
+
# iterate through the current state table, adding events => transition
|
40
61
|
state_table.each do |event, transition|
|
41
62
|
temp_state.add_transition(event, transition)
|
42
63
|
end
|
@@ -48,13 +69,20 @@ module BabyBots
|
|
48
69
|
end
|
49
70
|
end
|
50
71
|
|
72
|
+
# Return the current state's actual state.
|
51
73
|
def state
|
52
74
|
@curr.state
|
53
75
|
end
|
54
76
|
|
55
|
-
#
|
56
|
-
# will
|
57
|
-
#
|
77
|
+
# Process the finite state automata with the given event.
|
78
|
+
# This will first see if the finite state automata has a defined method
|
79
|
+
# named "pre_{current_state}", and if so "cook" the input event with this
|
80
|
+
# method. Process will then use the cooked input if available, or the raw
|
81
|
+
# input if there is none to compute the transition. Before actually
|
82
|
+
# transitioning, it will then send the raw output to a method of
|
83
|
+
# "post_{current_state}", which will be the return value. If a method named
|
84
|
+
# "post_cook_{current_state}" is supplied, it will send the cooked event to
|
85
|
+
# this method instead of using "post_{current_state}".
|
58
86
|
def process(event=nil)
|
59
87
|
# get the current state
|
60
88
|
curr_state = @curr
|
@@ -64,12 +92,15 @@ module BabyBots
|
|
64
92
|
cooked_event = send("pre_#{@curr.state}", event)
|
65
93
|
end
|
66
94
|
|
95
|
+
# calculate the next state
|
67
96
|
if cooked_event
|
68
97
|
next_state = @states[curr.table[cooked_event]]
|
69
98
|
else
|
70
99
|
next_state = @states[curr.table[event]]
|
71
100
|
end
|
72
101
|
|
102
|
+
# if there is no such transition, see if there is an a translation
|
103
|
+
# known as else, and use that as the next state
|
73
104
|
if next_state.nil?
|
74
105
|
next_state = @states[curr.table[:else]]
|
75
106
|
end
|
@@ -89,7 +120,6 @@ module BabyBots
|
|
89
120
|
ret_val = send("post_#{@curr.state}", cooked_event)
|
90
121
|
end
|
91
122
|
|
92
|
-
|
93
123
|
# actually transition, and make sure such a transition exists
|
94
124
|
@curr = next_state
|
95
125
|
if @curr.nil?
|
@@ -100,6 +130,7 @@ module BabyBots
|
|
100
130
|
return ret_val
|
101
131
|
end
|
102
132
|
|
133
|
+
# Reset the current state to be the start state.
|
103
134
|
def restart
|
104
135
|
@curr = @start
|
105
136
|
end
|
data/lib/baby_bots/state.rb
CHANGED
@@ -1,22 +1,49 @@
|
|
1
1
|
module BabyBots
|
2
|
+
# Transition state used to remove events from a transition table.
|
3
|
+
NOWHERE = :nowhere__
|
2
4
|
|
5
|
+
# The state contained within the BabyBots Finite State Automata.
|
6
|
+
# States have an event that transitions to a new state. They may also
|
7
|
+
# have an event named :else which will be the transition used by the
|
8
|
+
# containing BabyBot when calculating transitions where no such supplied
|
9
|
+
# events exist.
|
3
10
|
class State
|
4
11
|
attr_reader :state, :table
|
5
12
|
|
13
|
+
# Sets the state name, as well as an optionally supplied transition table.
|
6
14
|
def initialize(state, table={})
|
15
|
+
# state name
|
7
16
|
@state = state
|
17
|
+
|
18
|
+
# transition table
|
8
19
|
@table = table
|
9
20
|
end
|
10
21
|
|
11
|
-
|
12
|
-
|
22
|
+
# Adds a transition to the transition table. Table format is
|
23
|
+
# event => transition, where event is the "input" into the state.
|
24
|
+
#
|
25
|
+
# Transitions are allowed to be deleted by being set to NOWHERE.
|
26
|
+
def add_transition(event, transition)
|
27
|
+
if transition == NOWHERE
|
28
|
+
remove_transition(event)
|
29
|
+
else
|
30
|
+
@table[event] = transition
|
31
|
+
end
|
13
32
|
end
|
14
33
|
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
|
19
|
-
|
34
|
+
# Provided a table, merge the state's current transition table
|
35
|
+
# with the supplied one. Note that since this is part of a
|
36
|
+
# finite state machine, supplying events that already exist
|
37
|
+
# in the transition table override this transition.
|
38
|
+
def build(table)
|
39
|
+
table.each { |k,v| add_transition(k, v) }
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
# Delete an entry from the transition table.
|
44
|
+
def remove_transition(event)
|
45
|
+
@table.delete(event)
|
20
46
|
end
|
21
47
|
end
|
48
|
+
|
22
49
|
end
|
data/lib/baby_bots/version.rb
CHANGED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper.rb'
|
2
|
+
|
3
|
+
$loading = BabyBots::State.new(:loading, {1 => :ready, :else => :loading})
|
4
|
+
$ready = BabyBots::State.new(:ready, {1 => :run, :else => :loading})
|
5
|
+
$run = BabyBots::State.new(:run, {:else => :run})
|
6
|
+
|
7
|
+
|
8
|
+
TEST_MACHINE1 = { :loading => {1 => :ready, :else => :loading},
|
9
|
+
:ready => {1 => :run, :else => :loading},
|
10
|
+
:run => {:else => :run} }
|
11
|
+
|
12
|
+
describe BabyBots::BabyBot do
|
13
|
+
it "should be able to be initiated with no additional initialization" do
|
14
|
+
test = BabyBots::BabyBot.new
|
15
|
+
test.states.should == {}
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should be able to be initialized with a state table" do
|
19
|
+
test = BabyBots::BabyBot.new(TEST_MACHINE1)
|
20
|
+
test.states.should == TEST_MACHINE1
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should have states be able to be added using add_state" do
|
24
|
+
test = BabyBots::BabyBot.new
|
25
|
+
test.add_state($loading)
|
26
|
+
test.states.should == {:loading => $loading}
|
27
|
+
test.add_state($ready)
|
28
|
+
test.states.should == {:loading => $loading, :ready => $ready}
|
29
|
+
test.add_state($run)
|
30
|
+
test.states.should == {:loading => $loading, :ready => $ready, :run => $run}
|
31
|
+
end
|
32
|
+
end
|
data/spec/spec_helper.rb
ADDED
data/spec/state_spec.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'spec_helper.rb'
|
2
|
+
|
3
|
+
TEST_NAME = :test
|
4
|
+
TEST_TRANS1_1 = {0 => :test}
|
5
|
+
TEST_TRANS1_2 = {1 => :done}
|
6
|
+
TEST_TRANS1_3 = {:else => :test}
|
7
|
+
TEST_TABLE1 = TEST_TRANS1_1.merge TEST_TRANS1_2.merge TEST_TRANS1_3
|
8
|
+
TEST_TABLE2 = {2 => :two, 3 => :three}
|
9
|
+
|
10
|
+
|
11
|
+
describe BabyBots::State do
|
12
|
+
it "should require a state name" do
|
13
|
+
test = BabyBots::State.new(TEST_NAME)
|
14
|
+
test.state.should == TEST_NAME
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should provide an empty transition table if none is supplied" do
|
18
|
+
test = BabyBots::State.new(TEST_NAME)
|
19
|
+
test.state.should == TEST_NAME
|
20
|
+
test.table.should == {}
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should accept a state name and transition table" do
|
24
|
+
test = BabyBots::State.new(TEST_NAME, TEST_TABLE1)
|
25
|
+
test.state.should == TEST_NAME
|
26
|
+
test.table.should == TEST_TABLE1
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should be able to have its transition table built after construction" do
|
30
|
+
test = BabyBots::State.new(TEST_NAME)
|
31
|
+
test.table.should == {}
|
32
|
+
test.build(TEST_TABLE1)
|
33
|
+
test.table.should == TEST_TABLE1
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should merge changes between uses of build" do
|
37
|
+
test = BabyBots::State.new(TEST_NAME, TEST_TABLE2)
|
38
|
+
test.table.should == TEST_TABLE2
|
39
|
+
test.build(TEST_TABLE1)
|
40
|
+
test.table.should == TEST_TABLE2.merge(TEST_TABLE1)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should allow transitions to be added piece-by-piece via add_transition" do
|
44
|
+
test = BabyBots::State.new(TEST_NAME)
|
45
|
+
test.table.should == {}
|
46
|
+
test.add_transition(0, :test)
|
47
|
+
test.table.should == {0 => :test}
|
48
|
+
test.add_transition(1, :done)
|
49
|
+
test.table.should == {0 => :test, 1 => :done}
|
50
|
+
test.add_transition(:else, :test)
|
51
|
+
test.table.should == TEST_TABLE1
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should allow transitions to be overwritten using add_transition" do
|
55
|
+
test = BabyBots::State.new(TEST_NAME, TEST_TABLE1)
|
56
|
+
test.table.should == TEST_TABLE1
|
57
|
+
test.add_transition(:else, :error)
|
58
|
+
test.table.should == TEST_TRANS1_1.merge(TEST_TRANS1_2.merge({:else => :error}))
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should allow transitions to be overwritten using build" do
|
62
|
+
test = BabyBots::State.new(TEST_NAME, TEST_TABLE1)
|
63
|
+
test.build({:else => :error})
|
64
|
+
test.table.should == TEST_TRANS1_1.merge(TEST_TRANS1_2.merge({:else => :error}))
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should allow transitions to be removed using remove_transition" do
|
68
|
+
test = BabyBots::State.new(TEST_NAME, TEST_TABLE1)
|
69
|
+
test.remove_transition(:else)
|
70
|
+
test.table.should == TEST_TRANS1_1.merge(TEST_TRANS1_2)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should allow transitions to be deleted by setting their transition to NOWHERE" do
|
74
|
+
test = BabyBots::State.new(TEST_NAME, TEST_TABLE1)
|
75
|
+
test.add_transition(:else, BabyBots::NOWHERE)
|
76
|
+
test.table.should == TEST_TRANS1_1.merge(TEST_TRANS1_2)
|
77
|
+
test.build({0 => BabyBots::NOWHERE, 1 => BabyBots::NOWHERE})
|
78
|
+
test.table.should == {}
|
79
|
+
end
|
80
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: baby_bots
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -27,6 +27,9 @@ files:
|
|
27
27
|
- lib/baby_bots/baby_bot.rb
|
28
28
|
- lib/baby_bots/state.rb
|
29
29
|
- lib/baby_bots/version.rb
|
30
|
+
- spec/baby_bot_spec.rb
|
31
|
+
- spec/spec_helper.rb
|
32
|
+
- spec/state_spec.rb
|
30
33
|
homepage: https://github.com/jamiltron/BabyBots
|
31
34
|
licenses: []
|
32
35
|
post_install_message:
|
@@ -53,4 +56,7 @@ specification_version: 3
|
|
53
56
|
summary: While there are many fsa libraries out there, I wanted to implement my own
|
54
57
|
so I could learn how to create a module/gem, as I am not really a Ruby guy and have
|
55
58
|
no idea how.
|
56
|
-
test_files:
|
59
|
+
test_files:
|
60
|
+
- spec/baby_bot_spec.rb
|
61
|
+
- spec/spec_helper.rb
|
62
|
+
- spec/state_spec.rb
|