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,31 @@
|
|
1
|
+
module RoboDog
|
2
|
+
class Paddock
|
3
|
+
|
4
|
+
DataError = Class.new(StandardError)
|
5
|
+
|
6
|
+
def self.build(string)
|
7
|
+
raise DataError unless validate(string)
|
8
|
+
|
9
|
+
x, y = string.split(' ').map(&:to_i)
|
10
|
+
|
11
|
+
new(x, y)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(x_size = 1, y_size = 1)
|
15
|
+
@x_size = x_size
|
16
|
+
@y_size = y_size
|
17
|
+
end
|
18
|
+
|
19
|
+
def valid_coordinates?(coordinates)
|
20
|
+
coordinates && (0..x_size).include?(coordinates[:x]) && (0..y_size).include?(coordinates[:y])
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :x_size, :y_size
|
26
|
+
|
27
|
+
def self.validate(string)
|
28
|
+
string =~ /\A\d+ \d+\z/
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require_relative 'paddock'
|
2
|
+
require_relative 'robot'
|
3
|
+
|
4
|
+
module RoboDog
|
5
|
+
module Parser
|
6
|
+
DataError = Class.new(StandardError)
|
7
|
+
|
8
|
+
@paddock_factory = Paddock
|
9
|
+
@robot_factory = Robot
|
10
|
+
|
11
|
+
@extractor = -> (input) { e = input.gets and e.chomp }
|
12
|
+
|
13
|
+
@paddock_parser = lambda do |input|
|
14
|
+
paddock_attrs = @extractor.call(input)
|
15
|
+
|
16
|
+
raise DataError unless paddock_attrs
|
17
|
+
|
18
|
+
paddock = @paddock_factory.build(paddock_attrs)
|
19
|
+
end
|
20
|
+
|
21
|
+
@robot_extractor = lambda do |input|
|
22
|
+
pose = @extractor.call(input)
|
23
|
+
commands = @extractor.call(input)
|
24
|
+
|
25
|
+
if pose && commands
|
26
|
+
{
|
27
|
+
pose: pose,
|
28
|
+
commands: commands
|
29
|
+
}
|
30
|
+
else
|
31
|
+
raise DataError unless pose.nil? && commands.nil?
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module_function
|
37
|
+
|
38
|
+
def parse(input)
|
39
|
+
paddock = @paddock_parser.call(input)
|
40
|
+
robots = []
|
41
|
+
loop do
|
42
|
+
robot_attrs = @robot_extractor.call(input)
|
43
|
+
break unless robot_attrs
|
44
|
+
robots << @robot_factory.build(robot_attrs)
|
45
|
+
end
|
46
|
+
|
47
|
+
{
|
48
|
+
paddock: paddock,
|
49
|
+
robots: robots
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require_relative 'pose/orientation'
|
2
|
+
|
3
|
+
module RoboDog
|
4
|
+
class Pose
|
5
|
+
DataError = Class.new(StandardError)
|
6
|
+
|
7
|
+
def self.build(string)
|
8
|
+
raise DataError unless validate(string)
|
9
|
+
|
10
|
+
new(parse(string))
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(args = {})
|
14
|
+
@x = args[:x] || 0
|
15
|
+
@y = args[:y] || 0
|
16
|
+
@orientation = args[:orientation] || Orientation::NORTH
|
17
|
+
end
|
18
|
+
|
19
|
+
def report
|
20
|
+
coordinates.merge(
|
21
|
+
orientation: Orientation.stringify(orientation)
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def coordinates
|
26
|
+
{x: x, y: y}
|
27
|
+
end
|
28
|
+
|
29
|
+
def adjacent
|
30
|
+
dup.send(:adjacent!)
|
31
|
+
end
|
32
|
+
|
33
|
+
def rotate!(direction = :clockwise)
|
34
|
+
self.orientation = next_orientation(direction)
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_accessor :x, :y, :orientation
|
41
|
+
|
42
|
+
def self.validate(string)
|
43
|
+
string =~ /\A\d+ \d+ [NSWE]\z/
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.parse(string)
|
47
|
+
x_str, y_str, orientation_str = string.split(' ')
|
48
|
+
x, y = x_str.to_i, y_str.to_i
|
49
|
+
orientation = Orientation.constantize(orientation_str)
|
50
|
+
|
51
|
+
{
|
52
|
+
x: x,
|
53
|
+
y: y,
|
54
|
+
orientation: orientation
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def next_orientation(direction)
|
59
|
+
by = direction == :clockwise ? 1 : -1
|
60
|
+
|
61
|
+
orientations = [
|
62
|
+
Orientation::NORTH,
|
63
|
+
Orientation::EAST,
|
64
|
+
Orientation::SOUTH,
|
65
|
+
Orientation::WEST
|
66
|
+
]
|
67
|
+
|
68
|
+
orientations[(orientations.index(orientation) + by) % 4]
|
69
|
+
end
|
70
|
+
|
71
|
+
def adjacent!
|
72
|
+
case orientation
|
73
|
+
when Orientation::EAST
|
74
|
+
increment!(:x)
|
75
|
+
when Orientation::NORTH
|
76
|
+
increment!(:y)
|
77
|
+
when Orientation::WEST
|
78
|
+
decrement!(:x)
|
79
|
+
when Orientation::SOUTH
|
80
|
+
decrement!(:y)
|
81
|
+
end
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
def increment!(coordinate)
|
86
|
+
update_coordinate!(coordinate, 1)
|
87
|
+
end
|
88
|
+
|
89
|
+
def decrement!(coordinate)
|
90
|
+
update_coordinate!(coordinate, -1)
|
91
|
+
end
|
92
|
+
|
93
|
+
def update_coordinate!(coordinate, by = 1)
|
94
|
+
self.send("#{coordinate}=", self.send(coordinate) + by)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module RoboDog
|
2
|
+
class Pose
|
3
|
+
module Orientation
|
4
|
+
NORTH = :north
|
5
|
+
EAST = :east
|
6
|
+
SOUTH = :south
|
7
|
+
WEST = :west
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def constantize(string)
|
12
|
+
case string
|
13
|
+
when 'N'
|
14
|
+
NORTH
|
15
|
+
when 'E'
|
16
|
+
EAST
|
17
|
+
when 'S'
|
18
|
+
SOUTH
|
19
|
+
when 'W'
|
20
|
+
WEST
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def stringify(orientation)
|
25
|
+
case orientation
|
26
|
+
when NORTH
|
27
|
+
'N'
|
28
|
+
when EAST
|
29
|
+
'E'
|
30
|
+
when SOUTH
|
31
|
+
'S'
|
32
|
+
when WEST
|
33
|
+
'W'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require_relative 'pose'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module RoboDog
|
5
|
+
class Robot
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
DataError = Class.new(StandardError)
|
9
|
+
|
10
|
+
def_delegators :@pose, :report, :coordinates
|
11
|
+
|
12
|
+
def self.build(attrs)
|
13
|
+
raise DataError unless validate(attrs[:commands])
|
14
|
+
|
15
|
+
pose = Pose.build(attrs[:pose])
|
16
|
+
commands = lex(attrs[:commands])
|
17
|
+
|
18
|
+
new(pose: pose, commands: commands)
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(args = {})
|
22
|
+
@pose = args[:pose]
|
23
|
+
@commands = args[:commands] || []
|
24
|
+
@coordinators = args[:coordinators] || []
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_coordinator(coordinator)
|
28
|
+
coordinators << coordinator
|
29
|
+
end
|
30
|
+
|
31
|
+
def execute(mode)
|
32
|
+
case mode
|
33
|
+
when :all
|
34
|
+
commands.map! do |command|
|
35
|
+
self.send(command)
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
commands.compact!
|
39
|
+
when :next
|
40
|
+
command = commands.shift
|
41
|
+
if command
|
42
|
+
self.send(command)
|
43
|
+
true
|
44
|
+
else
|
45
|
+
false
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def move
|
51
|
+
adj_pose = pose.adjacent
|
52
|
+
self.pose = adj_pose if coordinators.all? { |c| c.valid_coordinates?(adj_pose.coordinates) }
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def right
|
57
|
+
pose.rotate!(:clockwise)
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def left
|
62
|
+
pose.rotate!(:counter)
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
attr_reader :commands, :coordinators
|
69
|
+
attr_accessor :pose
|
70
|
+
|
71
|
+
def self.validate(string)
|
72
|
+
string =~ /\A[MLR]*\z/
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.lex(commands_string)
|
76
|
+
commands_string.chars.map do |char|
|
77
|
+
case char
|
78
|
+
when 'M'
|
79
|
+
:move
|
80
|
+
when 'R'
|
81
|
+
:right
|
82
|
+
when 'L'
|
83
|
+
:left
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module RoboDog
|
2
|
+
class Simulation
|
3
|
+
def initialize(args = {})
|
4
|
+
@paddock = args[:paddock]
|
5
|
+
@robots = args[:robots] || []
|
6
|
+
end
|
7
|
+
|
8
|
+
def run(mode = :sequential)
|
9
|
+
mode ||= :sequential
|
10
|
+
warm_up
|
11
|
+
case mode
|
12
|
+
when :sequential
|
13
|
+
robots.each { |r| r.execute(:all) }
|
14
|
+
when :turns
|
15
|
+
run_in_turns
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def report
|
20
|
+
{
|
21
|
+
robots: robots.map { |r| r.report }
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def valid_coordinates?(coordinates)
|
26
|
+
coordinates &&
|
27
|
+
paddock.valid_coordinates?(coordinates) &&
|
28
|
+
robots.all? { |r| r.coordinates != coordinates } ||
|
29
|
+
fail_appropriately
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :robots, :paddock
|
35
|
+
|
36
|
+
def warm_up
|
37
|
+
fail_appropriately if robots.dup.uniq! { |r| r.coordinates }
|
38
|
+
robots.each { |r| r.add_coordinator(self) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def fail_appropriately
|
42
|
+
fail(
|
43
|
+
'Invalid coordinates. This means two '\
|
44
|
+
'robots collided or a robot hit '\
|
45
|
+
'the border of the paddock.'
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
def run_in_turns
|
50
|
+
loop do
|
51
|
+
executed = false
|
52
|
+
robots.each do |r|
|
53
|
+
_ = r.execute(:next)
|
54
|
+
executed ||= _
|
55
|
+
end
|
56
|
+
break unless executed
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/robodog.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Gem::Specification.new do |spec|
|
2
|
+
spec.name = 'robodog'
|
3
|
+
spec.version = '0.0.1'
|
4
|
+
spec.authors = ['Matias Anaya']
|
5
|
+
spec.email = ['matiasanaya@gmail.com']
|
6
|
+
spec.summary = %q{Fredwina the Farmer's robotic sheep dog simulator}
|
7
|
+
spec.description = %q{Fredwina the Farmer's robotic sheep dog simulator, used for the shock and awe showcase.}
|
8
|
+
spec.homepage = 'https://github.com/matiasanaya/robo-dog'
|
9
|
+
spec.license = 'UNLICENSE'
|
10
|
+
|
11
|
+
spec.files = `git ls-files -z`.split("\x0")
|
12
|
+
spec.executables = ['robodog']
|
13
|
+
spec.test_files = spec.files.grep(%r{^(spec)/})
|
14
|
+
spec.require_paths = ['lib']
|
15
|
+
|
16
|
+
spec.required_ruby_version = '~> 2.1'
|
17
|
+
|
18
|
+
spec.add_development_dependency 'rspec', '~> 3.1'
|
19
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative '../lib/robo_dog/application'
|
2
|
+
|
3
|
+
RSpec.describe RoboDog::Application 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 :run }
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '.build' do
|
11
|
+
it 'returns a instance of self' do
|
12
|
+
expect(described_class.build).to be_instance_of described_class
|
13
|
+
end
|
14
|
+
it 'builds with default Parser' do
|
15
|
+
expect(described_class.build.instance_variable_get(:@parser)).to eql RoboDog::Parser
|
16
|
+
end
|
17
|
+
it 'builds with default Simulation Class' do
|
18
|
+
expect(described_class.build.instance_variable_get(:@simulation_class)).to eql RoboDog::Simulation
|
19
|
+
end
|
20
|
+
it 'builds with correct input' do
|
21
|
+
expect(described_class.build('Hello World').instance_variable_get(:@input)).to eql 'Hello World'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require_relative '../../lib/robo_dog/application'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
RSpec.describe RoboDog::Application do
|
5
|
+
|
6
|
+
let(:app) { lambda { |input_stream| RoboDog::Application.build(input_stream).run(mode) } }
|
7
|
+
let(:mode) { :sequential }
|
8
|
+
|
9
|
+
shared_examples 'a correct application' do |input_string, correct_output|
|
10
|
+
it 'prints the correct output' do
|
11
|
+
expect { app.call(StringIO.new(input_string)) }.to output(correct_output).to_stdout
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
shared_examples 'a complaining application' do |input_string, error|
|
16
|
+
it 'screams at the user' do
|
17
|
+
expect { app.call(StringIO.new(input_string)) }.to raise_exception error
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'when valid input is provided' do
|
22
|
+
context 'when each robot executes all at once' do
|
23
|
+
context 'when robots do not run over each other' do
|
24
|
+
it_behaves_like 'a correct application',
|
25
|
+
<<-END.gsub(/^\s+\|/, '') ,
|
26
|
+
|5 5
|
27
|
+
|1 2 N
|
28
|
+
|LMLMLMLMM
|
29
|
+
|3 3 E
|
30
|
+
|MMRMMRMRRM
|
31
|
+
END
|
32
|
+
<<-END.gsub(/^\s+\|/, '')
|
33
|
+
|1 3 N
|
34
|
+
|5 1 E
|
35
|
+
END
|
36
|
+
end
|
37
|
+
context 'when robots run over each other' do
|
38
|
+
it_behaves_like 'a complaining application',
|
39
|
+
<<-END.gsub(/^\s+\|/, '') ,
|
40
|
+
|5 5
|
41
|
+
|0 0 E
|
42
|
+
|M
|
43
|
+
|1 0 N
|
44
|
+
|L
|
45
|
+
END
|
46
|
+
RuntimeError
|
47
|
+
|
48
|
+
it_behaves_like 'a complaining application',
|
49
|
+
<<-END.gsub(/^\s+\|/, '') ,
|
50
|
+
|5 5
|
51
|
+
|0 0 E
|
52
|
+
|M
|
53
|
+
|1 0 N
|
54
|
+
|L
|
55
|
+
END
|
56
|
+
RuntimeError
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'when robots run over then end of the paddock' do
|
60
|
+
it_behaves_like 'a complaining application',
|
61
|
+
<<-END.gsub(/^\s+\|/, '') ,
|
62
|
+
|5 5
|
63
|
+
|5 5 N
|
64
|
+
|M
|
65
|
+
END
|
66
|
+
RuntimeError
|
67
|
+
|
68
|
+
it_behaves_like 'a complaining application',
|
69
|
+
<<-END.gsub(/^\s+\|/, '') ,
|
70
|
+
|5 5
|
71
|
+
|0 0 N
|
72
|
+
|RMM
|
73
|
+
|1 0 E
|
74
|
+
|MMMM
|
75
|
+
END
|
76
|
+
RuntimeError
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'when robots are placed over each other' do
|
80
|
+
it_behaves_like 'a complaining application',
|
81
|
+
<<-END.gsub(/^\s+\|/, '') ,
|
82
|
+
|5 5
|
83
|
+
|0 0 E
|
84
|
+
|M
|
85
|
+
|0 0 N
|
86
|
+
|M
|
87
|
+
END
|
88
|
+
RuntimeError
|
89
|
+
end
|
90
|
+
end
|
91
|
+
context 'when robots take turns' do
|
92
|
+
let(:mode) { :turns }
|
93
|
+
it_behaves_like 'a correct application',
|
94
|
+
<<-END.gsub(/^\s+\|/, '') ,
|
95
|
+
|5 5
|
96
|
+
|0 0 N
|
97
|
+
|RMM
|
98
|
+
|1 0 E
|
99
|
+
|MMMM
|
100
|
+
END
|
101
|
+
<<-END.gsub(/^\s+\|/, '')
|
102
|
+
|2 0 E
|
103
|
+
|5 0 E
|
104
|
+
END
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|