This static class implements the scoring and region detection logic for park tiles within the match. It identifies contiguous park regions using a breadth-first search and awards points based on placement and growth effects defined in card rules.
using System;
using System.Collections.Generic;
using System.Linq;
using CitySim.Cards;
namespace CitySim.Match;
public sealed record RuleAward( Guid Player, int Vp, int Coins, string Reason, BoardCell Source = null );
public sealed record ParkScoreResult( BoardCell Added, int Size, int ScoringSize, IReadOnlyList<BoardCell> Cells, IReadOnlyList<RuleAward> Awards );
public static class ParkScoring
{
/// <summary>Every park card is one physical tile, including Fountain, Lake and mature Forest.</summary>
public static int SizeOf(BoardCell cell) => BoardRules.IsPark(cell) ? 1:0;
public static IReadOnlyList<BoardCell> Region(MatchState state,BoardCell origin)
{
var board = state.Cells().ToDictionary(c => (c.X,c.Y));
board[(origin.X,origin.Y)] = origin;
var visited = new HashSet<(int,int)>(); var queue = new Queue<BoardCell>(); var cells = new List<BoardCell>();
queue.Enqueue(origin);
while (queue.Count > 0)
{
var cell = queue.Dequeue();
if (!visited.Add((cell.X,cell.Y)) || SizeOf(cell) <= 0) continue;
cells.Add(cell);
foreach (var d in new[] {(0,-1),(1,0),(0,1),(-1,0)})
if (board.TryGetValue((cell.X+d.Item1,cell.Y+d.Item2),out var n) && !visited.Contains((n.X,n.Y))) queue.Enqueue(n);
}
return cells.OrderBy(c => c.Order).ToList().AsReadOnly();
}
public static ParkScoreResult OnTileAdded(MatchState state,BoardCell added,BoardCell before = null)
{
var cells = Region(state,added);
var card=CardLibrary.Get(added.CardId);
var awards = new List<RuleAward>();
if (state.Players.ContainsKey(added.Owner))
{
if (before!=null)
{
var growth=card.Effects.FirstOrDefault(e=>e.Hook==EffectHook.Growth && e.Rule=="forest");
if(growth!=null && before.Age<growth.Cap && added.Age>=growth.Cap)
awards.Add(new(added.Owner,growth.Amount,0,"#rules.park_scoring.forest_growth",added));
}
else
{
var points=card.Effects.Where(e=>e.Hook==EffectHook.Place && e.Rule=="park-score").Sum(e=>e.Amount);
if(points>0) awards.Add(new(added.Owner,points,0,"#rules.park_scoring.park_placement",added));
if(state.Neighbours(added.X,added.Y).Any(c=>CardLibrary.Get(c.CardId).Tags.HasFlag(Tag.Nature)))
{
var bonus=card.Effects.Where(e=>e.Hook==EffectHook.Place && e.Rule=="beside-nature-vp").Sum(e=>e.Amount);
if(bonus>0) awards.Add(new(added.Owner,bonus,0,"#rules.park_scoring.beside_nature",added));
}
}
}
return new(added,cells.Count,cells.Count,cells,awards.AsReadOnly());
}
public static IReadOnlyList<IReadOnlyList<BoardCell>> Regions(MatchState state)
{
var seen = new HashSet<int>(); var regions = new List<IReadOnlyList<BoardCell>>();
foreach (var cell in state.Cells().Where(c => SizeOf(c) > 0))
{
if (seen.Contains(cell.Order)) continue;
var region = Region(state,cell); foreach (var c in region) seen.Add(c.Order); regions.Add(region);
}
return regions.AsReadOnly();
}
}