Skip to content

Day 15. Warehouse Woes

Magic submarine is back and familiar lanternfish schools but the problem is different. They need help with broken robot that is supposed to move boxes around the warehouse. We have robot movements and need to predict it's final position.

This is a grid problem again.

Part One

Part one is straightforward. Box can move next box in line in the direction of movement. If there is the wall in the way, we can't move the box:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func moveRobot(grid *WhsGrid, robot Cell, move GridMove) Cell {
    next := grid.GetAnyByMove(robot.Row, robot.Col, move)
    if next.Value == EmptyChar {
        next.Value = RobotChar
        return next
    }
    if next.Value == WallChar {
        return robot
    }

    endCell := findBoxChainEnd(grid, next, move)
    if endCell.Value != EmptyChar {
        return robot
    }
    // Moving straight line of the boxes in our case can be coded as
    // "teleporting" the first box to the end of the chain. For our purposes
    // any box marker is indistinguishable from other boxes
    grid.Set(next.Row, next.Col, EmptyChar)
    grid.Set(endCell.Row, endCell.Col, BoxChar)

    next.Value = RobotChar
    return next
}

// Check next cell in the direction of the move until we hit a wall or empty cell
func findBoxChainEnd(grid *WhsGrid, startBox Cell, move GridMove) Cell {
    current, next := startBox, startBox
    for next.Value != WallChar && next.Value != EmptyChar {
        next = grid.GetAnyByMove(current.Row, current.Col, move)
        current = next
    }
    return current
}

So we simulate all the moves and calculate the required score:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
type (
    WhsGrid = Grid[rune]
    Cell    = Point[rune]
)

type WarehouseInfo struct {
    Map   [][]rune
    Moves []GridMove
    Robot Cell
}

type ParsedInput = WarehouseInfo

...

func PartOne(inp ParsedInput) int {
    robot := inp.Robot
    grid, err := NewGridFromSlices(inp.Map)
    if err != nil {
        panic(fmt.Sprintf("creating grid: %s", err))
    }

    // We can check if the move is the same, but it's quite fast as is
    for _, move := range inp.Moves {
        robot = moveRobot(grid, robot, move)
    }
    return calculateGPS(grid, BoxChar)
}

...

func calculateGPS(g *WhsGrid, boxChar rune) int {
    gps := 0
    for _, cell := range g.Cells {
        if cell.Value != boxChar {
            continue
        }
        gps += calcCellGPS(cell)
    }
    return gps
}

func calcCellGPS(cell Cell) int {
    return 100*cell.Row + cell.Col
}

Part Two

Twist of the second part is that we have another warehouse with the same robot moves, but this warehouse has twice as many columns in it's grid. So now boxes take two cells: []. Robot still moves one grid cell at a time: .@[].. => ..@[].

This complicates horizontal movement a bit:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Horizontal move is very similar to the PartOne's moveRobot, but we need to
// account for the new box size and recursively move all boxes in the chain
func moveBoxHorizontally(grid *WhsGrid, box Cell, move GridMove) bool {
    // In the end of the box chain we can decide if we can move all previous boxes
    // based on the next cell value
    if box.Value == EmptyChar || box.Value == WallChar {
        return box.Value == EmptyChar
    }
    side := toOtherBoxSide(box)
    next := grid.GetAnyByMove(box.Row, side.Col, move)
    if !moveBoxHorizontally(grid, next, move) {
        return false
    }

    grid.Set(next.Row, next.Col, side.Value)
    grid.Set(box.Row, side.Col, box.Value)
    grid.Set(box.Row, box.Col, EmptyChar)
    return true
}

Because vertical movement of the one box can now in some cases cause movement of the two boxes:

1
2
3
4
5
......       ......
......       .[][].
.[][].   ^   ..[]..
..[]..   |   ..@...
..@...       ......

We also need to account for all stacked boxes for the collision detection:

1
2
3
4
5
6
.........
..#......
..[][][].
...[][]..
....[]...
.....@...

First of all we need mechanism to determine our box stack (each part of the box separately):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// This is kind of a BFS, but with the specific neighbor finding
func findStackedParts(grid *WhsGrid, part Cell, move GridMove) []Cell {
    visited := make(map[int]struct{})

    sidePart := toOtherBoxSide(part)
    parts := make([]Cell, 0)
    queue := []Cell{part, sidePart}

    for len(queue) > 0 {
        // It is important to stack the boxes in the BFS order, layer by layer
        cur := queue[0]
        queue = queue[1:]
        key := calcCellGPS(cur)
        if _, ok := visited[key]; ok {
            continue
        }
        visited[key] = struct{}{}
        parts = append(parts, cur)

        following := findFollowingParts(grid, cur, move)
        if len(following) == 1 {
            return []Cell{}
        }

        for _, neighbor := range following {
            if _, ok := visited[calcCellGPS(neighbor)]; ok {
                continue
            }
            queue = append(queue, neighbor)
        }
    }
    return parts
}

func findFollowingParts(grid *WhsGrid, box Cell, move GridMove) []Cell {
    following := grid.GetAnyByMove(box.Row, box.Col, move)
    if following.Value == EmptyChar {
        return []Cell{}
    }
    if following.Value == WallChar {
        return []Cell{following}
    }
    // toOtherBoxSide is a helper function to get the other part of the box
    // with proper symbol and coordinates
    return []Cell{following, toOtherBoxSide(following)}
}

I think solution can be optimized with only one go through all the stack parts via recursive move function, but this one is fast and simple enough too.

With the stack of all parts found we can move them one by one in the reverse order:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func movePartVertically(grid *WhsGrid, box Cell, move GridMove) bool {
    stackedParts := findStackedParts(grid, box, move)
    if len(stackedParts) == 0 {
        return false
    }

    // It is important to move boxes from the far end of the stack
    for i := len(stackedParts) - 1; i >= 0; i-- {
        stackedPart := stackedParts[i]
        movePart(grid, stackedPart, move)
    }
    return true
}

func movePart(grid *WhsGrid, part Cell, move GridMove) {
    if part.Value != LboxChar && part.Value != RboxChar {
        panic(fmt.Sprintf("moving box %c at %d, %d", part.Value, part.Row, part.Col))
    }
    next := grid.GetAnyByMove(part.Row, part.Col, move)
    if next.Value != EmptyChar {
        panic(fmt.Sprintf("move to '%c' at %d, %d", next.Value, next.Row, next.Col))
    }

    grid.Set(next.Row, next.Col, part.Value)
    grid.Set(part.Row, part.Col, EmptyChar)
}

The main function is similar to the first part, but with choosing the correct movement function based on the move direction:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func PartTwo(inp ParsedInput) int {
    grid := expandWarehouse(inp.Map)
    // Expanded warehouse means that we need to double robot's starting column
    robot, moves := inp.Robot, inp.Moves
    robot.Col = robot.Col * 2

    for _, move := range moves {
        robot = moveRobotWide(grid, robot, move)
    }
    // Score is calculated based on the left part of the box
    return calculateGPS(grid, LboxChar)
}

func moveRobotWide(grid *WhsGrid, robot Cell, move GridMove) Cell {
    next := grid.GetAnyByMove(robot.Row, robot.Col, move)
    if next.Value == WallChar {
        return robot
    }
    if next.Value == EmptyChar || tryMoveBox(grid, next, move) {
        robot.Row, robot.Col = next.Row, next.Col
    }
    return robot
}

func tryMoveBox(grid *WhsGrid, box Cell, move GridMove) bool {
    if move == MoveLeft || move == MoveRight {
        return moveBoxHorizontally(grid, box, move)
    }
    return movePartVertically(grid, box, move)
}

Tags

  • grid
  • collision detection