|
8 | 8 | #ifndef DAY15_HPP_7IJC3NYE |
9 | 9 | #define DAY15_HPP_7IJC3NYE |
10 | 10 |
|
11 | | -#include "ds/grid.hpp" // for Grid |
12 | | -#include "lib.hpp" // for Pos, Delta, AbsDirection, DEBUG, FAST |
13 | | -#include <cassert> // for assert |
14 | | -#include <iostream> // for istream |
15 | | -#include <optional> // for optional |
16 | | -#include <string> // for string, getline |
17 | | -#include <utility> // for swap, move, pair |
18 | | -#include <vector> // for vector |
| 11 | +#include "ds/grid.hpp" // for Grid |
| 12 | +#include "unit_test/pretty_print.hpp" // for repr |
| 13 | + |
| 14 | +#include "lib.hpp" // for Pos, Delta, AbsDirection, DEBUG, FAST |
| 15 | +#include <cassert> // for assert |
| 16 | +#include <functional> // for hash (unordered_set) |
| 17 | +#include <iostream> // for istream |
| 18 | +#include <optional> // for optional |
| 19 | +#include <string> // for string, getline |
| 20 | +#include <unordered_set> // for unordered_set |
| 21 | +#include <utility> // for swap, move, pair |
| 22 | +#include <vector> // for vector |
19 | 23 |
|
20 | 24 | namespace aoc::day15 { |
21 | 25 |
|
22 | 26 | class Warehouse { |
23 | 27 | aoc::ds::Grid<char> grid; |
24 | | - Pos robot_pos; |
| 28 | + Pos robot_pos{-1, -1}; |
| 29 | + |
| 30 | + Warehouse(int width, int height) : grid(width, height) {} |
25 | 31 |
|
26 | | - std::optional<Pos> find_empty_tile(Pos pos, AbsDirection dir) const; |
| 32 | + std::optional<std::vector<Pos>> try_move(const Pos &pos, AbsDirection dir, |
| 33 | + bool wide_box = false) const; |
27 | 34 |
|
28 | 35 | public: |
29 | 36 | explicit Warehouse(const std::vector<std::string> &lines); |
| 37 | + Warehouse widen() const; |
| 38 | + |
30 | 39 | void move_robot(AbsDirection dir); |
31 | 40 | int gps_sum() const; |
32 | 41 |
|
33 | 42 | friend std::ostream &operator<<(std::ostream &, const Warehouse &); |
34 | 43 | }; |
35 | 44 |
|
36 | | -Warehouse::Warehouse(const std::vector<std::string> &lines) |
37 | | - : grid(lines), robot_pos(-1, -1) { |
| 45 | +Warehouse::Warehouse(const std::vector<std::string> &lines) : grid(lines) { |
38 | 46 | grid.for_each([this](char tile, const Pos &pos) { |
39 | 47 | if (tile == '@') { |
40 | 48 | this->robot_pos = pos; |
41 | 49 | } |
42 | 50 | }); |
43 | 51 | } |
44 | 52 |
|
| 53 | +Warehouse Warehouse::widen() const { |
| 54 | + Warehouse wider(grid.width * 2, grid.height); |
| 55 | + grid.for_each([&wider](char tile, const Pos &pos) { |
| 56 | + char &wider_left = wider.grid.at_unchecked(pos.x * 2, pos.y); |
| 57 | + char &wider_right = wider.grid.at_unchecked(pos.x * 2 + 1, pos.y); |
| 58 | + switch (tile) { |
| 59 | + case '#': |
| 60 | + wider_left = '#'; |
| 61 | + wider_right = '#'; |
| 62 | + break; |
| 63 | + case 'O': |
| 64 | + wider_left = '['; |
| 65 | + wider_right = ']'; |
| 66 | + break; |
| 67 | + case '.': |
| 68 | + wider_left = '.'; |
| 69 | + wider_right = '.'; |
| 70 | + break; |
| 71 | + case '@': |
| 72 | + wider_left = '@'; |
| 73 | + wider_right = '.'; |
| 74 | + wider.robot_pos = {pos.x * 2, pos.y}; |
| 75 | + break; |
| 76 | + } |
| 77 | + }); |
| 78 | + return wider; |
| 79 | +} |
| 80 | + |
45 | 81 | /** |
46 | | - * Returns the position directly in front of the robot if it's unoccupied, the |
47 | | - * new position for a pushed box if there's one or more in the way, or an empty |
48 | | - * optional if the robot is blocked in that direction. |
| 82 | + * Returns a collection of box positions that need to be updated, in order from |
| 83 | + * furthest to nearest, or an empty optional if the robot is blocked in that |
| 84 | + * direction |
49 | 85 | */ |
50 | | -std::optional<Pos> Warehouse::find_empty_tile(Pos pos, AbsDirection dir) const { |
| 86 | +std::optional<std::vector<Pos>> |
| 87 | +Warehouse::try_move(const Pos &pos, AbsDirection dir, bool wide_box) const { |
| 88 | + using pretty_print::repr; |
51 | 89 | Delta delta{dir, true}; |
52 | | - for (pos += delta; grid.in_bounds(pos); pos += delta) { |
53 | | - switch (grid[pos]) { |
54 | | - case '.': |
55 | | - // found an empty tile |
56 | | - return pos; |
57 | | - case '#': |
58 | | - // hit a wall |
59 | | - return {}; |
| 90 | + Pos new_pos = pos + delta; |
| 91 | + if (!grid.in_bounds(new_pos)) { |
| 92 | + // move is blocked |
| 93 | + return {}; |
| 94 | + } |
| 95 | + char tile = grid[new_pos]; |
| 96 | + if (tile == '#') { |
| 97 | + // move is blocked |
| 98 | + return {}; |
| 99 | + } |
| 100 | + if (tile == '.') { |
| 101 | + // move is valid |
| 102 | + return {{}}; |
| 103 | + } |
| 104 | + if (tile == '[' || tile == ']') { |
| 105 | + // treat a wide box as two connected small boxes when moving vertically, |
| 106 | + // and recurse on each, setting the wide_box flag so we only do it once |
| 107 | + if (delta.dy != 0 && !wide_box) { |
| 108 | + Pos left_side, right_side; |
| 109 | + if (tile == '[') { |
| 110 | + left_side = pos; |
| 111 | + right_side = pos + Delta{1, 0}; |
| 112 | + } else { |
| 113 | + left_side = pos + Delta{-1, 0}; |
| 114 | + right_side = pos; |
| 115 | + } |
| 116 | + auto positions_left = try_move(left_side, dir, true); |
| 117 | + auto positions_right = try_move(right_side, dir, true); |
| 118 | + if (positions_left && positions_right) { |
| 119 | + // append all values from positions_right into positions_left |
| 120 | + positions_left->insert(positions_left->end(), |
| 121 | + positions_right->begin(), |
| 122 | + positions_right->end()); |
| 123 | + return positions_left; |
| 124 | + } else { |
| 125 | + // movement was blocked somewhere |
| 126 | + return {}; |
| 127 | + } |
| 128 | + } else { |
| 129 | + // this will handle horizontal movements as two unconnected boxes |
| 130 | + tile = 'O'; |
| 131 | + } |
| 132 | + } |
| 133 | + if (tile == 'O') { |
| 134 | + auto positions = try_move(new_pos, dir); |
| 135 | + if (positions.has_value()) { |
| 136 | + // move is valid, add this position to the update list |
| 137 | + positions->push_back(new_pos); |
60 | 138 | } |
| 139 | + return positions; |
61 | 140 | } |
62 | | - // hit the edge of the map |
63 | | - return {}; |
| 141 | + std::cerr << "unhandled tile type " << pretty_print::repr(tile) << " at " |
| 142 | + << new_pos << "\n"; |
| 143 | + assert(false); |
64 | 144 | } |
65 | 145 |
|
66 | 146 | void Warehouse::move_robot(AbsDirection dir) { |
67 | | - if (auto empty_pos = find_empty_tile(robot_pos, dir)) { |
68 | | - // new position for the robot |
69 | | - Pos new_pos = robot_pos + Delta(dir, true); |
| 147 | + if (auto positions_to_move = try_move(robot_pos, dir); |
| 148 | + positions_to_move.has_value()) { |
| 149 | + Delta delta(dir, true); |
70 | 150 | if constexpr (aoc::DEBUG) { |
71 | 151 | std::cerr << "moving robot " << dir << " from " << robot_pos |
72 | | - << " to " << new_pos; |
| 152 | + << " to " << robot_pos + delta << "; box positions: " |
| 153 | + << pretty_print::repr(*positions_to_move) << "\n"; |
73 | 154 | } |
74 | | - if (grid[new_pos] == 'O') { |
75 | | - // need to move the box to the next empty position |
76 | | - if constexpr (!aoc::FAST) { |
77 | | - assert(grid[*empty_pos] == '.'); |
| 155 | + // add the robot to the update list, after any boxes |
| 156 | + positions_to_move->push_back(robot_pos); |
| 157 | + // execute all movements, skipping duplicate positions |
| 158 | + std::unordered_set<Pos> already_moved; |
| 159 | + for (const Pos &pos : *positions_to_move) { |
| 160 | + Pos new_pos = pos + delta; |
| 161 | + bool inserted = already_moved.emplace(new_pos).second; |
| 162 | + if (inserted) { |
| 163 | + if constexpr (!aoc::FAST) { |
| 164 | + assert(grid[new_pos] == '.'); |
| 165 | + } |
| 166 | + std::swap(grid[pos], grid[new_pos]); |
78 | 167 | } |
79 | | - if constexpr (aoc::DEBUG) { |
80 | | - int box_count = (*empty_pos - new_pos).manhattan_distance(); |
81 | | - std::cerr << " and pushing " << box_count << " " |
82 | | - << (box_count > 1 ? "boxes" : "box") << " to " |
83 | | - << *empty_pos; |
84 | | - } |
85 | | - std::swap(grid[*empty_pos], grid[new_pos]); |
86 | | - } |
87 | | - // nothing in the way, just move the robot |
88 | | - if constexpr (!aoc::FAST) { |
89 | | - assert(grid[new_pos] == '.'); |
90 | 168 | } |
91 | | - std::swap(grid[robot_pos], grid[new_pos]); |
92 | | - robot_pos = new_pos; |
| 169 | + // update the robot position |
| 170 | + robot_pos += delta; |
93 | 171 | } else { |
94 | 172 | if constexpr (aoc::DEBUG) { |
95 | | - std::cerr << "blocked from moving " << dir << " at " << robot_pos; |
| 173 | + std::cerr << "blocked from moving " << dir << " at " << robot_pos |
| 174 | + << "\n"; |
96 | 175 | } |
97 | 176 | } |
98 | | - if constexpr (aoc::DEBUG) { |
99 | | - std::cerr << "\n"; |
100 | | - } |
101 | 177 | } |
102 | 178 |
|
103 | 179 | int Warehouse::gps_sum() const { |
104 | 180 | int sum = 0; |
105 | 181 | grid.for_each([&sum](char tile, const Pos &pos) { |
106 | | - if (tile == 'O') { |
| 182 | + if (tile == 'O' || tile == '[') { |
107 | 183 | sum += 100 * pos.y + pos.x; |
108 | 184 | } |
109 | 185 | }); |
|
0 commit comments