robodog 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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