ported from perforce

This commit is contained in:
2026-04-19 00:43:27 +02:00
commit 6c0c33f5d4
700 changed files with 19735 additions and 0 deletions

View 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;
}
}

View 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; }
}

View File

@@ -0,0 +1,11 @@
namespace RobotAndDonkey.Game.Board;
public enum ECellType
{
Grass,
Dry,
Fertile,
Mud,
Blocked,
Rocky
}