using Nonemm.Contests.Udc; 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. N1MM checks it against the contest's section list; the /// lists are not held here, so an exchange that is all digits is taken 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); } /// 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"))); } }