12
\$\begingroup\$

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
200_success
146k22 gold badges190 silver badges479 bronze badges
asked Jul 8, 2015 at 4:10
\$\endgroup\$
0

1 Answer 1

8
+50
\$\begingroup\$

Initial impressions

There's a lot of code. Interviewers don't like to read a lot of code.

In many ways, you have overdelivered on the specification. Since you have indicated that this was an , I'm going to give you my personal value judgements on these points.

  • I'm ambivalent about the logger. On one hand, it is a nice debugging aid. On the other hand, the spec just says that any invalid instruction is to be ignored, so all of those infos, warnings, and errors are unnecessary complications. The spec certainly did not ask for a log/toy_robot.log file to be created by default, so that was a bit of a surprise.

    Do you really need to log "Toy Robot created successfully" and "Simulator created successfully" at the "info" level? I'd consider those to be "debug"-level messages, and even then, I'd wonder what could possibly go wrong in such a simple program that the ToyRobot and Simulator would fail to be instantiated.

    Instead of logging warnings and errors, you could have just thrown exceptions, catching and reporting them in one exception handler.

  • The very first requirement states that the table is a 5 ×ばつ 5 square. Why are you bothering to accept -x and -y flags to specify the width and height? You could have ditched the entire cli.rb file, accepting no command-line options, and just read from ARGF to satisfy the requirement that the input can come from a file or from standard input.

I'm not convinced that all of those classes need to exist.

  • As I mentioned, OptionParser isn't called for, so the CLI class can go away.
  • Why is App a separate class from CLI? What about Simulator? Isn't this whole project a simulator app?
  • The ToyRobot class looks more like a "dumb" struct rather than a "smart" object.
  • The code to support rotations and translations is split up between Direction and Position.
  • What is Position#equal? used for? What about Position#to_s — couldn't you have defined it strategically so that it can be reused for the REPORT command?

So, there is some overengineering going on, and some bloat, acting together to produce a solution that is larger than it needs to be.

Second impressions

You do seem familiar with Ruby. For example, you used ranges and === to perform the bounds check instead of 0 <= x && x < @MAX_X && 0 <= y && y < @MAX_Y.

 def inside? x,y
 return ((0..@MAX_X-1) === x) && ((0..@MAX_Y-1) === y) 
 end

On the other hand, it would have been slightly prettier with exclusive ranges, and without the explicit return:

 def inside?(x, y)
 (0...@MAX_X) === x && (0...@MAX_Y) === y
 end

The Direction enum class is not bad — it demonstrates familiarity with metaprogramming. The attr_accessors would be better as attr_readers, since they should all be constant.

Command dispatching

This is the main point of the exercise.

You've split up the work between App, CommandParser and Simulator. Within the Simulator constructor, there is a command hash that is mostly there to support the PLACE command parameters, and you're repeating yourself by having to list those commands explicitly.

In App#read_command, instead of calling exit, I suggest a more disciplined throw-catch.

Suggested solution

For comparison, this is what I came up with.

class Direction
 def initialize(sym, dx, dy, left, right)
 @name, @dx, @dy, @left, @right = sym.to_s, dx, dy, left, right
 end
 def to_s
 @name
 end
 def left
 Direction.const_get(@left)
 end
 def right
 Direction.const_get(@right)
 end
 def advance(x, y)
 [x + @dx, y + @dy]
 end
 @all = [
 [:EAST, +1, 0],
 [:NORTH, 0, +1],
 [:WEST, -1, 0],
 [:SOUTH, 0, -1],
 ]
 @all.each_with_index do |(sym, dx, dy), i|
 self.const_set(sym,
 Direction.new(sym, dx, dy, @all[(i + 1) % @all.size].first,
 @all[(i - 1) % @all.size].first))
 end
 def self.[](name)
 Direction.const_get(name) if Direction.const_defined?(name)
 end
end
class InvalidCommand < Exception ; end
# Mixin for Robot
module Commands
 def place(x, y, face)
 validate(x = (x.to_i if /\A\d+\Z/.match(x)),
 y = (y.to_i if /\A\d+\Z/.match(y)),
 face = Direction[face.upcase])
 @x, @y, @face = x, y, face
 end
 def move
 raise InvalidCommand.new unless @face
 new_x, new_y = @face.advance(@x, @y)
 validate(new_x, new_y, @face)
 @x, @y = new_x, new_y
 end
 def left
 raise InvalidCommand.new unless @face
 @face = @face.left
 end
 def right
 raise InvalidCommand.new unless @face
 @face = @face.right
 end
 def report
 raise InvalidCommand.new unless @face
 @output.puts "#{@x},#{@y},#{@face}"
 end
end
class Robot
 include Commands
 def initialize(board_size=5, output=STDOUT)
 @board_size = board_size
 @output = output
 end
 def execute(cmd)
 cmd = cmd.strip.downcase
 op, args = cmd.split(/\s+/, 2)
 args = args.split(/\s*,\s*/) if args
 raise InvalidCommand.new unless Commands.public_method_defined?(op)
 begin
 send(op, *args)
 rescue ArgumentError
 raise InvalidCommand.new
 end
 end
 def execute_script(io)
 io.each_line do |line|
 begin
 execute(line)
 rescue InvalidCommand
 #puts "(Ignored invalid command)"
 end
 end
 end
 private
 def validate(x, y, f)
 raise InvalidCommand.new unless x && y && f
 raise InvalidCommand.new unless (0..@board_size) === x
 raise InvalidCommand.new unless (0..@board_size) === y
 end
end
Robot.new.execute_script(ARGF)
answered Jul 13, 2015 at 8:37
\$\endgroup\$
0

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.