Oh thank goodness, I needed an easy win today, and AoC gave it to me. (I've got an intermittent bug over in my day-job, depending on which of two events trigger first, I understand what's happening, but have NO ideas on how to fix it yet... Tomorrow I start scraping OTHER people's brains!)
But that's there, and this is not that! Code below the fold, as well as grumbles
So, thinking about this logically, the very first thing I did was write a direction struct, so my debugging output would be easy to read.
enum RopeDir: String {
case up = "U"
case down = "D"
case left = "L"
case right = "R"
}
The whole purpose of that guy is to abstract away positive and negative numbers, and translate it directly into the concepts of up/down/left/right for readability. printing a RopeDir will print "up" or "down" not "U" or "D". When it comes to dyslexia and positional confusion (hai, it me!), making things more explicit is worth it later on!
This made reading the input extra easy, too.
fileprivate func parseInput(_ text: String) {
let cmds = text.split(separator: "\n")
for cmd in cmds {
let cmdParts = cmd.split(separator: " ")
if let dir = RopeDir(rawValue: String(cmdParts[0])), let dist = Int(cmdParts[1]) {
commands.append((dir, dist))
} else {
print("bad directional input, either \(cmdParts[0]) is not a direction or \(cmdParts[1]) is not an int")
}
}
}
I also ended up keeping things VERY basic with my rope objects, which are simply positions with a move() function that takes a direction. They are immutable struct objects, which means that each time I change something with it, I get a new one, which let me write one forehead slapping bug, but that's all! Good job, self!
struct Coord {
var x: Int
var y: Int
init(x: Int, y: Int) {
self.x = x
self.y = y
}
func move(_ dir: RopeDir) -> Coord {
switch(dir) {
case .up:
return Coord(x: x, y: y + 1)
case .down:
return Coord(x: x, y: y - 1)
case .left:
return Coord(x: x - 1, y: y)
case .right:
return Coord(x: x + 1, y: y)
}
}
}
extension Coord: Hashable {
static func == (lhs: Coord, rhs: Coord) -> Bool {
return lhs.x == rhs.x && lhs.y == rhs.y
}
func hash(into hasher: inout Hasher) {
hasher.combine(x)
hasher.combine(y)
}
}
Comically, the example code for implementing Hashable is actually exactly the code I needed, thanks for the little boost, Apple doc writer! I wouldn't have had to bother, but I knew I wanted to use a Set to track the different places the tail had been, so Hashable was a requirement, otherwise I would probably have gone farther trying to just use a CGPoint object. "you mutated a thing after you put it in the set and now it's duplicate!" is a good error message, and told me I was making things too complex for myself early on. ABORT! ABORT!
So, the actual problem, then, is "as the head moves, where is the tail?" The problem gives us 2 rules, the first is "if the head moves more than 1 space away in a cardinal direction, the next segment follows", while the second is "if the target is more than 1 space away and NOT on a cardinal direction, the next segment moves diagonally closer". Not too bad! And in fact, the answers to p1 and pt2 are so similar, I'm not even gonna share em both. the only difference is I switched to keeping the rope in an array for sanity's sake.
func runRopeSim2() {
var rope: [Coord] = Array(repeating: Coord(x:0, y:0), count: 10)
var tailPositions: Set<Coord> = Set()
tailPositions.insert(rope[9]) // rope[9] is the tail segment
for (dir, count) in commands {
var moveCount = count
while moveCount > 0 {
// move the head
rope[0] = rope[0].move(dir)
// head is the segment whose movement triggered this segment's movement
for i in 1...9 {
let head = rope[i - 1]
let tail = rope[i]
// tail chases if necessary
let xDelta = head.x - tail.x
let yDelta = head.y - tail.y
// is the tail 2 spots away?
if abs(xDelta) >= 2 || abs(yDelta) >= 2 {
// If the head is ever two steps directly up, down, left, or right from the tail, the tail must also move one step in that direction so it remains close enough.
if xDelta == 0 {
rope[i] = rope[i].move(yDelta > 0 ? .up : .down)
} else if yDelta == 0 {
rope[i] = rope[i].move(xDelta > 0 ? .right : .left)
} else {
// Otherwise, if the head and tail aren't touching and aren't in the same row or column, the tail always moves one step diagonally to keep up.
// in order for this to happen, our deltas will have to be 2,1 or 1,2
if xDelta == -2 {
// head delta is either (-2,-1), (-2,1),
rope[i] = rope[i].move(.left)
rope[i] = rope[i].move(yDelta > 0 ? .up: .down)
} else if xDelta == 2 {
rope[i] = rope[i].move(.right)
rope[i] = rope[i].move(yDelta > 0 ? .up: .down)
//(2, -1), (2, 1)
} else if yDelta == -2 {
rope[i] = rope[i].move(.down)
rope[i] = rope[i].move(xDelta > 0 ? .right: .left)
// (-1,-2), (1, -2)
} else if yDelta == 2 {
rope[i] = rope[i].move(.up)
rope[i] = rope[i].move(xDelta > 0 ? .right: .left)
// (-1,2), (1, 2)
} else {
print("****wtf movement should we do? xDelta = \(xDelta), yDelta = \(yDelta)")
}
}
}
if (i == 9) {
print("new Tail position: \(rope[i])")
tailPositions.insert(rope[i])
}
}
moveCount -= 1
}
}
print("final tail positions: \(tailPositions)")
print("final squares occupied: \(tailPositions.count)")
}
The only really interesting bit here is where my bug lay, before I performed the afore-mentioned forehead slap. diagonal movement is two moves, one on the x-axis and one on the y-axis. I had snagged a pointer to "tail" and did each move relative to that, instead of making them consecutive on successive positions, which meant I lost that first move, it's like it had never been, and once the rope segments start diverging, things get silly, quickly. "What do you mean, yDelta = 14, the segments are supposed to stay within 1 square of each other?!?!"
Quickly sorted, and victory was mine! Code, as always, here: (https://github.com/nothes/AdventOfCode/blob/main/AoC/AoC/Day%209/day9.swift)
edit: can I just add how much I love the Array(repeating: <thing>, count: <x times>) initializer? So good. So readable. So Compact.