The MatchState class manages the logic for the building renovation mechanic within a game match. It validates placement rules, previews scoring effects, and executes card and board state transitions upon successful renovation.
using System;
using System.Linq;
using CitySim.Cards;
namespace CitySim.Match;
public sealed partial class MatchState
{
/// <summary>Checks a replacement against the board with the old building excluded, without emitting events.</summary>
public string RenovationError(Guid owner,int eventSlot,int replacementSlot,int x,int y)
{
if (Phase!=MatchPhase.Build) return "#rules.match_state.wait_for_the_build_phase";
if (!players.TryGetValue(owner,out var player)) return "#rules.match_state.unknown_player";
if (eventSlot<0 || eventSlot>=player.Hand.Count || player.Hand[eventSlot]!="renovation") return "#rules.match_state._renovation.select_renovation_first";
if (replacementSlot<0 || replacementSlot>=player.Hand.Count || replacementSlot==eventSlot) return "#rules.match_state._renovation.select_a_different_building_card";
var replacement=player.Hand[replacementSlot];
if (!CardLibrary.TryGet(replacement,out var card) || card.Type is CardType.Road or CardType.Event) return "#rules.match_state._renovation.choose_a_building_card";
if (!InBounds(x,y)) return "#rules.match_state.outside_the_board";
var old=board[x,y];
if (old==null || old.Owner!=owner || CardLibrary.Get(old.CardId).Type is CardType.Road or CardType.Event) return "#rules.match_state._renovation.choose_one_of_your_buildings";
if (old.CardId==replacement) return "#rules.match_state._renovation.choose_a_different_building_type";
board[x,y]=null;
try { return PlacementError(owner,replacement,x,y); }
finally { board[x,y]=old; }
}
/// <summary>Preview is pure: the target is restored even if scoring throws.</summary>
public int? PreviewRenovation(Guid owner,int eventSlot,int replacementSlot,int x,int y)
{
if (RenovationError(owner,eventSlot,replacementSlot,x,y)!=null) return null;
var old=board[x,y]; board[x,y]=null;
try { return PlacementScoring.Preview(this,players[owner].Hand[replacementSlot],x,y,owner); }
finally { board[x,y]=old; }
}
/// <summary>Replaces an owned building; prior VP is retained and both selected cards are consumed.</summary>
public PlacementResult TryRenovate(Guid owner,int eventSlot,int replacementSlot,int x,int y)
{
var error=RenovationError(owner,eventSlot,replacementSlot,x,y);
if (error!=null) return new(false,error);
var player=players[owner]; var old=board[x,y]; var id=player.Hand[replacementSlot];
var cell=new BoardCell(x,y,id,owner,NextPlacementOrder,Round); PlacementEffects effects;
board[x,y]=null;
try { effects=PlacementScoring.Calculate(this,cell); }
finally { board[x,y]=old; }
player.MutableHand.RemoveAt(Math.Max(eventSlot,replacementSlot)); player.MutableHand.RemoveAt(Math.Min(eventSlot,replacementSlot));
order++; ChangeCoins(owner,-Settings.BuildCost(CardLibrary.Get(id))); CommitPlacement(cell,effects,old);
Events.Played(new(owner,"renovation"));
return new(true,"",cell);
}
}