463 lines
18 KiB
Common Lisp
463 lines
18 KiB
Common Lisp
;; https://adventofcode.com/2022/day/19
|
|
|
|
;; whelp. do i do somehow DP? generic over parameters of blueprints somehow
|
|
;; we could potentially calculate optimal step in forward time, but i'm not sure how.
|
|
;; and backward time - no easy way to know which robots will be available?
|
|
;;
|
|
;; intuitive idea, have some state of (resource1, recource2, robot1, robot2)
|
|
;; and have 2 types of steps, one - production is simple
|
|
;; then we can have different types of actions - build robot1 \ build robot2 - parallel these out
|
|
;; somehow prune ideally, but then move forward in time and get max of these. i guess it's ok plan
|
|
|
|
;; so.
|
|
;; - each robot collects 1 of it's own resource per minute
|
|
;; so function to update state with new resources is common
|
|
;; - cost of one new robot, consumed immediately, robot is produced in 1 minute
|
|
;; costs are different and listed in the bluerint
|
|
;; i suppose overall format of the blueprint is the same, only numbers change.
|
|
;; so could hardcode the translation
|
|
;;
|
|
;; and i guess i'd have one class of "blueprint" and several instances,
|
|
;; each instance does the updating of the state.
|
|
|
|
(in-package :day-19)
|
|
|
|
(defclass state ()
|
|
((geodes :initform 0 :initarg :geodes)
|
|
(ore :initform 0 :initarg :ore)
|
|
(clay :initform 0 :initarg :clay)
|
|
(obsidian :initform 0 :initarg :obsidian)
|
|
(geodes-robot :initform 0 :initarg :geodes-robot)
|
|
(ore-robot :initform 1 :initarg :ore-robot)
|
|
(clay-robot :initform 0 :initarg :clay-robot)
|
|
(obsidian-robot :initform 1 :initarg :obsidian-robot)))
|
|
|
|
(defclass blueprint ()
|
|
((ore-robot-cost :initarg :ore)
|
|
(clay-robot-cost :initarg :clay)
|
|
(obsidian-robot-cost :initarg :obsidian)
|
|
(geode-robot-cost :initarg :geode)))
|
|
;; wrote like this initially
|
|
|
|
(make-instance 'state)
|
|
|
|
;; i'd like to have code across building robots, checking whether robot can be build
|
|
;; to be shared. would i want to do it with classes?
|
|
;; state could have hashmaps from symbol to the amount
|
|
|
|
;; do i want to have plists?
|
|
(defparameter *my-plist* '(:ore 1 :clay 15))
|
|
*my-plist*
|
|
(getf *my-plist* :ore)
|
|
(getf *my-plist* :clay)
|
|
(setf (getf *my-plist* :clay) 3)
|
|
;; i guess i like that
|
|
;; then i could have robot costs as plists as well?
|
|
;; i could iterate over the plist with destructuring
|
|
|
|
;; and blueprint can just be plist of plists
|
|
;; we don't really need generic method dispatch
|
|
;; but for state - i'd kind of want to have static field
|
|
;; to share found max and pruning
|
|
|
|
(loop
|
|
for (resource cost) on '(:ore 3 :clay 14) by #'cddr
|
|
;; for (resource cost) in (getf *test-blueprint* :obsidian)
|
|
collect (list (* 2 cost) 'is 'cost resource) )
|
|
;; yes. comprehension is possible
|
|
|
|
(defparameter *test-state* (make-instance 'state))
|
|
(setf (getf (resources *test-state*) :ore 0) 5)
|
|
(setf (getf (robots *test-state*) :ore 0) 2)
|
|
(setf (getf (robots *test-state*) :obsidian 0) 5)
|
|
|
|
;; now let's check my function for can-create
|
|
(print *test-state*)
|
|
(can-create-robot *test-blueprint* :ore *test-state*)
|
|
(can-create-robot *test-blueprint* :clay *test-state*)
|
|
(can-create-robot *test-blueprint* :geode *test-state*)
|
|
(can-create-robot *test-blueprint* :obsidian *test-state*)
|
|
|
|
;; yay, i guess
|
|
;;
|
|
;; i had the error of putting quoted lists into the quoted list
|
|
;;
|
|
;; and now function to create new state with resources for a particular robot deducted?
|
|
;; and i guess with that robot already increased.
|
|
;; that function would signify passing of one turn \ minute
|
|
|
|
(setq *test-state* (make-instance 'state))
|
|
(setf (getf (resources *test-state*) :ore 0) 3)
|
|
(defparameter *another-state*
|
|
(create-robot *test-blueprint* :clay *test-state*))
|
|
|
|
;; well, that seems to work.
|
|
;; now i'd need to create blueprints from the lines.
|
|
;; then for each of the blueprint, calculate maximum of geodes.
|
|
;; multiply with the :id and sum.
|
|
;;
|
|
;; ok, i guess. i'd want a function that takes in blueprint.
|
|
;; gets initial state. and recurses searching for maximum
|
|
;; maybe even saving into blueprint as well.
|
|
|
|
;; how would that recursion look?
|
|
;; ore is added at the end of the minute.
|
|
;; resources for building is taken out at the beginning of the minute
|
|
;; built bot is added at the end of the minute
|
|
;;
|
|
;; so, i could
|
|
;; - calculate resources to be added
|
|
;; - for each possible (on old resources) bot build
|
|
;; recurse with bot cost deducted and new resources added
|
|
|
|
;; so next functions would be
|
|
;; - resources-to-be-collected :: just the plist of additional resources that would be
|
|
;; generated in 1 turn
|
|
;; and
|
|
;; - add-resources :: modifying operation that would update state
|
|
*test-state*
|
|
(calc-resources-to-be-collected *test-state*)
|
|
;; lol.
|
|
|
|
(add-resources '(:spagetty 1 :tuna 2) *test-state*)
|
|
;; and it works
|
|
|
|
;; so, now only main loop i suppose. and maybe-maybe later-later pruning
|
|
|
|
(get-possible-bot-builds *test-blueprint* *test-state*)
|
|
|
|
(defmethod find-max-geod (blueprints (s state) minute)
|
|
;; (format t "in step for ~a; with ~a~%" minute s)
|
|
(if (= 25 minute)
|
|
(getf (resources s) :geode 0)
|
|
(progn
|
|
(let* ((will-collect-this-minute (calc-resources-to-be-collected s))
|
|
(max-if-building
|
|
(loop
|
|
for bot-type in (get-possible-bot-builds blueprints s)
|
|
for state-with-new-bot = (create-robot blueprints bot-type s)
|
|
when state-with-new-bot
|
|
maximize (progn
|
|
(add-resources will-collect-this-minute state-with-new-bot)
|
|
(find-max-geod blueprints state-with-new-bot (1+ minute)))))
|
|
(if-not-building (let ((state-copy (copy-state s)))
|
|
;; (break)
|
|
(add-resources will-collect-this-minute state-copy)
|
|
(find-max-geod blueprints state-copy (1+ minute)))))
|
|
(max (or max-if-building 0) if-not-building)))))
|
|
|
|
;; Blueprint 1:
|
|
;; Each ore robot costs 4 ore.
|
|
;; Each clay robot costs 2 ore.
|
|
;; Each obsidian robot costs 3 ore and 14 clay.
|
|
;; Each geode robot costs 2 ore and 7 obsidian.
|
|
|
|
;; Blueprint 2:
|
|
;; Each ore robot costs 2 ore.
|
|
;; Each clay robot costs 3 ore.
|
|
;; Each obsidian robot costs 3 ore and 8 clay.
|
|
;; Each geode robot costs 3 ore and 12 obsidian.
|
|
;; do i just test this?
|
|
(setq *test-blueprint* '(:ore (:ore 4)
|
|
:clay (:ore 2)
|
|
:obsidian (:ore 3 :clay 14)
|
|
:geode (:ore 2 :obsidian 7)))
|
|
(setq *test-state* (make-instance 'state))
|
|
|
|
;; (print (find-max-geod *test-blueprint* *test-state* 1))
|
|
;; => 0
|
|
;; that's because i have no ability to "wait"
|
|
;; whoops
|
|
|
|
;; so. do i want, um. add one more attempted call after the loop in the iteration?
|
|
|
|
;; now. the looping is serious.
|
|
;; would it work for me to order keys geode first
|
|
|
|
;; now we seem to get geodes first, yay
|
|
;; maybe just run without printing?
|
|
|
|
;; let's check manually that when i do state copy, the plists are independent?
|
|
(setq *test-state* (make-instance 'state))
|
|
*test-state*
|
|
(setq *another-state* (copy-state *test-state*))
|
|
(incf (getf (resources *another-state*) :ore 0))
|
|
*another-state*
|
|
(add-resources '(:seeds 151) *another-state*)
|
|
(incf (getf (robots *another-state*) :obsidian 0))
|
|
|
|
;; oh, i didn't check that state returned from the "create bot" is independent
|
|
|
|
(setq *test-state* (make-instance 'state))
|
|
(add-resources '(:ore 10) *test-state*)
|
|
|
|
(setq *another-state* (create-robot *test-blueprint* :clay *test-state*))
|
|
;; ugh. resources stays shared.
|
|
;; WTF, why
|
|
|
|
;; manually create new list, i guess then then do set to the 'copied state'?
|
|
;; this is unpleasant
|
|
;; so, i guess use (copy-list
|
|
|
|
;; ok. the numbers seem ok. but this is long.
|
|
;; how do i trim this?
|
|
;; if on step 10 there's already a state with 3 obsidian machines.
|
|
;; does this state would always be ahead of
|
|
;; state on step 10 with 1 obsidian machine?
|
|
;;
|
|
;; it seems so!
|
|
;; for which reason? because if the state got 3 geode machines, it will be able to get more?
|
|
;; i suppose only when i reach case where each new step can add one more geod machine
|
|
;; only then i can guess the state is actually domeeneering?
|
|
;; but only over states with same amount of steps?
|
|
|
|
;; ok. let's commit what i have right now and try to trim?
|
|
|
|
;; how could i compare with that "cur-max" and update that cur-max?
|
|
;; no, i don't understand
|
|
|
|
(format t "some result ~a~%" (find-max-geod *test-blueprint* *test-state* 1))
|
|
|
|
;; well, yes one optimizaiton - stop building robots, when resource is to the top of
|
|
;; max daily expense
|
|
;; that would reduce a lot
|
|
;; 1)
|
|
;; would be nice to put these into (possible-robots-to-build)
|
|
;; so that it would also filtered out unnecessary new robots
|
|
;;
|
|
;; 2)
|
|
;; "keep global current max state" how would i compare and check if it's impossible to beat?
|
|
;; with "even if i could to build geod machine every day for rest N days"
|
|
|
|
*test-blueprint*
|
|
*test-state*
|
|
(add-resources '(:ore 10) *test-state*)
|
|
(incf (getf (robots *test-state*) :ore) 2)
|
|
(any-use-of-creating-robot *test-blueprint* *test-state* :ore)
|
|
(any-use-of-creating-robot *test-blueprint* *test-state* :clay)
|
|
(any-use-of-creating-robot *test-blueprint* *test-state* :obsidian)
|
|
(any-use-of-creating-robot *test-blueprint* *test-state* :geode)
|
|
|
|
(max-need *test-blueprint* *test-state* :ore)
|
|
(max-need *test-blueprint* *test-state* :clay)
|
|
(max-need *test-blueprint* *test-state* :obsidian)
|
|
(max-need *test-blueprint* *test-state* :geode)
|
|
|
|
;; and this is not good for :geode, we want as much as possible
|
|
|
|
(get-possible-bot-builds *test-blueprint* *test-state*)
|
|
|
|
;; and now let's add static "max state"?
|
|
;; i'd need comparison like "can catch up" with that found max "is dominated by"
|
|
;; and also way to update that maximal state?
|
|
;;
|
|
;; so, what will that be?
|
|
;; comparison for update, should it also be if i found a state that dominates?
|
|
;; only for satiated states?
|
|
;;
|
|
;; if day is same or more
|
|
;; but the amount of geode robots and geodes is smaller?
|
|
(is-satiated-p *test-blueprint* *test-state*)
|
|
(incf (getf (robots *test-state*) :obsidian 0) 20)
|
|
;; seems to work,
|
|
;; but i already want to utilize some test framework
|
|
|
|
;; whelp. i do want to test this
|
|
;; 4 14 7 to be satisfied
|
|
(is-satiated-p
|
|
*test-blueprint*
|
|
(make-instance 'state :robots '(:ore 4 :clay 14 :obsidian 7 :geode 2)))
|
|
|
|
(is-satiated-p
|
|
*test-blueprint*
|
|
(make-instance 'state :resources '(:ore 4 :clay 14 :obsidian 7 :geode 2))) ; not, need robots
|
|
|
|
(is-satiated-p
|
|
*test-blueprint*
|
|
(make-instance 'state :robots '(:ore 4 :clay 14 :obsidian 7 :geode 2)))
|
|
|
|
;; now for checking is-dominated. ugh.
|
|
(a-dominates-b-p
|
|
*test-blueprint*
|
|
(make-instance 'state :robots '(:ore 4 :clay 14 :obsidian 7 :geode 2))
|
|
(make-instance 'state :robots '(:ore 4 :clay 14 :obsidian 7 :geode 2)))
|
|
|
|
;; both satiated, but second bigger
|
|
(a-dominates-b-p
|
|
*test-blueprint*
|
|
(make-instance 'state :robots '(:ore 5 :clay 17 :obsidian 7 :geode 2))
|
|
(make-instance 'state :robots '(:ore 6 :clay 18 :obsidian 7 :geode 2)))
|
|
;;
|
|
;; both satiated, but second not always bigger
|
|
(a-dominates-b-p
|
|
*test-blueprint*
|
|
(make-instance 'state :robots '(:ore 5 :clay 17 :obsidian 9 :geode 2))
|
|
(make-instance 'state :robots '(:ore 6 :clay 18 :obsidian 8 :geode 2)))
|
|
|
|
;;
|
|
;; first not satiated, even though second is bigger - nil
|
|
(a-dominates-b-p
|
|
*test-blueprint*
|
|
(make-instance 'state :robots '(:ore 2 :clay 17 :obsidian 9 :geode 2))
|
|
(make-instance 'state :robots '(:ore 6 :clay 18 :obsidian 8 :geode 2)))
|
|
|
|
;; and that's not right. if we have on same amount of steps
|
|
;; reference as satiated and checking as not - wouldn't keep up
|
|
;; so big check should be whether steps are even in imagination permit
|
|
;; ugh. that would mean putting minute\step into state.
|
|
|
|
;; um, let's not do it right now?
|
|
;; for both satiated is a very weak check, but let's try it like this?
|
|
|
|
(setq *test-state* (make-instance 'state))
|
|
(setf (cur-found-max *test-state*) (make-instance 'state))
|
|
|
|
;; so whelp
|
|
;; should have committed after doing the "build makes sence list"
|
|
;;
|
|
;; my problems are because "is dominated by" is not simmetrical to "dominates"
|
|
;; and i want both
|
|
;;
|
|
;; now in the loop set first satiated as domination
|
|
;; after that compare if our set dominates that one and swap
|
|
;; and compare if it's dominated by that one and prune
|
|
|
|
;; so, only if ref earlier than checked state.
|
|
;; and then - if checked not saitated, or by all resources less than
|
|
;; but i do want tests
|
|
|
|
;; both satiated, but second bigger
|
|
(a-dominates-b-p
|
|
*test-blueprint*
|
|
(make-instance 'state :robots '(:ore 6 :clay 18 :obsidian 7 :geode 2) :minute 2)
|
|
(make-instance 'state :robots '(:ore 6 :clay 18 :obsidian 7 :geode 2) :minute 3))
|
|
;; should be NIL
|
|
;; now want to check for different amount of steps.
|
|
;; so if same resources but first is earlier - it dominates
|
|
(minute (make-instance 'state :robots '(:ore 6 :clay 18 :obsidian 7 :geode 2) :minute 2))
|
|
;; i was putting :minute 2 into :robots plist, cool, no thanks to you types
|
|
(minute (make-instance 'state :robots '(:ore 6 :clay 18 :obsidian 7 :geode 2) :minute 2))
|
|
|
|
;; ok. this also seems well.
|
|
;; then main loop? on fisrt satiated set as `cur-found-max`
|
|
;; and after that always check -
|
|
|
|
(defmethod find-max-geod-2 (blueprints (s state))
|
|
;; (declare (optimize (debug 3)))
|
|
;; (format t "in step for ~a; with ~a~%" (minute s) s)
|
|
(cond
|
|
((= 25 (minute s)) ; exit condition fully calculated
|
|
(getf (resources s) :geode 0))
|
|
((< (estimate s) (cur-found-max s))
|
|
;; (print "pruning")
|
|
0) ; pruning this branch
|
|
(t ; default check
|
|
(progn
|
|
(let* ((will-collect-this-minute (calc-resources-to-be-collected s))
|
|
(possible-bot-builds (get-possible-bot-builds blueprints s))
|
|
(max-if-building
|
|
(when possible-bot-builds
|
|
(loop
|
|
for bot-type in possible-bot-builds
|
|
for state-with-new-bot = (create-robot blueprints bot-type s)
|
|
when state-with-new-bot
|
|
maximize (progn
|
|
(add-resources will-collect-this-minute state-with-new-bot)
|
|
(incf (minute state-with-new-bot))
|
|
(find-max-geod-2 blueprints state-with-new-bot )))))
|
|
(if-not-building
|
|
(let ((state-copy (copy-state s)))
|
|
;; (break)
|
|
(add-resources will-collect-this-minute state-copy)
|
|
(incf (minute state-copy))
|
|
(find-max-geod-2 blueprints state-copy )))
|
|
(recursed-max (max (or max-if-building 0) if-not-building)))
|
|
;; (break)
|
|
;; (format t "would build ~a~%" possible-bot-builds)
|
|
(when (> recursed-max (cur-found-max s))
|
|
(setf (cur-found-max s) recursed-max))
|
|
recursed-max
|
|
)))))
|
|
|
|
(setq *test-state* (make-instance 'state))
|
|
(setf (cur-found-max *test-state*) 0)
|
|
(timing (print (find-max-geod-2 *test-blueprint* *test-state*)))
|
|
(timing (let ((a 1) (b 2)) (* a b 15)))
|
|
|
|
;; thank you CL-Cookbook: https://cl-cookbook.sourceforge.net/dates_and_times.html
|
|
(defmacro timing (&body forms)
|
|
(let ((real1 (gensym))
|
|
(real2 (gensym))
|
|
(run1 (gensym))
|
|
(run2 (gensym))
|
|
(result (gensym)))
|
|
`(let* ((,real1 (get-internal-real-time))
|
|
(,run1 (get-internal-run-time))
|
|
(,result (progn ,@forms))
|
|
(,run2 (get-internal-run-time))
|
|
(,real2 (get-internal-real-time)))
|
|
(format *debug-io* ";;; Computation took:~%")
|
|
(format *debug-io* ";;; ~f seconds of real time~%"
|
|
(/ (- ,real2 ,real1) internal-time-units-per-second))
|
|
(format t ";;; ~f seconds of run time~%"
|
|
(/ (- ,run2 ,run1) internal-time-units-per-second))
|
|
,result)))
|
|
;; so, why doesn't it ever builds obsidian?
|
|
*test-blueprint*
|
|
;; because is "has to build something if it can" whops
|
|
;; wow 20 seconds. cool
|
|
|
|
;; last things:
|
|
;; read in the line for the blueprints into plist
|
|
;; then loop over lines in file
|
|
;; for each line compute max, multiply by index
|
|
;; and sum
|
|
;; ok, i guess
|
|
|
|
(rest (remove-if-not #'identity
|
|
(mapcar (lambda (str) (parse-integer str :junk-allowed t))
|
|
(ppcre:split " "
|
|
"Blueprint 2: Each ore robot costs 2 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 8 clay. Each geode robot costs 3 ore and 12 obsidian."))))
|
|
|
|
(blueprint-line-to-plist "Blueprint 1: Each ore robot costs 4 ore. Each clay robot costs 2 ore. Each obsidian robot costs 3 ore and 14 clay. Each geode robot costs 2 ore and 7 obsidian.")
|
|
|
|
;; and now loop over file.
|
|
|
|
(print (with-open-file (in "day19-test.txt")
|
|
(loop
|
|
for line = (read-line in nil nil)
|
|
for n from 1
|
|
for blueprints = (when line (progn
|
|
(format t "Starting processing for ~a~%" line)
|
|
(blueprint-line-to-plist line)))
|
|
for max-geo = 0
|
|
;; for max-geo = (when blueprints
|
|
;; (progn
|
|
;; (setf (cur-found-max *test-state*) 0)
|
|
;; (timing (find-max-geod-2 blueprints (make-instance 'state)))))
|
|
while blueprints
|
|
do (format t "processed ~a : ~a. its max is ~a~%" n blueprints max-geo)
|
|
summing (* n max-geo))))
|
|
|
|
|
|
(format t "and the result is : ~a~%" (read-and-calc-part-1 "day19-test.txt"))
|
|
|
|
(format t "and the result is : ~a~%" (read-and-calc-part-1 "day19-input.txt"))
|
|
|
|
;; wtf is taking so long in between the processings?
|
|
|
|
(apply #'* (list 2 3 5))
|
|
|
|
;; but before that - change exit point
|
|
(format t "and the result is : ~a~%" (read-and-calc-part-2 "day19-2-input.txt"))
|
|
;; 261 is too low. so
|
|
;; but in previous my maxes were 0 1 2
|
|
;; and current are 3 3 29
|
|
;; but still not good
|
|
;; ugh. what about test input?
|
|
|
|
(format t "and the result is : ~a~%" (read-and-calc-part-2 "day19-test.txt"))
|
|
;; my calc : 18 and 55
|
|
;; correct : 56 and 62
|
|
;; coooool. let's move on.
|