robodog 0.0.1
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 +34 -0
- data/CONTRIBUTING.md +27 -0
- data/LICENSE +25 -0
- data/README.md +302 -0
- data/bin/robodog +5 -0
- data/data/fail_input_a.txt +5 -0
- data/data/valid_input_a.txt +5 -0
- data/lib/robo_dog.rb +1 -0
- data/lib/robo_dog/application.rb +37 -0
- data/lib/robo_dog/paddock.rb +31 -0
- data/lib/robo_dog/parser.rb +53 -0
- data/lib/robo_dog/pose.rb +97 -0
- data/lib/robo_dog/pose/orientation.rb +38 -0
- data/lib/robo_dog/robot.rb +88 -0
- data/lib/robo_dog/simulation.rb +60 -0
- data/robodog.gemspec +19 -0
- data/spec/application_spec.rb +24 -0
- data/spec/features/robo_dog_spec.rb +107 -0
- data/spec/orientation_spec.rb +37 -0
- data/spec/paddock_spec.rb +50 -0
- data/spec/parser_spec.rb +74 -0
- data/spec/pose_spec.rb +164 -0
- data/spec/robot_spec.rb +166 -0
- data/spec/simulation_spec.rb +131 -0
- metadata +92 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative '../lib/robo_dog/pose/orientation'
|
2
|
+
|
3
|
+
RSpec.describe RoboDog::Pose::Orientation do
|
4
|
+
describe 'the public interface' do
|
5
|
+
it { expect(described_class).to respond_to :constantize, :stringify }
|
6
|
+
end
|
7
|
+
|
8
|
+
describe '.constantize' do
|
9
|
+
shared_examples 'a constantizer' do |string, expect_constant|
|
10
|
+
subject { described_class.constantize(string) }
|
11
|
+
|
12
|
+
it 'returns the correct constant' do
|
13
|
+
is_expected.to eql expect_constant
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it_behaves_like 'a constantizer', 'N', described_class::NORTH
|
18
|
+
it_behaves_like 'a constantizer', 'E', described_class::EAST
|
19
|
+
it_behaves_like 'a constantizer', 'S', described_class::SOUTH
|
20
|
+
it_behaves_like 'a constantizer', 'W', described_class::WEST
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '.stringify' do
|
24
|
+
shared_examples 'a stringifier' do |constant, expected_string|
|
25
|
+
subject { described_class.stringify(constant) }
|
26
|
+
|
27
|
+
it 'returns the correct constant' do
|
28
|
+
is_expected.to eql expected_string
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it_behaves_like 'a stringifier', described_class::NORTH, 'N'
|
33
|
+
it_behaves_like 'a stringifier', described_class::EAST, 'E'
|
34
|
+
it_behaves_like 'a stringifier', described_class::SOUTH, 'S'
|
35
|
+
it_behaves_like 'a stringifier', described_class::WEST, 'W'
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require_relative '../lib/robo_dog/paddock'
|
2
|
+
|
3
|
+
RSpec.describe RoboDog::Paddock do
|
4
|
+
describe 'the public interface' do
|
5
|
+
it { expect(described_class).to respond_to :build }
|
6
|
+
|
7
|
+
subject{ described_class.new }
|
8
|
+
it { is_expected.to respond_to :valid_coordinates? }
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '.build' do
|
12
|
+
context 'with invalid data' do
|
13
|
+
let(:data) { '1' }
|
14
|
+
it 'screams at you' do
|
15
|
+
expect { described_class.build(data) }.to raise_exception described_class::DataError
|
16
|
+
end
|
17
|
+
end
|
18
|
+
context 'with valid data' do
|
19
|
+
let(:data) { '4 3'}
|
20
|
+
it 'returns a paddock instance' do
|
21
|
+
expect(described_class.build(data)).to be_instance_of described_class
|
22
|
+
end
|
23
|
+
it 'builds with correct x size' do
|
24
|
+
expect(described_class.build(data).instance_variable_get(:@x_size)).to eql 4
|
25
|
+
end
|
26
|
+
it 'builds with correct y size' do
|
27
|
+
expect(described_class.build(data).instance_variable_get(:@y_size)).to eql 3
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
let(:paddock) { described_class.new(5, 5) }
|
33
|
+
|
34
|
+
describe '#valid_coordinates?' do
|
35
|
+
context 'with coordinates within paddock' do
|
36
|
+
it 'returns truthy' do
|
37
|
+
[[0,0],[5,5],[2,4]].each do |coordinates|
|
38
|
+
expect(paddock.valid_coordinates?(x: coordinates[0], y: coordinates[1])).to be_truthy
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
context 'with coordinates outside of paddock' do
|
43
|
+
it 'returns falsy' do
|
44
|
+
[[-1,0],[6,5],[7,7]].each do |coordinates|
|
45
|
+
expect(paddock.valid_coordinates?(x: coordinates[0], y: coordinates[1])).to be_falsy
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/spec/parser_spec.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require_relative '../lib/robo_dog/parser'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
RSpec.describe RoboDog::Parser do
|
5
|
+
describe 'the public interface' do
|
6
|
+
it { expect(described_class).to respond_to :parse }
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '.parse' do
|
10
|
+
shared_examples 'a complaining parser' do |input_string|
|
11
|
+
it 'screams at you' do
|
12
|
+
str_io = StringIO.new(input_string)
|
13
|
+
expect { described_class.parse(str_io) }.to raise_exception described_class::DataError
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'with invalid input data' do
|
18
|
+
context 'with no data' do
|
19
|
+
it_behaves_like 'a complaining parser',
|
20
|
+
''
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'with missing robot data' do
|
24
|
+
it_behaves_like 'a complaining parser',
|
25
|
+
<<-END.gsub(/^\s+\|/, '')
|
26
|
+
|1 1
|
27
|
+
|only robot_1 coordinates
|
28
|
+
END
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'with valid input data' do
|
33
|
+
let(:valid_input) do
|
34
|
+
s = <<-END.gsub(/^\s+\|/, '')
|
35
|
+
|a paddock description
|
36
|
+
|robot_1 coordinate
|
37
|
+
|robot_1 commands
|
38
|
+
|robot_2 coordinates
|
39
|
+
|robot_2 commands
|
40
|
+
END
|
41
|
+
StringIO.new(s)
|
42
|
+
end
|
43
|
+
|
44
|
+
subject do
|
45
|
+
described_class.instance_variable_set(:@paddock_factory, paddock_factory)
|
46
|
+
described_class.instance_variable_set(:@robot_factory, robot_factory)
|
47
|
+
described_class.parse(valid_input)
|
48
|
+
end
|
49
|
+
|
50
|
+
let(:paddock_factory) do
|
51
|
+
dbl = double
|
52
|
+
allow(dbl).to receive(:build).and_return('a paddock')
|
53
|
+
dbl
|
54
|
+
end
|
55
|
+
let(:robot_factory) do
|
56
|
+
dbl = double
|
57
|
+
allow(dbl).to receive(:build).and_return('robot_1','robot_2')
|
58
|
+
dbl
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'returns a hash' do
|
62
|
+
is_expected.to be_instance_of Hash
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'includes paddock data' do
|
66
|
+
is_expected.to include paddock: 'a paddock'
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'includes robots data' do
|
70
|
+
is_expected.to include robots: ['robot_1', 'robot_2']
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/spec/pose_spec.rb
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
require_relative '../lib/robo_dog/pose'
|
2
|
+
|
3
|
+
RSpec.describe RoboDog::Pose do
|
4
|
+
describe 'the public interface' do
|
5
|
+
it { expect(described_class).to respond_to :build }
|
6
|
+
subject{ described_class.new }
|
7
|
+
it { is_expected.to respond_to :report, :coordinates, :adjacent, :rotate! }
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '.build' do
|
11
|
+
context 'with invalid data' do
|
12
|
+
let(:data) { '1' }
|
13
|
+
it 'screams at you' do
|
14
|
+
expect { described_class.build(data) }.to raise_exception described_class::DataError
|
15
|
+
end
|
16
|
+
end
|
17
|
+
context 'with valid data' do
|
18
|
+
let(:data) { '4 3 N'}
|
19
|
+
it 'returns a instance of self' do
|
20
|
+
expect(described_class.build(data)).to be_instance_of described_class
|
21
|
+
end
|
22
|
+
it 'builds with correct x coordinate' do
|
23
|
+
expect(described_class.build(data).instance_variable_get(:@x)).to eql 4
|
24
|
+
end
|
25
|
+
it 'builds with correct y coordinate' do
|
26
|
+
expect(described_class.build(data).instance_variable_get(:@y)).to eql 3
|
27
|
+
end
|
28
|
+
it 'builds with correct orientation' do
|
29
|
+
expect(described_class.build(data).instance_variable_get(:@orientation)).to eql described_class::Orientation::NORTH
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
let(:pose) do
|
35
|
+
described_class.new(x: 0, y: 0, orientation: :north)
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '#report' do
|
39
|
+
subject { pose.report }
|
40
|
+
it 'returns a hash' do
|
41
|
+
is_expected.to be_instance_of Hash
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'includes x, y and orientation' do
|
45
|
+
is_expected.to include x: 0, y: 0, orientation: 'N'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#coordinates' do
|
50
|
+
subject { pose.coordinates }
|
51
|
+
|
52
|
+
it 'returns a hash' do
|
53
|
+
is_expected.to be_instance_of Hash
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'includes x and y coordinates' do
|
57
|
+
is_expected.to include x: 0, y: 0
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '#adjacent' do
|
62
|
+
let(:pose) { described_class.new(x: 2, y: 2, orientation: orientation) }
|
63
|
+
|
64
|
+
subject do
|
65
|
+
adj = pose.adjacent
|
66
|
+
{
|
67
|
+
x: adj.instance_variable_get(:@x),
|
68
|
+
y: adj.instance_variable_get(:@y),
|
69
|
+
orientation: adj.instance_variable_get(:@orientation)
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'when facing north' do
|
74
|
+
let(:orientation) { described_class::Orientation::NORTH }
|
75
|
+
it 'returns the correct coordinates' do
|
76
|
+
is_expected.to include x: 2, y: 3, orientation: orientation
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context 'when facing east' do
|
81
|
+
let(:orientation) { described_class::Orientation::EAST }
|
82
|
+
it 'returns the correct coordinates' do
|
83
|
+
is_expected.to include x: 3, y: 2, orientation: orientation
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'when facing south' do
|
88
|
+
let(:orientation) { described_class::Orientation::SOUTH }
|
89
|
+
it 'returns the correct coordinates' do
|
90
|
+
is_expected.to include x: 2, y: 1, orientation: orientation
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'when facing west' do
|
95
|
+
let(:orientation) { described_class::Orientation::WEST }
|
96
|
+
it 'returns the correct coordinates' do
|
97
|
+
is_expected.to include x: 1, y: 2, orientation: orientation
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe '#rotate!' do
|
103
|
+
let(:pose) { described_class.new(x: 2, y: 2, orientation: init_orientation) }
|
104
|
+
|
105
|
+
shared_examples 'a rotatable pose' do |direction, correct_orientation|
|
106
|
+
subject do
|
107
|
+
rotated = pose.rotate!(direction)
|
108
|
+
{
|
109
|
+
x: rotated.instance_variable_get(:@x),
|
110
|
+
y: rotated.instance_variable_get(:@y),
|
111
|
+
orientation: rotated.instance_variable_get(:@orientation)
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'returns the correct orientation' do
|
116
|
+
is_expected.to include x: 2, y: 2, orientation: correct_orientation
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context 'when facing north' do
|
121
|
+
let(:init_orientation) { described_class::Orientation::NORTH }
|
122
|
+
it_behaves_like 'a rotatable pose',
|
123
|
+
:clockwise,
|
124
|
+
described_class::Orientation::EAST
|
125
|
+
|
126
|
+
it_behaves_like 'a rotatable pose',
|
127
|
+
:counter,
|
128
|
+
described_class::Orientation::WEST
|
129
|
+
end
|
130
|
+
|
131
|
+
context 'when facing east' do
|
132
|
+
let(:init_orientation) { described_class::Orientation::EAST }
|
133
|
+
it_behaves_like 'a rotatable pose',
|
134
|
+
:clockwise,
|
135
|
+
described_class::Orientation::SOUTH
|
136
|
+
|
137
|
+
it_behaves_like 'a rotatable pose',
|
138
|
+
:counter,
|
139
|
+
described_class::Orientation::NORTH
|
140
|
+
end
|
141
|
+
|
142
|
+
context 'when facing south' do
|
143
|
+
let(:init_orientation) { described_class::Orientation::SOUTH }
|
144
|
+
it_behaves_like 'a rotatable pose',
|
145
|
+
:clockwise,
|
146
|
+
described_class::Orientation::WEST
|
147
|
+
|
148
|
+
it_behaves_like 'a rotatable pose',
|
149
|
+
:counter,
|
150
|
+
described_class::Orientation::EAST
|
151
|
+
end
|
152
|
+
|
153
|
+
context 'when facing west' do
|
154
|
+
let(:init_orientation) { described_class::Orientation::WEST }
|
155
|
+
it_behaves_like 'a rotatable pose',
|
156
|
+
:clockwise,
|
157
|
+
described_class::Orientation::NORTH
|
158
|
+
|
159
|
+
it_behaves_like 'a rotatable pose',
|
160
|
+
:counter,
|
161
|
+
described_class::Orientation::SOUTH
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
data/spec/robot_spec.rb
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
require_relative '../lib/robo_dog/robot'
|
2
|
+
|
3
|
+
RSpec.describe RoboDog::Robot do
|
4
|
+
describe 'the public interface' do
|
5
|
+
it { expect(described_class).to respond_to :build }
|
6
|
+
subject{ described_class.new }
|
7
|
+
it { is_expected.to respond_to :execute, :report, :coordinates, :move, :right, :left, :add_coordinator }
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '.build' do
|
11
|
+
context 'with invalid data' do
|
12
|
+
let(:data) { {commands: 'wrong'} }
|
13
|
+
it 'screams at you' do
|
14
|
+
expect { described_class.build(data) }.to raise_exception described_class::DataError
|
15
|
+
end
|
16
|
+
end
|
17
|
+
context 'with valid data' do
|
18
|
+
let(:data) do
|
19
|
+
{
|
20
|
+
commands: 'LRMR',
|
21
|
+
pose: '0 0 N'
|
22
|
+
}
|
23
|
+
end
|
24
|
+
it 'returns a instance of self' do
|
25
|
+
expect(described_class.build(data)).to be_instance_of described_class
|
26
|
+
end
|
27
|
+
it 'builds with correct commands' do
|
28
|
+
expect(described_class.build(data).instance_variable_get(:@commands)).to be == [:left, :right, :move, :right]
|
29
|
+
end
|
30
|
+
it 'delegates pose building' do
|
31
|
+
expect(RoboDog::Pose).to receive(:build).with(data[:pose])
|
32
|
+
described_class.build(data)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
let(:robot) do
|
38
|
+
described_class.new(
|
39
|
+
pose: pose,
|
40
|
+
commands: commands,
|
41
|
+
coordinators: [coordinator]
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
let(:pose) { double }
|
46
|
+
let(:commands) { [] }
|
47
|
+
let(:coordinator) { double }
|
48
|
+
|
49
|
+
describe '#add_coordinator' do
|
50
|
+
it 'adds a coordinator to the coordinators array' do
|
51
|
+
robot.add_coordinator('a added coordinator')
|
52
|
+
expect(robot.instance_variable_get(:@coordinators)).to include 'a added coordinator'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#execute' do
|
57
|
+
let(:commands) { [:left, :move, :left, :right] }
|
58
|
+
|
59
|
+
context 'with :all' do
|
60
|
+
it 'executes all commands' do
|
61
|
+
expect(robot).to receive(:left).ordered
|
62
|
+
expect(robot).to receive(:move).ordered
|
63
|
+
expect(robot).to receive(:left).ordered
|
64
|
+
expect(robot).to receive(:right).ordered
|
65
|
+
|
66
|
+
robot.execute(:all)
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'empties the commands' do
|
70
|
+
allow(robot).to receive_messages(left: nil, move: nil, right: nil)
|
71
|
+
robot.execute(:all)
|
72
|
+
expect(robot.instance_variable_get(:@commands)).to be_empty
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'with :next' do
|
77
|
+
it 'executes the next command' do
|
78
|
+
expect(robot).to receive(:left)
|
79
|
+
|
80
|
+
robot.execute(:next)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'removes the first command' do
|
84
|
+
allow(robot).to receive(:left)
|
85
|
+
robot.execute(:next)
|
86
|
+
expect(robot.instance_variable_get(:@commands)).to match_array [:move, :left, :right]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe '#report' do
|
92
|
+
let(:pose) do
|
93
|
+
dbl = double
|
94
|
+
allow(dbl).to receive(:report).and_return('some report')
|
95
|
+
dbl
|
96
|
+
end
|
97
|
+
|
98
|
+
it "returns pose's report" do
|
99
|
+
expect(robot.report).to match('some report')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe '#coordinates' do
|
104
|
+
let(:pose) do
|
105
|
+
dbl = double
|
106
|
+
allow(dbl).to receive(:coordinates).and_return('some coordinates')
|
107
|
+
dbl
|
108
|
+
end
|
109
|
+
|
110
|
+
it "returns pose's coordinates" do
|
111
|
+
expect(robot.coordinates).to match('some coordinates')
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe '#move' do
|
116
|
+
let(:pose) do
|
117
|
+
dbl = double
|
118
|
+
adj_pose = 'adjacent pose'
|
119
|
+
allow(adj_pose).to receive(:coordinates).and_return('some coordinates')
|
120
|
+
allow(dbl).to receive(:adjacent).and_return(adj_pose)
|
121
|
+
dbl
|
122
|
+
end
|
123
|
+
|
124
|
+
let(:coordinator) do
|
125
|
+
dbl = double
|
126
|
+
allow(dbl).to receive(:valid_coordinates?).and_return(coordinator_return)
|
127
|
+
dbl
|
128
|
+
end
|
129
|
+
|
130
|
+
let(:coordinator_return) { [true, false].sample }
|
131
|
+
|
132
|
+
it 'returns nil' do
|
133
|
+
expect(robot.move).to be_nil
|
134
|
+
end
|
135
|
+
|
136
|
+
context 'with valid adjacent pose' do
|
137
|
+
let(:coordinator_return) { true }
|
138
|
+
it 'updates its pose' do
|
139
|
+
robot.move
|
140
|
+
expect(robot.instance_variable_get(:@pose)).to eql('adjacent pose')
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
context 'with invalid adjacent pose' do
|
145
|
+
let(:coordinator_return) { false }
|
146
|
+
it 'does not update its pose' do
|
147
|
+
robot.move
|
148
|
+
expect(robot.instance_variable_get(:@pose)).to eql(pose)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe '#right' do
|
154
|
+
it 'rotates its pose' do
|
155
|
+
expect(pose).to receive(:rotate!).with(:clockwise)
|
156
|
+
robot.right
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe '#left' do
|
161
|
+
it 'rotates its pose' do
|
162
|
+
expect(pose).to receive(:rotate!).with(:counter)
|
163
|
+
robot.left
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|