Match/HospitalChecks.cs

This static class tests the functionality of hospital emergency mechanics within a simulated CitySim match. It validates various scenarios such as invalid input rejection, distance calculations, tie resolution, and the state persistence of match rounds.

using System;
using System.Collections.Generic;
using System.Linq;

namespace CitySim.Match;

public static class HospitalChecks
{
 static readonly Guid A = new("11111111-1111-1111-1111-111111111111"), B = new("22222222-2222-2222-2222-222222222222");
 public static IReadOnlyList<string> Run()
 {
  var results = new List<string>();
  void Check(bool ok,string name) { if (!ok) throw new InvalidOperationException(name); results.Add("PASS " + name); }
  (MatchState,RoundMachine) Board()
  {
   var state=new MatchState(12,12,1729,new MatchSettings { EnableAuctions=false, EnableGoals=false, EnableContracts=false, DefaultStreets=0, DefaultCommons=0 });
   state.AddPlayer(A,"A"); state.AddPlayer(B,"B");
   for (var x=0;x<12;x++) state.SeedStreet(x,5);
   state.GiveCard(B,"grant"); // Keep Build open for midpoint emergency checks.
   var rounds=new RoundMachine(state); rounds.Start(0); rounds.Ready(A,0); return (state,rounds);
  }
  void Place(MatchState state,Guid owner,string id,int x,int y)
  {
   if (!state.GiveCard(owner,id) || !state.TryPlace(owner,id,x,y).Success) throw new InvalidOperationException("Hospital test placement: "+id);
  }
  foreach (var invalid in new[] { -0.01, 1.01, double.NaN, double.PositiveInfinity })
  {
   var rejected=false; try { _=new MatchState(2,2,0,new MatchSettings { HospitalEmergencyFraction=invalid }); } catch (ArgumentException) { rejected=true; }
   Check(rejected,"invalid hospital schedule fraction rejected");
  }
  var (none,nr)=Board(); HospitalEmergencyResult empty=null; var count=0;
  none.Events.HospitalEmergency += e => { empty=e; count++; };
  nr.Tick(20); nr.Tick(20); Check(count==1 && empty.House==null && !empty.Answered,"no homes attempts once without a draw");
  var (state,r)=Board(); Place(state,A,"house",5,4); Place(state,B,"hospital",2,4); Place(state,A,"hospital",8,4);
  HospitalEmergencyResult notice=null; state.Events.HospitalEmergency += e=>notice=e;
  var before=state.Players[B].Vp; r.Tick(19); Check(notice==null,"emergency waits until midpoint"); r.Tick(20);
  Check(notice.Answered && notice.House.X==5 && notice.Hospital.X==2 && state.Players[B].Vp==before+notice.Award && notice.Award==CitySim.Cards.CardLibrary.Get("hospital").Effects.Single(e=>e.Rule=="hospital-emergency").Amount,"equal-distance hospital tie chooses oldest and awards its owner");
  r.Tick(40); r.Tick(40); Check(state.Players[B].Vp==before+1,"deadline cannot repeat emergency");
  var (far,fr)=Board(); Place(far,A,"cottage",1,1); Place(far,B,"hospital",9,4);
  HospitalEmergencyResult unanswered=null; far.Events.HospitalEmergency += e=>unanswered=e; fr.Ready(A,1); fr.Ready(B,1);
  Check(unanswered.House.CardId=="cottage" && !unanswered.Answered && unanswered.Hospital==null,"early ready attempts unanswered residential emergency");
  var (diag,dr)=Board(); Place(diag,A,"cottage",1,1); Place(diag,B,"hospital",5,4);
  HospitalEmergencyResult diagonal=null; diag.Events.HospitalEmergency += e=>diagonal=e; dr.Tick(20);
  Check(diagonal.Answered && BoardRules.Distance(diagonal.House,diagonal.Hospital)==4,"range uses Chebyshev distance including diagonals");
  var (source,sr)=Board(); Place(source,A,"house",2,4); Place(source,A,"house",4,4); Place(source,B,"hospital",6,4);
  var beforeCheckpoint=source.Capture(); var roundCheckpoint=sr.Capture();
  var restored=MatchState.Restore(beforeCheckpoint); var rr=RoundMachine.Restore(restored,roundCheckpoint);
  HospitalEmergencyResult first=null,second=null; source.Events.HospitalEmergency += e=>first=e; restored.Events.HospitalEmergency += e=>second=e;
  sr.Tick(20); rr.Tick(20);
  Check(first==second && sr.Capture().RandomState==rr.Capture().RandomState,"checkpoint preserves next emergency draw and RNG state");
  var post=MatchState.Restore(source.Capture()); var postRound=RoundMachine.Restore(post,sr.Capture()); var repeats=0; post.Events.HospitalEmergency += _=>repeats++; postRound.Tick(30); postRound.Tick(40);
  Check(repeats==0,"checkpoint after attempt does not replay emergency");
  var json=System.Text.Json.JsonSerializer.Serialize(first); Check(!json.Contains("Hand") && !json.Contains("Seed") && first.Round==1,"public result exposes board cells and round without hands or seed");
  return results.AsReadOnly();
 }
}