Part of ClubCareer that completes the rest of a world cup tournament in the background. It validates the saved bracket, plans the remaining fixtures and scorelines, simulates them on a worker task, then commits results and flags completion on the main thread with retry support.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Socer;
public sealed partial class ClubCareer
{
sealed class CupWorldPlan
{
public string Stamp;
public List<ClubFixture> Games;
public readonly Dictionary<(int Round,int Home,int Away),MatchSimulation> Matches=new();
public readonly List<(ClubFixture Fixture,MatchSimulation Match)> Results=new();
}
sealed class CupWorldJob
{
public CupWorldPlan Plan;
public Task Task;
}
readonly Dictionary<string,CupWorldJob> cupWorldJobs=new();
static bool FullCupDraw(CupTournament cup)
{
var first=cup.Games.Where(f=>f.Round==1).ToArray();
return first.Length==4 && first.SelectMany(f=>new[]{f.Home,f.Away}).Order().SequenceEqual(Enumerable.Range(0,8)) &&
cup.Games.Select(f=>f.Id).Distinct().Count()==cup.Games.Count &&
cup.Games.Count(f=>f.Round==2)<=2 && cup.Games.Count(f=>f.Round==3)<=1;
}
void RestoreCupWorldCompletions()
{
foreach(var cup in Tournaments)
{
cup.WorldCompletionError??="";
bool lost=cup.Games.Any(f=>f.Played && (f.Home==0 || f.Away==0) && f.Winner!=0);
if(!lost || cup.Final is {Played:true} || !FullCupDraw(cup))continue;
// Old versions marked the whole world cup finished at our exit.
// Preserve the loss and draw; dispatch its repair only after the
// controller installs a worker, never during save deserialization.
cup.Eliminated=true;cup.Finished=false;
if(ActiveCupId==cup.Id)ActiveCupId="";
}
}
string CupWorldStamp(CupTournament cup)=>$"{ClubSeed}:{Season}:{cup.Id}:{string.Join(',',cup.ClubIds)}|"+
string.Join('|',cup.Games.OrderBy(f=>f.Id).Select(f=>$"{f.Id}:{f.Round}:{f.Home}:{f.Away}:{f.Played}:{f.Winner}:{f.HomeGoals}:{f.AwayGoals}:{f.HomePenalties}:{f.AwayPenalties}"));
static ClubFixture CupWorldCopy(ClubFixture f)=>new(){Id=f.Id,Round=f.Round,Home=f.Home,Away=f.Away,
Played=f.Played,Winner=f.Winner,HomeGoals=f.HomeGoals,AwayGoals=f.AwayGoals,Penalties=f.Penalties,
HomePenalties=f.HomePenalties,AwayPenalties=f.AwayPenalties,PlayedSequence=f.PlayedSequence};
CupWorldPlan PrepareCupWorldPlan(CupTournament cup)
{
if(!FullCupDraw(cup))throw new InvalidOperationException("The saved cup draw is incomplete; its missing entrants cannot be invented.");
var plan=new CupWorldPlan{Stamp=CupWorldStamp(cup),Games=cup.Games.OrderBy(f=>f.Round).ThenBy(f=>f.Id).Select(CupWorldCopy).ToList()};
int[] Possibilities(ClubFixture f)
{
if(f.Played)
{
if(f.Winner!=f.Home && f.Winner!=f.Away)throw new InvalidOperationException("A played cup fixture has no winner.");
return new[]{f.Winner};
}
AddSnapshot(f.Round,f.Home,f.Away);
return new[]{f.Home,f.Away};
}
void AddSnapshot(int round,int home,int away)
{
if(home==0 || away==0)throw new InvalidOperationException("World cup completion cannot play another match for your club.");
var key=(round,home,away);
if(!plan.Matches.ContainsKey(key))plan.Matches.Add(key,CreateFixtureSimulation(new(){Round=round,Home=home,Away=away},cup));
}
var candidates=plan.Games.Where(f=>f.Round==1).Select(Possibilities).ToArray();
for(int round=2;round<=3;round++)
{
var existing=plan.Games.Where(f=>f.Round==round).ToArray();
var next=new List<int[]>();
for(int i=0;i<candidates.Length;i+=2)
{
if(i/2<existing.Length)
{
var f=existing[i/2];
if(!candidates[i].Contains(f.Home) || !candidates[i+1].Contains(f.Away))
throw new InvalidOperationException("The saved cup round disagrees with its preceding winners.");
next.Add(Possibilities(f));
}
else
{
foreach(int home in candidates[i])foreach(int away in candidates[i+1])AddSnapshot(round,home,away);
next.Add(candidates[i].Concat(candidates[i+1]).ToArray());
}
}
candidates=next.ToArray();
}
return plan;
}
static void RunCupWorldPlan(CupWorldPlan plan)
{
int nextId=plan.Games.Max(f=>f.Id)+1;
var games=plan.Games.Where(f=>f.Round==1).ToArray();
for(int round=1;round<=3;round++)
{
foreach(var f in games)
{
if(f.Played)continue;
var match=plan.Matches[(f.Round,f.Home,f.Away)];
RunFixture(match);
f.Played=true;
f.Winner=match.Goals[0]>match.Goals[1]?f.Home:match.Goals[0]<match.Goals[1]?f.Away:
match.ShootoutComplete?(match.ShootoutWinner==0?f.Home:f.Away):-1;
if(f.Winner<0)throw new InvalidOperationException("A background cup match did not produce a winner.");
plan.Results.Add((f,match));
}
if(round==3)break;
var existing=plan.Games.Where(f=>f.Round==round+1).ToArray();
var next=new List<ClubFixture>();
for(int i=0;i<games.Length;i+=2)
{
ClubFixture f;
if(i/2<existing.Length)
{
f=existing[i/2];
if(f.Home!=games[i].Winner || f.Away!=games[i+1].Winner)
throw new InvalidOperationException("The saved cup round disagrees with its preceding winners.");
}
else f=new(){Id=nextId++,Round=round+1,Home=games[i].Winner,Away=games[i+1].Winner};
next.Add(f);
}
games=next.ToArray();
}
}
/// <summary>Poll/commit on the main thread. Workers own only pure match snapshots.</summary>
public bool UpdateCupCompletions()
{
bool changed=false;
foreach(var cup in Tournaments.Where(t=>t.WorldCompletionPending).ToArray())
{
if(!cupWorldJobs.TryGetValue(cup.Id,out var job))
{
if(RunBackground==null || !string.IsNullOrEmpty(cup.WorldCompletionError))continue;
try
{
var plan=PrepareCupWorldPlan(cup);
var task=RunBackground(()=>RunCupWorldPlan(plan));
if(task==null)throw new InvalidOperationException("The background cup calculation did not start.");
job=new(){Plan=plan,Task=task};cupWorldJobs.Add(cup.Id,job);
}
catch(Exception error){cup.WorldCompletionError=error.Message;changed=true;continue;}
}
if(!job.Task.IsCompleted)continue;
try
{
if(job.Task.IsCanceled)throw new InvalidOperationException("The background cup calculation was canceled.");
if(job.Task.IsFaulted)throw job.Task.Exception.GetBaseException();
if(CupWorldStamp(cup)!=job.Plan.Stamp)throw new InvalidOperationException("The cup changed during calculation. Retry its remaining matches.");
foreach(var (result,match) in job.Plan.Results)
{
var fixture=cup.Games.FirstOrDefault(f=>f.Id==result.Id);
if(fixture==null){fixture=new(){Id=result.Id,Round=result.Round,Home=result.Home,Away=result.Away};cup.Games.Add(fixture);}
if(fixture.Played)continue;
fixture.HomeGoals=match.Goals[0];fixture.AwayGoals=match.Goals[1];
fixture.Statistics=ClubMatchStatistics.From(match);
FinishFixture(fixture,true,cup,match);
}
if(cup.Final is not {Played:true})throw new InvalidOperationException("The background cup calculation has no final result.");
cup.Finished=true;cup.WorldCompletionError="";
RecordCupResults();
}
catch(Exception error){cup.WorldCompletionError=error.Message;}
cupWorldJobs.Remove(cup.Id);changed=true;
}
return changed;
}
public bool RetryCupCompletion(string cupId)
{
var cup=Tournaments.FirstOrDefault(t=>t.Id==cupId && t.WorldCompletionPending);
if(cup==null || string.IsNullOrEmpty(cup.WorldCompletionError) || cupWorldJobs.TryGetValue(cupId,out var job) && !job.Task.IsCompleted)return false;
cupWorldJobs.Remove(cupId);cup.WorldCompletionError="";
UpdateCupCompletions();return true;
}
}