I have already flunked the test with this code so don't feel bad for cheating any employer. I didn't get any feedback though just wondering what could have gone wrong.
You don't need to get into the functionality, just the overall design. What are short-coming of this code which disqualifies it from being production quality.
Toy Robot Simulator
Description
- The application is a simulation of a toy robot moving on a square tabletop, of dimensions 5 units x 5 units.
- There are no other obstructions on the table surface.
- The robot is free to roam around the surface of the table, but must be prevented from falling to destruction. Any movement that would result in the robot falling from the table must be prevented, however further valid movement commands must still be allowed.
Create an application that can read in commands of the following form:
PLACE X,Y,F MOVE LEFT RIGHT REPORT
- PLACE will put the toy robot on the table in position X,Y and facing NORTH, SOUTH, EAST or WEST.
- The origin (0,0) can be considered to be the SOUTH WEST most corner.
- The first valid command to the robot is a PLACE command, after that, any sequence of commands may be issued, in any order, including another PLACE command. The application should discard all commands in the sequence until a valid PLACE command has been executed.
- MOVE will move the toy robot one unit forward in the direction it is currently facing.
- LEFT and RIGHT will rotate the robot 90 degrees in the specified direction without changing the position of the robot.
REPORT will announce the X,Y and F of the robot. This can be in any form, but standard output is sufficient.
A robot that is not on the table can choose the ignore the MOVE, LEFT, RIGHT and REPORT commands.
- Input can be from a file, or from standard input, as the developer chooses.
- Provide test data to exercise the application.
Constraints
- The toy robot must not fall off the table during movement. This also includes the initial placement of the toy robot.
- Any move that would cause the robot to fall must be ignored.
Example Input and Output
Example a
PLACE 0,0,NORTH MOVE REPORT
Expected output:
0,1,NORTH
Example b
PLACE 0,0,NORTH LEFT REPORT
Expected output:
0,0,WEST
Example c
PLACE 1,2,EAST MOVE MOVE LEFT MOVE REPORT
Expected output
3,3,NORTH
Deliverables
The Ruby source files, the test data and any test code.
It is not required to provide any graphical output showing the movement of the toy robot.
GitHub (Please refer to see the whole project)
cli.rb
module Robot
class CLI
def initialize
options = {:file => nil , :x => 5 , :y => 5}
parser = OptionParser.new do|opts|
opts.banner = "Usage: toyrobot [options]"
opts.on('-f', '--file filepath', 'Filepath for input commands') do |filename|
options[:file] = filename
end
opts.on('-x', '--xcoordinate X', 'Max X co-ordinate(Number)') do |max_x|
begin
options[:x] = Integer(max_x)
rescue
puts "Invalid x argument"
puts opts
exit
end
end
opts.on('-y', '--ycoordinate Y', 'Max Y co-ordinate(Number)') do |max_y|
begin
options[:y] = Integer(max_y)
rescue
puts "Invalid y argument"
puts opts
exit
end
end
opts.on('-h', '--help', 'Displays Help') do
puts opts
exit
end
end
parser.parse!
@application = Robot::App.new(options)
end
def start
@application.start
end
end
end
app.rb
module Robot
class App
def initialize(opts)
@input_file = opts[:file].nil? ? STDIN : File.open(opts[:file])
@simulator = Robot::Simulator.new opts[:x], opts[:y]
end
def start
command = read_command
while (command) do
$logger.debug("Received command #{command}")
begin
@simulator.execute command
rescue => e
$logger.error(e)
end
command = read_command
end
end
def read_command
print "# " if @input_file == STDIN
command = @input_file.gets
exit if command.nil? || (command.chomp.downcase == ".quit")
command
end
end
end
command_parser.rb
module Robot
class CommandParser
@@number_reg = /^\d+$/
# allowed_commnads is a hash
# with value as :method => [[arg types],[regex_range_for_strings_only]]
def initialize(allowed_commands)
@allowed_commands = allowed_commands
$logger.info("Allowed commands are #{@allowed_commands.keys}")
end
def parse command
$logger.debug("Parsing command #{command}")
args = command.split " "
method = args.delete_at(0)
if valid?(method, args)
update_args! method , args
yield method , args
return true
else
$logger.warn("Parsing failed. Invalid #{command}")
return false
end
end
private
def update_args! method , args
@allowed_commands[method][0].each_with_index do |arg_type,i|
case arg_type
when :number
args[i] = args[i].to_i
when :string
end
end
end
def valid? (method , args)
return false unless @allowed_commands.has_key? method
unless @allowed_commands[method].nil?
return false unless @allowed_commands[method][0].size == args.size
@allowed_commands[method][0].each_with_index do |arg_type,i|
case arg_type
when :number
return false unless args[i] =~ @@number_reg
when :string
allowed_reg = @allowed_commands[method][1][i]
unless allowed_reg.nil?
return false unless args[i] =~ /#{allowed_reg}/
end
end
end
end
return true
end
end
end
direction.rb
module Robot
class Direction
attr_accessor :direction
def initialize(direction)
@direction = direction
end
@NORTH = Direction.new "NORTH"
@SOUTH = Direction.new "SOUTH"
@EAST = Direction.new "EAST"
@WEST = Direction.new "WEST"
@CLOCKWISE_DIRECTIONS = [@NORTH,@EAST,@SOUTH,@WEST]
def to_s
@direction
end
class << self
attr_accessor :NORTH, :SOUTH, :EAST, :WEST
end
def self.find(direction)
@CLOCKWISE_DIRECTIONS.find{|d| d.direction == direction.upcase }
end
def self.left(direction)
@CLOCKWISE_DIRECTIONS[( @CLOCKWISE_DIRECTIONS.index(direction) - 1 ) % 4]
end
def self.right(direction)
@CLOCKWISE_DIRECTIONS[( @CLOCKWISE_DIRECTIONS.index(direction) + 1 ) % 4]
end
end
end
position.rb
module Robot
class Position
attr_accessor :x, :y, :direction
def initialize(x,y,direction)
@x = x
@y = y
@direction = direction
end
def to_s
"X= #{@x} Y=#{@y} Facing=#{@direction.to_s}"
end
def equal?(another_position)
self.x == another_position.x &&
self.y == another_position.y &&
self.direction == another_position.direction
end
def move
curr_postion = self.dup
case curr_postion.direction
when Direction.NORTH
curr_postion.y +=1
when Direction.SOUTH
curr_postion.y -=1
when Direction.EAST
curr_postion.x +=1
when Direction.WEST
curr_postion.x -=1
end
curr_postion
end
def left
curr_postion = self.dup
curr_postion.direction = Direction.left @direction
curr_postion
end
def right
curr_postion = self.dup
curr_postion.direction = Direction.right @direction
curr_postion
end
end
end
simulator.rb
module Robot
class Simulator
attr_accessor :toy_robot
def initialize max_x, max_y
commands = {
"PLACE" => [
[:number , :number , :string],
[nil,nil,"^NORTH$|^SOUTH$|^EAST$|^WEST$"]
],
"MOVE" => [[],[]],
"LEFT" => [[],[]],
"RIGHT" => [[],[]],
"REPORT" => [[],[]]
}
@command_parser = CommandParser.new(commands)
@table = Table.new max_x , max_y
@toy_robot = ToyRobot.new
$logger.info "Simulator created successfully."
end
def execute(command)
r = @command_parser.parse(command) do |method,args|
$logger.debug("#{method.downcase} - args #{args}")
self.send( method.downcase , * args)
end
$logger.debug(@toy_robot.to_s)
end
def place x , y , face
if @table.inside?(x, y)
@toy_robot.position = Position.new(x, y, Direction.find(face))
@toy_robot.placed = true
end
end
def move
return unless @toy_robot.placed
next_position = @toy_robot.position.move
if @table.inside? next_position.x , next_position.y
@toy_robot.position = next_position
else
ignore
end
end
def left
return unless @toy_robot.placed
@toy_robot.position = @toy_robot.position.left
end
def right
return unless @toy_robot.placed
@toy_robot.position = @toy_robot.position.right
end
def report
if @toy_robot.placed
puts "#{@toy_robot.position.x} #{@toy_robot.position.y} #{@toy_robot.position.direction}"
else
puts "Robot is not placed yet. Please use PLACE command to place the robot."
end
end
def ignore
$logger.debug "Ignored step towards #{toy_robot.position.direction}"
end
end
end
table.rb
module Robot
class Table
def initialize max_x , max_y
@MAX_X = max_x
@MAX_Y = max_y
$logger.info "Table boundaries are #{@MAX_X},#{@MAX_Y}"
end
def inside? x,y
return ((0..@MAX_X-1) === x) && ((0..@MAX_Y-1) === y)
end
end
end
toyrobot.rb
module Robot
class ToyRobot
attr_accessor :position, :placed
def initialize
@position = nil
@placed = false
$logger.info "Toy Robot created successfully."
end
def to_s
if @placed
"Placed at #{@position.to_s}"
else
"Not placed"
end
end
end
end
robot.rb
$LOAD_PATH << File.join(File.dirname(__FILE__))
require "optparse"
require "logger"
require "robot/version"
require 'robot/command_parser'
require 'robot/table'
require 'robot/position'
require 'robot/toy_robot'
require "robot/direction"
require "robot/simulator"
require "robot/app"
require "robot/cli"
$logger = Logger.new('log/toy_robot.log')
Test code
command_parser_spec.rb
require_relative '../spec_helper'
require_relative '../../lib/robot'
include Robot
describe CommandParser do
subject(:command_parser) {CommandParser.new({
"Salute" => [
[:string],
["^Hello$|^Namaste$"]
],
"Name" => [
[:string],
[]
] ,
"Age" => [
[:number],
[]
]
})}
context "#with valid command" do
context "with range" do
before {
@ran = false
@called = command_parser.parse "Salute Hello" do |m,a|
@ran = true
@m = m
@a = a
end
}
it { expect(@ran).to be true }
it { expect(@called).to be true }
it { expect(@a.size).to be 1 }
it { expect(@a[0]).to match "Hello" }
it { expect(@m).to match "Salute" }
end
context "without range" do
context "with string" do
before {
@ran = false
@called = command_parser.parse "Name Tom" do |m,a|
@ran = true
@m = m
@a = a
end
}
it { expect(@ran).to be true }
it { expect(@called).to be true }
it { expect(@a.size).to be 1 }
it { expect(@a[0]).to match "Tom" }
it { expect(@m).to match "Name" }
end
context "with number" do
before {
@ran = false
@called = command_parser.parse "Age 50" do |m,a|
@ran = true
@m = m
@a = a
end
}
it { expect(@ran).to be true }
it { expect(@called).to be true }
it { expect(@a.size).to be 1 }
it { expect(@a[0]).to be 50 }
it { expect(@m).to match "Age"}
end
end
end
context "#with invalid command" do
before {
@ran = false
@called = command_parser.parse "Salute Hi" do |m,a|
@ran = true
end
}
it { expect(@called).to be false }
it { expect(@called).to be false }
end
end
direction_spec.rb
require_relative '../spec_helper'
require_relative '../../lib/robot'
include Robot
describe Direction do
describe ":: finds directions" do
it { expect( Direction.find "NORTH" ).to be Direction.NORTH }
it { expect( Direction.find "SOUTH" ).to be Direction.SOUTH }
it { expect( Direction.find "EAST" ).to be Direction.EAST }
it { expect( Direction.find "WEST" ).to be Direction.WEST }
end
describe ":: turns left" do
it { expect( Direction.left Direction.NORTH ).to be Direction.WEST }
it { expect( Direction.left Direction.SOUTH ).to be Direction.EAST }
it { expect( Direction.left Direction.EAST ).to be Direction.NORTH }
it { expect( Direction.left Direction.WEST ).to be Direction.SOUTH }
end
describe ":: turns right" do
it { expect( Direction.right Direction.NORTH ).to be Direction.EAST }
it { expect( Direction.right Direction.SOUTH ).to be Direction.WEST }
it { expect( Direction.right Direction.EAST ).to be Direction.SOUTH }
it { expect( Direction.right Direction.WEST ).to be Direction.NORTH }
end
end
position_spec.rb
require_relative '../spec_helper'
require_relative '../../lib/robot'
include Robot
describe Position do
context "#moves correctly to north" do
before {
@position = Position.new(2, 3, Direction.NORTH)
@curr_position = @position.move
}
it { expect(@curr_position.x).to be 2 }
it { expect(@curr_position.y).to be 4 }
it { expect(@curr_position.direction).to be Direction.NORTH }
end
context "#moves correctly to south" do
before {
@position = Position.new(2, 3, Direction.SOUTH)
@curr_position = @position.move
}
it { expect(@curr_position.x).to be 2 }
it { expect(@curr_position.y).to be 2 }
it { expect(@curr_position.direction).to be Direction.SOUTH }
end
context "#moves correctly to east" do
before {
@position = Position.new(2, 3, Direction.EAST)
@curr_position = @position.move
}
it { expect(@curr_position.x).to be 3 }
it { expect(@curr_position.y).to be 3 }
it { expect(@curr_position.direction).to be Direction.EAST }
end
context "#moves correctly to west" do
before {
@position = Position.new(2, 3, Direction.WEST)
@curr_position = @position.move
}
it { expect(@curr_position.x).to be 1 }
it { expect(@curr_position.y).to be 3 }
it { expect(@curr_position.direction).to be Direction.WEST }
end
context "#turns left" do
before {
start_position = Position.new(2, 3, Direction.WEST)
@position = start_position.left
}
it { expect(@position.x).to be 2 }
it { expect(@position.y).to be 3 }
it { expect(@position.direction).to be Direction.SOUTH }
end
context "#turns right" do
before {
start_position = Position.new(2, 3, Direction.WEST)
@position = start_position.right
}
it { expect(@position.x).to be 2 }
it { expect(@position.y).to be 3 }
it { expect(@position.direction).to be Direction.NORTH }
end
end
simualtor_spec.rb
require_relative '../spec_helper'
require_relative '../../lib/robot'
include Robot
describe Simulator do
describe "#gets placed" do
before {
@simulator = Simulator.new 5,5
@simulator.place 2, 3, "NORTH"
}
it { expect(@simulator.toy_robot.position).to be Position.new(2,3,Direction.NORTH) }
end
describe "#moves" do
context "when inside table" do
before {
@simulator = Simulator.new 5,5
@simulator.place 2, 3, "NORTH"
@simulator.move
}
it { expect(@simulator.toy_robot.position).to be Position.new(2,4,Direction.NORTH) }
end
context "when at edge of table" do
before {
@simulator = Simulator.new 5,5
@simulator.place 4, 4, "NORTH"
@simulator.move
}
it { expect(@simulator.toy_robot.position).to be Position.new(4,4,Direction.NORTH) }
end
end
describe "# turns" do
context "when faced north" do
before {
@simulator = Simulator.new 5,5
@simulator.place 2, 2, "NORTH"
}
it { @simulator.left ; expect(@simulator.toy_robot.position).to be Position.new(2,2,Direction.WEST) }
it { @simulator.right ; expect(@simulator.toy_robot.position).to be Position.new(2,2,Direction.EAST) }
end
context "when faced south" do
before {
@simulator = Simulator.new 5,5
@simulator.place 2, 2, "SOUTH"
}
it { @simulator.left ; expect(@simulator.toy_robot.position).to be Position.new(2,2,Direction.EAST) }
it { @simulator.right ; expect(@simulator.toy_robot.position).to be Position.new(2,2,Direction.WEST) }
end
context "when faced east" do
before {
@simulator = Simulator.new 5,5
@simulator.place 2, 2, "EAST"
}
it { @simulator.left ; expect(@simulator.toy_robot.position).to be Position.new(2,2,Direction.NORTH) }
it { @simulator.right ; expect(@simulator.toy_robot.position).to be Position.new(2,2,Direction.SOUTH) }
end
context "when faced west" do
before {
@simulator = Simulator.new 5,5
@simulator.place 2, 2, "WEST"
}
it { @simulator.left ; expect(@simulator.toy_robot.position).to be Position.new(2,2,Direction.SOUTH) }
it { @simulator.right ; expect(@simulator.toy_robot.position).to be Position.new(2,2,Direction.NORTH) }
end
end
end
table_spec.rb
require_relative '../spec_helper'
require_relative '../../lib/robot'
include Robot
describe Table do
context "#checks boundry" do
before { @t = Table.new 5,5 }
it {expect(@t.inside?(2,3)).to be true}
it {expect(@t.inside?(0,0)).to be true}
it {expect(@t.inside?(5,5)).to be false}
it {expect(@t.inside?(5,3)).to be false}
it {expect(@t.inside?(3,5)).to be false}
it {expect(@t.inside?(6,3)).to be false}
it {expect(@t.inside?(3,6)).to be false}
it {expect(@t.inside?(-1,0)).to be false}
it {expect(@t.inside?(0,-1)).to be false}
end
end