toy-robot-simulator 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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2b4b3c0db1ec9db67b04a8c7956260295a9184f566d58226882d889034c27760
4
+ data.tar.gz: 5ed46e5643fec38a6114029cf43ce63d862c9a94b3c2f221ce3c7c41f8b67916
5
+ SHA512:
6
+ metadata.gz: 70e4ed4c2e4972bb367fa02e6b4469f1b15393c5250fee6999c1975b18e5f9984ce9c9e63840a581f1738a517e0992a803d612ac5f9101d42041197aab35900d
7
+ data.tar.gz: 48d8a2823e3c988416442f09f87b3adb14726c7cef432be4ab11e6edd07fa44c6e36916d5d9012829c9f6103f0333f7cafe97426c47b282f876430149fffdf4a
File without changes
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ DIRECTIONS = %w[NORTH EAST SOUTH WEST].freeze
4
+ COMMANDS = %w[MOVE LEFT RIGHT REPORT PLACE].freeze
5
+ PLACE = COMMANDS.last.freeze
6
+ PLACE_COMMAND_PATTERN = Regexp.new(/#{PLACE}\s*(\d+)\s*,\s*(\d+)\s*,\s*(#{DIRECTIONS.join('|')})\s*$/)
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Command
4
+ class InvalidOrEmptyCommands < StandardError; end
5
+ class InvalidCommandStreamType < StandardError; end
6
+ class InvalidCommandType < StandardError; end
7
+ class NoValidCommandsFound < StandardError; end
8
+ class PlaceCommandNotFound < StandardError; end
9
+ class EmptyLocationProvided < StandardError; end
10
+ class FormatNotSupported < StandardError; end
11
+ class SourceNotSupported < StandardError; end
12
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ToyRobot
4
+ class InvalidFacing < StandardError; end
5
+ class RobotIsNotPlaced < StandardError; end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Surface
4
+ class TableOutOfBound < StandardError; end
5
+ class TableIsNotSet < StandardError; end
6
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../constants/application_constants'
4
+ # CommandHelper module
5
+ module CommandHelper
6
+ def a_valid_place_command?(command)
7
+ return false if command.match(PLACE_COMMAND_PATTERN).nil?
8
+
9
+ true
10
+ end
11
+
12
+ def split_place_command(place_command)
13
+ result = place_command.match(PLACE_COMMAND_PATTERN)
14
+ result&.captures
15
+ end
16
+
17
+ def command_type(command)
18
+ return if command.nil?
19
+ return PLACE if command.include? PLACE
20
+ return command if COMMANDS.include? command
21
+ end
22
+
23
+ def directions
24
+ DIRECTIONS
25
+ end
26
+
27
+ def commands
28
+ COMMANDS
29
+ end
30
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../constants/application_constants'
4
+ require_relative 'command_helper'
5
+ # CommandParserHelper module
6
+ module CommandParserHelper
7
+ include CommandHelper
8
+
9
+ def parse_command(command)
10
+ type_of_command = command_type(command)
11
+ return if type_of_command.nil?
12
+ return if type_of_command == PLACE && a_valid_place_command?(command) == false
13
+
14
+ command_object(type_of_command, command_value(command))
15
+ end
16
+
17
+ private
18
+
19
+ def command_value(command)
20
+ return split_place_command(command) if place_command_and_valid?(command)
21
+
22
+ command
23
+ end
24
+
25
+ def command_object(type, value, status = '', error = '')
26
+ Struct.new(:type, :value, :status, :error).new(type, value, status, error)
27
+ end
28
+
29
+ def place_command_and_valid?(command)
30
+ command_type(command) == PLACE && a_valid_place_command?(command)
31
+ end
32
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors/command'
4
+ # CommandSetLoader module
5
+ module CommandSetLoader
6
+ def read_commands(location = '', source = 'file', format = 'text')
7
+ raise Command::FormatNotSupported, 'Only text format is supported' unless format == 'text'
8
+ raise Command::SourceNotSupported, 'Only file as a source is supported' unless source == 'file'
9
+ raise Command::EmptyLocationProvided if location.strip == ''
10
+
11
+ @location = location
12
+ read_from_file
13
+ end
14
+
15
+ private
16
+
17
+ def read_from_file
18
+ commands = []
19
+ File.readlines(@location).each { |line| commands << line.strip }
20
+ commands
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'table_interface'
4
+ require_relative '../errors/table'
5
+ module Surface
6
+ # Table class
7
+ class Table
8
+ include TableInterface
9
+ attr_accessor :rows, :columns, :grid
10
+
11
+ def initialize(rows = 5, columns = 5)
12
+ @grid = Array.new(rows) { Array.new(columns) { 0 } }
13
+ @rows = rows
14
+ @columns = columns
15
+ end
16
+
17
+ def can_be_placed?(x, y)
18
+ return false unless (0..(rows - 1)).include?(x)
19
+ return false unless (0..(columns - 1)).include?(y)
20
+
21
+ true
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # module Surface
4
+ module Surface
5
+ # TableInterface to valid can_be_placed?(_x, _y) implemented by Table
6
+ module TableInterface
7
+ def can_be_placed?(_x, _y)
8
+ raise 'Not implemented'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors/command'
4
+ require_relative '../errors/robot'
5
+ module ToyRobot
6
+ # Robot class
7
+ class Robot
8
+ attr_accessor :x, :y, :facing, :placed, :table
9
+
10
+ def initialize(table = nil)
11
+ @placed = false
12
+ @table = table
13
+ @x = nil
14
+ @y = nil
15
+ @facing = nil
16
+ end
17
+
18
+ def place(x, y, facing)
19
+ return if @placed
20
+ raise Surface::TableIsNotSet if @table.nil?
21
+ raise Surface::TableOutOfBound unless @table.can_be_placed?(x, y)
22
+ raise ToyRobot::InvalidFacing unless DIRECTIONS.include?(facing)
23
+
24
+ @x = x
25
+ @y = y
26
+ @facing = facing
27
+ @placed = true
28
+ end
29
+
30
+ def left
31
+ check_pre_conditions
32
+ @facing = next_facing('left')
33
+ end
34
+
35
+ def right
36
+ check_pre_conditions
37
+ @facing = next_facing('right')
38
+ end
39
+
40
+ def move
41
+ check_pre_conditions
42
+ result = calculate_potential_movement(facing)
43
+
44
+ x_should_be = result[:x].nil? ? @x : (@x + result[:x])
45
+ y_should_be = result[:y].nil? ? @y : (@y + result[:y])
46
+
47
+ raise Surface::TableOutOfBound unless @table.can_be_placed?(x_should_be, y_should_be)
48
+
49
+ @x = x_should_be
50
+ @y = y_should_be
51
+ end
52
+
53
+ def report(format = 'console')
54
+ raise ToyRobot::RobotIsNotPlaced unless @placed
55
+
56
+ case format
57
+ when 'console'
58
+ to_s
59
+ else
60
+ { x: @x, y: @y, facing: @facing }
61
+ end
62
+ end
63
+
64
+ def calculate_potential_movement(facing)
65
+ case facing
66
+ when 'NORTH'
67
+ { x: nil, y: 1 }
68
+ when 'WEST'
69
+ { x: -1, y: nil }
70
+ when 'SOUTH'
71
+ { x: nil, y: -1 }
72
+ when 'EAST'
73
+ { x: 1, y: nil }
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def to_s
80
+ "#{@x},#{@y},#{@facing}"
81
+ end
82
+
83
+ def next_facing(arrow)
84
+ return DIRECTIONS.last if @facing == DIRECTIONS.first && arrow == 'left'
85
+ return DIRECTIONS.first if @facing == DIRECTIONS.last && arrow == 'right'
86
+
87
+ increment_factor = arrow == 'left' ? -1 : 1
88
+ new_index = DIRECTIONS.index(@facing) + increment_factor
89
+ DIRECTIONS[new_index]
90
+ end
91
+
92
+ def check_pre_conditions
93
+ raise Surface::TableIsNotSet if @table.nil?
94
+ raise ToyRobot::RobotIsNotPlaced unless @placed
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'toy_robot/robot'
4
+ require_relative 'surface/table'
5
+ require_relative 'helpers/command_parser_helper'
6
+ require_relative 'errors/command'
7
+ # ToyRobotController module and main entry of the program
8
+ module ToyRobotController
9
+ class << self
10
+ attr_accessor :robot, :commands
11
+ include CommandParserHelper
12
+
13
+ def init(commands_stream, table_size = 5)
14
+ validate_command_stream(commands_stream)
15
+
16
+ @commands_stream = commands_stream
17
+ @table = Surface::Table.new(table_size, table_size)
18
+ @robot = ToyRobot::Robot.new(@table)
19
+ @commands = []
20
+ populate_commands
21
+ end
22
+
23
+ def report(format = 'console')
24
+ @robot.report(format)
25
+ end
26
+
27
+ def execute_commands
28
+ raise Command::NoValidCommandsFound if @commands.size.zero?
29
+ raise Command::PlaceCommandNotFound if @commands.select { |c| c.type == 'PLACE' }.size.zero?
30
+
31
+ @commands.each { |command| send_command_to_robot(command) }
32
+ end
33
+
34
+ private
35
+
36
+ def send_command_to_robot(command)
37
+ case command.type
38
+ when 'PLACE'
39
+ @robot.place(command.value[0].to_i, command.value[1].to_i, command.value[2])
40
+ when 'MOVE', 'RIGHT', 'LEFT', 'REPORT'
41
+ @robot.send(command.type.downcase)
42
+ end
43
+ rescue ToyRobot::RobotIsNotPlaced, Surface::TableOutOfBound => e
44
+ command.status = 'ignored'
45
+ command.error = e.to_s
46
+ end
47
+
48
+ def validate_command_stream(commands_stream)
49
+ raise Command::InvalidCommandStreamType unless commands_stream.is_a?(Array)
50
+ raise Command::InvalidOrEmptyCommands if commands_stream.size.zero?
51
+ raise Command::InvalidCommandType unless commands_stream.all? { |i| i.is_a? String }
52
+ end
53
+
54
+ def populate_commands
55
+ @commands_stream.each do |command|
56
+ current_command = parse_command(command)
57
+ @commands << current_command unless current_command.nil?
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe CommandHelper do
4
+ include CommandHelper
5
+
6
+ describe '#command_type' do
7
+ it { expect(command_type('MOVE')).to eq('MOVE') }
8
+ it { expect(command_type('LEFT')).to eq('LEFT') }
9
+ it { expect(command_type('RIGHT')).to eq('RIGHT') }
10
+ it { expect(command_type('REPORT')).to eq('REPORT') }
11
+ it { expect(command_type('SOMETHING')).to eq(nil) }
12
+ it { expect(command_type('PLACE 0, 0, NORTH')).to eq('PLACE') }
13
+ it { expect(command_type('PLACE')).to eq('PLACE') }
14
+ it { expect(command_type(nil)).to eq(nil) }
15
+ end
16
+
17
+ describe '#valid_place_command?' do
18
+ context 'when PLACE command is invalid' do
19
+ it { expect(a_valid_place_command?('RANDOM')).to eq(false) }
20
+ it { expect(a_valid_place_command?('A, B, PLACE, Y')).to eq(false) }
21
+ it { expect(a_valid_place_command?('A, PLACE,B, Y')).to eq(false) }
22
+ it { expect(a_valid_place_command?('A, B, PLACE, Y')).to eq(false) }
23
+ it { expect(a_valid_place_command?('PLACE')).to eq(false) }
24
+ it { expect(a_valid_place_command?('PLACE A, B')).to eq(false) }
25
+ it { expect(a_valid_place_command?('PLACE A, B, F, G')).to eq(false) }
26
+ it { expect(a_valid_place_command?('PLACE A, B, Y')).to eq(false) }
27
+ it { expect(a_valid_place_command?('PLACE A, 0, Y')).to eq(false) }
28
+ it { expect(a_valid_place_command?('PLACE 0, B, Y')).to eq(false) }
29
+ it { expect(a_valid_place_command?('PLACE 0, 0, Y')).to eq(false) }
30
+ end
31
+ end
32
+
33
+ describe '#split_place_command' do
34
+ context 'when the PLACE command is valid as per PLACE_COMMAND_PATTERN' do
35
+ it { expect(split_place_command('PLACE 0, 0, NORTH')).to match_array(%w[0 0 NORTH]) }
36
+ it { expect(split_place_command('PLACE 20, 0, NORTH')).to match_array(%w[20 0 NORTH]) }
37
+ it { expect(split_place_command('PLACE 0, 0, WEST')).to match_array(%w[0 0 WEST]) }
38
+ it { expect(split_place_command('PLACE 0, 0, SOUTH')).to match_array(%w[0 0 SOUTH]) }
39
+ it { expect(split_place_command('PLACE 100, 0, EAST')).to match_array(%w[100 0 EAST]) }
40
+ end
41
+
42
+ context 'when the PLACE command is not according to the PLACE_COMMAND_PATTERN' do
43
+ it { expect(split_place_command('PLACE 0, 0, Y')).to eq(nil) }
44
+ it { expect(split_place_command('PLACE A, 0, NORTH')).to eq(nil) }
45
+ it { expect(split_place_command('PLACE B, 0, SOUTH')).to eq(nil) }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe CommandParserHelper do
4
+ include CommandParserHelper
5
+
6
+ describe '#parse_command' do
7
+ let(:struct_blue_print) { { type: 'PLACE', value: %w[0 0 NORTH], status: '', error: '' } }
8
+
9
+ context 'when the command is a valid PLACE command' do
10
+ it 'will return an onject containing type, value, status and error' do
11
+ expect(parse_command('PLACE 0, 0, NORTH')).to have_attributes(struct_blue_print)
12
+ end
13
+
14
+ it { expect(parse_command('PLACE 0, 0, NORTH').type).to eq('PLACE') }
15
+ it { expect(parse_command('PLACE 0, 1, NORTH').value).to match_array(%w[0 1 NORTH]) }
16
+ it { expect(parse_command('PLACE 0, 0, NORTH').status).to eq('') }
17
+ it { expect(parse_command('PLACE 0, 0, NORTH').error).to eq('') }
18
+ end
19
+
20
+ context 'when any other valid command is provided' do
21
+ it { expect(parse_command('RIGHT').type).to eq('RIGHT') }
22
+ it { expect(parse_command('RIGHT').value).to eq('RIGHT') }
23
+ it { expect(parse_command('LEFT').type).to eq('LEFT') }
24
+ it { expect(parse_command('MOVE').type).to eq('MOVE') }
25
+ it { expect(parse_command('REPORT').type).to eq('REPORT') }
26
+ end
27
+
28
+ context 'when an invalid command is provided' do
29
+ it { expect(parse_command('REPORTME')).to eq(nil) }
30
+ it { expect(parse_command('RIGHTT')).to eq(nil) }
31
+ it { expect(parse_command('right')).to eq(nil) }
32
+ end
33
+
34
+ context 'when an invalid PLACE command is provided' do
35
+ it { expect(parse_command('PLACE')).to eq(nil) }
36
+ it { expect(parse_command('PLACE A, B')).to eq(nil) }
37
+ it { expect(parse_command('PLACE A, B, F, G')).to eq(nil) }
38
+ it { expect(parse_command('PLACE A, B, Y')).to eq(nil) }
39
+ it { expect(parse_command('PLACE A, 0, Y')).to eq(nil) }
40
+ it { expect(parse_command('PLACE 0, B, Y')).to eq(nil) }
41
+ it { expect(parse_command('PLACE 0, 0, Y')).to eq(nil) }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe CommandSetLoader do
4
+ include CommandSetLoader
5
+
6
+ describe '#read_commands' do
7
+ it 'will load the commands from test1.txt file and return array of commands' do
8
+ commands = read_commands('./test_data/test1.txt')
9
+ expect(commands).to match_array(['PLACE 0,0,NORTH', 'MOVE', 'REPORT'])
10
+ end
11
+
12
+ it 'will load the commands from test2.txt file and return array of commands' do
13
+ commands = read_commands('./test_data/test2.txt')
14
+ expect(commands).to match_array(['PLACE 0,0,NORTH', 'LEFT', 'REPORT'])
15
+ end
16
+
17
+ it 'will load the commands from test3.txt file and return array of commands' do
18
+ commands = read_commands('./test_data/test3.txt')
19
+ expect(commands).to match_array(['PLACE 1,2,EAST', 'MOVE', 'MOVE', 'LEFT', 'MOVE', 'REPORT'])
20
+ end
21
+
22
+ it 'will return SourceNotSupported exception if source is not file' do
23
+ expect do
24
+ read_commands('./test_data/test3.txt', 'remote')
25
+ end.to raise_error(Command::SourceNotSupported)
26
+ end
27
+
28
+ it 'will return FormatNotSupported exception if source is not text' do
29
+ expect do
30
+ read_commands('./test_data/test3.txt', 'file', 'json')
31
+ end.to raise_error(Command::FormatNotSupported)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe ToyRobot::Robot do
4
+ let(:robot) { described_class.new }
5
+ let(:table) { Surface::Table.new }
6
+ let(:robot_with_table) { described_class.new(table) }
7
+
8
+ it 'should initialize and set the instance variables' do
9
+ expect(robot.placed).to eq(false)
10
+ end
11
+
12
+ describe '#place' do
13
+ context 'when table size is 5x5 and x, y are within the table bounds along with valid facing position' do
14
+ it 'will successfully place the robot onto the table' do
15
+ robot_with_table.place(0, 0, 'SOUTH')
16
+ expect(robot_with_table.placed).to eq(true)
17
+ end
18
+ end
19
+
20
+ context 'when facing value is not valid' do
21
+ it 'will raise InvalidFacing Exception' do
22
+ expect { robot_with_table.place(0, 0, 'RANDOM') }.to raise_error(ToyRobot::InvalidFacing)
23
+ end
24
+ end
25
+
26
+ context 'robot is already place' do
27
+ it 'will ignore any other place commands' do
28
+ robot_with_table.place(0, 0, 'SOUTH')
29
+ robot_with_table.place(1, 1, 'NORTH')
30
+ robot_with_table.place(1, 1, 'WEST')
31
+ robot_with_table.place(1, 1, 'EAST')
32
+ expect(robot_with_table.x).to eq(0)
33
+ expect(robot_with_table.y).to eq(0)
34
+ expect(robot_with_table.facing).to eq('SOUTH')
35
+ end
36
+ end
37
+
38
+ context 'when coordinates are out of bounds' do
39
+ it 'will raise TableOutOfBound Exception' do
40
+ expect { robot_with_table.place(0, 50, 'SOUTH') }.to raise_error(Surface::TableOutOfBound)
41
+ end
42
+ end
43
+
44
+ context 'when table is nil' do
45
+ it 'will raise TableIsNotSet Exception' do
46
+ robot_with_table.table = nil
47
+ expect { robot_with_table.place(0, 0, 'SOUTH') }.to raise_error(Surface::TableIsNotSet)
48
+ end
49
+ end
50
+ end
51
+
52
+ describe '#left' do
53
+ context 'when robot is not placed on the table yet' do
54
+ it 'raises RobotIsNotPlaced exception' do
55
+ expect { robot_with_table.left }.to raise_error(ToyRobot::RobotIsNotPlaced)
56
+ end
57
+ end
58
+
59
+ context 'when table is not set' do
60
+ it 'raises TableIsNotSet exception' do
61
+ expect { robot.left }.to raise_error(Surface::TableIsNotSet)
62
+ end
63
+ end
64
+
65
+ context 'when robot is facing NORTH' do
66
+ it 'will rotate the robot towards WEST' do
67
+ robot_with_table.place(0, 0, 'NORTH')
68
+ robot_with_table.left
69
+ expect(robot_with_table.facing).to eq('WEST')
70
+ end
71
+ end
72
+
73
+ context 'when robot is facing WEST' do
74
+ it 'will rotate the robot towards SOUTH' do
75
+ robot_with_table.place(0, 0, 'WEST')
76
+ robot_with_table.left
77
+ expect(robot_with_table.facing).to eq('SOUTH')
78
+ end
79
+ end
80
+
81
+ context 'when robot is facing SOUTH' do
82
+ it 'will rotate the robot towards EAST' do
83
+ robot_with_table.place(0, 0, 'SOUTH')
84
+ robot_with_table.left
85
+ expect(robot_with_table.facing).to eq('EAST')
86
+ end
87
+ end
88
+
89
+ context 'when robot is facing EAST' do
90
+ it 'will rotate the robot towards NORTH' do
91
+ robot_with_table.place(0, 0, 'EAST')
92
+ robot_with_table.left
93
+ expect(robot_with_table.facing).to eq('NORTH')
94
+ end
95
+ end
96
+ end
97
+
98
+ describe '#right' do
99
+ context 'when robot is not placed on the table yet' do
100
+ it 'raises RobotIsNotPlaced exception' do
101
+ expect { robot_with_table.right }.to raise_error(ToyRobot::RobotIsNotPlaced)
102
+ end
103
+ end
104
+
105
+ context 'when table is not set' do
106
+ it 'raises TableIsNotSet exception' do
107
+ expect { robot.right }.to raise_error(Surface::TableIsNotSet)
108
+ end
109
+ end
110
+
111
+ context 'when robot is facing WEST' do
112
+ it 'will rotate the robot towards NORTH' do
113
+ robot_with_table.place(0, 0, 'WEST')
114
+ robot_with_table.right
115
+ expect(robot_with_table.facing).to eq('NORTH')
116
+ end
117
+ end
118
+
119
+ context 'when robot is facing NORTH' do
120
+ it 'will rotate the robot towards EAST' do
121
+ robot_with_table.place(0, 0, 'NORTH')
122
+ robot_with_table.right
123
+ expect(robot_with_table.facing).to eq('EAST')
124
+ end
125
+ end
126
+
127
+ context 'when robot is facing EAST' do
128
+ it 'will rotate the robot towards SOUTH' do
129
+ robot_with_table.place(0, 0, 'EAST')
130
+ robot_with_table.right
131
+ expect(robot_with_table.facing).to eq('SOUTH')
132
+ end
133
+ end
134
+
135
+ context 'when robot is facing SOUTH' do
136
+ it 'will rotate the robot towards WEST' do
137
+ robot_with_table.place(0, 0, 'SOUTH')
138
+ robot_with_table.right
139
+ expect(robot_with_table.facing).to eq('WEST')
140
+ end
141
+ end
142
+ end
143
+
144
+ describe '#move' do
145
+ context 'when the table is not set' do
146
+ it 'raises TableIsNotSet exception' do
147
+ expect { robot.move }.to raise_error(Surface::TableIsNotSet)
148
+ end
149
+ end
150
+
151
+ context 'when the robot is not placed' do
152
+ it 'raises TableIsNotSet exception' do
153
+ expect { robot_with_table.move }.to raise_error(ToyRobot::RobotIsNotPlaced)
154
+ end
155
+ end
156
+
157
+ context 'when robot is placed at 0,0 NORTH' do
158
+ it 'will result in 0, 1 NORTH' do
159
+ robot_with_table.place(0, 0, 'NORTH')
160
+ robot_with_table.move
161
+ expect(robot_with_table.x).to eq(0)
162
+ expect(robot_with_table.y).to eq(1)
163
+ expect(robot_with_table.facing).to eq('NORTH')
164
+ end
165
+ end
166
+
167
+ context 'when robot is placed at 0,4 NORTH' do
168
+ it 'raises TableOutOfBound exception' do
169
+ robot_with_table.place(0, 4, 'NORTH')
170
+ expect { robot_with_table.move }.to raise_error(Surface::TableOutOfBound)
171
+ end
172
+ end
173
+ end
174
+
175
+ describe '#report' do
176
+ it 'will report the current coordinates and facing of robot wherever it is placed' do
177
+ robot_with_table.place(0, 4, 'NORTH')
178
+ expect(robot_with_table.report).to eq('0,4,NORTH')
179
+ end
180
+
181
+ context 'When the robot is not placed onto the table' do
182
+ it 'will rais RobotIsNotPlaced exception' do
183
+ expect { robot_with_table.report }.to raise_error(ToyRobot::RobotIsNotPlaced)
184
+ end
185
+ end
186
+ end
187
+
188
+ describe '#calculate_potential_movement' do
189
+ context 'when facing NORTH' do
190
+ it 'should do one step positive in y direction' do
191
+ result = robot_with_table.calculate_potential_movement('NORTH')
192
+ expect(result[:y]).to eq(1)
193
+ end
194
+ end
195
+
196
+ context 'when facing WEST' do
197
+ it 'should do one step positive in y direction' do
198
+ result = robot_with_table.calculate_potential_movement('WEST')
199
+ expect(result[:x]).to eq(-1)
200
+ end
201
+ end
202
+
203
+ context 'when facing SOUTH' do
204
+ it 'should do one step positive in y direction' do
205
+ result = robot_with_table.calculate_potential_movement('SOUTH')
206
+ expect(result[:y]).to eq(-1)
207
+ end
208
+ end
209
+
210
+ context 'when facing EAST' do
211
+ it 'should do one step positive in y direction' do
212
+ result = robot_with_table.calculate_potential_movement('EAST')
213
+ expect(result[:x]).to eq(1)
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file was generated by the `rspec --init` command. Conventionally, all
4
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
5
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
6
+ # this file to always be loaded, without a need to explicitly require it in any
7
+ # files.
8
+ #
9
+ # Given that it is always loaded, you are encouraged to keep this file as
10
+ # light-weight as possible. Requiring heavyweight dependencies from this file
11
+ # will add to the boot time of your test suite on EVERY test run, even for an
12
+ # individual file that may not need all of that loaded. Instead, consider making
13
+ # a separate helper file that requires the additional dependencies and performs
14
+ # the additional setup, and require it from the spec files that actually need
15
+ # it.
16
+ #
17
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
18
+ require_relative '../lib/toy_robot/robot'
19
+ require_relative '../lib/surface/table'
20
+ require_relative '../lib/toy_robot_controller'
21
+ require_relative '../lib/helpers/command_helper'
22
+ require_relative '../lib/helpers/command_parser_helper'
23
+ require_relative '../lib/helpers/command_set_loader'
24
+ require 'byebug'
25
+ # require 'byebug'
26
+ RSpec.configure do |config|
27
+ # rspec-expectations config goes here. You can use an alternate
28
+ # assertion/expectation library such as wrong or the stdlib/minitest
29
+ # assertions if you prefer.
30
+ config.expect_with :rspec do |expectations|
31
+ # This option will default to `true` in RSpec 4. It makes the `description`
32
+ # and `failure_message` of custom matchers include text for helper methods
33
+ # defined using `chain`, e.g.:
34
+ # be_bigger_than(2).and_smaller_than(4).description
35
+ # # => "be bigger than 2 and smaller than 4"
36
+ # ...rather than:
37
+ # # => "be bigger than 2"
38
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
39
+ end
40
+
41
+ # rspec-mocks config goes here. You can use an alternate test double
42
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
43
+ config.mock_with :rspec do |mocks|
44
+ # Prevents you from mocking or stubbing a method that does not exist on
45
+ # a real object. This is generally recommended, and will default to
46
+ # `true` in RSpec 4.
47
+ mocks.verify_partial_doubles = true
48
+ end
49
+
50
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
51
+ # have no way to turn it off -- the option exists only for backwards
52
+ # compatibility in RSpec 3). It causes shared context metadata to be
53
+ # inherited by the metadata hash of host groups and examples, rather than
54
+ # triggering implicit auto-inclusion in groups with matching metadata.
55
+ config.shared_context_metadata_behavior = :apply_to_host_groups
56
+
57
+ # The settings below are suggested to provide a good initial experience
58
+ # with RSpec, but feel free to customize to your heart's content.
59
+ # # This allows you to limit a spec run to individual examples or groups
60
+ # # you care about by tagging them with `:focus` metadata. When nothing
61
+ # # is tagged with `:focus`, all examples get run. RSpec also provides
62
+ # # aliases for `it`, `describe`, and `context` that include `:focus`
63
+ # # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
64
+ # config.filter_run_when_matching :focus
65
+ #
66
+ # # Allows RSpec to persist some state between runs in order to support
67
+ # # the `--only-failures` and `--next-failure` CLI options. We recommend
68
+ # # you configure your source control system to ignore this file.
69
+ # config.example_status_persistence_file_path = "spec/examples.txt"
70
+ #
71
+ # # Limits the available syntax to the non-monkey patched syntax that is
72
+ # # recommended. For more details, see:
73
+ # # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
74
+ # # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
75
+ # # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
76
+ # config.disable_monkey_patching!
77
+ #
78
+ # # This setting enables warnings. It's recommended, but in some cases may
79
+ # # be too noisy due to issues in dependencies.
80
+ # config.warnings = true
81
+ #
82
+ # # Many RSpec users commonly either run the entire suite or an individual
83
+ # # file, and it's useful to allow more verbose output when running an
84
+ # # individual spec file.
85
+ # if config.files_to_run.one?
86
+ # # Use the documentation formatter for detailed output,
87
+ # # unless a formatter has already been configured
88
+ # # (e.g. via a command-line flag).
89
+ # config.default_formatter = "doc"
90
+ # end
91
+ #
92
+ # # Print the 10 slowest examples and example groups at the
93
+ # # end of the spec run, to help surface which specs are running
94
+ # # particularly slow.
95
+ # config.profile_examples = 10
96
+ #
97
+ # # Run specs in random order to surface order dependencies. If you find an
98
+ # # order dependency and want to debug it, you can fix the order by providing
99
+ # # the seed, which is printed after each run.
100
+ # # --seed 1234
101
+ # config.order = :random
102
+ #
103
+ # # Seed global randomization in this process using the `--seed` CLI option.
104
+ # # Setting this allows you to use `--seed` to deterministically reproduce
105
+ # # test failures related to randomization by passing the same `--seed` value
106
+ # # as the one that triggered the failure.
107
+ # Kernel.srand config.seed
108
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Surface::Table do
4
+ let(:table) { described_class.new }
5
+
6
+ it 'should initialize and set the instance variables' do
7
+ expect(table.rows).to eq(5)
8
+ expect(table.columns).to eq(5)
9
+ end
10
+
11
+ describe '#can_be_placed?' do
12
+ context 'when table is 5x5' do
13
+ it { expect(table.can_be_placed?(4, 4)).to eq(true) }
14
+ it { expect(table.can_be_placed?(0, 0)).to eq(true) }
15
+ it { expect(table.can_be_placed?(4, 5)).to eq(false) }
16
+ it { expect(table.can_be_placed?(5, 5)).to eq(false) }
17
+ it { expect(table.can_be_placed?(-5, 5)).to eq(false) }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe ToyRobotController do
4
+ let(:controller) { described_class }
5
+
6
+ describe '.init' do
7
+ it 'will instanciate the robot' do
8
+ comamnds_stream = ['PLACE 0,0,NORTH', 'MOVE', 'REPORT']
9
+ controller.init(comamnds_stream)
10
+ expect(controller.robot).to be_an_instance_of(ToyRobot::Robot)
11
+ end
12
+
13
+ it 'will populate the commands with valid commands only' do
14
+ comamnds_stream = ['PLACE 0,0,NORTH', 'MOVE', 'REPORT', 'RANDOM Command']
15
+ controller.init(comamnds_stream)
16
+ expect(controller.commands.map(&:value)).to match_array([%w[0 0 NORTH], 'MOVE', 'REPORT'])
17
+ end
18
+ context 'when command steams type is not Array' do
19
+ it 'will raise InvalidCommandStreamType exception' do
20
+ expect { controller.init('') }.to raise_error(Command::InvalidCommandStreamType)
21
+ end
22
+ end
23
+
24
+ context 'when command steams is empty' do
25
+ it 'will raise InvalidOrEmptyCommands exception' do
26
+ expect { controller.init([]) }.to raise_error(Command::InvalidOrEmptyCommands)
27
+ end
28
+ end
29
+
30
+ context 'when command steams Array contains non String types' do
31
+ it 'will raise InvalidOrEmptyCommands exception' do
32
+ comamnds_stream = [1, 'PLACE 0,0,NORTH', 'REPORT']
33
+ expect { controller.init(comamnds_stream) }.to raise_error(Command::InvalidCommandType)
34
+ end
35
+ end
36
+ end
37
+
38
+ describe '.execute_commands' do
39
+ context 'when all commands are invalid' do
40
+ it 'will raise NoValidCommandsFound exception' do
41
+ comamnds_stream = ['RANDOM Command', 'adfadfadf', 'adfrtrett']
42
+ controller.init(comamnds_stream)
43
+ expect { controller.execute_commands }.to raise_error(Command::NoValidCommandsFound)
44
+ end
45
+ end
46
+
47
+ it 'will ignore all commands before PLACE' do
48
+ controller.init(['MOVE', 'REPORT', 'PLACE 1,1,NORTH', 'MOVE', 'REPORT'])
49
+ controller.execute_commands
50
+ expect(controller.commands.select { |c| c.status == 'ignored' }.size).to eq(2)
51
+ end
52
+
53
+ context 'when PLACE command is not provided' do
54
+ it 'will raise PlaceCommandNotFound exception' do
55
+ controller.init(%w[MOVE MOVE LEFT MOVE REPORT])
56
+ expect { controller.execute_commands }.to raise_error(Command::PlaceCommandNotFound)
57
+ end
58
+ it 'will raise PlaceCommandNotFound exception with a PLACE command without X,Y,Facing' do
59
+ controller.init(%w[PLACE MOVE MOVE LEFT MOVE REPORT])
60
+ expect { controller.execute_commands }.to raise_error(Command::PlaceCommandNotFound)
61
+ end
62
+ end
63
+ end
64
+
65
+ describe '.report' do
66
+ context 'when command sequence is PLACE 0,0,NORTH MOVE REPORT ' do
67
+ it 'will move the robot to 0,1,NORTH' do
68
+ controller.init(['PLACE 0,0,NORTH', 'MOVE', 'REPORT'])
69
+ controller.execute_commands
70
+ expect(controller.report).to eq('0,1,NORTH')
71
+ end
72
+ end
73
+
74
+ context 'when command sequence is PLACE 1,2,EAST MOVE MOVE LEFT MOVE REPORT' do
75
+ it 'will move the robot to 3,3,NORTH' do
76
+ controller.init(['PLACE 1,2,EAST', 'MOVE', 'MOVE', 'LEFT', 'MOVE', 'REPORT'])
77
+ controller.execute_commands
78
+ expect(controller.report).to eq('3,3,NORTH')
79
+ end
80
+ end
81
+ context 'when command sequence is PLACE 1,2,WEST MOVE MOVE LEFT MOVE REPORT' do
82
+ it 'will move the robot to 0,1,SOUTH' do
83
+ controller.init(['PLACE 1,2,WEST', 'MOVE', 'MOVE', 'LEFT', 'MOVE', 'REPORT'])
84
+ controller.execute_commands
85
+ expect(controller.report).to eq('0,1,SOUTH')
86
+ end
87
+ end
88
+
89
+ context 'when command sequence contains only PLACE command' do
90
+ it 'will report where the robot was PLACED' do
91
+ controller.init(['PLACE 0,0,SOUTH'])
92
+ controller.execute_commands
93
+ expect(controller.report).to eq('0,0,SOUTH')
94
+ end
95
+ end
96
+ context 'when PLACE command is not provided' do
97
+ it 'will raise RobotIsNotPlaced exception' do
98
+ controller.init(%w[MOVE MOVE LEFT MOVE REPORT])
99
+ expect { controller.report }.to raise_error(ToyRobot::RobotIsNotPlaced)
100
+ end
101
+ end
102
+ end
103
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: toy-robot-simulator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Salman Sohail
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-08-27 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Let the Toy Robot move onto the table without dying.
14
+ email: salmansohail@agilis-lab.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - README.md
20
+ - lib/constants/application_constants.rb
21
+ - lib/errors/command.rb
22
+ - lib/errors/robot.rb
23
+ - lib/errors/table.rb
24
+ - lib/helpers/command_helper.rb
25
+ - lib/helpers/command_parser_helper.rb
26
+ - lib/helpers/command_set_loader.rb
27
+ - lib/surface/table.rb
28
+ - lib/surface/table_interface.rb
29
+ - lib/toy_robot/robot.rb
30
+ - lib/toy_robot_controller.rb
31
+ - spec/command_helper_spec.rb
32
+ - spec/command_parser_helper_spec.rb
33
+ - spec/command_set_loader_spec.rb
34
+ - spec/robot_spec.rb
35
+ - spec/spec_helper.rb
36
+ - spec/table_spec.rb
37
+ - spec/toy_robot_controller_spec.rb
38
+ homepage: https://github.com/saluminati/toy-robot
39
+ licenses:
40
+ - MIT
41
+ metadata: {}
42
+ post_install_message:
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 2.3.0
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubygems_version: 3.0.3
58
+ signing_key:
59
+ specification_version: 4
60
+ summary: Ruby gem for Toy Robot test
61
+ test_files:
62
+ - spec/robot_spec.rb
63
+ - spec/spec_helper.rb
64
+ - spec/command_set_loader_spec.rb
65
+ - spec/toy_robot_controller_spec.rb
66
+ - spec/command_parser_helper_spec.rb
67
+ - spec/table_spec.rb
68
+ - spec/command_helper_spec.rb