# Advent of Code 2023 - Day 2 ¶

### Just Part 1, since I'm late to the party again today...

## 1. Part 1

Here's the link to the exercise - as usual, we get some input data, and need to read it and check some of the properties. In this case, the question is as follows:

Determine which games would have been possible if the bag had been loaded with only 12 red cubes, 13 green cubes, and 14 blue cubes.

Okay - before we start, let's import the libraries we'll need. Strictly
speaking, none if these are actually **needed** - you can see other
implementations that don't use anything outside of `subr`

and `simple`

- but I
want to showcase the tools modern Elisper has at their disposal.

1: (require 'b) 2: (require 'cl-lib) 3: (require 'map) 4: (require 'dash)

With that out of the way, we can take a stab at the task. First, let's assume we
have the input in a buffer already, in `input.txt`

. You could implement the
task by imperatively traversing the buffer, which would be very Emacs-y way of
doing this, but since there's an implementation like that already, I'll go for
a more Python-like approach and split the input into a list of lines:

5: (defconst aoc-games-played-input 6: (-> "input.txt" get-buffer b-string-no-properties s-trim s-lines))

We now have 100 lines of text, ready to be further processed:

(length aoc-games-played-input)

100

Here's how the lines look like:

(-take 2 aoc-games-played-input)

("Game 1: 12 blue, 15 red, 2 green; 17 red, 8 green, 5 blue; 8 red, 17 blue; 9 green, 1 blue, 4 red" "Game 2: 6 red, 6 blue, 2 green; 1 blue, 1 red; 6 green, 1 red, 10 blue")

We need to verify that on each line all the "subsets" - separated by
semicolons - have color values that **do not exceed the limits** quoted at the
beginning. First, let's further split the lines into chunks, separating game ID
from subsets, and then further separating subsets from each other:

(let ((line (car aoc-games-played-input))) (pcase-let* ((`(,game ,subsets) (s-split (rx ": ") line)) (subset-list (s-split (rx "; ") subsets)) (subsets (--map (s-split (rx ", ") it) subset-list))) (cons (cl-second (s-match (rx "Game " (group (1+ num))) game)) subsets)))

("1" ("12 blue" "15 red" "2 green") ("17 red" "8 green" "5 blue") ("8 red" "17 blue") ("9 green" "1 blue" "4 red"))

It works, so let's refactor it a bit before moving on. As we're doing the refactor, we can also convert strings to numbers and symbols as appropriate, and subsets into alists:

7: (cl-defun get-game-id (game-name) 8: (let ((re (rx "Game " (group (1+ num))))) 9: (->> game-name (s-match re) cl-second string-to-number))) 10: 11: (cl-defun subset->alist (colors) 12: (cl-loop for color in colors 13: for (num color) = (s-split (rx " ") color) 14: collect (cons (intern-soft color) (string-to-number num)))) 15: 16: (cl-defun line->game (line) 17: (pcase-let* ((`(,game ,subsets) (s-split (rx ": ") line)) 18: (subset-list (s-split (rx "; ") subsets)) 19: (subsets (--map (s-split (rx ", ") it) subset-list))) 20: (cons 21: (get-game-id game) 22: (-map #'subset->alist subsets))) ) 23: 24: (let ((line (car aoc-games-played-input))) 25: (line->game line))

(1 ((blue . 12) (red . 15) (green . 2)) ((red . 17) (green . 8) (blue . 5)) ((red . 8) (blue . 17)) ((green . 9) (blue . 1) (red . 4)))

So we have the data in a form that's convenient to work with. Now, what does it mean for a game to be possible? All subsets must have color values within a limit. Let's define a function for checking this:

26: (defconst limits '((red . 12) (green . 13) (blue . 14))) 27: 28: (cl-defun game-possible-p (game) 29: (cl-loop for subset in (cdr game) 30: unless (cl-loop for (color . limit) in limits 31: always (<= (map-elt subset color 0) limit)) 32: return nil 33: finally return t)) 34: 35: (defconst line-impossible 36: "Game 1: 12 blue, 15 red, 2 green; 17 red, 8 green, 5 blue; 8 red, 17 blue; 9 green, 1 blue, 4 red") 37: (defconst line-possible 38: "Game 2: 6 red, 6 blue, 2 green; 1 blue, 1 red; 6 green, 1 red, 10 blue") 39: 40: (list (game-possible-p (line->game line-possible)) 41: (game-possible-p (line->game line-impossible)))

(t nil)

The predicate seems to be working. The two things left to do are to filter out
the games for which the predicate returns `t`

and then sum the game IDs of those
games:

42: (cl-loop for line in aoc-games-played-input 43: for game = (line->game line) 44: if (game-possible-p game) 45: sum (car game))

2716

And that's, according to the AoC page, the correct answer for my dataset, for part 1!

## 2. Part 2

For completeness sake, here's the answer to the second part of the puzzle. Here, we need to find the maximum values for each color across all subsets of a given game:

46: (defconst maxes '((red . 0) (green . 0) (blue . 0))) 47: 48: (cl-macrolet 49: ((alist-maximize (key acc alist) 50: `(setf (alist-get ,key ,acc) 51: (max (alist-get ,key ,acc) (alist-get ,key ,alist 0))))) 52: (cl-loop for line in aoc-games-played-input 53: for game = (line->game line) 54: sum (cl-loop with maxs = (copy-tree maxes) 55: for x in (cdr game) 56: do (progn 57: (alist-maximize 'red maxs x) 58: (alist-maximize 'green maxs x) 59: (alist-maximize 'blue maxs x) ) 60: finally return (apply #'* (map-values maxs)))))

72227

The result happens to be correct for my dataset, here too.

The main highlight here is the `cl-macrolet`

form, which defines local macros,
valid only in the lexical scope of the form. It's great for getting syntactic
clutter out of the way without polluting the global namespace.

A honorary mention goes to `setf`

- a versatile tool for updating contents of
things. There are many "things" that you can set/update, and you can your own
"places" and have `setf`

work with them, too. The selling point of `setf`

is the
symmetry: most often, the "place" has the same syntactic form as the getter for
a given thing. Here, `alist-get`

is used with `setf`

to update a value in an
alist - it's way cleaner than `assoc`

and `setcdr`

to which it expands to.

Notice also the `copy-tree`

- it's important because of the mutability of
Elisp's lists. It's obvious now when the alist is defined outside of the
function, but the call to `copy-tree`

would be needed even if the variable was
inlined! That is, this:

(cl-loop with maxs = '((red . 0) (green . 0) (blue . 0)) ...)

Would still produce wrong output. An alternative to `copy-tree`

would be
constructing the list at runtime by calling `list`

and `cons`

functions. I'll
leave the detail as to why this happens for a future post.