diff --git a/2022/day19/main.go b/2022/day19/main.go new file mode 100644 index 0000000..fcbf43e --- /dev/null +++ b/2022/day19/main.go @@ -0,0 +1,211 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + blueprints := parseInput(input) + + // how many geodes can be opened in 24 minutes? + sum := 0 + for _, bp := range blueprints { + st := newState(bp) + geodesMade := st.calcMostGeodes(0, map[string]int{}, 24, 24) + // fmt.Println("ID:", bp.id, geodesMade) + sum += st.blueprint.id * geodesMade + } + + // total quality of all blueprints, quality = id * (# geodes in 24 min) + return sum +} + +func part2(input string) int { + blueprints := parseInput(input) + if len(blueprints) > 3 { + blueprints = blueprints[:3] + } + + prod := 1 + for _, bp := range blueprints { + st := newState(bp) + geodesMade := st.calcMostGeodes(0, map[string]int{}, 32, 32) + // fmt.Println(bp.id, geodesMade) + prod *= geodesMade + } + + // total quality of all blueprints, quality = id * (# geodes in 24 min) + return prod +} + +type blueprint struct { + id int + oreForOreRobot int + oreForClayRobot int + oreForObsidianRobot, clayForObsidianRobot int + oreForGeodeRobot, obsidianForGeodeRobot int +} + +type state struct { + blueprint + ore, clay, obsidian, geode int + oreRobots, clayRobots, obsidianRobots, geodeRobots int +} + +func newState(blueprint blueprint) state { + return state{ + blueprint: blueprint, + oreRobots: 1, + } +} + +func (s *state) farm() { + s.ore += s.oreRobots + s.clay += s.clayRobots + s.obsidian += s.obsidianRobots + s.geode += s.geodeRobots +} + +func (s *state) hash(time int) string { + return fmt.Sprint(time, s.ore, s.clay, s.obsidian, + s.geode, s.oreRobots, s.clayRobots, s.obsidianRobots, s.geodeRobots) +} + +// NOT A POINTER METHOD SO A COPY CAN BE MADE +// this is some cheeky Go struct copying, it'd be easier to read if it was just +// directly recreating all the fields +func (s state) copy() state { + return s +} + +func (s *state) calcMostGeodes(time int, memo map[string]int, totalTime int, earliestGeode int) int { + if time == totalTime { + return s.geode + } + + h := s.hash(time) + if v, ok := memo[h]; ok { + return v + } + + if s.geode == 0 && time > earliestGeode { + return 0 + } + + // factory can try to make any possible robot, will backtrack if necessary + mostGeodes := s.geode + + // always make geode robots + if s.ore >= s.oreForGeodeRobot && + s.obsidian >= s.obsidianForGeodeRobot { + cp := s.copy() + + cp.farm() + + cp.ore -= cp.oreForGeodeRobot + cp.obsidian -= cp.obsidianForGeodeRobot + cp.geodeRobots++ + if cp.geodeRobots == 1 { + earliestGeode = mathy.MinInt(earliestGeode, time+1) + } + mostGeodes = mathy.MaxInt(mostGeodes, cp.calcMostGeodes(time+1, memo, totalTime, earliestGeode)) + + memo[h] = mostGeodes + return mostGeodes + } + + if time <= totalTime-16 && + s.oreRobots < s.oreForObsidianRobot*2 && + s.ore >= s.oreForOreRobot { + cp := s.copy() + cp.ore -= cp.oreForOreRobot + + cp.farm() + + cp.oreRobots++ + mostGeodes = mathy.MaxInt(mostGeodes, cp.calcMostGeodes(time+1, memo, totalTime, earliestGeode)) + } + if time <= totalTime-8 && + s.clayRobots < s.clayForObsidianRobot && + s.ore >= s.oreForClayRobot { + cp := s.copy() + cp.ore -= cp.oreForClayRobot + + cp.farm() + + cp.clayRobots++ + mostGeodes = mathy.MaxInt(mostGeodes, cp.calcMostGeodes(time+1, memo, totalTime, earliestGeode)) + } + if time <= totalTime-4 && + s.obsidianRobots < s.obsidianForGeodeRobot && + s.ore >= s.oreForObsidianRobot && s.clay >= s.clayForObsidianRobot { + + cp := s.copy() + cp.ore -= cp.oreForObsidianRobot + cp.clay -= cp.clayForObsidianRobot + cp.farm() + + cp.obsidianRobots++ + mostGeodes = mathy.MaxInt(mostGeodes, cp.calcMostGeodes(time+1, memo, totalTime, earliestGeode)) + } + + // or no factory production this minute + cp := s.copy() + cp.ore += cp.oreRobots + cp.clay += cp.clayRobots + cp.obsidian += cp.obsidianRobots + cp.geode += cp.geodeRobots + mostGeodes = mathy.MaxInt(mostGeodes, cp.calcMostGeodes(time+1, memo, totalTime, earliestGeode)) + + memo[h] = mostGeodes + return mostGeodes +} + +func parseInput(input string) (ans []blueprint) { + // Blueprint 1: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 20 clay. Each geode robot costs 2 ore and 12 obsidian. + for _, line := range strings.Split(input, "\n") { + bp := blueprint{} + _, err := fmt.Sscanf(line, "Blueprint %d: Each ore robot costs %d ore. Each clay robot costs %d ore. Each obsidian robot costs %d ore and %d clay. Each geode robot costs %d ore and %d obsidian.", + &bp.id, &bp.oreForOreRobot, &bp.oreForClayRobot, &bp.oreForObsidianRobot, + &bp.clayForObsidianRobot, &bp.oreForGeodeRobot, &bp.obsidianForGeodeRobot) + if err != nil { + panic("parsing: " + err.Error()) + } + ans = append(ans, bp) + } + return ans +} diff --git a/2022/day19/main_test.go b/2022/day19/main_test.go new file mode 100644 index 0000000..93eccc2 --- /dev/null +++ b/2022/day19/main_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "testing" +) + +var example = `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.` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 33, + }, + { + name: "actual", + input: input, + want: 1127, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 56 * 62, + }, + // I actually have zero idea how long this took to run, I ran it overnight + // 21 27 38 + // { + // name: "actual", + // input: input, + // want: 21546, + // }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +}