Files
Nonemm/tests/Nonemm.Contests.Tests/UdcScoringTests.cs
ericek111 38e9010802 Let the published .udc files score the contests they are written for
I wrote SA 10 and OK/OM DX SSB in code after two failed attempts at the
download. The download works — the page posts a nonce with the form — and the
files say I got both contests wrong.

SA 10 is deleted from the code. Its file scores the station's log to the
contact, where the code had the wrong Cabrillo name and counted its multipliers
once in the contest rather than once per mode.

OK/OM DX now covers the CW running only, which is what N1MM's class is for and
what N1MM's own documentation says: the SSB running has different rules and a
pair of .udc files, one for each side. The CW rules go back to the class, so
Czechia and Slovakia are one side of the contest again. With the SSB files in
place both of the station's SSB logs score to the contact.

Reading those files turned up four things in the .udc reader: a file whose
extension is upper case was passed over, `Country` was not read as a
multiplier source, `OtherCountry` was not a points condition, and a section
multiplier counted the serial numbers the other side of a contest sends.

All Asian stays in code — N1MM has a class for it after all — with the Cabrillo
name and the band table it uses.

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

296 lines
11 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. 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);
}
/// `Country` is what a published file writes where the editor writes
/// `CountryPrefix`.
[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")));
}
}