This is a program bad_robot_simulator.rb that crams as many mistakes, misfeatures, and bad design decisions as I could into a program that nonetheless gets the right output. Read the code and weep in relief that you don’t have to work with code like this (or in despair that you do).
It was written for fun and as an entry for the contest BadCode.rocks for the month of 2018-12, and it won an Honorable Mention for that month:
It was a close race this time, so we’ll take more time than usual to talk about our runner-up. In Ruby, from Rory O’Kane, this submission had some basic stuff: inconsistently formatted magic comments (though the presence of those comments in Ruby is almost a good practise, for shame!), use of eval to parse numbers, many global variables, useless assignments, and changing the input to another format for no good reason.
The really impenetrable, truly bad part of this submission though, is the final algorithm to determine the robot’s position. Combining a global variable with an algorithm that re-processes already-handled instructions over and over in a loop, it took our judges several read-throughs to understand why this code worked at all. Impossible to understand without being hard to read, this is exactly the kind of bad code this competition is all about.
The contest prompt was to write a program simulating a robot moving around a grid. The robot is given starting coordinates, a starting orientation (north, south, east or west), and a list of instructions to follow. The supported instructions are “advance one space forward”, “turn left 90°”, and “turn right 90°”. The expected output of the program is the robot’s new coordinates and orientation. For example, running the program with initial state 0 0 N and the instructions RA (turn right, advance) should leave the robot at 1 0 E.
A comprehensive list of misfeatures in my implementation bad_robot_simulator.rb:
-
redundant work for “simplicity” or “optimization”, such as calculating direction and position separately
-
runis called four times for every single time it needs to be called; the other three results are discarded -
Reuses global variable
$directioninstead of passing it as a parameter, preventing code from being reordered and making the program harder to understand. This comes into play in the finalwhileloop, which sets$directionand then calls a method that eventually calls the previously-definedleftandrightmethods that change$direction. The program also has other unnecessarily global variables. -
Converts the input directions (NESW) into a different data format (URDL) and does the reverse conversion before output, but this “intuitive” data format is not worth the high amount of code used for the conversion.
- Background: I only came up with this data format because I started writing the program without remembering the full problem description and chose the set of characters that was more intuitive to me. When I reread the problem description, I realized it would make the program more interesting if I converted the data to work with my existing code than to edit my program to handle the native data.
-
Mixes up the values for current directions and instructions to turn – both are called “left” and “right” instead of calling one “east” and “west”.
- This results in useless empty
# not neededlines for handling'U'and'D'directions to turn. Those cases could have been omitted from the code.
- This results in useless empty
-
poorly named methods
leftinstead ofturn_leftandrightinstead ofturn_righthandle_turns_chars(turns_chars)instead ofmake_turns(turns)orchange_direction_with_turns(turns)runinstead offind_runorfind_run_of_advances– it sounds like it runs (executes) the commands, but that comes laterdirection?instead ofis_a_direction?
-
ARGVprocessing is overly DRY; it’s more complicated than the equivalent four lines assigning the four variables-
The de-abstracted version of that code is easier to understand and debug:
init_x = eval(ARGV[0]) init_y = eval(ARGV[1]) init_dir = map_dir_to_intuitive_dir(ARGV[2]) cmd_str = ARGV[3]
-
-
deduplicates some code into
handle_turns_chars, but fails to realize that it is almost the same as the initial calculation oflast_direction -
The snippet
this_run.end(0) # index after endis just a confusing way of writingthis_run[0].size. It is equivalent because the match always starts at index 0 of the slicedcmd_str.- This comment
# index after endis also confusing because it looks like it could describe the whole line, which references the relevant-sounding variable$last_run_idx, while it actually describes only some of the code at the end of the line.
- This comment
-
pointless distinction between
mov[-1]andmov[1]in the two-elementmovarray near the end – they both refer to the same element ofmov -
unidiomatic Ruby
- unnecessary
cloneof$directionbefore assigning todir;clonedoes nothing onNumerics anyway - unnecessary
dir =assignment ofcasestatements who were going to be returned anyway, or whose result is ignored anyway - when slicing string ranges, uses
.chars[…].join('')to split characters in the string and then join them after instead of just using[…] - uses
evalinstead ofto_i; classic JavaScript mistake ported to Ruby - use of
.chars.eachinstead of.each_char - Uses symbols
:not_initand:nullinstead ofnil. Those custom symbols document the purpose of the value, but at the cost of surprising any reader who knows Ruby. - uses
reducewhereinjectis more appropriate (minor style nit) - fails to use
+=when setting$last_run_idx
- unnecessary
-
excessive or redundant initial magic comments, not used in such a short program
- the first one is formatted differently from the others for no reason
- The last comment, “by: Rory O’Kane”, initially looks like a magic comment to due to being formatted similarly to the magic comments above, but it has no effect on the interpreter.
-
Overly repetitive verbose code in
leftandrightto find the new direction – a hash-map of old directions to new directions would have made each method simpler. -
Useless capturing groups in the regex
/([RL]*)(A+)/withinrun./ [RL]* A+ /xwould be easier to read and would probably run faster due to not saving the capture groups.
To run the tests, which confirm that the program works despite all the above flaws:
./test.sh ./bad_robot_simulator.rbTo call the program directly, make sure Ruby is installed, then pass four arguments: initial x position, initial y position, initial direction, and command string. An example call and its output:
$ ./bad_robot_simulator.rb 0 0 N "RA"
1 0 EYou can see more examples of valid command-line arguments in test.sh.
If you need to cleanse the bad taste from your mouth, you can read my attempts at writing actually good solutions. They’re in this repository. I think of those two other solutions, the program good robot simulator, mutable state (Bad Code Rocks version of Exercism problem).rb is the better one.
This work is licensed under the Creative Commons Attribution 4.0 International License. To view a copy of this license, visit https://creativecommons.org/licenses/by/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.