On this page:
<day02>
2.1 What is the access code for the bathroom?
<day02-setup>
<day02-basics>
<day02-dolines>
<day02-q1>
2.2 What is the actual access code?
<day02-q2>
2.3 Testing Day 2
<day02-test>
6.7

2 Day 2: Hacking Into the Bathroom

Read the description of today’s puzzle. Here is my input.

2.1 What is the access code for the bathroom?

We need a program that will follow directions like LURDL (Left, Up, Right, Down, Left) and spit out the resulting keypad button. We’ll start with some constants to represent the keypad and input strings.

(require racket rackunit)
(provide (all-defined-out))
 
(define imagined-keypad '((1 2 3)
                          (4 5 6)
                          (7 8 9)))
 
(define test-lines '("ULL" "RRDDD" "LURDL" "UUUUD"))
(define input-lines (string-split (file->string "day02input.txt") "\n"))

Reading the puzzle description carefully, it says that we don’t yet know the shape of the keypad. So, just in case, let’s make sure we design our program to be able to handle any shape of keypad, and just default to the imaginary one we’re envisioning at the moment. You never know, right?

The function button-at will tell us the button at a given set of coordinates on the keypad. And move will take a set of coordinates and a character, and return a new set of coordinates. Both will default to the imagined-keypad but allow the use of any other keypad (that is, any list of lists) using an optional #:keypad keyword argument.

Keep in mind the coordinates in this program are zero-based: (button-at '(1 0)) will return the zeroth button in the middle row, or 4.

(define (button-at coords #:keypad [keypad imagined-keypad])
  (match-define (list row col) coords)
  (list-ref (list-ref keypad row) col))
 
(define (move start-coords char #:keypad [pad imagined-keypad])
  (match-define (list row col) start-coords)
  (define try-move
    (cond
      [(char=? char #\U) (list (max 0 (- row 1)) col)]
      [(char=? char #\D) (list (min (sub1 (length pad)) (+ row 1)) col)]
      [(char=? char #\L) (list row (max 0 (- col 1)))]
      [(char=? char #\R) (list row (min (sub1 (length (first pad))) (+ col 1)))]))
  (cond
    [(not (equal? 0 (button-at try-move #:keypad pad))) try-move]
    [else start-coords]))

Now it’s time to do a few lines, if you’ll forgive the expression. The do-line function takes a line of instructions (and an optional alternative keypad) and gives us the coordinates where our fingers would land after following those instructions. And of course do-lines simply chains multiples lines of instructions in do-line and returns a list of ending coordinates for each line.

I swore after the code diarrhea I had on Day 1 that I’d look into more Racket-idiomatic forms like for/fold for problems like this. I did, and I’m glad I did. The results are much more concise.

(define (do-line str start #:keypad [pad imagined-keypad])
  (for/fold ([last-result start])
            ([char (in-string str)])
    (move last-result char #:keypad pad)))
 
(define (do-lines lines start #:keypad [pad imagined-keypad])
  (drop
   (for/fold ([coords-so-far (list start)])
             ([line (in-list lines)])
     (append coords-so-far (list (do-line line (last coords-so-far) #:keypad pad))))
   1))

The do-lines function takes the starting coordinates and returns a list of the coordinates for each button in the access code. We can use map to transform each of the coordinates in that list into its corresponding button.

(define (q1 lines)
  (map button-at (do-lines lines '(1 1))))

2.2 What is the actual access code?

Well, paint me green and call me Gumby: the actual keypad did end up being different than the one we imagined! Go figure!

Thankfully we have already designed for this possibility. All we need to do is create a description for the actual keypad (using 0 to denote non-button locations), and call our button-at and do-lines functions specifying that keypad.

(define actual-keypad '((0 0 1 0 0)
                        (0 2 3 4 0)
                        (5 6 7 8 9)
                        (0 A B C 0)
                        (0 0 D 0 0)))
 
(define (q2 lines)
  (define (q2-button coord) (button-at coord #:keypad actual-keypad))
  (map q2-button (do-lines lines '(2 0) #:keypad actual-keypad)))

2.3 Testing Day 2

(module+ test
  (check-equal? (q1 test-lines)  '(1 9 8 5))
  (check-equal? (q1 input-lines) '(7 8 9 8 5))
  (check-equal? (q2 test-lines)  '(5 D B 3))
  (check-equal? (q2 input-lines) '(5 7 D D 8)))