diff --git a/TODO.md b/TODO.md index ca1d1d6..d5b674f 100644 --- a/TODO.md +++ b/TODO.md @@ -2,4 +2,4 @@ * Write more unit tests * Try out clipperhouse/gen library * Try out urfave/cli library -* Get the second stars from day 14 and beyond \ No newline at end of file +* Day 14 pt2, day 15 pt2, day 16 pt2, day 17 pt1&2 \ No newline at end of file diff --git a/day-18/main.go b/day-18/main.go new file mode 100644 index 0000000..cc83c24 --- /dev/null +++ b/day-18/main.go @@ -0,0 +1,283 @@ +/* Day 18: Snailfish */ +package main + +import ( + "AoC2021/utils/args" + "AoC2021/utils/log" + "AoC2021/utils/timer" + "bufio" + "fmt" + "os" + "strconv" +) + +func check(e error) { + if e != nil { + log.Println(log.DEBUG, "Oh snap!") + panic(e) + } +} + +func main() { + argMap := args.Parse(os.Args) + log.SetLogLevelFromArgs(argMap) + selectedPart, partSpecified := argMap["part"] + + log.Println(log.NORMAL, "--- Day 18: Snailfish ---") + timer.Start() + + if !partSpecified || selectedPart == "" || selectedPart == "1" { + part1() + timer.Tick() + log.Println(log.NORMAL) + } + + if !partSpecified || selectedPart == "" || selectedPart == "2" { + part2() + timer.Tick() + log.Println(log.NORMAL) + } +} + +func part1() { + log.Println(log.NORMAL, "* Part 1 *") + log.Println(log.NORMAL, " Goal: Add up all of the snailfish numbers from the homework assignment in the order they appear.") + log.Println(log.NORMAL, " Answer: What is the magnitude of the final sum?") + + input, err := os.Open("./input.txt") + check(err) + defer input.Close() + + scanner := bufio.NewScanner(input) + lines := 0 + var s Snum + + for scanner.Scan() { + line := scanner.Text() + if lines == 0 { + s = ParseSnumStr(line) + } else { + s = AddSnum(s, ParseSnumStr(line)) + } + + // fmt.Printf("SNum #%d: %s\n", lines+1, s.ToString()) + + lines++ + if lines > 1 { + break + } + } + + log.Printf(log.NORMAL, "Read %d lines from input\n", lines) + log.Printf(log.NORMAL, "The solution is: %s\n", s.ToString()) + log.Printf(log.NORMAL, " with a magnitude of %d\n", s.CalcMagnitude()) +} + +func part2() { + log.Println(log.NORMAL, "* Part 2 *") + log.Println(log.NORMAL, " Goal: ") + log.Println(log.NORMAL, " Answer: ") +} + +type Direction int + +const ( + LEFT Direction = 0 + RIGHT Direction = 1 + UP Direction = 2 +) + +// Snailfish number +// consists of a pair of numbers +// the "left" and "right" side can consist of EITHER a normal value or another pair +type Snum struct { + lVal int + rVal int + lPtr *Snum + rPtr *Snum + parentPtr *Snum + dir Direction +} + +func ParseSnumStr(line string) Snum { + var s Snum + var sPtr = &s + log.Printf(log.DIAGNOSTIC, "Starting to parse snum line \"%s\"\n", line) + + // start on idx 1 and skip the initial open bracket + for idx := 1; idx < len(line); idx++ { + switch line[idx] { + case '[': + if sPtr.dir == LEFT { + // create a new snum on the left side + sPtr.lPtr = &Snum{parentPtr: sPtr} + sPtr.dir = RIGHT + sPtr = sPtr.lPtr + } else { + sPtr.rPtr = &Snum{parentPtr: sPtr} + sPtr = sPtr.rPtr + } + case ']': + // go up a level + sPtr.dir = LEFT + sPtr = sPtr.parentPtr + case ',': + continue + default: + // if it's not a bracket or a comma then it must be a number + endIdx := idx + 1 + for ; line[endIdx] >= '0' && line[endIdx] <= '9'; endIdx++ { + } + + strVal := line[idx:endIdx] + iVal, err := strconv.Atoi(strVal) + check(err) + + if sPtr.dir == LEFT { + sPtr.lVal = iVal + sPtr.dir = RIGHT + } else { + sPtr.rVal = iVal + } + idx = endIdx - 1 + + } + } + + return s +} + +func (s Snum) ToString() string { + str := "[" + if s.lPtr != nil { + str += s.lPtr.ToString() + } else { + str += fmt.Sprintf("%d", s.lVal) + } + str += "," + if s.rPtr != nil { + str += s.rPtr.ToString() + } else { + str += fmt.Sprintf("%d", s.rVal) + } + str += "]" + + return str +} + +func AddSnum(s1, s2 Snum) Snum { + log.Printf(log.DEBUG, " %s\n+ %s\n", s1.ToString(), s2.ToString()) + s := Snum{lPtr: &s1, rPtr: &s2} + s = s.Simplify() + log.Printf(log.DEBUG, "= %s\n", s.ToString()) + + return s +} + +func (s1 Snum) Add(s2 Snum) Snum { + log.Printf(log.DEBUG, " %s\n+ %s\n", s1.ToString(), s2.ToString()) + s := Snum{lPtr: &s1, rPtr: &s2} + s = s.Simplify() + log.Printf(log.DEBUG, "= %s\n", s.ToString()) + + return s +} + +func (s Snum) Simplify() Snum { + madeAChange := true + for depth := 0; madeAChange; { + madeAChange = false + for sPtr := &s; sPtr != nil; { + switch { + case sPtr.ShouldExplode(depth): + ExplodeValue(sPtr) + madeAChange = true + sPtr = nil + + // case sPtr.shouldSplit() + + // travel the left side + case sPtr.dir == LEFT: + sPtr.dir = RIGHT + if sPtr.lPtr != nil { + sPtr = sPtr.lPtr + depth++ + } + // travel the right side + case sPtr.dir == RIGHT: + sPtr.dir = UP + if sPtr.rPtr != nil { + sPtr = sPtr.rPtr + depth++ + } + case sPtr.dir == UP: + sPtr = sPtr.parentPtr + depth-- + } + } + } + + // check to explode + // check to split + return s +} + +func (s Snum) IsNormal() bool { + return s.lPtr == nil && s.rPtr == nil +} + +func (s Snum) ShouldExplode(depth int) bool { + return s.IsNormal() && depth > 3 +} + +func ExplodeValue(at *Snum) { + log.Printf(log.DIAGNOSTIC, "Exploding the snum at %s\n", at.ToString()) + if !at.IsNormal() { + panic("tried to explode a non-normal snum") + } + lVal, rVal := at.lVal, at.rVal + var sPtr *Snum + // explode left + log.Printf(log.NEVER, "Starting at %s\n", at.parentPtr.ToString()) + for sPtr = at.parentPtr; sPtr.parentPtr != nil && sPtr.lPtr != nil; sPtr = sPtr.parentPtr { + log.Printf(log.NEVER, " traveled left to %s\n", sPtr.ToString()) + } + if sPtr != nil { + log.Printf(log.NEVER, " added left %d to %d\n", lVal, sPtr.lVal) + sPtr.lVal += lVal + } + + // explode right + for sPtr = at.parentPtr; sPtr.parentPtr != nil && sPtr.rPtr != nil; { + log.Printf(log.NEVER, " traveled right to %s\n", sPtr.ToString()) + sPtr = sPtr.parentPtr + } + if sPtr != nil { + log.Printf(log.NEVER, " added right %d to %d\n", rVal, sPtr.rVal) + sPtr.rVal += rVal + } + + // delete yourself + pPtr := at.parentPtr + if pPtr.lPtr == at { + pPtr.lPtr = nil + } else if pPtr.rPtr == at { + pPtr.rPtr = nil + } +} + +func (s Snum) ShouldSplit() bool { + return s.lVal > 9 || s.rVal > 9 +} + +func (s Snum) CalcMagnitude() int { + lVal, rVal := s.lVal, s.rVal + if s.lPtr != nil { + lVal = s.lPtr.CalcMagnitude() + } + if s.rPtr != nil { + rVal = s.rPtr.CalcMagnitude() + } + + return 3*lVal + 2*rVal +} diff --git a/day-18/main_test.go b/day-18/main_test.go new file mode 100644 index 0000000..870c1b8 --- /dev/null +++ b/day-18/main_test.go @@ -0,0 +1,95 @@ +package main + +import ( + "AoC2021/utils/assert" + "AoC2021/utils/log" + "testing" +) + +func TestDay18_ParseSnumStr(t *testing.T) { + s1 := ParseSnumStr("[1,2]") + s2 := ParseSnumStr("[11,22]") + + assert.EqualsInt(t, s1.lVal, 1, "should parse left value") + assert.EqualsInt(t, s1.rVal, 2, "should parse right value") + + assert.EqualsInt(t, s2.lVal, 11, "should parse larger left value") + assert.EqualsInt(t, s2.rVal, 22, "should parse larger right value") +} + +func TestDay18_ToString(t *testing.T) { + s := Snum{lVal: 1, rVal: 2} + assert.EqualsStr(t, s.ToString(), "[1,2]", "should convert to string") +} + +func TestDay18_AddSnum(t *testing.T) { + s1 := Snum{lVal: 1, rVal: 2} + s2 := Snum{lVal: 2, rVal: 3} + s3 := AddSnum(s1, s2) + + assert.EqualsStr(t, s3.ToString(), "[[1,2],[2,3]]", "should add snums") +} + +func TestDay18_Add(t *testing.T) { + log.SetLogLevel(log.DIAGNOSTIC) + s := Snum{lVal: 1, rVal: 1} + s = s.Add(Snum{lVal: 2, rVal: 2}) + s = s.Add(Snum{lVal: 3, rVal: 3}) + s = s.Add(Snum{lVal: 4, rVal: 4}) + + assert.EqualsStr(t, s.ToString(), "[[[[1,1],[2,2]],[3,3]],[4,4]]", "should add snums") + + s = s.Add(Snum{lVal: 5, rVal: 5}) + assert.EqualsStr(t, s.ToString(), "[[[[3,0],[5,3]],[4,4]],[5,5]]", "should simplify after adding snums") + + s = s.Add(Snum{lVal: 6, rVal: 6}) + assert.EqualsStr(t, s.ToString(), "[[[[5,0],[7,4]],[5,5]],[6,6]]", "should simplify after adding snums") +} + +func TestDay18_Simplify(t *testing.T) { + s := ParseSnumStr("[0,[[[1,[2,3]],4],5]]") + + s = s.Simplify() + + assert.EqualsStr(t, s.ToString(), "[0,[[[3,0],7],5]]", "should convert to string") +} + +func TestDay18_IsNormal(t *testing.T) { + s1 := ParseSnumStr("[0,1]") + s2 := ParseSnumStr("[0,[[[1,[2,3]],4],5]]") + + assert.IsTrue(t, s1.IsNormal(), "should be a normal snum") + assert.IsFalse(t, s2.IsNormal(), "should NOT be a normal snum") +} + +func TestDay18_ShouldExplode(t *testing.T) { + s1 := ParseSnumStr("[0,1]") + s2 := ParseSnumStr("[0,[[[1,[2,3]],4],5]]") + + assert.IsFalse(t, s1.ShouldExplode(3), "should NOT explode a shallow normal snum") + assert.IsTrue(t, s1.ShouldExplode(6), "should explode a deep normal snum") + assert.IsFalse(t, s2.ShouldExplode(3), "should NOT explode a shallow nested snum") + assert.IsFalse(t, s2.ShouldExplode(9), "should NOT explode a deep nested snum") +} + +// func TestDay18_ExplodeValue(t *testing.T) { +// // skip +// } + +func TestDay18_ShouldSplit(t *testing.T) { + s1 := ParseSnumStr("[0,1]") + s2 := ParseSnumStr("[11,1]") + s3 := ParseSnumStr("[[1,2],13]") + + assert.IsFalse(t, s1.ShouldSplit(), "should NOT split a small normal snum") + assert.IsTrue(t, s2.ShouldSplit(), "should split a large normal snum") + assert.IsTrue(t, s3.ShouldSplit(), "should split a large nested snum") +} + +func TestDay18_CalcMagnitude(t *testing.T) { + s1 := ParseSnumStr("[3,1]") + s2 := ParseSnumStr("[1,[2,3]]") + + assert.EqualsInt(t, s1.CalcMagnitude(), 11, "should calc normal snum") + assert.EqualsInt(t, s2.CalcMagnitude(), 27, "should calc nested snum") +} diff --git a/utils/assert/assert.go b/utils/assert/assert.go index 46a621c..3df9d29 100644 --- a/utils/assert/assert.go +++ b/utils/assert/assert.go @@ -9,6 +9,13 @@ func IsTrue(t *testing.T, expr bool, message string) { } } +func IsFalse(t *testing.T, expr bool, message string) { + if expr == true { + t.Logf("Expected false but got true: %s", message) + t.Fail() + } +} + func EqualsStr(t *testing.T, actual, expected, message string) { if actual != expected { t.Logf("Expected value '%s' but got '%s': %s", expected, actual, message)