Files
Nonemm/tests/Nonemm.Contests.Tests/UdcScoringTests.cs
ericek111 1d3167cd04 Check a .udc section multiplier against the list the file names
MultWindowType names the list N1MM validates a received section against,
and every exchange that was not all digits counted as a multiplier here.
Four of N1MM's lists are held now -- the ARRL sections, the US states,
the Canadian provinces and the 165 OK/OM districts, which came out of
N1MM's own exe -- and a file naming one of them counts only the values
on it. A file naming any of the hundred-odd others keeps the old rule.

The 2023 OK/OM DX log now scores contact for contact as N1MM did. Two
contacts in the 2026 log still differ: the district code is stored with
a trailing space, which N1MM matches against its list untrimmed, so it
counts no multiplier where this counts one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 13:30:27 +00:00

312 lines
12 KiB
C#

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. 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);
/// 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")));
}
}