Command line - Mars Rover Tech Challenge in common lisp

In this post I’ll discuss the steps to finalise the Mars Rover Tech Challenge.

In particular how to read the configuration settings from a file and then run the code.

As with the last post, please forgive all the spurious (princ ...), it’s the only way I know to get Org mode to export the results for inclusion in this blog post.

This series of posts

This is the second of three posts on this topic. I’ll update these links as I publish the other posts. I’ve used lisp (the second oldest programming language still in use!) for this, and in the third post I’ll explore why:

  • Part 1 : Initial code - running from a REPL
  • Part 2 : Read the configuration file and running from the command line
  • Part 3 : Benchmarking (AKA why this funny looking language?)

(Updated: [2020-02-15 Sat])

Get the code

You can download the code from for this and the previous post from my GitHub account:

https://github.com/stewart123579/mars-rover-challenge-lisp

Load the previous code

(load "mars-rover.lisp")

Read in the input file

Here we use the UIOP read-file-lines function for cross lisp-implementation and cross machine support.

(defun read-input-file (filename)
  ;; Throw away blank lines
  (let ((setup (loop for line in (uiop:read-file-lines filename)
                  if (not (equal line ""))
                  collect (read-from-string (concatenate 'string "(" line ")")))))
    (set-plateau-size (car setup))
    (loop-over-starting-positions (cdr setup))))

I’ve also used a loop to

  1. convert the input into a list whilst
  2. throwing away blank lines; and
  3. converting the text of each line into a list

I’ve made an assumption that the input file is of the form:

X_MAX Y_MAX

START_X_1 START_Y_1 ORIENTATION_1

COMMANDS_1

START_X_2 START_Y_2 ORIENTATION_2

COMMANDS_2

...

…but what does this variable setup actually look like?

(princ (loop for line in (uiop:read-file-lines "mars-rover-test-input.txt")
          if (not (equal line ""))
          collect (read-from-string (concatenate 'string "(" line ")"))))
((5 5) (1 2 N) (LMLMLMLMM) (3 3 E) (MMRMMRMRRM))

Set the plateau size

This is only really needed as I’m not writing this in the functional style. The plateau shape is the first element in the setup list.

(defun set-plateau-size (plateau-shape)
  (setf *x_max* (car plateau-shape))
  (setf *y_max* (cadr plateau-shape)))

Loop over each of the starting positions

This is probably the least transparent piece of code in this discussion.

Let’s work through the logic: What do we need for running update-position?

Firstly we need the pos and orientation. Looking at the results above, the list (1 2 N) has all the information. So the pos is simply a cons of the first (car) and second (cadr) elements. The orientation is the third (caddr) element.

Now what about the actual command? They need to be a list of the individual letters in the third (cadr) list. So we need the car of that list.

Initially I thought something like this would work:

(let ((cmds_list '(LMLMLMLMM)))
  (coerce (car cmds_list) 'list))

but it results in the error: The value LMLMLMLMM is not of type SEQUENCE.

Let’s think about that for a moment. Here LMLMLMLMM isn’t a string of letters, it’s simply an atom (think an object/variable in other languages). Lisp doesn’t see any difference between it and any other atom. To get it treated like a string of letters we need to convert it to a string (and that’s the SEQUENCE give-away in the error message). So let’s try:

(let ((cmds_list '(LMLMLMLMM)))
  (princ (coerce (string (car cmds_list)) 'list)))
(L M L M L M L M M)

Perfect! So let’s go…

The code to loop over the elements of the configuration file

Here we take the first two elements of the input list, convert them to pos, orientation and commands, call update-position and then call this function again with the remainder of the input list. Recursion again!

(defun loop-over-starting-positions (input)
  "Loop over the starting poition/orientation and commands

INPUT is a list of pairs of lists (x y orientation) (commands)"
  (when input
    (let* ((start (car input))
           (commands (coerce (string (car (cadr input))) 'list))
           (pos (cons (car start) (cadr start)))
           (orientation (caddr start)))
      (update-position pos orientation commands))
    (loop-over-starting-positions (cddr input))))

Note - I’ve left this code reading (car (cadr input)) rather than simplifying to (caadr input) to make it more consistent with the explanation above. (If that’s all Greek to you, do some more learning about lisp!)

Run the code over the input file

(read-input-file "mars-rover-test-input.txt")
1 3 N

5 1 E

This is the expected output. Success!

Now let’s make a command line tool

This is a remarkably easy process using Roswell.

I’ll make the assumption that you’ve installed Roswell and have it working.

Scripting with Roswell

The script to run the code we’ve just developed is the following:

#!/bin/sh
#|-*- mode:lisp -*-|#
#|
exec ros -Q -- $0 "$@"
|#
(progn ;;init forms
  (ros:ensure-asdf)
  #+quicklisp(ql:quickload '() :silent t)
  )

(defpackage :ros.script.rover.3790038153
  (:use :cl))
(in-package :ros.script.rover.3790038153)

(load "mars-rover-v2.lisp")

(defun main (filename &rest argv)
       (declare (ignorable argv))
       (read-input-file filename))
;;; vim: set ft=lisp lisp:

There are only two lines of interest, and what they do is pretty obvious.

(load "mars-rover-v2.lisp")

;; ...and

       (read-input-file filename)

Now we can simply run the code like:

./mars-rover.ros mars-rover-test-input.txt

What’s next?

In the next post I’ll talk about benchmarking this implementation - and why use Lisp.