From 50d7ec0416dedc3c54c9b350bf5592b1b8859e1c Mon Sep 17 00:00:00 2001 From: Eric Mertens Date: Sat, 26 Nov 2022 22:32:57 -0800 Subject: [PATCH] 2018-15 --- 2018/15.cpp | 494 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 386 insertions(+), 108 deletions(-) diff --git a/2018/15.cpp b/2018/15.cpp index c26c781..a1d43fc 100644 --- a/2018/15.cpp +++ b/2018/15.cpp @@ -1,12 +1,14 @@ #include +#include #include #include +#include +#include +#include #include #include #include #include -#include -#include #include @@ -16,8 +18,6 @@ using namespace aocpp; namespace { -int const initial_hp = 200; - auto IsUnit(char c) -> bool { return c == 'G' || c == 'E'; } @@ -26,183 +26,461 @@ auto IsOpposed(char x, char y) -> bool { return x == 'G' && y == 'E' || x == 'E' && y == 'G'; } -auto PickTarget( - Grid const& grid, - std::map & hp, - Coord my_coord, char my_team -) -> std::optional +struct IsBefore { + auto operator()(Coord const& a, Coord const& b) const -> bool { + return a.y < b.y || a.y == b.y && a.x < b.x; + } +}; + +int const initial_hp = 200; +Coord const reading_order[] {{0,-1}, {-1,0}, {1,0}, {0,1}}; + +struct Game { + Grid grid_; + std::map hp_; + int e_damage_; + int g_damage_; + bool no_elves_can_die; + std::ostream * debug_out_; + + Game(Grid grid) + : grid_(std::move(grid)) + , hp_{} + , e_damage_(3) + , g_damage_(3) + , no_elves_can_die{false} + , debug_out_{} + { + // set each unit to its initial hit points + grid_.each([&](auto k, auto v) { + if (IsUnit(v)) { + hp_[k] = initial_hp; + } + }); + } + + auto Simulate() -> std::optional; + auto PickTarget(Coord my_coord, char my_team) + -> std::optional::iterator>; + auto Move(Coord my_coord, char my_team) -> std::optional; +}; + +auto operator<<(std::ostream & out, Game const& game) -> std::ostream & { + for (std::size_t y = 0; y < game.grid_.rows.size(); ++y) { + out << game.grid_.rows[y]; + bool first = true; + for (std::size_t x = 0; x < game.grid_.rows[y].size(); ++x) { + auto c = game.grid_.rows[y][x]; + if (IsUnit(c)) { + if (first) { + first = false; + out << " "; + } else { + out << ", "; + } + Coord coord {std::int64_t(x),std::int64_t(y)}; + out << c << "(" << game.hp_.at(coord) << ")"; + } + } + out << std::endl; + } + return out; +} + +// Determine the best target to attack from the current position, if there is one. +auto Game::PickTarget(Coord my_coord, char my_team) +-> std::optional::iterator> { - std::optional best; + std::optional::iterator> best; int best_hp; - for (auto const dir : {Coord{0,-1}, Coord{-1,0}, Coord{1,0}, Coord{0,1}}) { //reading order + for (auto const dir : reading_order) { auto here = dir + my_coord; - if (IsOpposed(my_team, grid[here]) && - (!best || best_hp > hp[here])) { - best = here; - best_hp = hp[here]; + if (IsOpposed(my_team, grid_[here])) { + auto it = hp_.find(here); + if (!best || best_hp > it->second) { + best = it; + best_hp = it->second; + } } } return best; } -auto Before(Coord a, Coord b) { - return a.y < b.y || a.y == b.y && a.x < b.x; -} - -auto Move( - Grid const& grid, - std::map & hp, - Coord my_coord, char my_team -) -> std::optional +// Determine the best location to move to, if there is one. +auto Game::Move(Coord my_coord, char my_team) -> std::optional { - int best_distance; - std::optional best_start, best_end; - - std::map> seen; + std::optional best_start; + Coord best_end; + + std::set seen; std::deque> todo; - for (auto const dir : {Coord{0,-1}, Coord{-1,0}, Coord{1,0}, Coord{0,1}}) { + for (auto const dir : reading_order) { auto start = my_coord + dir; - todo.push_back({start,start,1}); + if (grid_[start] == '.') { + todo.push_back({start,start,1}); + } } while (!todo.empty()) { auto [start, cursor, steps] = todo.front(); todo.pop_front(); - - if (grid[cursor] != '.') { - continue; - } - if (best_start && best_distance < steps) break; - - auto [it,success] = seen.insert({cursor, {start, steps}}); - if (!success) { - if (Before(start, it->second.first)) continue; - it->second.first = start; - } - - for (auto const dir : {Coord{0,-1}, Coord{-1,0}, Coord{1,0}, Coord{0,1}}) { - auto next = cursor + dir; - if (seen.insert({next, {start, steps+1}}).second) { - todo.push_back({start, next, steps+1}); + if (best_start) { + if (best_distance < steps) break; + if (best_start && !IsBefore()(cursor, best_end)) continue; + } else { + if (!seen.insert(cursor).second) { continue; } + for (auto const dir : reading_order) { + auto const next = cursor + dir; + if (grid_[next] == '.') { + todo.push_back({start, cursor + dir, steps+1}); + } } } - - if (PickTarget(grid, hp, cursor, my_team) && - (!best_start || - steps < best_distance || - steps == best_distance && Before(cursor, *best_end) || - steps == best_distance && cursor == *best_end && Before(start, *best_start))) - { + if (PickTarget(cursor, my_team)) { + best_start = start; best_end = cursor; best_distance = steps; - best_start = start; } } return best_start; } -auto Simulate(Grid & grid) { - - // hit points for unit at coordinate - std::map hp; - - // set each unit to its initial hit points - grid.each([&](auto k, auto v) { - if (IsUnit(v)) { - hp[k] = initial_hp; - } - }); +auto Game::Simulate() -> std::optional { for (int rounds = 0;; rounds++) { - std::cout << "round " << rounds << std::endl; - for (auto const& row : grid.rows) { - std::cout << row << std::endl; + if (debug_out_) { + *debug_out_ << "After " << rounds << " rounds:" << std::endl + << *this << std::endl; } // determine order of unit updates - std::deque turn_order; - grid.each([&](auto k, auto v) { - if (IsUnit(v)) { - turn_order.push_back(k); - } - }); + std::set turn_order; + for (auto const& [k,_] : hp_) { + turn_order.insert(k); + } - while (!turn_order.empty()) { - auto active_coord = turn_order.front(); - char active_team = grid[active_coord]; - turn_order.pop_front(); + for (auto active_coord : turn_order) { + char active_team = grid_[active_coord]; // Detect when no targets remain for this unit and end the game bool game_over = true; - grid.each([&](auto k, auto v) { + grid_.each([&](auto k, auto v) { if (IsOpposed(active_team, v)) game_over = false; }); if (game_over) { int total_hp = 0; - for (auto const& [_,v] : hp) { total_hp += v; } - return rounds * total_hp; + for (auto const& [_,v] : hp_) { total_hp += v; } + return {rounds * total_hp}; } // Try to pick a target without moving - auto target = PickTarget(grid, hp, active_coord, active_team); + auto target = PickTarget(active_coord, active_team); // only move when necessary if (!target) { // move if we can - if (auto const destination = Move(grid, hp, active_coord, active_team)) { - std::swap(grid[*destination], grid[active_coord]); + if (auto const destination = Move(active_coord, active_team)) { + std::swap(grid_[*destination], grid_[active_coord]); - auto const it = hp.find(active_coord); - hp[*destination] = it->second; - hp.erase(it); + auto const it = hp_.find(active_coord); + hp_[*destination] = it->second; + hp_.erase(it); active_coord = *destination; } - target = PickTarget(grid, hp, active_coord, active_team); + target = PickTarget(active_coord, active_team); } // target in range, do the attack if (target) { + auto & [target_coord, target_hp] = **target; + auto damage = active_team == 'G' ? g_damage_ : e_damage_; - // apply damage - auto remaining = hp[*target] -= 3; + // lethal + if (target_hp <= damage) { + if (no_elves_can_die && grid_[target_coord] == 'E') return {}; - // handle death - if (remaining <= 0) { - hp.erase(*target); - - // free up location on the map - grid[*target] = '.'; - - // possibly remove dead unit from turn queue - auto it = std::find(turn_order.begin(), turn_order.end(), *target); - if (it != turn_order.end()) { - turn_order.erase(it); - } - - + grid_[target_coord] = '.'; + turn_order.erase(target_coord); + hp_.erase(*target); // invalidates target, target_coord, target_hp + } else { + target_hp -= damage; } } } } } +auto Part2(Game game) { + game.no_elves_can_die = true; + int best; + auto const attempt = [&](int const power) { + auto g = game; + g.e_damage_ = power; + return g.Simulate(); + }; + + int too_low = 3; + int high = 4; + for(;;) { + if (auto outcome = attempt(high)) { + best = *outcome; break; + } + too_low = high; + high *= 2; + } + + while (too_low+1 < high) { + auto x = std::midpoint(too_low, high); + if (auto outcome = attempt(x)) { + best = *outcome; high = x; + } else { + too_low = x; + } + } + + return std::make_pair(best, high); +} } // namespace TEST_SUITE("2018-15 examples") { - TEST_CASE("example") { + + auto case1(int expect, std::string start, std::string end) -> void { + std::istringstream in { std::move(start) }; + std::ostringstream out; + Game game {Grid::Parse(in)}; + auto outcome = game.Simulate(); + out << game; + CHECK(out.str() == end); + CHECK(outcome == std::optional{expect}); + } + + TEST_CASE("example 1") { + case1(27730, + "#######\n" + "#.G...#\n" + "#...EG#\n" + "#.#.#G#\n" + "#..G#E#\n" + "#.....#\n" + "#######\n", + "#######\n" + "#G....# G(200)\n" + "#.G...# G(131)\n" + "#.#.#G# G(59)\n" + "#...#.#\n" + "#....G# G(200)\n" + "#######\n"); + } + TEST_CASE("example 2") { + case1(36334, + "#######\n" + "#G..#E#\n" + "#E#E.E#\n" + "#G.##.#\n" + "#...#E#\n" + "#...E.#\n" + "#######\n", + "#######\n" + "#...#E# E(200)\n" + "#E#...# E(197)\n" + "#.E##.# E(185)\n" + "#E..#E# E(200), E(200)\n" + "#.....#\n" + "#######\n"); + } + TEST_CASE("example 3") { + case1(39514, + "#######\n" + "#E..EG#\n" + "#.#G.E#\n" + "#E.##E#\n" + "#G..#.#\n" + "#..E#.#\n" + "#######\n", + "#######\n" + "#.E.E.# E(164), E(197)\n" + "#.#E..# E(200)\n" + "#E.##.# E(98)\n" + "#.E.#.# E(200)\n" + "#...#.#\n" + "#######\n"); + } + TEST_CASE("example 4") { + case1(27755, + "#######\n" + "#E.G#.#\n" + "#.#G..#\n" + "#G.#.G#\n" + "#G..#.#\n" + "#...E.#\n" + "#######\n", + "#######\n" + "#G.G#.# G(200), G(98)\n" + "#.#G..# G(200)\n" + "#..#..#\n" + "#...#G# G(95)\n" + "#...G.# G(200)\n" + "#######\n"); + } + TEST_CASE("example 5") { + case1(28944, + "#######\n" + "#.E...#\n" + "#.#..G#\n" + "#.###.#\n" + "#E#G#G#\n" + "#...#G#\n" + "#######\n", + "#######\n" + "#.....#\n" + "#.#G..# G(200)\n" + "#.###.#\n" + "#.#.#.#\n" + "#G.G#G# G(98), G(38), G(200)\n" + "#######\n"); + } + TEST_CASE("example 6") { + case1(18740, + "#########\n" + "#G......#\n" + "#.E.#...#\n" + "#..##..G#\n" + "#...##..#\n" + "#...#...#\n" + "#.G...G.#\n" + "#.....G.#\n" + "#########\n", + "#########\n" + "#.G.....# G(137)\n" + "#G.G#...# G(200), G(200)\n" + "#.G##...# G(200)\n" + "#...##..#\n" + "#.G.#...# G(200)\n" + "#.......#\n" + "#.......#\n" + "#########\n" + ); + } + + auto case2(int expect, std::string start, std::string end) -> void { + std::istringstream in { std::move(start) }; + std::ostringstream out; + Game game {Grid::Parse(in)}; + auto [outcome, e_damage] = Part2(game); + game.e_damage_ = e_damage; + game.Simulate(); + out << game; + CHECK(out.str() == end); + CHECK(outcome == expect); + } + + TEST_CASE("example 7") { + case2(4988, + "#######\n" + "#.G...#\n" + "#...EG#\n" + "#.#.#G#\n" + "#..G#E#\n" + "#.....#\n" + "#######\n", + "#######\n" + "#..E..# E(158)\n" + "#...E.# E(14)\n" + "#.#.#.#\n" + "#...#.#\n" + "#.....#\n" + "#######\n" + ); + } + + TEST_CASE("example 8") { + case2(31284, + "#######\n" + "#E..EG#\n" + "#.#G.E#\n" + "#E.##E#\n" + "#G..#.#\n" + "#..E#.#\n" + "#######\n", + "#######\n" + "#.E.E.# E(200), E(23)\n" + "#.#E..# E(200)\n" + "#E.##E# E(125), E(200)\n" + "#.E.#.# E(200)\n" + "#...#.#\n" + "#######\n"); + } + + TEST_CASE("example 9") { + case2(3478, + "#######\n" + "#E.G#.#\n" + "#.#G..#\n" + "#G.#.G#\n" + "#G..#.#\n" + "#...E.#\n" + "#######\n", + "#######\n" + "#.E.#.# E(8)\n" + "#.#E..# E(86)\n" + "#..#..#\n" + "#...#.#\n" + "#.....#\n" + "#######\n"); + } + + TEST_CASE("example 10") { + case2(6474, + "#######\n" + "#.E...#\n" + "#.#..G#\n" + "#.###.#\n" + "#E#G#G#\n" + "#...#G#\n" + "#######\n", + "#######\n" + "#...E.# E(14)\n" + "#.#..E# E(152)\n" + "#.###.#\n" + "#.#.#.#\n" + "#...#.#\n" + "#######\n"); + } + + TEST_CASE("example 11") { + case2(1140, + "#########\n" + "#G......#\n" + "#.E.#...#\n" + "#..##..G#\n" + "#...##..#\n" + "#...#...#\n" + "#.G...G.#\n" + "#.....G.#\n" + "#########\n", + "#########\n" + "#.......#\n" + "#.E.#...# E(38)\n" + "#..##...#\n" + "#...##..#\n" + "#...#...#\n" + "#.......#\n" + "#.......#\n" + "#########\n"); } } auto main(int argc, char** argv) -> int { auto grid = Grid::Parse(*aocpp::Startup(argc, argv)); - auto part1 = Simulate(grid); - std::cout << "Part 1: " << part1 << std::endl; - //std::cout << "Part 2: " << Run2(std::move(input)) << std::endl; + Game game {std::move(grid)}; + //game.debug_out_ = &std::cout; + std::cout << "Part 1: " << *Game{game}.Simulate() << std::endl; + std::cout << "Part 2: " << Part2(std::move(game)).first << std::endl; }