Read the rest of what a .udc file says, and score what N1MM scores

Two pieces of work on the scoring engine.

The .udc reader now takes N1MM's published scoring vocabulary: the full
PointsPerContact condition set, the PointsMultBy family, PowerMult, BonusPoints,
the multiplier sources and the settings that say which stations bring a
multiplier in. MultMult is a weight rather than a switch, as in N1MM's
ComputeScore. The entry categories and the sent exchange reach the rules in a
new ContestEntry, so a contest can score by the power category it is entered in
or by what this station sends.

Then every contest in a real N1MM log was scored again from these rules and
compared with the points and multiplier flags N1MM wrote. That found five bugs:
ARRL DX and IARU read the exchange from the wrong column, a stored " " was read
as a value rather than as empty, CQ WW RTTY's own Canadian area names were not
counted, a maritime mobile scored nothing, and a contact in a mode the contest
does not run scored as if it were in the contest.

docs/n1mm-interop.md has the check and what still differs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
2026-08-31 08:33:40 +00:00
co-authored by Claude Opus 5
parent 9424c488ea
commit 04568d0ca3
28 changed files with 1088 additions and 142 deletions
+13 -3
View File
@@ -5,7 +5,7 @@ namespace Nonemm.Contests;
/// A QSO together with what the country file says about it, so a contest can
/// score by country, zone or continent without looking anything up itself.
public sealed record QsoContext(Qso Qso, CountryLookup? Country, StationInfo Me)
public sealed record QsoContext(Qso Qso, CountryLookup? Country, StationInfo Me, ContestEntry Entry)
{
public Band? Band => Qso.Band;
@@ -15,10 +15,12 @@ public sealed record QsoContext(Qso Qso, CountryLookup? Country, StationInfo Me)
/// the operator corrected the country by hand, and then the correction is
/// the point.
public string Continent =>
Qso.Continent.Length > 0 ? Qso.Continent : Country?.Continent ?? "";
Qso.Continent.Trim().Length > 0 ? Qso.Continent.Trim() : Country?.Continent ?? "";
public string CountryPrefix =>
Qso.CountryPrefix.Length > 0 ? Qso.CountryPrefix : Country?.Entity.PrimaryPrefix ?? "";
Qso.CountryPrefix.Trim().Length > 0
? Qso.CountryPrefix.Trim()
: Country?.Entity.PrimaryPrefix ?? "";
public int CqZone => Country?.CqZone ?? Qso.Zone;
@@ -26,5 +28,13 @@ public sealed record QsoContext(Qso Qso, CountryLookup? Country, StationInfo Me)
public bool IsSameCountry => CountryPrefix.Length > 0 && CountryPrefix == Me.CountryPrefix;
/// Great-circle kilometres between the two grid squares, and zero when
/// either station has no locator.
public double DistanceKm =>
GridSquare.TryParse(Qso.GridSquare, out GridSquare theirs)
&& GridSquare.TryParse(Me.GridSquare, out GridSquare mine)
? mine.DistanceTo(theirs)
: 0.0;
public bool IsSameContinent => Continent.Length > 0 && Continent == Me.Continent;
}