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
This commit is contained in:
2026-08-31 10:00:21 +00:00
co-authored by Claude Opus 5
parent 264036f0f2
commit 38e9010802
13 changed files with 151 additions and 155 deletions
+14 -5
View File
@@ -4,7 +4,8 @@ namespace Nonemm.Contests.Rules;
/// The JARL All Asian DX contest. Only contacts with Asia count for a station
/// outside it. The exchange is the operator's age, the points go by band, and
/// each prefix worked counts once per band.
/// each prefix worked counts once per band. An Asian station scores three
/// times what a station outside Asia scores for the same contact.
public sealed class AllAsian : Contest
{
private readonly ModeCategory mode;
@@ -15,7 +16,7 @@ public sealed class AllAsian : Contest
public string DisplayName => $"All Asian DX {ModeLabel()}";
public string CabrilloName => mode == ModeCategory.Cw ? "AA-CW" : "AA-SSB";
public string CabrilloName => mode == ModeCategory.Cw ? "AADX-CW" : "AADX-SSB";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[
@@ -33,7 +34,14 @@ public sealed class AllAsian : Contest
public string SentExchangeFor(StationInfo me) => "";
public int PointsFor(QsoContext qso) => IsWorkable(qso) ? PointsOn(qso.Band) : 0;
public int PointsFor(QsoContext qso)
{
if (!IsWorkable(qso))
{
return 0;
}
return PointsOn(qso.Band) * (qso.Me.Continent == "AS" ? 3 : 1);
}
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso) =>
IsWorkable(qso) && qso.Qso.Call.WpxPrefix() is { } prefix
@@ -60,10 +68,11 @@ public sealed class AllAsian : Contest
private static bool IsWorkable(QsoContext qso) =>
qso.Continent == "AS" || qso.Me.Continent == "AS";
/// 160 metres is worth 3, 80 and 10 metres 2, and every other band 1.
private static int PointsOn(Band? band) => band?.Name switch
{
"160M" or "80M" => 3,
"40M" => 2,
"160M" => 3,
"80M" or "10M" => 2,
null => 0,
_ => 1,
};
+26 -26
View File
@@ -3,19 +3,19 @@ using Nonemm.Core.Country;
namespace Nonemm.Contests.Rules;
/// OK/OM DX. An OK or OM station sends a county code and everyone else a
/// OK/OM DX, CW. An OK or OM station sends a county code and everyone else a
/// serial number, and which side the operator is on decides what a contact
/// scores.
/// scores and what it counts for.
///
/// The SSB running of this contest has its own rules and its own pair of `.udc`
/// files, `OKOMDXS` and `OKOMDXS_DX`, one for each side. N1MM says not to use
/// this contest for it, and neither should we.
///
/// The received exchange lands in the section column for an OK or OM station
/// and in the exchange column for everyone else, which is where N1MM keeps
/// each of them.
public sealed class OkOmDx : Contest
{
private readonly ModeCategory mode;
public OkOmDx(ModeCategory mode) => this.mode = mode;
public string Name => "OKOMDX";
public string DisplayName => "Czech and Slovak Republics DX";
@@ -39,52 +39,52 @@ public sealed class OkOmDx : Contest
_ => false,
};
public IReadOnlyList<string> MultiplierNames => ["Countries", "Counties"];
public IReadOnlyList<string> MultiplierNames => ["Mults"];
public DupeScope DupeScope => DupeScope.PerBand;
public bool HasSerialNumbers => true;
public IReadOnlyList<ModeCategory> Modes => [mode];
public IReadOnlyList<ModeCategory> Modes => [ModeCategory.Cw];
public string SentExchangeFor(StationInfo me) => IsOkOm(me.CountryPrefix) ? me.County : "001";
/// A maritime mobile is 5 points to either side. An OK or OM operator gets
/// 2 points at home, 3 elsewhere on the continent and 5 from another —
/// Czechia and Slovakia are two countries, so they are 3 points to each
/// other. Everyone else gets 10 points for an OK or OM station, 1 at home,
/// 3 on their own continent and 5 from another.
/// 2 points for another OK or OM station, 3 for the rest of Europe and 5
/// for another continent. Everyone else gets 10 points for an OK or OM
/// station, 1 at home, 3 on their own continent and 5 from another.
public int PointsFor(QsoContext qso)
{
if (qso.Qso.Call.IsMaritimeMobile)
{
return 5;
}
if (!IsOkOm(qso.Me.CountryPrefix) && IsOkOm(qso.CountryPrefix))
bool theirs = IsOkOm(qso.CountryPrefix);
if (IsOkOm(qso.Me.CountryPrefix))
{
return theirs ? 2 : qso.Continent == "EU" ? 3 : 5;
}
if (theirs)
{
return 10;
}
int home = IsOkOm(qso.Me.CountryPrefix) ? 2 : 1;
return qso.IsSameCountry ? home : qso.IsSameContinent ? 3 : 5;
return qso.IsSameCountry ? 1 : qso.IsSameContinent ? 3 : 5;
}
/// Both sides count the same two, once per band each: every country, and
/// every OK or OM county. N1MM's own class counts prefixes for an OK or OM
/// operator, which was the rule before the counties came in.
/// Once per band: an OK or OM operator counts the prefixes of the stations
/// it works, and everyone else counts the OK and OM counties.
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso)
{
string band = qso.Band?.Name ?? "";
List<Multiplier> found = [];
if (qso.CountryPrefix.Length > 0)
bool theirs = IsOkOm(qso.CountryPrefix);
if (IsOkOm(qso.Me.CountryPrefix))
{
found.Add(new Multiplier(1, qso.CountryPrefix, band));
return theirs || qso.Qso.Call.WpxPrefix() is not { } prefix
? []
: [new Multiplier(1, prefix, band)];
}
string county = County(qso.Qso);
if (IsOkOm(qso.CountryPrefix) && county.Length > 0)
{
found.Add(new Multiplier(2, county, band));
}
return found;
return theirs && county.Length > 0 ? [new Multiplier(1, county, band)] : [];
}
public int TotalScore(ScoreTally tally, ContestEntry entry) =>
@@ -1,67 +0,0 @@
using Nonemm.Core;
namespace Nonemm.Contests.Rules;
/// The SA 10 metre contest. A South American station is worth 4 points and
/// everyone else 2. The exchange is a CQ zone, and both the prefixes and the
/// zones are multipliers. One band, so nothing is counted per band.
///
/// N1MM logs this one through a `.udc` file, so the Cabrillo name here is the
/// contest name and has not been checked against the sponsor's robot.
public sealed class SouthAmerica10 : Contest
{
public string Name => "SA10";
public string DisplayName => "SA 10 Metre Contest";
public string CabrilloName => "SA10";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Zone", ExchangeSlot.Section, ExchangeFieldKind.CqZone),
];
public IReadOnlyList<string> MultiplierNames => ["Prefixes", "Zones"];
public DupeScope DupeScope => DupeScope.PerMode;
public bool HasSerialNumbers => false;
public IReadOnlyList<ModeCategory> Modes => [ModeCategory.Cw, ModeCategory.Phone];
public string SentExchangeFor(StationInfo me) => me.CqZone.ToString();
public int PointsFor(QsoContext qso) => qso.Continent == "SA" ? 4 : 2;
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso)
{
List<Multiplier> found = [];
if (qso.Qso.Call.WpxPrefix() is { } prefix)
{
found.Add(new Multiplier(1, prefix, ""));
}
string zone = qso.Qso.Section.Trim();
if (zone.Length > 0)
{
found.Add(new Multiplier(2, zone, ""));
}
return found;
}
public int TotalScore(ScoreTally tally, ContestEntry entry) =>
tally.Points * tally.TotalMultipliers;
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me, ContestEntry entry) =>
new(
[
new CabrilloField(me.Callsign, 13),
new CabrilloField(qso.SentReport, 3),
new CabrilloField(me.CqZone.ToString(), 3),
],
[
new CabrilloField(qso.Call.Text, 13),
new CabrilloField(qso.ReceivedReport, 3),
new CabrilloField(qso.Section, 3),
]);
}