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 -8
View File
@@ -73,17 +73,17 @@ public sealed class CqWorldWide : Contest
public string SentExchangeFor(StationInfo me) =>
IsRtty ? $"{me.CqZone} {HomeOf(me)}" : me.CqZone.ToString();
/// A maritime mobile station counts for no country, and still scores by
/// the continent it is operating from, which is what N1MM logs.
public int PointsFor(QsoContext qso)
{
if (!qso.Qso.Call.CountsForEntity)
{
return 0;
}
if (IsRtty)
{
return qso.IsSameCountry ? 1 : qso.IsSameContinent ? 2 : 3;
return qso.IsSameCountry && qso.Qso.Call.CountsForEntity ? 1
: qso.IsSameContinent ? 2
: 3;
}
if (qso.IsSameCountry)
if (qso.IsSameCountry && qso.Qso.Call.CountsForEntity)
{
return 0;
}
@@ -107,14 +107,14 @@ public sealed class CqWorldWide : Contest
found.Add(new Multiplier(2, qso.CountryPrefix, band));
}
string home = qso.Qso.Section.Trim().ToUpperInvariant();
if (IsRtty && StatesAndProvinces.IsStateOrProvince(home))
if (IsRtty && StatesAndProvinces.CqWwRttyAreas.Contains(home))
{
found.Add(new Multiplier(3, home, band));
}
return found;
}
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) => IsRtty
? new CabrilloExchange(