Code for the Advent of Code event
aoc advent-of-code
at rust 294 lines 5.6 kB view raw
1#!/usr/bin/env ruby 2# frozen_string_literal: true 3 4require 'colorize' 5require 'matrix' 6 7require_relative '../../../lib/aoc/intcode/cpu' 8 9DEBUG = ENV.fetch('DEBUG', nil) 10 11class Vector 12 def x; self[0]; end 13 def y; self[1]; end 14end 15 16def term_clear 17 print "\e[H" 18end 19 20def term_setpos(x, y) 21 print "\e[#{y},#{x}H" 22end 23 24PATH = 'input' 25DRAW = !ARGV.empty? 26 27ORIGIN = Vector[0, 0] 28 29DIRECTIONS = { 30 north: 1, 31 south: 2, 32 west: 3, 33 east: 4 34}.freeze 35 36TRANSFORMS = { 37 north: Vector[0, -1], 38 south: Vector[0, 1], 39 west: Vector[-1, 0], 40 east: Vector[1, 0] 41}.freeze 42 43STATUSES = { 44 0 => :wall, 45 1 => :empty, 46 2 => :oxygen 47}.freeze 48 49TILES = { 50 wall: '#', 51 oxygen: 'O', 52 empty: '.', 53 droid: 'D', 54 unknown: ' ' 55}.freeze 56 57SIMPLE_TILES = { 58 origin: '██'.magenta, 59 wall: '██', 60 oxygen: '██'.green, 61 empty: ' ', 62 droid: 'DD', 63 deadend: '██'.red, 64 deadpath: '██'.yellow, 65 path: '██'.light_black, 66 unknown: '██' 67}.freeze 68 69# grid = Hash.new :unknown 70 71def set_status(text) # rubocop:disable Naming/AccessorMethodName 72 Curses.setpos 0, 0 73 Curses.clrtoeol 74 Curses.addstr text 75end 76 77class Bot 78 attr_reader :position 79 80 def initialize(cpu, position = Vector[0, 0]) 81 @cpu = cpu 82 @position = position 83 @active = true 84 end 85 86 def dup 87 Bot.new @cpu.dup, @position 88 end 89 90 def move!(direction) 91 return unless @active 92 value = DIRECTIONS[direction] 93 @cpu.input! value 94 @cpu.run! 95 status = STATUSES[@cpu.output[-1]] 96 @cpu.clear_output! 97 @position += TRANSFORMS[direction] 98 @active = false if status == :wall 99 status 100 end 101 102 def active? 103 @active 104 end 105 106 def kill! 107 @active = false 108 end 109end 110 111Bounds = Struct.new :top, :bottom, :left, :right, :offset 112 113class Map 114 attr_reader :oxygen_position, :oxygen_distance, :fill_time 115 116 def initialize(base_cpu) 117 @base_cpu = base_cpu 118 @grid = Hash.new :unknown 119 @bots = Set.new 120 first_bot = Bot.new base_cpu.dup 121 @bots.add first_bot 122 end 123 124 def step! 125 new_bots = [] 126 deleted_bots = [] 127 @bots.each do |bot| 128 DIRECTIONS.each_key do |direction| 129 clone = bot.dup 130 status = clone.move! direction 131 132 if @grid[clone.position] == :unknown 133 @grid[clone.position] = status 134 new_bots << clone unless status == :wall 135 end 136 end 137 138 deleted_bots << bot 139 end 140 141 @bots.subtract deleted_bots 142 @bots.merge new_bots 143 144 draw_simple 145 end 146 147 def process! 148 empty_poses = @grid.select { |_, v| v == :empty }.map(&:first) 149 150 empty_poses.each do |pos| 151 if pos == ORIGIN 152 @grid[pos] = :origin 153 elsif deadend? pos 154 @grid[pos] = :deadend 155 end 156 end 157 158 draw_simple 159 160 # Calculate deadend paths 161 deadends = @grid.select { |_, v| v == :deadend }.map(&:first) 162 deadends.each do |de| 163 process_deadend! de 164 draw_simple 165 end 166 167 path_cells = @grid.select { |_, v| v == :empty }.map(&:first) 168 path_cells.each do |pos| 169 @grid[pos] = :path 170 end 171 172 @oxygen_position = calc_oxygen_position 173 @oxygen_distance = calc_oxygen_distance 174 @processed = true 175 fill! 176 end 177 178 def fill! 179 process! unless @processed 180 181 time = process_fill! @oxygen_position, 0 182 draw_simple 183 184 @fill_time = time 185 end 186 187 def process_deadend!(pos) 188 others = surroundings pos 189 return if others[:empty] > 1 190 @grid[pos] = :deadpath if @grid[pos] == :empty 191 192 dirs = [ 193 pos + TRANSFORMS[:north], 194 pos + TRANSFORMS[:south], 195 pos + TRANSFORMS[:west], 196 pos + TRANSFORMS[:east] 197 ] 198 199 pos = dirs.find { |p| @grid[p] == :empty } 200 process_deadend! pos 201 end 202 203 def process_fill!(pos, time) 204 return time if @grid[pos] == :wall 205 return time if @grid[pos] == :oxygen && pos != @oxygen_position 206 others = surroundings pos 207 @grid[pos] = :oxygen 208 return time if others[:oxygen] + others[:wall] == 4 209 210 draw_simple 211 212 dirs = [ 213 pos + TRANSFORMS[:north], 214 pos + TRANSFORMS[:south], 215 pos + TRANSFORMS[:west], 216 pos + TRANSFORMS[:east] 217 ] 218 219 dirs.map { |dir| process_fill! dir, time + 1 }.max 220 end 221 222 def done? 223 @bots.empty? 224 end 225 226 def calc_oxygen_position 227 @grid.find { |_, v| v == :oxygen }.first 228 end 229 230 def calc_oxygen_distance 231 @grid.count { |_, v| v == :path } + 1 232 end 233 234 def draw_simple(only_if_debug: true) 235 return unless DRAW 236 return if only_if_debug && !DEBUG 237 term_clear 238 bounds = calc_bounds 239 (bounds.top..bounds.bottom).each do |y| 240 (bounds.left..bounds.right).each do |x| 241 pos = Vector[x, y] 242 type = @grid[pos] 243 print SIMPLE_TILES[type] 244 end 245 puts 246 end 247 end 248 249 private 250 251 def surroundings(pos) 252 others = [ 253 @grid[pos + TRANSFORMS[:north]], 254 @grid[pos + TRANSFORMS[:south]], 255 @grid[pos + TRANSFORMS[:west]], 256 @grid[pos + TRANSFORMS[:east]] 257 ] 258 259 Hash.new(0).tap { |h| others.each { |t| h[t] += 1 } } 260 end 261 262 def calc_bounds 263 xs = @grid.keys.map(&:x) 264 ys = @grid.keys.map(&:y) 265 min_x = xs.min 266 max_x = xs.max 267 min_y = ys.min 268 max_y = ys.max 269 x_offset = min_x < 0 ? min_x.abs : 0 270 y_offset = min_y < 0 ? min_y.abs : 0 271 Bounds.new min_y, max_y, min_x, max_x, Vector[x_offset, y_offset] 272 end 273 274 def deadend?(pos) 275 surroundings(pos)[:wall] == 3 276 end 277end 278 279base_cpu = AoC::Intcode::CPU.new.load!(PATH).print_output!(false) 280 281bots = Set.new 282# cpus = Set.new 283 284bots.add Bot.new base_cpu.dup 285 286map = Map.new base_cpu 287 288map.step! until map.done? 289 290map.process! 291map.draw_simple(false) 292 293puts "Part 1: #{map.oxygen_distance}" 294puts "Part 2: #{map.fill_time}"