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:
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -74,7 +74,7 @@ public sealed class CqWpx : Contest
|
||||
return prefix is null ? [] : [new Multiplier(1, prefix, "")];
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -35,7 +35,7 @@ public sealed class GeneralLogging : Contest
|
||||
|
||||
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso) => [];
|
||||
|
||||
public int TotalScore(ScoreTally tally) => tally.Qsos;
|
||||
public int TotalScore(ScoreTally tally, ContestEntry entry) => tally.Qsos;
|
||||
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me) =>
|
||||
new(
|
||||
|
||||
@@ -4,6 +4,9 @@ namespace Nonemm.Contests.Rules;
|
||||
|
||||
/// IARU HF World Championship. The exchange is an ITU zone or, for a society
|
||||
/// headquarters station, its abbreviation; both count as multipliers per band.
|
||||
///
|
||||
/// The received zone or abbreviation goes in the section column, which is where
|
||||
/// N1MM keeps it.
|
||||
public sealed class IaruHf : Contest
|
||||
{
|
||||
public string Name => "IARU";
|
||||
@@ -15,7 +18,7 @@ public sealed class IaruHf : Contest
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("Zone/HQ", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 6 },
|
||||
new ExchangeField("Zone/HQ", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 6 },
|
||||
];
|
||||
|
||||
public IReadOnlyList<string> MultiplierNames => ["Zones", "HQ"];
|
||||
@@ -55,7 +58,7 @@ public sealed class IaruHf : Contest
|
||||
return [new Multiplier(IsZone(exchange) ? 1 : 2, exchange, 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(
|
||||
@@ -67,10 +70,10 @@ public sealed class IaruHf : Contest
|
||||
[
|
||||
new CabrilloField(qso.Call.Text, 13),
|
||||
new CabrilloField(qso.ReceivedReport, 3),
|
||||
new CabrilloField(qso.Exchange1, 6),
|
||||
new CabrilloField(qso.Section, 6),
|
||||
]);
|
||||
|
||||
private static string Exchange(QsoContext qso) => qso.Qso.Exchange1.Trim().ToUpperInvariant();
|
||||
private static string Exchange(QsoContext qso) => qso.Qso.Section.Trim().ToUpperInvariant();
|
||||
|
||||
private static bool IsZone(string exchange) =>
|
||||
exchange.Length > 0 && exchange.All(char.IsAsciiDigit);
|
||||
|
||||
@@ -54,7 +54,7 @@ public sealed class NorthAmericanQsoParty : Contest
|
||||
: [];
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed class RttyRoundup : Contest
|
||||
return qso.CountryPrefix.Length == 0 ? [] : [new Multiplier(1, qso.CountryPrefix, "")];
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed class Sweepstakes : Contest
|
||||
return ArrlSections.IsSection(section) ? [new Multiplier(1, section, "")] : [];
|
||||
}
|
||||
|
||||
public int TotalScore(ScoreTally tally) => tally.Points * tally.TotalMultipliers;
|
||||
public int TotalScore(ScoreTally tally, ContestEntry entry) => tally.Points * tally.TotalMultipliers;
|
||||
|
||||
/// Sweepstakes puts the callsign after the serial number and precedence
|
||||
/// rather than first, which is why the writer takes the whole column list.
|
||||
|
||||
@@ -94,7 +94,7 @@ public sealed class Wae : Contest
|
||||
: [new Multiplier(1, value, qso.Band?.Name ?? "", weight)];
|
||||
}
|
||||
|
||||
public int TotalScore(ScoreTally tally) => tally.Points * tally.TotalMultipliers;
|
||||
public int TotalScore(ScoreTally tally, ContestEntry entry) => tally.Points * tally.TotalMultipliers;
|
||||
|
||||
/// A QTC is written as its own Cabrillo record: the station that received
|
||||
/// the traffic, the series, the station that sent it, and then the contact
|
||||
|
||||
Reference in New Issue
Block a user