using System.Collections.Immutable; using System.Diagnostics; using System.Runtime.InteropServices; using RobotAndDonkey.Game.Data; using RobotAndDonkey.Game.Modifiers; using RobotAndDonkey.Game.Pois; using RobotAndDonkey.Game.Utils; namespace RobotAndDonkey.Game.Board; public record Board(ImmutableArray Cells, int TargetDeliveryAmount) { public Board(Board clone) { Cells = [.. clone.Cells.Select(c => new Cell(c))]; TargetDeliveryAmount = clone.TargetDeliveryAmount; } public int FindCellIndex(Hex hex) { // TODO calculate in O(1) with BoardSize for (var i = 0; i < Cells.Length; i++) if (Cells[i].Hex == hex) return i; return -1; } public static Board Generate(ref SRandom random, EDifficulty difficulty) { var cells = new List(); var fDifficulty = 0.5f + (int)difficulty / 4.0f; var boardSize = (int)(2 + fDifficulty * 2); // TODO: find better order of adding cells to facilitate FindCellIndex for (var x = -boardSize; x <= boardSize; x++) for (var y = -boardSize; y <= boardSize; y++) { var hex = new Hex(x, y); if (Hex.Distance(hex, new()) > boardSize) continue; var cell = new Cell(hex) { Type = ECellType.Grass }; cells.Add(cell); } var avatarCell = cells.Single(c => c.Hex is { X: 0, Y: 0 }); var avatar = new Avatar { Direction = (EDirection)random.Next(6) }; avatarCell.Poi = avatar; var freeCells = cells.ToList(); freeCells.Remove(avatarCell); for (var x = -boardSize - 1; x <= boardSize + 1; x++) for (var y = -boardSize - 1; y <= boardSize + 1; y++) { var hex = new Hex(x, y); if (Hex.Distance(hex, new()) != boardSize + 1) continue; var cell = new Cell(hex) { Type = ECellType.Grass }; cells.Add(cell); } var blockedAmount = (int)(freeCells.Count * Balancing.Instance.BlockedAmount * fDifficulty); Splat(freeCells, freeCells.Where(c => Hex.Distance(c.Hex, new()) is 1 or 2).ToList(), ref random, blockedAmount, Balancing.Instance.BlockedSpread, c => { if (!PlacementKeepsConnectivity(cells, c)) return false; c.Type = ECellType.Blocked; return true; }); Sprinkle(freeCells, ref random, Balancing.Instance.DonkeySprinkle, c => c != ECellType.Blocked, c => c.Poi = new Donkey()); Sprinkle(freeCells, ref random, Balancing.Instance.ShedSprinkle, c => c != ECellType.Blocked, c => c.Poi = new Shed()); Sprinkle(freeCells, ref random, Balancing.Instance.CrateSprinkle, c => c != ECellType.Blocked, c => c.Poi = new Crate { Amount = Balancing.Instance.CrateAmount }); Sprinkle(freeCells, ref random, Balancing.Instance.TowerSprinkle, c => c != ECellType.Blocked, c => c.Poi = new Tower()); var poiCells = freeCells.Where(c => { if (c.Poi != null) return false; for (var dir = EDirection.Right; dir <= EDirection.BottomRight; ++dir) { var neighbour = c.Hex.GetNeighbour(dir); var next = cells.FindIndex(n => n.Hex == neighbour); if (next >= 0 && cells[next].Poi != null) return true; } return false; }).ToList(); freeCells.RemoveAll(c => c.Poi != null); var goodCells = cells.Where(c => c.Type != ECellType.Blocked).ToList(); var dryAmount = (int)(goodCells.Count * Balancing.Instance.DryAmount * fDifficulty); var fertileAmount = (int)(goodCells.Count * Balancing.Instance.FertileAmount * (1 - fDifficulty)); var mudAmount = (int)(goodCells.Count * Balancing.Instance.MudAmount * fDifficulty); var rockyAmount = (int)(goodCells.Count * Balancing.Instance.RockyAmount * fDifficulty); Splat(goodCells, goodCells, ref random, dryAmount, Balancing.Instance.DrySpread, c => { c.Type = ECellType.Dry; return true; }); Splat(goodCells, goodCells, ref random, fertileAmount, Balancing.Instance.FertileSpread, c => { c.Type = ECellType.Fertile; return true; }); Splat(goodCells, goodCells, ref random, mudAmount, Balancing.Instance.MudSpread, c => { c.Type = ECellType.Mud; return true; }); Splat(goodCells, goodCells, ref random, rockyAmount, Balancing.Instance.RockySpread, c => { c.Type = ECellType.Rocky; return true; }); foreach (var cell in cells) { if (cell.Poi is Tower) { if (cell.Modifiers.All(m => m.Id != EModifierId.Unreliable)) { freeCells.Remove(cell); cell.AddModifier(new UnreliableCellModifier(EModifierDuration.Permanent), []); } var hex = cell.Hex; for (var dir = EDirection.Right; dir <= EDirection.BottomRight; ++dir) { var neighbour = hex.GetNeighbour(dir); var neighbourCell = cells.FirstOrDefault(c => c.Hex == neighbour); if (neighbourCell == null) continue; if (neighbourCell.Modifiers.All(m => m.Id != EModifierId.Unreliable)) { freeCells.Remove(neighbourCell); poiCells.Remove(neighbourCell); neighbourCell.AddModifier(new UnreliableCellModifier(EModifierDuration.Permanent), []); } } } else if (cell.Poi is Donkey donkey) donkey.Direction = (EDirection)random.Next(6); } var corruptAmount = (int)(freeCells.Count * Balancing.Instance.CorruptedAmount * fDifficulty); Splat(freeCells, poiCells, ref random, corruptAmount, Balancing.Instance.CorruptedSpread, c => { if (!PlacementKeepsConnectivity(cells, c)) return false; c.AddModifier(new CorruptCellModifier(EModifierDuration.Permanent), []); return true; }); random.Shuffle(CollectionsMarshal.AsSpan(cells)); var sheds = cells.Where(c => c.Poi is Shed).Select(c => (Shed)c.Poi!).ToArray(); var crates = cells.Where(c => c.Poi is Crate).Select(c => (Crate)c.Poi!).ToArray(); var targetDeliveryAmount = sheds.Length * Balancing.Instance.DefaultShedRequest; var totalRequests = 0; for (var i = 0; i < sheds.Length - 1; ++i) { var randomness = (random.NextSingle() - 0.5f) * Balancing.Instance.CrateShedRandomness; var request = (int)Math.Ceiling(Balancing.Instance.DefaultShedRequest * (1 + randomness)); sheds[i].Requested = request; totalRequests += request; } if (totalRequests >= targetDeliveryAmount) targetDeliveryAmount = totalRequests + 1; sheds[^1].Requested = targetDeliveryAmount - totalRequests; var targetOfferAmount = (int)((1 + Balancing.Instance.CrateOfferBonus * (1 - fDifficulty)) * targetDeliveryAmount); var defaultCrateOffer = targetOfferAmount / crates.Length; var totalOffers = 0; for (var i = 0; i < crates.Length - 1; ++i) { var randomness = (random.NextSingle() - 0.5f) * Balancing.Instance.CrateShedRandomness; var offer = (int)Math.Max(1, Math.Floor(defaultCrateOffer * (1 + randomness))); crates[i].Amount = offer; totalOffers += offer; } crates[^1].Amount = targetOfferAmount - totalOffers; if (!AllPoisReachable(cells)) throw new InvalidOperationException(); return new([.. cells], targetDeliveryAmount); } private static void Splat(List cells, List startCells, ref SRandom random, int needed, float spread, Func callback) { var converted = 0; while (converted < needed) { if (startCells.Count == 0) break; var open = new Queue(); open.Enqueue(startCells[random.Next(startCells.Count)]); var closed = new HashSet(); while (open.TryDequeue(out var cell)) { if (!closed.Add(cell)) continue; if (!callback(cell)) continue; cells.Remove(cell); startCells.Remove(cell); converted += 1; if (converted >= needed) break; var hex = cell.Hex; for (var dir = EDirection.Right; dir <= EDirection.BottomRight; ++dir) { if (random.NextSingle() < spread) continue; var neighbour = hex.GetNeighbour(dir); var next = cells.FindIndex(c => c.Hex == neighbour); if (next >= 0) open.Enqueue(cells[next]); } } } } private static void Sprinkle(List cells, ref SRandom random, float intensity, Predicate filter, Action callback) { var needed = (int)(cells.Count * intensity); var candidates = cells.Select((c, i) => (Cell: c, OriginalIndex: i)).Where(c => c.Cell.Poi == null && filter(c.Cell.Type)).ToArray(); random.Shuffle(candidates.AsSpan()); var placed = 0; for (var i = 0; i < candidates.Length && placed < needed; ++i) { var (_, originalIndex) = candidates[i]; var cell = cells[originalIndex]; if (cell.Poi != null || !filter(cell.Type)) continue; if (!PlacementKeepsConnectivity(cells, cell)) continue; callback(cell); placed++; } } private static HashSet GetReachableFromAvatar(List cells) { var avatarCell = cells.Single(c => c.Poi is Avatar); var visited = new HashSet(); var queue = new Queue(); visited.Add(avatarCell); queue.Enqueue(avatarCell); while (queue.TryDequeue(out var current)) { var hex = current.Hex; for (var dir = EDirection.Right; dir <= EDirection.BottomRight; ++dir) { var neighbourHex = hex.GetNeighbour(dir); var neighbour = cells.FirstOrDefault(c => c.Hex == neighbourHex); if (neighbour == null) continue; if (!IsWalkable(neighbour) || neighbour.Modifiers.Any(c => c.Id == EModifierId.Corrupt)) continue; if (visited.Add(neighbour)) queue.Enqueue(neighbour); } } return visited; } private static bool AllPoisReachable(List cells) { var reachable = GetReachableFromAvatar(cells); bool IsPoiAccessible(Cell poiCell) { if (reachable.Contains(poiCell)) return true; var hex = poiCell.Hex; for (var dir = EDirection.Right; dir <= EDirection.BottomRight; ++dir) { var neighbourHex = hex.GetNeighbour(dir); var neighbour = cells.FirstOrDefault(c => c.Hex == neighbourHex); if (neighbour != null && reachable.Contains(neighbour)) return true; } return false; } foreach (var poiCell in cells.Where(c => c.Poi is Donkey or Shed or Crate or Tower)) { if (!IsPoiAccessible(poiCell)) return false; } return true; } private static bool IsWalkable(Cell cell) { return cell.Type != ECellType.Blocked && cell.Poi is Avatar or null; } private static int CountComponentSize(List cells, Hex start, Cell? blocked = null) { var visited = new HashSet(); var queue = new Queue(); visited.Add(start); queue.Enqueue(start); while (queue.TryDequeue(out var current)) { var hex = current; for (var dir = EDirection.Right; dir <= EDirection.BottomRight; ++dir) { var neighbourHex = hex.GetNeighbour(dir); var neighbour = cells.FirstOrDefault(c => c.Hex == neighbourHex); if (neighbour == null) continue; if (neighbour == blocked) continue; if (neighbour.Type == ECellType.Blocked || neighbour.Modifiers.Any(m => m.Id == EModifierId.Corrupt)) continue; if (visited.Add(neighbourHex)) { if (neighbour.Poi is Avatar or null) queue.Enqueue(neighbourHex); } } } return visited.Count; } private static bool PlacementKeepsConnectivity(List cells, Cell candidate) { if (!IsWalkable(candidate)) return true; var sizeWithCandidate = CountComponentSize(cells, new()); var sizeWithoutCandidate = CountComponentSize(cells, new(), candidate); return sizeWithoutCandidate == sizeWithCandidate - 1; } }