373 lines
13 KiB
C#
373 lines
13 KiB
C#
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<Cell> 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<Cell>();
|
|
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<Cell> cells, List<Cell> startCells, ref SRandom random, int needed, float spread, Func<Cell, bool> callback)
|
|
{
|
|
var converted = 0;
|
|
while (converted < needed)
|
|
{
|
|
if (startCells.Count == 0)
|
|
break;
|
|
|
|
var open = new Queue<Cell>();
|
|
open.Enqueue(startCells[random.Next(startCells.Count)]);
|
|
var closed = new HashSet<Cell>();
|
|
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<Cell> cells, ref SRandom random, float intensity, Predicate<ECellType> filter, Action<Cell> 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<Cell> GetReachableFromAvatar(List<Cell> cells)
|
|
{
|
|
var avatarCell = cells.Single(c => c.Poi is Avatar);
|
|
|
|
var visited = new HashSet<Cell>();
|
|
var queue = new Queue<Cell>();
|
|
|
|
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<Cell> 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<Cell> cells, Hex start, Cell? blocked = null)
|
|
{
|
|
var visited = new HashSet<Hex>();
|
|
var queue = new Queue<Hex>();
|
|
|
|
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<Cell> cells, Cell candidate)
|
|
{
|
|
if (!IsWalkable(candidate))
|
|
return true;
|
|
|
|
var sizeWithCandidate = CountComponentSize(cells, new());
|
|
var sizeWithoutCandidate = CountComponentSize(cells, new(), candidate);
|
|
|
|
return sizeWithoutCandidate == sizeWithCandidate - 1;
|
|
}
|
|
} |