aboutsummaryrefslogtreecommitdiffstats
path: root/AoC2022/24/solver.lisp
blob: 24725ef07c580f2d65860ee400705890adc20ca9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
(ql:quickload '(fiveam uiop trivia arrows))


(defun distance-1 (a b)
  (apply #'+
         (mapcar (lambda (e1 e2)
                   (abs (- e1 e2))) a b)))

(defun boundary-conditions (width height)
  "The boundary is a wall meaning 0 & (N-1) are never occupied."
  (lambda (x y dir)
    (list
     dir
     (cond
       ((zerop x) (- width 2))
       ((= x (- width 1)) 1)
       (x))
     (cond
       ((zerop y) (- height 2))
       ((= y (- height 1)) 1)
       (y)))))

(defun wall-p (width height)
  (lambda (point)
    (destructuring-bind (x y) point
      (cond
        ;; not entry - exit
        ((or (and (= x 1) (zerop y))
             (and (= x (- width 2)) (= y (1- height))))
         nil)
        ;; yes wall or out of map
        ((or (<= x 0) (<= y 0)
             (>= x (1- width))
             (>= y (1- height))))))))

(fiveam:test boundaries
  (let ((bc (boundary-conditions 6 9)))
    (fiveam:is (equal (funcall bc 5 3 t) '(t 1 3)))
    (fiveam:is (equal (funcall bc 5 8 t) '(t 1 1)))
    (fiveam:is (equal (funcall bc 0 8 t) '(t 4 1)))))

(defstruct valley
  blizzards
  boundaries
  wallp
  size)

(defun data-input (filename)
  (let* ((lines (uiop:read-file-lines filename))
         (width (length (car lines)))
         (height (length lines)))
    (make-valley
     :blizzards
     (loop for row in lines
           and y from 0
           nconc (loop for place across row
                       and x from 0
                       unless (or (eq #\# place) (eq #\. place))
                         collect (list (ecase place
                                         (#\> '>)
                                         (#\< '<)
                                         (#\v 'v)
                                         (#\^ '^))
                                       x y)))
     :boundaries (boundary-conditions width height)
     :wallp (wall-p width height)
     :size (list width height))))

(defun evolve (valley)
  (with-slots (blizzards boundaries) valley
    (mapcar
     (lambda (blizzard)
       (trivia:match blizzard
         ((list '> x y) (funcall boundaries (1+ x) y '>))
         ((list '< x y) (funcall boundaries (1- x) y '<))
         ((list 'v x y) (funcall boundaries x (1+ y) 'v))
         ((list '^ x y) (funcall boundaries x (1- y) '^))))
     blizzards)))


(defun elf-moves (elf valley)
  (with-slots (blizzards wallp) valley
    (destructuring-bind (x y) elf
      (arrows:-<>
       (list (list (1+ x)    y)
             (list x     (1+ y))
             (list (1- x)    y)
             (list x     (1- y))
             (list x         y))
       (delete-if wallp <>)
       (set-difference <> (mapcar #'cdr blizzards) :test #'equal)))))

(defun cutoff-distance (positions goal)
  (loop for p in positions
        for dist = (distance-1 goal p)
        maximize dist into ma
        minimize dist into mi
        finally (return (values (max (ceiling (+ ma mi) 2)
                                     (* 1.2 mi)
                                     (* 0.8 ma)
                                     30)
                                mi ma))))

(defun discard-hopeless-positions (positions goal cutoff-distance)
  (delete-if (lambda (p) (< cutoff-distance (distance-1 goal p))) positions))

(defun time-step! (time-left walkers goal valley)
  (if (or (zerop time-left) (member goal walkers :test #'equal))
      time-left
      (progn
        (setf (valley-blizzards valley) (evolve valley))
        (let* ((new-positions (reduce
                               (lambda (options point)
                                 (union options (elf-moves point valley) :test #'equal))
                               walkers
                               :initial-value nil))
               (shortened-positions (discard-hopeless-positions new-positions goal
                                                                (cutoff-distance new-positions goal))))
          (time-step! (1- time-left)
                      shortened-positions
                      goal valley)))))

(defun solver (filename repeats)
  (let ((valley (data-input filename)))
    (destructuring-bind (width height) (valley-size valley)
      (let ((exit (list (- width 2) (1- height)))
            (start (list 1 0))
            (max-time 500))
        (loop for (start goal) on (loop with l = (list start exit)
                                        repeat repeats
                                        append l)
              while goal
              sum (- max-time (time-step! max-time (list start) goal valley)))))))

(fiveam:test solutions
  (fiveam:is (= 18 (solver "eg-in" 1)))
  (fiveam:is (= 299 (solver "input" 1)))
  (fiveam:is (= 54 (solver "eg-in" 2)))
  (fiveam:is (= 899 (solver "input" 2))))