01020304050607080910111213141516171819202122232425

Advent of Code

2018/15

Beverage Bandits

in C#

by encse

Having perfected their hot chocolate, the Elves have a new problem: the Goblins that live in these caves will do anything to steal it. Looks like they're here for a fight.

You scan the area, generating a map of the walls (#), open cavern (.), and starting position of every Goblin (G) and Elf (E) (your puzzle input).

Read the full puzzle.

using System;
using System.Collections.Generic;
using System.Linq;

namespace AdventOfCode.Y2018.Day15;

[ProblemName("Beverage Bandits")]
class Solution : Solver {

    public object PartOne(string input) {
        return Outcome(input, 3, 3, false).score;
    }

    public object PartTwo(string input) {
        var elfAp = 4;
        while (true) {
            var outcome = Outcome(input, 3, elfAp, false);
            if (outcome.noElfDied) {
                return outcome.score;
            }
            elfAp++;
        }
    }

    (bool noElfDied, int score) Outcome(string input, int goblinAp, int elfAp, bool tsto) {
        var game = Parse(input, goblinAp, elfAp);
        var elfCount = game.players.Count(player => player.elf);

        if (tsto) {
            Console.WriteLine(game.Tsto());
        }

        while (!game.Finished()) {
            game.Step();
            if (tsto) {
                Console.WriteLine(game.Tsto());
            }
        }

        return (game.players.Count(p => p.elf) == elfCount, game.rounds * game.players.Select(player => player.hp).Sum());
    }


    Game Parse(string input, int goblinAp, int elfAp) {
        var players = new List<Player>();
        var lines = input.Split("\n");
        var mtx = new Block[lines.Length, lines[0].Length];

        var game = new Game { mtx = mtx, players = players };

        for (var irow = 0; irow < lines.Length; irow++) {
            for (var icol = 0; icol < lines[0].Length; icol++) {
                switch (lines[irow][icol]) {
                    case '#':
                        mtx[irow, icol] = Wall.Block;
                        break;
                    case '.':
                        mtx[irow, icol] = Empty.Block;
                        break;
                    case var ch when ch == 'G' || ch == 'E':
                        var player = new Player {
                            elf = ch == 'E',
                            ap = ch == 'E' ? elfAp : goblinAp,
                            pos = (irow, icol),
                            game = game
                        };
                        players.Add(player);
                        mtx[irow, icol] = player;
                        break;
                }
            }
        }
        return game;
    }


}

class Game {
    public Block[,] mtx;
    public List<Player> players;
    public int rounds;

    private bool ValidPos((int irow, int icol) pos) =>
        pos.irow >= 0 && pos.irow < this.mtx.GetLength(0) && pos.icol >= 0 && pos.icol < this.mtx.GetLength(1);

    public Block GetBlock((int irow, int icol) pos) =>
        ValidPos(pos) ? mtx[pos.irow, pos.icol] : Wall.Block;

    public void Step() {
        var finishedBeforeEndOfRound = false;
        foreach (var player in players.OrderBy(player => player.pos).ToArray()) {
            if (player.hp > 0) {
                finishedBeforeEndOfRound |= Finished();
                player.Step();
            }
        }

        if (!finishedBeforeEndOfRound) {
            rounds++;
        }
    }

    public bool Finished() =>
        players.Where(p => p.elf).All(p => p.hp == 0) ||
        players.Where(p => !p.elf).All(p => p.hp == 0);

    public string Tsto() {

        var res = "";
        res += rounds == 0 ? "Initial:\n" : $"After round {rounds}:\n";
        for (var irow = 0; irow < mtx.GetLength(0); irow++) {
            for (var icol = 0; icol < mtx.GetLength(1); icol++) {
                res += GetBlock((irow, icol)) switch {
                    Player p when p.elf => "E",
                    Player p when !p.elf => "G",
                    Empty _ => ".",
                    Wall _ => "#",
                    _ => throw new ArgumentException()
                };
            }

            foreach (var player in players.Where(player => player.pos.irow == irow).OrderBy(player => player.pos)) {
                var ch = player.elf ? 'E' : 'G';
                res += $" {ch}{{{player.pos.irow}, {player.pos.icol}}}({player.hp})";
            }
            res += "\n";
        }
        res += "\n";
        return res;
    }
}

abstract class Block { }

class Empty : Block {
    public static readonly Empty Block = new Empty();
    private Empty() { }
}

class Wall : Block {
    public static readonly Wall Block = new Wall();
    private Wall() { }
}

class Player : Block {
    public (int irow, int icol) pos;
    public bool elf;
    public int ap = 3;
    public int hp = 200;
    public Game game;

    public bool Step() {
        if (hp <= 0) {
            return false;
        } else if (Attack()) {
            return true;
        } else if (Move()) {
            Attack();
            return true;
        } else {
            return false;
        }
    }

    private bool Move() {
        var targets = FindTargets();
        if (!targets.Any()) {
            return false;
        }
        var opponent = targets.OrderBy(a => a.target).First();
        var nextPos = targets.Where(a => a.player == opponent.player).Select(a => a.firstStep).OrderBy(_ => _).First();
        (game.mtx[nextPos.irow, nextPos.icol], game.mtx[pos.irow, pos.icol]) =
            (game.mtx[pos.irow, pos.icol], game.mtx[nextPos.irow, nextPos.icol]);
        pos = nextPos;
        return true;
    }

    private IEnumerable<(Player player, (int irow, int icol) firstStep, (int irow, int icol) target)> FindTargets() {

        var minDist = int.MaxValue;
        foreach (var (otherPlayer, firstStep, target, dist) in BlocksNextToOpponentsByDistance()) {
            if (dist > minDist) {
                break;
            } else {
                minDist = dist;
                yield return (otherPlayer, firstStep, target);
            }
        }
    }

    private IEnumerable<(Player player, (int irow, int icol) firstStep, (int irow, int icol) target, int dist)> BlocksNextToOpponentsByDistance() {
        var seen = new HashSet<(int irow, int icol)>();
        seen.Add(pos);
        var q = new Queue<((int irow, int icol) pos, (int drow, int dcol) origDir, int dist)>();

        foreach (var (drow, dcol) in new[] { (-1, 0), (0, -1), (0, 1), (1, 0) }) {
            var posT = (pos.irow + drow, pos.icol + dcol);
            q.Enqueue((posT, posT, 1));
        }

        while (q.Any()) {
            var (pos, firstStep, dist) = q.Dequeue();

            if (game.GetBlock(pos) is Empty) {
                foreach (var (drow, dcol) in new[] { (-1, 0), (0, -1), (0, 1), (1, 0) }) {
                    var posT = (pos.irow + drow, pos.icol + dcol);
                    if (!seen.Contains(posT)) {
                        seen.Add(posT);
                        q.Enqueue((posT, firstStep, dist + 1));

                        var nextBlock = game.GetBlock(posT);
                        if (nextBlock is Player) {
                            var player = nextBlock as Player;
                            if (player.elf != this.elf) {
                                yield return (player, firstStep, pos, dist);
                            }
                        }
                    }
                }
            }
        }
    }

    private bool Attack() {
        var opponents = new List<Player>();

        foreach (var (drow, dcol) in new[] { (-1, 0), (0, -1), (0, 1), (1, 0) }) {
            var posT = (this.pos.irow + drow, this.pos.icol + dcol);
            var block = game.GetBlock(posT);
            switch (block) {
                case Player otherPlayer when otherPlayer.elf != this.elf:
                    opponents.Add(otherPlayer);
                    break;
            }
        }

        if (!opponents.Any()) {
            return false;
        }
        var minHp = opponents.Select(a => a.hp).Min();
        var opponent = opponents.First(a => a.hp == minHp);
        opponent.hp -= this.ap;
        if (opponent.hp <= 0) {
            game.players.Remove(opponent);
            game.mtx[opponent.pos.irow, opponent.pos.icol] = Empty.Block;
        }
        return true;
    }

}

Please ☆ my repo if you like it!

© 2025 Advent of Code is a registered trademark in the US Images provided by Bing image creator