The file defines interaction data for card placement and implements a method to preview scoring changes on the game board. This method calculates the difference in points and coins when placing a card versus not placing it or placing other nearby cards.
using System;
using System.Collections.Generic;
using System.Linq;
using CitySim.Cards;
namespace CitySim.Match;
public sealed record PlacementInteraction(BoardCell Cell, bool Positive, bool Negative, string Detail);
public sealed partial class MatchState
{
// Compare the placement with and without each nearby building. All board changes are
// temporary and the same scoring functions used by the match supply the outcomes.
public IReadOnlyList<PlacementInteraction> PreviewInteractions(Guid owner,string cardId,int x,int y,bool replacement=false)
{
if (!InBounds(x,y) || !CardLibrary.TryGet(cardId,out var card) || card.Type == CardType.Event) return Array.Empty<PlacementInteraction>();
var old=board[x,y]; var added=new BoardCell(x,y,cardId,owner,NextPlacementOrder,Round);
var candidates=Cells().Where(c => (c.X!=x || c.Y!=y) && BoardRules.Distance(c,added)<=3).ToArray();
var result=new List<PlacementInteraction>();
Dictionary<(Guid,string),int> Outcome(bool place)
{
board[x,y]=place ? added : old;
var values=new Dictionary<(Guid,string),int>();
void Add(Guid player,string kind,int amount) { var key=(player,kind); values[key]=values.GetValueOrDefault(key)+amount; }
foreach(var entry in Payout.Calculate(this).Entries) { Add(entry.Player,"#rules.placement_interactions.coins_per_payout",entry.Coins); Add(entry.Player,"#rules.placement_interactions.vp_per_payout",entry.Vp); }
foreach(var line in FinalScoring.CardEffects(this)) Add(line.Player,"#rules.placement_interactions.vp_at_game_end",line.Vp);
if(place)
{
board[x,y]=null;
foreach(var award in PlacementScoring.Calculate(this,added).Awards) { Add(award.Player,"#rules.placement_interactions.vp_on_placement",award.Vp); Add(award.Player,"#rules.placement_interactions.coins_on_placement",award.Coins); }
board[x,y]=added;
}
return values;
}
try
{
var before=Outcome(false); var after=Outcome(true);
foreach(var candidate in candidates)
{
board[candidate.X,candidate.Y]=null;
var withoutBefore=Outcome(false); var withoutAfter=Outcome(true);
board[candidate.X,candidate.Y]=candidate;
var keys=before.Keys.Concat(after.Keys).Concat(withoutBefore.Keys).Concat(withoutAfter.Keys).Distinct();
var changes=keys.Select(k => (k.Item1,k.Item2,Amount:after.GetValueOrDefault(k)-before.GetValueOrDefault(k)-withoutAfter.GetValueOrDefault(k)+withoutBefore.GetValueOrDefault(k))).Where(v=>v.Amount!=0).ToArray();
if(changes.Length==0) continue;
var details=changes.Select(v => $"{(v.Amount>0 ? "+" : "")}{v.Amount} {v.Item2} ({(v.Item1==owner ? "you" : Players.TryGetValue(v.Item1,out var p) ? p.Name : "other")})");
result.Add(new(candidate,changes.Any(v=>v.Amount>0),changes.Any(v=>v.Amount<0),CardLibrary.Get(candidate.CardId).Name+": "+string.Join(", ",details)));
}
}
finally { board[x,y]=old; foreach(var candidate in candidates) board[candidate.X,candidate.Y]=candidate; }
return result.AsReadOnly();
}
}