Add Day 20 part 1 solution
parent
99bbf71cb3
commit
1957e1b39a
|
@ -0,0 +1,375 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const BroadcasterName = "broadcaster"
|
||||
|
||||
type WorkQueue []PendingPulse
|
||||
|
||||
type Pulse int
|
||||
|
||||
const (
|
||||
PulseLow Pulse = iota
|
||||
PulseHigh
|
||||
)
|
||||
|
||||
type ParsedModuleKind int
|
||||
|
||||
type ParsedModule struct {
|
||||
Name string
|
||||
Kind ParsedModuleKind
|
||||
Children []string
|
||||
}
|
||||
|
||||
const (
|
||||
ParsedModuleBroadcaster ParsedModuleKind = iota
|
||||
ParsedModuleFlipFlop
|
||||
ParsedModuleConjunction
|
||||
)
|
||||
|
||||
type PulseHandler interface {
|
||||
ID() string
|
||||
HandlePulse(source PulseHandler, pulse Pulse)
|
||||
}
|
||||
|
||||
var _ PulseHandler = &FlipFlop{}
|
||||
var _ PulseHandler = &Conjunction{}
|
||||
var _ PulseHandler = &Broadcaster{}
|
||||
var _ PulseHandler = &ModuleShim{}
|
||||
|
||||
type ModuleInfo struct {
|
||||
name string
|
||||
children []PulseHandler
|
||||
}
|
||||
|
||||
type FlipFlop struct {
|
||||
ModuleInfo
|
||||
state bool
|
||||
}
|
||||
|
||||
type Conjunction struct {
|
||||
ModuleInfo
|
||||
lastPulseFrom map[string]Pulse
|
||||
}
|
||||
|
||||
type Broadcaster struct {
|
||||
ModuleInfo
|
||||
}
|
||||
|
||||
type ModuleShim struct {
|
||||
name string
|
||||
knownModules map[string]PulseHandler
|
||||
pulseQueue *WorkQueue
|
||||
}
|
||||
|
||||
type PendingPulse struct {
|
||||
pulse Pulse
|
||||
sendPulse func()
|
||||
}
|
||||
|
||||
func NewFlipFlop(name string, children []PulseHandler) *FlipFlop {
|
||||
return &FlipFlop{
|
||||
ModuleInfo: ModuleInfo{
|
||||
name: name,
|
||||
children: children,
|
||||
},
|
||||
state: false,
|
||||
}
|
||||
}
|
||||
|
||||
func NewBroadcaster(name string, children []PulseHandler) *Broadcaster {
|
||||
return &Broadcaster{
|
||||
ModuleInfo: ModuleInfo{
|
||||
name: name,
|
||||
children: children,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewConjunction(name string, sources []PulseHandler, children []PulseHandler) *Conjunction {
|
||||
receivedHighPulseFrom := make(map[string]Pulse)
|
||||
for _, source := range sources {
|
||||
receivedHighPulseFrom[source.ID()] = PulseLow
|
||||
}
|
||||
|
||||
return &Conjunction{
|
||||
lastPulseFrom: receivedHighPulseFrom,
|
||||
ModuleInfo: ModuleInfo{
|
||||
name: name,
|
||||
children: children,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewShimmedModule(name string, allModules map[string]PulseHandler, pulseQueue *WorkQueue) ModuleShim {
|
||||
return ModuleShim{
|
||||
name: name,
|
||||
knownModules: allModules,
|
||||
pulseQueue: pulseQueue,
|
||||
}
|
||||
}
|
||||
|
||||
func (handler ModuleShim) ID() string {
|
||||
return handler.name
|
||||
}
|
||||
|
||||
func (handler ModuleShim) HandlePulse(source PulseHandler, pulse Pulse) {
|
||||
module, ok := handler.knownModules[handler.name]
|
||||
pulseFunc := func() {
|
||||
module.HandlePulse(source, pulse)
|
||||
}
|
||||
if !ok {
|
||||
// nop, nothing to send it to
|
||||
pulseFunc = func() {}
|
||||
}
|
||||
|
||||
pendingPulse := PendingPulse{
|
||||
pulse: pulse,
|
||||
sendPulse: pulseFunc,
|
||||
}
|
||||
|
||||
handler.pulseQueue.Push(pendingPulse)
|
||||
}
|
||||
|
||||
func (moduleInfo *ModuleInfo) ID() string {
|
||||
return moduleInfo.name
|
||||
}
|
||||
|
||||
func (flipFlop *FlipFlop) HandlePulse(_ PulseHandler, pulse Pulse) {
|
||||
if pulse == PulseHigh {
|
||||
return
|
||||
} else if pulse != PulseLow {
|
||||
panic(fmt.Sprintf("invalid pulse value %d", pulse))
|
||||
}
|
||||
|
||||
nextPulse := PulseHigh
|
||||
if flipFlop.state {
|
||||
nextPulse = PulseLow
|
||||
}
|
||||
|
||||
broadcastPulse(flipFlop, flipFlop.children, nextPulse)
|
||||
|
||||
flipFlop.state = !flipFlop.state
|
||||
}
|
||||
|
||||
func (broadcaster *Broadcaster) HandlePulse(_ PulseHandler, pulse Pulse) {
|
||||
broadcastPulse(broadcaster, broadcaster.children, pulse)
|
||||
}
|
||||
|
||||
func (conjunction *Conjunction) HandlePulse(source PulseHandler, pulse Pulse) {
|
||||
conjunction.lastPulseFrom[source.ID()] = pulse
|
||||
if conjunction.allInputsHigh() {
|
||||
broadcastPulse(conjunction, conjunction.children, PulseLow)
|
||||
} else {
|
||||
broadcastPulse(conjunction, conjunction.children, PulseHigh)
|
||||
}
|
||||
}
|
||||
|
||||
func (conjunction *Conjunction) allInputsHigh() bool {
|
||||
for _, pulse := range conjunction.lastPulseFrom {
|
||||
if pulse == PulseLow {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (pulse Pulse) String() string {
|
||||
switch pulse {
|
||||
case PulseHigh:
|
||||
return "PulseHigh"
|
||||
case PulseLow:
|
||||
return "PulseLow"
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid pulse value %d", pulse))
|
||||
}
|
||||
}
|
||||
|
||||
func (queue *WorkQueue) Push(pulse PendingPulse) {
|
||||
*queue = append(*queue, pulse)
|
||||
}
|
||||
|
||||
func (queue *WorkQueue) Pop() PendingPulse {
|
||||
if len(*queue) == 0 {
|
||||
panic("cannot pop empty queue")
|
||||
}
|
||||
|
||||
front := (*queue)[0]
|
||||
*queue = (*queue)[1:]
|
||||
|
||||
return front
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 && len(os.Args) != 3 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s inputfile\n", os.Args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
inputFilename := os.Args[1]
|
||||
inputFile, err := os.Open(inputFilename)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("could not open input file: %s", err))
|
||||
}
|
||||
|
||||
defer inputFile.Close()
|
||||
|
||||
inputBytes, err := io.ReadAll(inputFile)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("could not read input file: %s", err))
|
||||
}
|
||||
|
||||
input := strings.TrimSpace(string(inputBytes))
|
||||
inputLines := strings.Split(input, "\n")
|
||||
|
||||
workQueue := make(WorkQueue, 0)
|
||||
modules, err := buildModulesFromInput(inputLines, &workQueue)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("could not parse modules: %s", err))
|
||||
}
|
||||
|
||||
fmt.Printf("Part 1: %d\n", part1(modules, &workQueue))
|
||||
}
|
||||
|
||||
func part1(modules map[string]PulseHandler, workQueue *WorkQueue) int {
|
||||
button := NewBroadcaster("button", []PulseHandler{
|
||||
NewShimmedModule("broadcaster", modules, workQueue),
|
||||
})
|
||||
|
||||
pulseCounts := map[Pulse]int{}
|
||||
for i := 0; i < 1000; i++ {
|
||||
button.HandlePulse(nil, PulseLow)
|
||||
|
||||
for len(*workQueue) > 0 {
|
||||
work := workQueue.Pop()
|
||||
pulseCounts[work.pulse]++
|
||||
work.sendPulse()
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
fmt.Println(pulseCounts)
|
||||
total := 1
|
||||
for _, n := range pulseCounts {
|
||||
total *= n
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
func buildModulesFromInput(inputLines []string, pulseQueue *WorkQueue) (map[string]PulseHandler, error) {
|
||||
parsedModules, err := tryParse(inputLines, parseModule)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse modules: %w", err)
|
||||
}
|
||||
|
||||
return buildModules(parsedModules, pulseQueue), nil
|
||||
}
|
||||
|
||||
func parseModule(inputLine string) (ParsedModule, error) {
|
||||
modulePattern := regexp.MustCompile(`^([%&]?)([a-z]+) -> ((?:[a-z]+(?:, )?)+)$`)
|
||||
matches := modulePattern.FindStringSubmatch(inputLine)
|
||||
if matches == nil {
|
||||
return ParsedModule{}, errors.New("malformed module")
|
||||
}
|
||||
|
||||
name := matches[2]
|
||||
children := strings.Split(matches[3], ", ")
|
||||
moduleKind, err := moduleKindFromPrefix(matches[1])
|
||||
if err != nil {
|
||||
return ParsedModule{}, fmt.Errorf("module kind: %w", err)
|
||||
} else if moduleKind == ParsedModuleBroadcaster && name != BroadcasterName {
|
||||
return ParsedModule{}, errors.New("got unexpected broadcaster with different name")
|
||||
}
|
||||
|
||||
return ParsedModule{
|
||||
Name: name,
|
||||
Children: children,
|
||||
Kind: moduleKind,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildModules(parsed []ParsedModule, pulseQueue *WorkQueue) map[string]PulseHandler {
|
||||
registry := make(map[string]PulseHandler, len(parsed))
|
||||
for _, module := range parsed {
|
||||
childModules := makeLookupModules(registry, module.Children, pulseQueue)
|
||||
switch module.Kind {
|
||||
case ParsedModuleBroadcaster:
|
||||
registry[module.Name] = NewBroadcaster(module.Name, childModules)
|
||||
case ParsedModuleFlipFlop:
|
||||
registry[module.Name] = NewFlipFlop(module.Name, childModules)
|
||||
case ParsedModuleConjunction:
|
||||
parentModuleNames := findParentModules(parsed, module.Name)
|
||||
parentModules := makeLookupModules(registry, parentModuleNames, pulseQueue)
|
||||
registry[module.Name] = NewConjunction(module.Name, parentModules, childModules)
|
||||
}
|
||||
}
|
||||
|
||||
return registry
|
||||
}
|
||||
|
||||
func findParentModules(parsed []ParsedModule, desiredModule string) []string {
|
||||
parents := []string{}
|
||||
for _, module := range parsed {
|
||||
moduleIdx := slices.Index(module.Children, desiredModule)
|
||||
if moduleIdx != -1 {
|
||||
parents = append(parents, module.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return parents
|
||||
}
|
||||
|
||||
func makeLookupModules(registry map[string]PulseHandler, childNames []string, pulseQueue *WorkQueue) []PulseHandler {
|
||||
res := []PulseHandler{}
|
||||
for _, childName := range childNames {
|
||||
handler := NewShimmedModule(childName, registry, pulseQueue)
|
||||
res = append(res, handler)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func moduleKindFromPrefix(prefix string) (ParsedModuleKind, error) {
|
||||
switch prefix {
|
||||
case "":
|
||||
return ParsedModuleBroadcaster, nil
|
||||
case "%":
|
||||
return ParsedModuleFlipFlop, nil
|
||||
case "&":
|
||||
return ParsedModuleConjunction, nil
|
||||
default:
|
||||
return ParsedModuleBroadcaster, fmt.Errorf("invalid module prefix %q", prefix)
|
||||
}
|
||||
}
|
||||
|
||||
func broadcastPulse(source PulseHandler, recipients []PulseHandler, pulse Pulse) {
|
||||
for _, child := range recipients {
|
||||
fmt.Printf("%s --%s--> %s\n", source.ID(), pulse, child.ID())
|
||||
|
||||
child.HandlePulse(source, pulse)
|
||||
}
|
||||
}
|
||||
|
||||
func tryParse[T any](items []string, parse func(string) (T, error)) ([]T, error) {
|
||||
res := make([]T, 0, len(items))
|
||||
for i, item := range items {
|
||||
parsed, err := parse(item)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid item #%d: %w", i+1, err)
|
||||
}
|
||||
|
||||
res = append(res, parsed)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type FakePulseHandler struct {
|
||||
pulses []Pulse
|
||||
}
|
||||
|
||||
func (handler *FakePulseHandler) ID() string {
|
||||
return "fake"
|
||||
}
|
||||
|
||||
func (handler *FakePulseHandler) HandlePulse(_ PulseHandler, pulse Pulse) {
|
||||
handler.pulses = append(handler.pulses, pulse)
|
||||
}
|
||||
|
||||
func TestFlipFlopDoesNothingInDefaultStateWithHighPulse(t *testing.T) {
|
||||
source := NewFlipFlop("1337", []PulseHandler{})
|
||||
receiver := FakePulseHandler{}
|
||||
|
||||
flipFlop := NewFlipFlop("asdf", []PulseHandler{&receiver})
|
||||
|
||||
flipFlop.HandlePulse(source, PulseHigh)
|
||||
if len(receiver.pulses) != 0 {
|
||||
t.Fatalf("Got %d pulses, not 0", len(receiver.pulses))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlipFlopEmitsHighWhenTurningOn(t *testing.T) {
|
||||
source := NewFlipFlop("1337", []PulseHandler{})
|
||||
receiver := FakePulseHandler{}
|
||||
|
||||
flipFlop := NewFlipFlop("asdf", []PulseHandler{&receiver})
|
||||
|
||||
flipFlop.HandlePulse(source, PulseLow)
|
||||
if !slices.Equal(receiver.pulses, []Pulse{PulseHigh}) {
|
||||
t.Fatalf("Got %d pulses, not 1 high", len(receiver.pulses))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlipFlopEmitsLowWhenTurningOff(t *testing.T) {
|
||||
source := NewFlipFlop("1337", []PulseHandler{})
|
||||
receiver := FakePulseHandler{}
|
||||
|
||||
flipFlop := NewFlipFlop("asdf", []PulseHandler{&receiver})
|
||||
|
||||
flipFlop.HandlePulse(source, PulseLow)
|
||||
flipFlop.HandlePulse(source, PulseLow)
|
||||
if !slices.Equal(receiver.pulses, []Pulse{PulseHigh, PulseLow}) {
|
||||
t.Fatalf("Got %d pulses, not high then low", len(receiver.pulses))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBroadcasterSendsPulseToAllChildren(t *testing.T) {
|
||||
source := NewFlipFlop("1337", []PulseHandler{})
|
||||
receiver1 := FakePulseHandler{}
|
||||
receiver2 := FakePulseHandler{}
|
||||
receiver3 := FakePulseHandler{}
|
||||
|
||||
broadcaster := NewBroadcaster("asdf", []PulseHandler{&receiver1, &receiver2, &receiver3})
|
||||
broadcaster.HandlePulse(source, PulseHigh)
|
||||
broadcaster.HandlePulse(source, PulseLow)
|
||||
|
||||
if !slices.Equal(receiver1.pulses, []Pulse{PulseHigh, PulseLow}) {
|
||||
t.Fatalf("Got %d pulses, not high then low for receiver1", len(receiver1.pulses))
|
||||
}
|
||||
|
||||
if !slices.Equal(receiver2.pulses, []Pulse{PulseHigh, PulseLow}) {
|
||||
t.Fatalf("Got %d pulses, not high then low for receiver2", len(receiver2.pulses))
|
||||
}
|
||||
|
||||
if !slices.Equal(receiver2.pulses, []Pulse{PulseHigh, PulseLow}) {
|
||||
t.Fatalf("Got %d pulses, not high then low for receiver3", len(receiver2.pulses))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConjunctionSendsLowPulseForEveryIncompleteSource(t *testing.T) {
|
||||
source1 := NewFlipFlop("1337", []PulseHandler{})
|
||||
source2 := NewFlipFlop("1338", []PulseHandler{})
|
||||
source3 := NewFlipFlop("1339", []PulseHandler{})
|
||||
receiver := FakePulseHandler{}
|
||||
|
||||
conjunction := NewConjunction("asdf", []PulseHandler{source1, source2, source3}, []PulseHandler{&receiver})
|
||||
|
||||
conjunction.HandlePulse(source1, PulseHigh)
|
||||
conjunction.HandlePulse(source2, PulseHigh)
|
||||
|
||||
if !slices.Equal(receiver.pulses, []Pulse{PulseHigh, PulseHigh}) {
|
||||
t.Fatalf("Got %d pulses, not high x2", len(receiver.pulses))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConjunctionSendsHighPulseOnceAllSourcesHaveEmitted(t *testing.T) {
|
||||
source1 := NewFlipFlop("1337", []PulseHandler{})
|
||||
source2 := NewFlipFlop("1338", []PulseHandler{})
|
||||
source3 := NewFlipFlop("1339", []PulseHandler{})
|
||||
receiver := FakePulseHandler{}
|
||||
|
||||
conjunction := NewConjunction("asdf", []PulseHandler{source1, source2, source3}, []PulseHandler{&receiver})
|
||||
|
||||
conjunction.HandlePulse(source1, PulseHigh)
|
||||
conjunction.HandlePulse(source2, PulseHigh)
|
||||
conjunction.HandlePulse(source3, PulseHigh)
|
||||
conjunction.HandlePulse(source1, PulseHigh)
|
||||
conjunction.HandlePulse(source2, PulseHigh)
|
||||
conjunction.HandlePulse(source3, PulseHigh)
|
||||
|
||||
if !slices.Equal(receiver.pulses, []Pulse{PulseHigh, PulseHigh, PulseLow, PulseLow, PulseLow, PulseLow}) {
|
||||
t.Fatalf("Got %d pulses, not high x2 + low x4", len(receiver.pulses))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue