362 lines
8 KiB
Ruby
362 lines
8 KiB
Ruby
# frozen_string_literal: true
|
|
# typed: true
|
|
|
|
require 'sorbet-runtime'
|
|
require 'sorbet-struct-comparable'
|
|
|
|
extend T::Sig
|
|
|
|
sig { params(cond: T::Boolean, msg: String).void }
|
|
def assert(cond, msg)
|
|
raise StandardError, msg unless cond
|
|
end
|
|
|
|
sig { params(a: Integer, b: Integer).returns(Integer) }
|
|
def gcd(a, b)
|
|
while b != 0
|
|
a, b = b, a % b
|
|
end
|
|
a
|
|
end
|
|
|
|
sig { params(nums: T::Array[Integer]).returns(Integer) }
|
|
def gcd_all(nums)
|
|
if nums.length == 0
|
|
return 0
|
|
end
|
|
|
|
result = 1
|
|
nums.each do |n|
|
|
result = gcd(result, T.must(n))
|
|
end
|
|
result
|
|
end
|
|
|
|
|
|
sig { params(nums: T::Array[Integer]).returns(Integer) }
|
|
def lcm(nums)
|
|
gcd = gcd_all(nums)
|
|
|
|
result = 1
|
|
nums.each do |n|
|
|
result *= n / gcd
|
|
end
|
|
result
|
|
end
|
|
|
|
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
|
|
|
|
sig { returns(T::Array[Integer])}
|
|
def run
|
|
gate_firsts = T.let({}, T::Hash[String, Integer])
|
|
|
|
count = 0
|
|
while true
|
|
count += 1
|
|
|
|
queue = T.let([], T::Array[Wire])
|
|
queue << Wire::new(src: 'button', dst: 'broadcaster', pulse: Pulse::Low)
|
|
|
|
while !queue.empty?
|
|
wire = T.must(queue.shift)
|
|
|
|
if wire.pulse == Pulse::High and @gates.any?(wire.src)
|
|
gate_firsts[wire.src] = gate_firsts[wire.src] || count
|
|
end
|
|
|
|
mod = @modules[wire.dst] or next
|
|
mod.handle(wire.src, wire.pulse).each do |output|
|
|
queue << output
|
|
end
|
|
end
|
|
|
|
if @gates.all? { |g| !gate_firsts[g].nil? }
|
|
return gate_firsts.values
|
|
end
|
|
end
|
|
end
|
|
|
|
def assert_conditions
|
|
tx = []
|
|
@modules.each_pair do |name, mod|
|
|
tx << name if mod.dests.any?('rx')
|
|
end
|
|
|
|
case tx.length
|
|
when 1
|
|
@tx = tx[0]
|
|
when 0
|
|
raise StandardError, 'no transmitter'
|
|
else
|
|
pp tx
|
|
raise StandardError, 'more than one transmitter'
|
|
end
|
|
|
|
@gates = []
|
|
@modules.each_pair do |name, mod|
|
|
if mod.dests.any?(@tx)
|
|
@gates << name
|
|
assert mod.dests.length == 1, 'Gate emits to non-tx'
|
|
end
|
|
end
|
|
|
|
@exits = []
|
|
@modules.each_pair do |name, mod|
|
|
ix = mod.dests & @gates
|
|
next if ix.empty?
|
|
|
|
@exits << name
|
|
end
|
|
end
|
|
|
|
sig { params(gv: String, png: String).void }
|
|
def show_graph(gv, png)
|
|
File.open(gv, 'w') do |f|
|
|
f.puts('strict digraph Day20 {')
|
|
@modules.each_pair do |name, mod|
|
|
color = case mod
|
|
when FlipFlop then '#5FD65D'
|
|
when Conjunction then '#00D7FF'
|
|
else if name == 'rx' then '#FF875F' else 'black' end
|
|
end
|
|
label = T.must(mod.class.name).split('::').last
|
|
exit = if @exits.any?(name) then '*' else '' end
|
|
|
|
f.puts("#{name} [fontcolor=\"#{color}\",label=\"\\N\\n#{exit}#{label}\"];")
|
|
mod.dests.each do |dst|
|
|
f.puts("#{name} -> #{dst};")
|
|
end
|
|
end
|
|
f.puts('}')
|
|
end
|
|
|
|
system('dot', '-Tpng', gv, out: png)
|
|
# system('imv', png)
|
|
end
|
|
|
|
sig { returns(T::Hash[String, T.any(T::Boolean, T::Hash[String, Pulse])])}
|
|
def state
|
|
state = {}
|
|
@modules.each_pair do |name, mod|
|
|
|
|
end
|
|
state
|
|
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
|
|
|
|
sig { params(path: String).returns(Integer) }
|
|
def part2(path)
|
|
text = File.read(path)
|
|
machine = Machine::new(text)
|
|
|
|
machine.assert_conditions
|
|
|
|
gv = File.join(File.dirname(path), "day20.gv")
|
|
png = File.join(File.dirname(path), "day20.png")
|
|
machine.show_graph(gv, png)
|
|
counts = machine.run
|
|
lcm(counts)
|
|
end
|
|
end
|
|
end
|
|
|
|
# puts Day20::part1 File.join(__dir__, 'input/test.txt')
|
|
puts Day20::part1 File.join(__dir__, 'input/day20.txt')
|
|
puts Day20::part2 File.join(__dir__, 'input/day20.txt')
|