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.
@@ -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
@@ -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