using Nonemm.Contests.Udc; using Nonemm.Core.Calls; using Nonemm.Core; namespace Nonemm.Contests.Tests; /// The scoring settings a `.udc` file can carry: the points conditions, the /// points multipliers, the bonus, and where a multiplier comes from. public class UdcScoringTests { private static UserDefinedContest Contest(params string[] settings) => new(UdcFile.Parse("[Contest]\nName=SCORETEST\n" + string.Join("\n", settings))); private static ContestLog LogFor( UserDefinedContest contest, StationInfo? me = null, ContestEntry? entry = null) => new(contest, me ?? TestLog.Germany, TestLog.CountryFile, entry); private static int Points(UserDefinedContest contest, Qso qso, StationInfo? me = null) => LogFor(contest, me).Judge(qso).Points; [Theory] [InlineData("CT", 5)] [InlineData("MA", 1)] public void SectIsMatchesTheReceivedSection(string section, int expected) => Assert.Equal( expected, Points( Contest("PointsPerContact=SectIs_CT, 5, OtherContinent, 1"), TestLog.Contact("K1ABC", section: section))); [Fact] public void ExchIsNumSeparatesANumberFromAWord() => Assert.Equal( 3, Points( Contest("PointsPerContact=ExchIsNum, 3, ExchIsNotNum, 7"), TestLog.Contact("K1ABC", exchange: "129"))); [Fact] public void ExchIsEmptyScoresTheContactWithNoExchange() => Assert.Equal( 2, Points(Contest("PointsPerContact=ExchIs_Empty, 2, ExchIsNum, 9"), TestLog.Contact("K1ABC"))); [Fact] public void AStarScoresTheNumberInTheExchangeBox() => Assert.Equal( 42, Points(Contest("PointsPerContact=Exchange, *"), TestLog.Contact("K1ABC", exchange: "42"))); [Fact] public void MyCqZoneScoresAStationInMyOwnZone() => Assert.Equal( 1, Points(Contest("PointsPerContact=MyCQZone, 1, OtherContinent, 4"), TestLog.Contact("DL9XYZ"))); /// `SAMECONTINENT+160` is the same continent on one band only. [Fact] public void AContinentConditionCanCarryABand() { UserDefinedContest contest = Contest("PointsPerContact=SameContinent+160, 6, SameContinent, 2"); Assert.Equal(6, Points(contest, TestLog.Contact("IK2XYZ", 1_830))); Assert.Equal(2, Points(contest, TestLog.Contact("IK2XYZ"))); } [Fact] public void PointsMultByBandScalesWhatTheContactScores() { UserDefinedContest contest = Contest("PointsPerContact=3", "PointsMultByBand=160M, 3, 20M, 1"); Assert.Equal(9, Points(contest, TestLog.Contact("IK2XYZ", 1_830))); Assert.Equal(3, Points(contest, TestLog.Contact("IK2XYZ"))); } [Fact] public void PointsMultAtTimeGmtScalesInsideItsWindow() { UserDefinedContest contest = Contest("PointsPerContact=2", "PointsMultAtTimeGMT=2300, 0300, 2"); DateTime midnight = new(2026, 5, 30, 0, 30, 0, DateTimeKind.Utc); Assert.Equal(4, Points(contest, TestLog.Contact("IK2XYZ", at: midnight))); Assert.Equal(2, Points(contest, TestLog.Contact("IK2XYZ"))); } [Fact] public void BonusPointsAreAddedToWhatTheContactScores() => Assert.Equal( 26, Points( Contest("PointsPerContact=1", "BonusPoints=IK2XYZ, 25"), TestLog.Contact("IK2XYZ"))); /// A contest can pay the bonus for a new multiplier, so the multiplier has /// to be worked out before the points are. [Fact] public void BonusPointsCanBePaidForANewMultiplier() { UserDefinedContest contest = Contest( "PointsPerContact=1", "MultSqlString=CountryPrefix", "IsMultPer=1", "BonusPoints=IsMult1, 10"); ContestLog log = LogFor(contest); Assert.Equal(11, log.Judge(TestLog.Contact("IK2XYZ")).Points); log.Add(TestLog.Contact("IK2XYZ")); Assert.Equal(1, log.Judge(TestLog.Contact("IK3XYZ")).Points); } [Fact] public void ADistanceTableScoresByTheRangeTheContactFallsIn() { UserDefinedContest contest = Contest("PointsPerContact=0/100/7"); StationInfo me = TestLog.Germany with { GridSquare = "JO60" }; Qso near = TestLog.Contact("DL9XYZ") with { GridSquare = "JO60" }; Qso far = TestLog.Contact("JA1XYZ") with { GridSquare = "PM95" }; Assert.Equal(7, Points(contest, near, me)); Assert.True(Points(contest, far, me) > 1_000); } [Theory] [InlineData("MultSqlString=2LPREFIX", "IK")] [InlineData("MultSqlString=LastLetter", "Z")] [InlineData("MultSqlString=EU_Country", "I")] public void TheMultiplierValueComesFromTheSource(string setting, string expected) { UserDefinedContest contest = Contest(setting, "IsMultPer=4"); Verdict verdict = LogFor(contest).Judge(TestLog.Contact("IK2XYZ")); Assert.Equal(expected, Assert.Single(verdict.NewMultipliers).Value); } [Fact] public void ACountryOnAnotherContinentBringsNoContinentMultiplier() => Assert.Empty( LogFor(Contest("MultSqlString=EU_Country", "IsMultPer=4")) .Judge(TestLog.Contact("JA1XYZ")) .NewMultipliers); [Fact] public void CountMultOnlyForTakesAContinentAsWellAsACountry() { UserDefinedContest contest = Contest( "MultSqlString=CountryPrefix", "IsMultPer=4", "CountMultOnlyFor=EU"); Assert.Single(LogFor(contest).Judge(TestLog.Contact("IK2XYZ")).NewMultipliers); Assert.Empty(LogFor(contest).Judge(TestLog.Contact("JA1XYZ")).NewMultipliers); } [Fact] public void DoNotCountMultOnlyForLeavesThoseStationsOut() => Assert.Empty( LogFor(Contest("MultSqlString=CountryPrefix", "IsMultPer=4", "DoNotCountMultOnlyFor=JA")) .Judge(TestLog.Contact("JA1XYZ")) .NewMultipliers); [Fact] public void DoNotCountMeAsMultLeavesOutMyOwnCountry() => Assert.Empty( LogFor(Contest("MultSqlString=CountryPrefix", "IsMultPer=4", "DoNotCountMeAsMult=True")) .Judge(TestLog.Contact("DL9XYZ")) .NewMultipliers); [Fact] public void MultMultSaysWhatAMultiplierIsWorth() { ContestLog log = LogFor(Contest( "PointsPerContact=1", "MultSqlString=CountryPrefix", "IsMultPer=4", "MultMult=2")); log.Add(TestLog.Contact("JA1XYZ")); log.Add(TestLog.Contact("IK2XYZ")); Assert.Equal(8, log.TotalScore); } /// A multiplier weighted zero is still worked, and adds nothing. [Fact] public void MultMultZeroLeavesTheScoreAtThePoints() { ContestLog log = LogFor(Contest( "PointsPerContact=3", "MultSqlString=CountryPrefix", "IsMultPer=4", "MultMult=0")); log.Add(TestLog.Contact("JA1XYZ")); Assert.Equal(3, log.TotalScore); } [Fact] public void PowerMultScalesTheWholeScoreByTheEntryCategory() { UserDefinedContest contest = Contest("PointsPerContact=2", "PowerMult=QRP, 3, LP, 2, HP, 1"); ContestLog qrp = LogFor(contest, entry: new ContestEntry { PowerCategory = "QRP" }); ContestLog low = LogFor(contest, entry: new ContestEntry { PowerCategory = "LOW" }); qrp.Add(TestLog.Contact("JA1XYZ")); low.Add(TestLog.Contact("JA1XYZ")); Assert.Equal(6, qrp.TotalScore); Assert.Equal(4, low.TotalScore); } /// `PointsMultByCategory` is about the station being worked, not the entry: /// N1MM scales only a callsign signing QRP. [Fact] public void PointsMultByCategoryScalesAStationSigningQrp() { UserDefinedContest contest = Contest("PointsPerContact=2", "PointsMultByCategory=/QRP, 3"); Assert.Equal(6, Points(contest, TestLog.Contact("JA1XYZ/QRP"))); Assert.Equal(2, Points(contest, TestLog.Contact("JA1XYZ"))); } [Fact] public void MyExchangeComparesWhatThisStationSends() { UserDefinedContest contest = Contest("PointsPerContact=MyExchange, 1, OtherContinent, 5"); ContestEntry entry = new() { SentExchange = "14" }; Assert.Equal( 1, LogFor(contest, entry: entry).Judge(TestLog.Contact("JA1XYZ", section: "14")).Points); Assert.Equal( 5, LogFor(contest, entry: entry).Judge(TestLog.Contact("JA1XYZ", section: "15")).Points); } [Fact] public void NumMultsCapsHowManyAreCounted() { UserDefinedContest contest = Contest( "MultSqlString=CountryPrefix", "MultSqlString2=Continent", "IsMultPer=4", "NumMults=1"); Assert.Single(LogFor(contest).Judge(TestLog.Contact("JA1XYZ")).NewMultipliers); } [Fact] public void DupeSqlStringMakesASecondSectionADifferentContact() { UserDefinedContest contest = Contest("DupeType=1", "DupeSqlString=1"); ContestLog log = LogFor(contest); log.Add(TestLog.Contact("K1ABC", section: "CT")); Assert.False(log.Judge(TestLog.Contact("K1ABC", section: "MA")).IsDupe); Assert.True(log.Judge(TestLog.Contact("K1ABC", section: "CT")).IsDupe); } [Fact] public void IsWorkableLeavesOutTheStationsTheContestDoesNotCount() { UserDefinedContest contest = Contest( "PointsPerContact=3", "MultSqlString=CountryPrefix", "IsMultPer=4", "IsWorkable=EUonly"); Verdict outside = LogFor(contest).Judge(TestLog.Contact("JA1XYZ")); Assert.Equal(0, outside.Points); Assert.Empty(outside.NewMultipliers); Assert.Equal(3, Points(contest, TestLog.Contact("IK2XYZ"))); } [Fact] public void ZoneTypeIaruPutsTheContestOnItuZones() { Assert.True(Contest("ZoneType=IARU").UsesItuZones); Assert.False(Contest("ZoneType=CQ").UsesItuZones); } /// The received exchange of a contest where one side sends a section and /// the other a serial number lands in the same column, and only the /// section counts. A file that names no list this program holds falls back /// to taking an exchange of all digits for a serial number. [Fact] public void ASerialNumberIsNoSectionMultiplier() { UserDefinedContest contest = Contest("MultSqlString=Section", "IsMultPer=1"); Assert.Empty(LogFor(contest).Judge(TestLog.Contact("IK2XYZ", section: "058")).NewMultipliers); Assert.Single(LogFor(contest).Judge(TestLog.Contact("OM3XYZ", section: "NOV")).NewMultipliers); } /// `MultWindowType` names the list N1MM checks the section against. `GZS` /// is an OK/OM district and counts; `NOV` and the US state `MA` are not on /// that list and count for neither program. [Theory] [InlineData("GZS", 1)] [InlineData("NOV", 0)] [InlineData("MA", 0)] public void ASectionOffTheContestsListIsNoMultiplier(string section, int expected) => Assert.Equal( expected, LogFor(Contest( "MultSqlString=Section", "IsMultPer=1", "MultWindowType=OKOMDX")) .Judge(TestLog.Contact("OM3XYZ", section: section)) .NewMultipliers.Count); /// `DupeQSOMinutesAgo = 10` lets the same station be worked again ten /// minutes after the last contact, and not before. [Fact] public void ADupeWindowLetsAStationBeWorkedAgainLater() { UserDefinedContest contest = Contest("DupeType=4", "DupeQSOMinutesAgo=10"); ContestLog log = LogFor(contest); DateTime first = new(2026, 5, 30, 12, 0, 0, DateTimeKind.Utc); log.Add(TestLog.Contact("OM3XYZ") with { TimestampUtc = first }); Assert.True(log.Judge(TestLog.Contact("OM3XYZ") with { TimestampUtc = first.AddMinutes(9), }).IsDupe); Assert.False(log.Judge(TestLog.Contact("OM3XYZ") with { TimestampUtc = first.AddMinutes(11), }).IsDupe); } /// `IgnoreBand` looks for the earlier contact on every band rather than /// this one, so the same station on another band is still a dupe. [Fact] public void ADupeWindowThatIgnoresTheBandCoversEveryBand() { UserDefinedContest contest = Contest("DupeType=4", "DupeQSOMinutesAgo=10, True, True"); ContestLog log = LogFor(contest); DateTime first = new(2026, 5, 30, 12, 0, 0, DateTimeKind.Utc); log.Add(TestLog.Contact("OM3XYZ") with { TimestampUtc = first }); Assert.True(log.Judge(TestLog.Contact("OM3XYZ", 7_040) with { TimestampUtc = first.AddMinutes(5), }).IsDupe); } /// `MultipleSessions = 0000/30` runs the contest in half-hour sessions, /// and a station worked in one of them may be worked again in the next. [Fact] public void AStationWorkedInAnEarlierSessionIsNoDupe() { UserDefinedContest contest = Contest("MultipleSessions=0000/30"); ContestLog log = LogFor(contest); DateTime first = new(2026, 5, 30, 12, 10, 0, DateTimeKind.Utc); log.Add(TestLog.Contact("OM3XYZ") with { TimestampUtc = first }); Assert.True(log.Judge(TestLog.Contact("OM3XYZ") with { TimestampUtc = first.AddMinutes(10), }).IsDupe); Assert.False(log.Judge(TestLog.Contact("OM3XYZ") with { TimestampUtc = first.AddMinutes(25), }).IsDupe); } /// `ResetMultsEverySession` counts the same multiplier again in the next /// session. Without it the multiplier stands for the whole contest. [Theory] [InlineData("ResetMultsEverySession=1", 1)] [InlineData("ResetMultsEverySession=0", 0)] public void MultipliersStartOverEachSessionWhenTheFileSaysSo(string setting, int expected) { UserDefinedContest contest = Contest( "MultipleSessions=0000/30", "MultSqlString=CountryPrefix", "IsMultPer=4", setting); ContestLog log = LogFor(contest); DateTime first = new(2026, 5, 30, 12, 10, 0, DateTimeKind.Utc); log.Add(TestLog.Contact("OM3XYZ") with { TimestampUtc = first }); Assert.Equal( expected, log.Judge(TestLog.Contact("OM5ZZZ") with { TimestampUtc = first.AddMinutes(25) }) .NewMultipliers.Count); } /// `BonusPoints2` names how much a station on a list is worth and the file /// the list is in. `+50` adds to what the contact scored. [Theory] [InlineData("+50, bonus.txt", 53)] [InlineData("*2, bonus.txt", 6)] [InlineData("50, bonus.txt", 50)] public void BonusPoints2PaysForTheCallsignsInAFile(string setting, int expected) { string folder = Directory.CreateTempSubdirectory().FullName; try { File.WriteAllText(Path.Combine(folder, "bonus.txt"), "OM3XYZ,a club station\nOM5ZZZ,\n"); UserDefinedContest contest = new( UdcFile.Parse($"[Contest]\nName=BONUS\nPointsPerContact=3\nBonusPoints2={setting}"), folder); Assert.Equal(expected, Points(contest, TestLog.Contact("OM3XYZ"))); Assert.Equal(3, Points(contest, TestLog.Contact("IK2XYZ"))); } finally { Directory.Delete(folder, recursive: true); } } /// A file that is not there leaves the bonus off and the contest opens. [Fact] public void BonusPoints2WithoutItsFileScoresTheContactAsItIs() => Assert.Equal( 3, Points( new UserDefinedContest( UdcFile.Parse("[Contest]\nName=BONUS\nPointsPerContact=3\nBonusPoints2=+50, missing.txt"), Path.Combine(Path.GetTempPath(), "nonemm-no-such-folder")), TestLog.Contact("OM3XYZ"))); /// `MultSqlString = CallHist` counts every station the call history file /// lists, once per callsign. A station the file says nothing about brings /// no multiplier. [Fact] public void CallHistCountsTheStationsTheFileLists() { UserDefinedContest contest = Contest("MultSqlString=CallHist", "IsMultPer=4"); ContestLog log = LogFor(contest); log.History = CallHistory.Parse("OM3XYZ,JOE\nOM5ZZZ,SAM\n"); Assert.Equal( "OM3XYZ", log.Judge(TestLog.Contact("OM3XYZ")).NewMultipliers.Single().Value); Assert.Empty(log.Judge(TestLog.Contact("IK2XYZ")).NewMultipliers); } /// Without a call history file loaded, a `CallHist` multiplier counts /// nothing rather than counting everybody. [Fact] public void CallHistCountsNothingWithoutAFile() => Assert.Empty( LogFor(Contest("MultSqlString=CallHist", "IsMultPer=4")) .Judge(TestLog.Contact("OM3XYZ")) .NewMultipliers); /// Published files hold `Country` in this key; the UDC editor puts /// `CountryPrefix` there. [Fact] public void CountryIsTheSameSourceAsCountryPrefix() => Assert.Equal( "I", LogFor(Contest("MultSqlString=Country", "IsMultPer=4")) .Judge(TestLog.Contact("IK2XYZ")) .NewMultipliers.Single().Value); [Fact] public void OtherCountryIsEveryCountryButMine() { UserDefinedContest contest = Contest("PointsPerContact=MyCountry, 0, OtherCountry, 2"); Assert.Equal(0, Points(contest, TestLog.Contact("DL9XYZ"))); Assert.Equal(2, Points(contest, TestLog.Contact("JA1XYZ"))); } }