ADDED day15.rkt Index: day15.rkt ================================================================== --- day15.rkt +++ day15.rkt @@ -0,0 +1,240 @@ +#lang racket/base + +(require racket/match + racket/function + racket/list + racket/vector + racket/set + threading) + +;; PRIMITIVES AND CONSTANTS ---------------------------------------------------- + +;; Good ol’ positions. +(struct posn (x y) #:transparent) + +;; The concept of “reading order” is an important one in this puzzle. Fighters move, +;; targets and paths are chosen in order of how you’d encounter them reading the grid +;; top to bottom, left-to-right. +;; The two functions below are going to do all the work of determining sort order +;; for us, whenever we need it. +;; This is also where I mention that this program views 0,0 as “top left”. +(define (posnvector + (map string->list line-strs))) + row-count + col-count)) + +;; Reference the value at given position in a grid +(define (grid-ref g p) + (vector-ref (grid-vec g) (posn→index g p))) + +;; Change the value at given position +(define (grid-mark! g pos v) + (vector-set! (grid-vec g) (posn→index g pos) v)) + +;; Used to determine if a fighter could move into a given spot. +;; Anything besides "." counts as an obstruction (incl. other fighters) +(define (grid-clear-at? g p) + (equal? (grid-ref g p) #\.)) + +;; Make a blank grid of the same dimensions, for use in making “path grids” (see +;; further below) +(define (copy-blank-grid g) + (match-define (grid _ rows cols) g) + (grid (make-vector (* rows cols) #f) rows cols)) + +;; (For debugging) Represent the grid as a square of single-character values +(define (display-grid g [g2 #f]) + (define grid-size (* (grid-cols g) (grid-rows g))) + (display + (apply string-append + (for/fold ([lst '()] + #:result (reverse (cons "\n" lst))) + ([val (in-vector (grid-vec g))] + [i (in-naturals 1)]) + (define ch + (cond [(number? val) (number->string (modulo val 10))] + [(boolean? val) "-"] + [(string? val) val] + [else (format "~a" val)])) + (cond [(and (equal? 0 (modulo i (grid-cols g))) + (< i grid-size)) + (cons "\n" (cons ch lst))] + [else (cons ch lst)]))))) + +;; Is point p inside grid g? Film at 11 +(define (inside-grid? g p) + (match-define (posn px py) p) + (and (>= px 0) + (>= py 0) + (< px (grid-cols g)) + (< py (grid-rows g)))) + +;; Get a list of a positions neighboring points, ensuring none are out of bounds +(define (neighbor-coords g pos) + (match-define (posn x y) pos) + (filter (curry inside-grid? g) + (list (posn (- x 1) y) + (posn x (+ y 1)) + (posn (+ x 1) y) + (posn x (- y 1))))) + +;; Get all the EMPTY neighboring points of a given spot OR list of spots. +;; If a (listof posn?) is passed, ensures the returned list does not include +;; any of the original positions. +(define (free-neighbors-of world pos) + (cond [(posn? pos) + (~> (neighbor-coords world pos) + (filter (curry grid-clear-at? world) _))] + [(list? pos) + (~> (map (curry neighbor-coords world) pos) + flatten + (filter (curry grid-clear-at? world) _) + (set-subtract pos) + remove-duplicates)])) + +;; Working with PATHS ---------------------------------------------------------- + +;; Find the most direct path(s) to a fighter from an end-position. +;; This is the function you are probably looking for if you are reading this file at all. +;; The algorithm starts at the end position and works outwards, finding unoccupied positions +;; and marking them (on a blank copy of the map) with their distance from the end-point. +;; As soon as any of the considered points includes one or more free neighbors of the given +;; fighter, recursion stops and returns a path. +(define (build-path world f end-pos) + (define result-grid (copy-blank-grid world)) + (define (not-yet-checked? pos) (not (grid-ref result-grid pos))) + (define goal-pts (free-neighbors-of world f)) + (grid-mark! result-grid end-pos 0) + + (let loop ([pts-to-check (list end-pos)] + [i 1]) + (define new-coords (~> (free-neighbors-of world pts-to-check) + (filter not-yet-checked? _))) + (define maybe-first-steps (set-intersect new-coords goal-pts)) + (cond + [(not (empty? maybe-first-steps)) + (path (posn-x end-pos) (posn-y end-pos) i maybe-first-steps)] + [(empty? new-coords) #f] + [else + (for-each (lambda (p) (grid-mark! result-grid p i)) new-coords) + (loop new-coords (+ 1 i))]))) + +;; Convenience: +(define (make-pathfinder world f) + (curry build-path world f)) + +;; Get only the shortest path(s) from a list of paths +(define (shortest plst) + (define shortest-distance (apply min (map path-distance plst))) + (define (among-shortest? pmap) (equal? shortest-distance (path-distance pmap))) + (filter among-shortest? plst)) + +;; Working with FIGHTERS ------------------------------------------------------- + +;; Let’s start doing stuff with fighters + +;; Make a list of fighters from a grid, with the results in reading order. +(define (grid->fighters g) + (for/fold ([fighters '()] + #:result (reading-order fighters)) + ([val (in-vector (grid-vec g))] + [idx (in-naturals)]) + (cond [(member val '(#\G #\E)) + (match-define (posn x y) (index→posn g idx)) + (cons (fighter x y val STARTING-HP) fighters)] + [else fighters]))) + +;; I’ll give you three guesses each what these do +(define (fighter-located-in? f posns) + (not (empty? (filter (curry posn=? f) posns)))) + +(define (enemies? f1 f2) + (not (equal? (fighter-type f1) (fighter-type f2)))) + +(define (enemies-of f1 flst) + (filter (curry enemies? f1) flst)) + +(define (adjacent-enemies world f all-enemies) + (define adjacent-posns (neighbor-coords world f)) + (filter (curryr fighter-located-in? adjacent-posns) all-enemies)) + +(define (fighter-alive? f) + (positive? (fighter-hp f))) + +;; Here’s a proof of concept. +;; As you can see, after all that work it is trivially easy to tie everything together. +(module+ test + (require rackunit) + (define test-map + (lines→grid + '("#######" + "#E..G.#" + "#...#.#" + "#.G.#G#" + "#######"))) + (define fs (grid->fighters test-map)) + (define f (first fs)) + (define es (enemies-of f fs)) + + ;; Work through the algorithm specified in the problem description to find the next move + ;; for the elf at 1,1 (skipping the test for adjacent enemies): + (define possible-targets (free-neighbors-of test-map es)) + (define possible-paths (filter-map (make-pathfinder test-map f) possible-targets)) + (define shortest-paths (shortest possible-paths)) + (define selected-path (first (reading-order shortest-paths))) + (define next-step (first (reading-order (path-first-steps selected-path)))) + + ;; Ta-da + (check-equal? next-step (posn 2 1))) +