2021-24, yay a whole day staring at assembly and reverse engineering it to hard code it -_-

This commit is contained in:
alexchao26
2021-12-26 23:03:02 -05:00
parent dd88e03f2c
commit dfe5ee9343
2 changed files with 258 additions and 0 deletions
+224
View File
@@ -0,0 +1,224 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strconv"
"strings"
"github.com/alexchao26/advent-of-code-go/cast"
"github.com/alexchao26/advent-of-code-go/util"
)
//go:embed input.txt
var input string
func init() {
// do this in init (not main) so test file has same input
input = strings.TrimRight(input, "\n")
if len(input) == 0 {
panic("empty input.txt file")
}
}
func main() {
var part int
flag.IntVar(&part, "part", 1, "part 1 or 2")
flag.Parse()
fmt.Println("Running part", part)
ans := aluDay24(input, part)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
}
func aluDay24(input string, part int) int64 {
instLines := strings.Split(input, "\n")
/*
"Compiled" the assembly to understand that it was 14 separate "programs"
one for each input character.
z is the only stateful variable, doing some algebra shows that this is
basically a stack pairing a=1 and a=26
determining the relationships between the input characters reduces the
search size, so it can be brute forced
*/
/*
NOTE: this does not need to be looped to find the largest and smallest
because the differences are all known so it's just a matter of finding
the largest or smallest digits from the start
w0 + 4 = w13 -> max w0 is 5, so w13 = 9
*/
// w10 + 8 = w11
// w5 - 8 = w6
// do not need to loop these bc they are 8 apart so only one possibility
w11, w10 := 9, 1
w5, w6 := 9, 1
var largest, smallest int64 = 0, 99999999999999
// w0 + 4 = w13
for w13, w0 := 9, 5; w0 >= 1; w13, w0 = w13-1, w0-1 {
// w1 - 6 = w12
for w12, w1 := 3, 9; w12 >= 1; w12, w1 = w12-1, w1-1 {
// w3 - 3 = w4
for w3, w4 := 9, 6; w4 >= 1; w3, w4 = w3-1, w4-1 {
// w2 - 1 = w9
for w2, w9 := 9, 8; w9 >= 1; w2, w9 = w2-1, w9-1 {
// w7 + 7 = w8
for w7, w8 := 2, 9; w7 >= 1; w7, w8 = w7-1, w8-1 {
str := fmt.Sprintf("%d%d%d%d%d%d%d%d%d%d%d%d%d%d",
w0, w1, w2, w3, w4, w5, w6,
w7, w8, w9, w10, w11, w12, w13,
)
num, err := strconv.ParseInt(str, 10, 64)
if err != nil {
panic("parseint" + err.Error())
}
// check against hardcoded alu and actual alu implementation
lastZ := hardcodedALU(num)
lastZ2 := runALU(num, instLines)
if lastZ == 0 && lastZ2 == 0 {
if num > largest {
largest = num
}
if num < smallest {
smallest = num
}
}
}
}
}
}
}
if part == 1 {
return largest
}
return smallest
}
// a literal implementation of the ALU, useful for checking the final answer?
func runALU(model int64, instLines []string) (z int64) {
registers := map[string]int64{}
// ALU will only have w,x,y,z keys
// sub model numbers to test are 14 digit numbers (1-9 ONLY)
// uses 14 separate inp instructions, each a single digit of the model number (starting from left)
// 1357... has inputs 1, 3, 5, 7...
// model number is valid IF variable z == 0
modelNumberIndex := 0
modelString := fmt.Sprint(model)
if len(modelString) != 14 {
panic("model string should be 14 characters: " + modelString)
}
for _, line := range instLines {
parts := strings.Split(line, " ")
var bVal int64
if len(parts) == 3 {
maybeInt64, err := strconv.Atoi(parts[2]) // could use a regexp too...
if err != nil {
// parts[2] is not a number, look up its register
bVal = registers[parts[2]]
} else {
bVal = int64(maybeInt64)
}
}
switch parts[0] {
case "inp":
val := cast.ToInt(modelString[modelNumberIndex : modelNumberIndex+1])
modelNumberIndex++
registers[parts[1]] = int64(val)
case "add":
a := parts[1]
registers[a] += bVal
case "mul":
a := parts[1]
registers[a] *= bVal
case "div":
a := parts[1]
registers[a] /= bVal
case "mod":
a := parts[1]
registers[a] %= bVal
case "eql":
a := parts[1]
if registers[a] == bVal {
registers[a] = 1
} else {
registers[a] = 0
}
default:
panic("unexpected command type " + parts[0])
}
}
return registers["z"]
}
func hardcodedALU(model int64) (z int64) {
registers := map[string]int64{}
modelChars := strings.Split(fmt.Sprint(model), "")
if len(modelChars) != 14 {
panic(fmt.Sprintf("expected 14 digit number, got %d", len(modelChars)))
}
// all 14 steps are the same except for 3 values
/*
inp w
mul x 0
add x z
mod x 26
div z 1 <--
add x 14 <--
eql x w
eql x 0
mul y 0
add y 25
mul y x
add y 1
mul z y
mul y 0
add y w
add y 16 <--
mul y x
add z y
*/
// div z, add x, add y
differentVals := [][3]int{
{1, 14, 16}, // 0
{1, 11, 3}, // 1
{1, 12, 2}, // 2
{1, 11, 7}, // 3
{26, -10, 13}, // 4
{1, 15, 6}, // 5
{26, -14, 10}, // 6
{1, 10, 11}, // 7
{26, -4, 6}, // 8
{26, -3, 5}, // 9
{1, 13, 11}, // 10
{26, -3, 4}, // 11
{26, -9, 4}, // 12
{26, -12, 6}, // 13
}
for i, char := range modelChars {
inp := int64(cast.ToInt(char))
x := registers["z"]%26 + int64(differentVals[i][1])
// always divided by 1 or 26... 1 is a no-op
registers["z"] /= int64(differentVals[i][0])
if x != inp {
registers["z"] = 26*registers["z"] + (inp + int64(differentVals[i][2]))
}
}
return registers["z"]
}
+34
View File
@@ -0,0 +1,34 @@
package main
import (
"testing"
)
func Test_aluDay24(t *testing.T) {
tests := []struct {
name string
input string
part int
want int64
}{
{
name: "actual",
input: input,
part: 1,
want: 59996912981939,
},
{
name: "actual",
input: input,
part: 2,
want: 17241911811915,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := aluDay24(tt.input, tt.part); got != tt.want {
t.Errorf("part1() = %v, want %v", got, tt.want)
}
})
}
}