diff --git a/2023/day20.rb b/2023/day20.rb new file mode 100644 index 0000000..db84d1d --- /dev/null +++ b/2023/day20.rb @@ -0,0 +1,210 @@ +# frozen_string_literal: true +# typed: true + +require 'sorbet-runtime' +require 'sorbet-struct-comparable' + +module Day20 + class Pulse < T::Enum + enums do + High = new + Low = new + end + end + + class Wire < T::Struct + include T::Struct::ActsAsComparable + + const :src, String + const :dst, String + const :pulse, Pulse + end + + class Module + extend T::Sig + extend T::Helpers + + abstract! + + sig { abstract.returns(String) } + def name; end + + sig { abstract.returns(T::Array[String]) } + def dests; end + + sig { abstract.params(source: String, pulse: Pulse).returns(T::Array[Wire]) } + def handle(source, pulse); end + end + + class Broadcaster < Module + extend T::Sig + + sig { override.returns(String) } + attr_reader :name + + sig { override.returns(T::Array[String]) } + attr_reader :dests + + sig { params(name: String, dests: T::Array[String]).void } + def initialize(name, dests) + @name = name + @dests = dests + end + + sig { override.params(source: String, pulse: Pulse).returns(T::Array[Wire]) } + def handle(source, pulse) + @dests.map do |dst| + Wire::new(src: @name, dst: dst, pulse: pulse) + end + end + end + + class FlipFlop < Module + extend T::Sig + + sig { override.returns(String) } + attr_reader :name + + sig { override.returns(T::Array[String]) } + attr_reader :dests + + sig { params(name: String, dests: T::Array[String]).void } + def initialize(name, dests) + @name = name + @dests = dests + @inputs = T.let([], T::Array[String]) + @on = false + end + + sig { override.params(source: String, pulse: Pulse).returns(T::Array[Wire]) } + def handle(source, pulse) + return [] if pulse == Pulse::High + + if @on + @on = false + output = Pulse::Low + else + @on = true + output = Pulse::High + end + + @dests.map do |dst| + Wire::new(src: @name, dst: dst, pulse: output) + end + end + end + + class Conjunction < Module + extend T::Sig + + sig { override.returns(String) } + attr_reader :name + + sig { override.returns(T::Array[String]) } + attr_reader :dests + + sig { override.params(name: String, dests: T::Array[String]).void } + def initialize(name, dests) + @name = name + @dests = dests + @inputs = T.let({}, T::Hash[String, Pulse]) + end + + sig { params(name: String).void } + def register_input(name) + @inputs[name] = Pulse::Low + end + + sig { override.params(source: String, pulse: Pulse).returns(T::Array[Wire]) } + def handle(source, pulse) + @inputs[source] = pulse + + output = if @inputs.all? { |k, v| v == Pulse::High } + Pulse::Low + else + Pulse::High + end + + @dests.map do |dst| + Wire::new(src: @name, dst: dst, pulse: output) + end + end + end + + class Machine + extend T::Sig + + def initialize(text) + @modules = T.let({}, T::Hash[String, Module]) + + text.strip.split("\n").each do |line| + name, dests = line.split('->').map(&:strip) + dests = dests.split(',').map(&:strip) + + mod = if name[0] == '%' + FlipFlop::new(name[1..name.length], dests) + elsif name[0] == '&' + Conjunction::new(name[1..name.length], dests) + elsif name == 'broadcaster' + Broadcaster::new(name, dests) + else + raise StandardError, name + end + + @modules[mod.name] = mod + end + + @modules.each_pair do |src_name, src_mod| + src_mod.dests.each do |dst_name| + dst = @modules[dst_name] + if dst.is_a? Conjunction + dst.register_input(src_name) + end + end + end + end + + sig { returns(T::Hash[Pulse, Integer] )} + def push_button + counts = { + Pulse::Low => 0, + Pulse::High => 0, + } + + queue = T.let([], T::Array[Wire]) + queue << Wire::new(src: 'button', dst: 'broadcaster', pulse: Pulse::Low) + + while !queue.empty? + wire = T.must(queue.shift) + counts[wire.pulse] += 1 + + mod = @modules[wire.dst] or next + mod.handle(wire.src, wire.pulse).each do |output| + queue << output + end + end + + counts + end + end + + class << self + extend T::Sig + + sig { params(path: String).returns(Integer) } + def part1(path) + text = File.read(path) + machine = Machine::new(text) + low = high = 0 + 1_000.times do + pulses = machine.push_button + high += pulses.fetch(Pulse::High) + low += pulses.fetch(Pulse::Low) + end + high * low + end + end +end + +puts Day20::part1 File.join(__dir__, 'input/test.txt') +puts Day20::part1 File.join(__dir__, 'input/day20.txt')