Achievement catalogue and eligibility rules for the career mode. Holds 17 bilingual achievement definitions with scores, deduplicates archived match reports and current fixtures into canonical results, then derives which achievement ids a given ClubCareer has earned.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Socer;
public sealed record PlatformAchievementDefinition(string Id,string EnglishTitle,string RussianTitle,
string EnglishDescription,string RussianDescription,int Score);
// Pure eligibility rules. The platform adapter decides whether the current
// session may submit anything; inspecting a career never changes it or pays prizes.
public static class PlatformAchievementRules
{
const int CatalogVersion=2;
static int loadedCatalogVersion;
static IReadOnlyList<PlatformAchievementDefinition> definitions;
// Hotload preserves static initializers. Rebuild the catalogue when its
// version changes so an already-open editor sees the same rules as a fresh client.
public static IReadOnlyList<PlatformAchievementDefinition> All
{
get
{
if(definitions==null || loadedCatalogVersion!=CatalogVersion)
{definitions=CreateDefinitions();loadedCatalogVersion=CatalogVersion;}
return definitions;
}
}
static IReadOnlyList<PlatformAchievementDefinition> CreateDefinitions()=>Array.AsReadOnly(new PlatformAchievementDefinition[]
{
new PlatformAchievementDefinition("first-match","First whistle","Первый свисток","Finish a competitive career match.","Завершите официальный матч карьеры.",5),
new("first-win","Off the mark","Первая победа","Win a competitive career match, including a penalty shootout.","Выиграйте официальный матч карьеры, в том числе по пенальти.",10),
new("clean-sheet-win","Clean sheet","Сухая победа","Win a competitive career match without conceding, before a penalty shootout.","Выиграйте официальный матч карьеры без пропущенных голов до серии пенальти.",20),
new("big-win","Clear advantage","Уверенная победа","Win a competitive career match by at least three goals.","Выиграйте официальный матч карьеры с разницей не менее трёх голов.",20),
new("ten-wins","Winning habit","Привычка побеждать","Win 10 competitive matches in one career.","Выиграйте 10 официальных матчей в одной карьере.",30),
new("century-matches","One hundred matches","Сотня матчей","Finish 100 competitive matches in one career.","Завершите 100 официальных матчей в одной карьере.",75),
new("legendary-pull","A legend joins","Легенда в команде","Obtain a legendary player from a pack.","Получите легендарного игрока из пака.",75),
new("reach-second-league","Second League","Вторая лига","Reach the Second League in your career.","Доберитесь до Второй лиги в карьере.",30),
new("reach-first-league","First League","Первая лига","Reach the First League in your career.","Доберитесь до Первой лиги в карьере.",50),
new("reach-national-league","National League","Национальная лига","Reach the National League in your career.","Доберитесь до Национальной лиги в карьере.",75),
new("reach-premier-league","Premier League","Премьер-лига","Reach the Premier League in your career.","Доберитесь до Премьер-лиги в карьере.",100),
new("first-league-title","First league title","Первое чемпионство","Win any career league.","Выиграйте любую лигу в карьере.",50),
new("cup-winner","Cup winner","Обладатель кубка","Win a career cup.","Выиграйте кубок в карьере.",50),
new("premier-champion","Premier champion","Чемпион Премьер-лиги","Win the Premier League.","Выиграйте Премьер-лигу.",100),
new("unbeaten-champion","Unbeaten champion","Непобеждённый чемпион","Win the Premier League without losing a league match that season.","Выиграйте Премьер-лигу без поражений в лиге за сезон.",100),
new("domestic-double","Domestic double","Золотой дубль","Win the Premier League and a cup in the same season.","Выиграйте Премьер-лигу и кубок в одном сезоне.",100),
new("champions-cup","The final trophy","Главный трофей","Win the Champions Cup and complete the career campaign.","Выиграйте финальный Champions Cup и пройдите карьеру.",100)
});
static readonly string[] TitleIds={"first-league-title","cup-winner","premier-champion","unbeaten-champion","domestic-double"};
static readonly string[] LeagueIds={"reach-second-league","reach-first-league","reach-national-league","reach-premier-league"};
readonly record struct Result(int OwnGoals,int OtherGoals,bool Penalties,int OwnPenalties,int OtherPenalties)
{
public bool Won=>OwnGoals>OtherGoals || Penalties && OwnGoals==OtherGoals && OwnPenalties>OtherPenalties;
}
public static IReadOnlyCollection<string> Earned(ClubCareer career)
{
if(career==null)return Array.Empty<string>();
var earned=new HashSet<string>(StringComparer.Ordinal);
var results=MatchResults(career).Values.ToArray();
int wins=results.Count(r=>r.Won);
if(results.Length>0)earned.Add("first-match");
if(wins>0)earned.Add("first-win");
if(wins>=10)earned.Add("ten-wins");
if(results.Length>=100)earned.Add("century-matches");
if(career.LegendaryPackObtained)earned.Add("legendary-pull");
if(career.CareerWon)earned.Add("champions-cup");
if(results.Any(r=>!r.Penalties && r.OwnGoals>0 && r.OtherGoals==0))earned.Add("clean-sheet-win");
if(results.Any(r=>r.OwnGoals-r.OtherGoals>=3))earned.Add("big-win");
var seasons=(career.LeagueHistory??new()).Where(r=>r!=null && r.Season>0 && r.Season<=career.Season &&
r.Division is >=1 and <=5 && r.Place is >=1 and <=8 && r.Played>=14).ToArray();
int reached=career.Division is >=1 and <=5?career.Division:1;
foreach(var season in seasons)
{
reached=Math.Max(reached,season.Division);
// A completed current table proves its own league. Only a prior
// season proves that the next-season transition already happened.
if(season.Season<career.Season && season.Place<=2)reached=Math.Max(reached,Math.Min(5,season.Division+1));
}
for(int division=2;division<=reached;division++)earned.Add(LeagueIds[division-2]);
var cups=career.CupResults.Where(c=>c!=null && c.Season>0 && c.Season<=career.Season &&
c.Round==3 && c.PlaceFrom==1 && c.PlaceTo==1).ToArray();
if(cups.Length>0)earned.Add("cup-winner");
foreach(var season in seasons.Where(s=>s.Place==1))
{
earned.Add("first-league-title");
if(season.Division!=5)continue;
earned.Add("premier-champion");
// A zero default alone cannot prove an undefeated old season.
if(season.Losses==0 && season.Wins+season.Draws==season.Played)earned.Add("unbeaten-champion");
if(cups.Any(c=>c.Season==season.Season))earned.Add("domestic-double");
}
// Existing club honours were awarded by the career settlement resolver.
// Preserve them across legacy saves without re-running reward resolution.
foreach(var achievement in career.Achievements??new())
if(achievement!=null && achievement.EarnedSeason>0 && achievement.EarnedSeason<=career.Season && TitleIds.Contains(achievement.Id))
earned.Add(achievement.Id);
return All.Where(d=>earned.Contains(d.Id)).Select(d=>d.Id).ToArray();
}
static Dictionary<string,Result> MatchResults(ClubCareer career)
{
var results=new Dictionary<string,Result>(StringComparer.Ordinal);
foreach(var report in career.MatchHistory??new())
{
if(report==null || report.Season<=0 || report.Season>career.Season || report.OwnSide is <0 or >1)continue;
string key=CanonicalKey(report);
if(key==null || !ValidScore(report.HomeGoals,report.AwayGoals,report.Penalties,report.HomePenalties,report.AwayPenalties,report.Cup))continue;
results.TryAdd(key,report.OwnSide==0
?new(report.HomeGoals,report.AwayGoals,report.Penalties,report.HomePenalties,report.AwayPenalties)
:new(report.AwayGoals,report.HomeGoals,report.Penalties,report.AwayPenalties,report.HomePenalties));
}
void AddFixture(ClubFixture fixture,string cupId=null)
{
if(fixture==null || !fixture.Played || fixture.Id<0 || (fixture.Home==0)==(fixture.Away==0) ||
!ValidScore(fixture.HomeGoals,fixture.AwayGoals,fixture.Penalties,fixture.HomePenalties,fixture.AwayPenalties,cupId!=null))return;
string key=cupId==null?$"{career.Season}:league:{fixture.Id}":$"{career.Season}:cup:{cupId}:{fixture.Id}";
results.TryAdd(key,fixture.Home==0
?new(fixture.HomeGoals,fixture.AwayGoals,fixture.Penalties,fixture.HomePenalties,fixture.AwayPenalties)
:new(fixture.AwayGoals,fixture.HomeGoals,fixture.Penalties,fixture.AwayPenalties,fixture.HomePenalties));
}
foreach(var fixture in career.League??new())AddFixture(fixture);
foreach(var cup in career.Tournaments??new())
if(cup!=null && !string.IsNullOrWhiteSpace(cup.Id))
foreach(var fixture in cup.Games??new())AddFixture(fixture,cup.Id);
return results;
}
static bool ValidScore(int home,int away,bool penalties,int homePenalties,int awayPenalties,bool cup)=>home>=0 && away>=0 &&
(!penalties || cup && home==away && homePenalties>=0 && awayPenalties>=0 && homePenalties!=awayPenalties);
static string CanonicalKey(ArchivedClubMatch report)
{
string[] parts=(report.Key??"").Split(':');
if(parts.Length is <3 or >4 || !int.TryParse(parts[0],out int season) || season!=report.Season ||
!int.TryParse(parts[^1],out int fixture) || fixture<0)return null;
if(!report.Cup)return parts.Length==3 && parts[1]=="league"?$"{season}:league:{fixture}":null;
if(parts.Length==4)return parts[1]=="cup" && !string.IsNullOrWhiteSpace(parts[2])?$"{season}:cup:{parts[2]}:{fixture}":null;
return !string.IsNullOrWhiteSpace(parts[1])?$"{season}:cup:{parts[1]}:{fixture}":null;
}
}