ported from perforce
This commit is contained in:
373
RobotAndDonkey.Game/Board/Board.cs
Normal file
373
RobotAndDonkey.Game/Board/Board.cs
Normal file
@@ -0,0 +1,373 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
25
RobotAndDonkey.Game/Board/Cell.cs
Normal file
25
RobotAndDonkey.Game/Board/Cell.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using RobotAndDonkey.Game.Modifiers;
|
||||
using RobotAndDonkey.Game.Pois;
|
||||
using RobotAndDonkey.Game.Utils;
|
||||
|
||||
namespace RobotAndDonkey.Game.Board;
|
||||
|
||||
public record Cell(Hex Hex) : Entity
|
||||
{
|
||||
public Cell(Cell clone)
|
||||
: base(clone)
|
||||
{
|
||||
Hex = clone.Hex;
|
||||
Type = clone.Type;
|
||||
Poi = clone.Poi?.DeepClone();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Hex={Hex}, {Type}, Poi={Poi}" + base.ToString();
|
||||
}
|
||||
|
||||
public Poi? Poi { get; set; }
|
||||
|
||||
public ECellType Type { get; set; }
|
||||
}
|
||||
11
RobotAndDonkey.Game/Board/ECellType.cs
Normal file
11
RobotAndDonkey.Game/Board/ECellType.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace RobotAndDonkey.Game.Board;
|
||||
|
||||
public enum ECellType
|
||||
{
|
||||
Grass,
|
||||
Dry,
|
||||
Fertile,
|
||||
Mud,
|
||||
Blocked,
|
||||
Rocky
|
||||
}
|
||||
Reference in New Issue
Block a user