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
+8 -5
View File
@@ -6,6 +6,9 @@ namespace Nonemm.Contests.Rules;
/// ARRL International DX, CW and SSB. W and VE stations work DX and nobody
/// else, so the exchange and the multiplier both depend on which side the
/// operator is on.
///
/// The received power or state goes in the section column, which is where N1MM
/// keeps it, so a log written by either program scores the same in the other.
public sealed class ArrlDx : Contest
{
private readonly ModeCategory mode;
@@ -22,8 +25,8 @@ public sealed class ArrlDx : Contest
[
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
IsNorthAmericanHome(me)
? new ExchangeField("Pwr", ExchangeSlot.Exchange1, ExchangeFieldKind.Power)
: new ExchangeField("S/P", ExchangeSlot.Exchange1, ExchangeFieldKind.UsStateOrCanadianProvince),
? new ExchangeField("Pwr", ExchangeSlot.Section, ExchangeFieldKind.Power)
: new ExchangeField("S/P", ExchangeSlot.Section, ExchangeFieldKind.UsStateOrCanadianProvince),
];
public IReadOnlyList<string> MultiplierNames => ["Mults"];
@@ -53,13 +56,13 @@ public sealed class ArrlDx : Contest
{
return qso.CountryPrefix.Length == 0 ? [] : [new Multiplier(1, qso.CountryPrefix, band)];
}
string received = qso.Qso.Exchange1.Trim().ToUpperInvariant();
string received = StatesAndProvinces.ArrlDxArea(qso.Qso.Section);
return StatesAndProvinces.ArrlDxMultipliers.Contains(received)
? [new Multiplier(1, received, band)]
: [];
}
public int TotalScore(ScoreTally tally) => tally.Points * tally.TotalMultipliers;
public int TotalScore(ScoreTally tally, ContestEntry entry) => tally.Points * tally.TotalMultipliers;
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me) =>
new(
@@ -71,7 +74,7 @@ public sealed class ArrlDx : Contest
[
new CabrilloField(qso.Call.Text, 13),
new CabrilloField(qso.ReceivedReport, 3),
new CabrilloField(qso.Exchange1, 6),
new CabrilloField(qso.Section, 6),
]);
private static bool IsNorthAmericanHome(StationInfo me) => IsNorthAmerican(me.CountryPrefix);