using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; namespace D15._1 { abstract class Unit { public int HP = 200; public int Atk = 3; public (int X, int Y) Coord; public bool IsHealthy = true; public void Move((int mx, int my) mv) => Coord = (Coord.X + mv.mx, Coord.Y + mv.my); public void Attack(Unit target) { target.ReceiveAttack(Atk); } private void ReceiveAttack(int atk) { HP -= atk; if (HP <= 0) IsHealthy = false; } } class Gob : Unit { } class Elf : Unit { } class Map : HashSet<(int x, int y)> { } class Units : List { } class Program { static readonly (int mx, int my)[] MoveSequence = new [] { (0, -1), (-1, 0), (1, 0), (0, 1) }; static int round = 0; static bool PRINT = true; static void Main(string[] args) { if (args.Length < 1) return; if (File.Exists(args[0]) == false) return; var file = File.OpenText(args[0]); var map = new Map(); var strMap = new List(); var units = new Units(); FillMap(file, map, strMap, units); if (PRINT) PrintMap(map, strMap, units, 0); bool continueCombat = true; do { continueCombat = Tick(map, units); if (continueCombat) round++; if (PRINT) PrintMap(map, strMap, units, round); } while (continueCombat); TheAnswerIs(units, round); } private static void PrintMap(Map map, List strMap, Units units, int round) { Console.SetCursorPosition(0, 0); if (round > 0) Console.WriteLine($"Playing round {round + 1} "); else Console.WriteLine($"STARTING SIMULATION"); for (int y = 0; y < strMap.Count; ++y) { var line = strMap[y]; List ul = new List(); for (var x = 0; x < line.Length; ++x) { if (!map.Contains((x, y))) Console.Write("#"); else { var u = units.FirstOrDefault(un => un.Coord == (x, y)); if (u?.IsHealthy == false) u = null; if (u != null) ul.Add(u); if (u != null && u is Gob) Console.Write("G"); else if (u != null && u is Elf) Console.Write("E"); else Console.Write("."); } } if (ul.Count > 0) Console.Write($"\t{ string.Join(", ", ul.Select(u => $"{ (u is Gob ? 'G' : 'E') }({u.HP})")) }"); else for (var i = 0; i < 180; ++i) Console.Write(" "); Console.WriteLine(); } } private static void TheAnswerIs(Units units, int round) { var hitPoints = 0; foreach (var unit in units) { Console.WriteLine($"Fighter {unit.GetType().Name} : { (unit.IsHealthy ? $"{unit.HP}HP" : $" - DEAD - ({unit.HP})") }"); if (unit.IsHealthy) hitPoints += unit.HP; } Console.WriteLine($"\nCombat ends after { round } rounds with { hitPoints } remaining HP"); Console.WriteLine($"The answer is : { hitPoints * round }\n"); } private static bool Tick(Map map, Units units) { units.Sort((a, b) => { if (a.Coord.Y == b.Coord.Y) return a.Coord.X - b.Coord.X; return a.Coord.Y - b.Coord.Y; }); for (int i = 0; i < units.Count; ++i) { var unit = units[i]; if (unit.IsHealthy == false) continue; List filtered = FilterOnHealthyFoes(units, unit); if (filtered == null || filtered.Count == 0) return false; var inRange = new Map(); var unitNeedsToMove = GetTilesInRange(map, inRange, unit, filtered); if (unitNeedsToMove) MoveUnit(map, units, unit, inRange); var targetHasDied = AttackNearestTarget(unit, filtered); // If last target has died we end up the simulation if (targetHasDied && filtered.Count == 1) { bool isFullRound = true; for (var j = i + 1; j < units.Count; ++j) { if (units[j].IsHealthy == false) continue; isFullRound = false; break; } return isFullRound; } } return true; } private static bool AttackNearestTarget(Unit unit, List filtered) { var target = GetTarget(unit, filtered); if (target != null) { unit.Attack(target); if (target.IsHealthy == false) return true; } return false; } private static Unit GetTarget(Unit unit, List filtered) { Unit minHpTarget = null; foreach (var mv in MoveSequence) { (int x, int y) targetc = (unit.Coord.X + mv.mx, unit.Coord.Y + mv.my); var target = filtered.FirstOrDefault(f => f.Coord == targetc); if (target != default && (minHpTarget == null || target.HP < minHpTarget.HP)) minHpTarget = target; } return minHpTarget; } private static void MoveUnit(Map map, Units units, Unit unit, Map inRange) { var inRangeActions = new Dictionary<(int x, int y), List<(int x, int y)>>(); Parallel.ForEach(inRange, (r) => { GetBreadthFirstSearch(unit, map, units, inRangeActions, r); }); if (inRangeActions.Count == 0) return; var shortest = inRangeActions .OrderBy(ir => ir.Value.Count) .FirstOrDefault(); unit.Move(shortest.Value.First()); } // https://en.wikipedia.org/wiki/Breadth-first_search private static void GetBreadthFirstSearch(Unit unit, Map map, Units units, Dictionary<(int x, int y), List<(int x, int y)>> inRangeActions, (int x, int y) root) { var nodesToVisit = new Queue<(int x, int y)>(); var visitedNodes = new HashSet<(int x, int y)>(); var meta = new Dictionary<(int x, int y), (int x, int y)>() { { unit.Coord, (0, 0) } }; nodesToVisit.Enqueue(unit.Coord); while (nodesToVisit.Count > 0) { var node = nodesToVisit.Dequeue(); // Found it! if (node == root) { GetActionList(inRangeActions, root, meta, node); break; } foreach (var mv in MoveSequence) { (int x, int y) successor = (node.x + mv.mx, node.y + mv.my); // Continue if successor is not a valid tile if (map.Contains(successor) == false) continue; if (units.FirstOrDefault(u => u.IsHealthy && u.Coord == successor) != default) continue; if (visitedNodes.Contains(successor)) continue; if (nodesToVisit.Contains(successor) == false) { meta.TryAdd(successor, mv); nodesToVisit.Enqueue(successor); } } visitedNodes.Add(node); } } private static void GetActionList(Dictionary<(int x, int y), List<(int x, int y)>> inRangeActions, (int x, int y) root, Dictionary<(int x, int y), (int x, int y)> meta, (int x, int y) node) { var actionList = new List<(int x, int y)>(); while (meta[node] != (0, 0)) { var action = meta[node]; node = (node.x - action.x, node.y - action.y); actionList.Add(action); } actionList.Reverse(); lock (inRangeActions) inRangeActions.Add(root, actionList); } private static bool GetTilesInRange(Map map, Map inRange, Unit unit, List filtered) { foreach (var foe in filtered) { foreach (var move in MoveSequence) { (int x, int y) nc = (foe.Coord.X + move.mx, foe.Coord.Y + move.my); // Unit has no need to move if (nc.x == unit.Coord.X && nc.y == unit.Coord.Y) return false; if (map.Contains(nc)) inRange.Add(nc); } } return inRange.Count > 0; } private static List FilterOnHealthyFoes(Units units, Unit unit) { List filtered = null; switch (unit) { case var un when un is Gob: filtered = units.Where(u => u is Elf && u.IsHealthy).ToList(); break; case var un when un is Elf: filtered = units.Where(u => u is Gob && u.IsHealthy).ToList(); break; } return filtered; } private static void FillMap(StreamReader file, Map map, List strMap, Units units) { int y = 0; do { var line = file.ReadLine(); if (line == null) break; strMap.Add(line); for (int x = 0; x < line.Length; ++x) { (int x, int y) coord = (x, y); if (line[x] == 'G') units.Add(new Gob() { Coord = coord }); if (line[x] == 'E') units.Add(new Elf() { Coord = coord }); if (line[x] != '#') map.Add(coord); } y++; } while (true); } } }