Add the YOTA and OK/OM DX contests

Both are in the station's log and neither could be opened. The rules are
written from the published ones and checked by replaying the log.

YOTA scores a contact by the age the other operator sends: 13 points down to 10
for anyone 25 or under, whatever the continent, and 1 point or 3 for everyone
else. Every age worked on a band is a multiplier. The 2024, 2025 and 2026 logs
come out to the contact.

OK/OM DX is the first contest whose exchange differs by the other station's
country: a county from OK and OM stations, a serial number from everyone else,
in the two columns N1MM keeps them in, and `SkipsField` walks the entry window
past the box that does not apply. Points depend on which side the operator is
on, and Czechia and Slovakia count as the two countries they are. The 2023 log
comes out to the contact.

The Cabrillo exchange now gets the entry, because YOTA sends an age that only
the contest setup knows.

Two differences are left in the log, both from the .udc files the operator
used: their YOTA file only lists ages 7 to 25, so a younger operator scored as
an adult, and one OK/OM log was made with the file for stations outside OK and
OM while signing an OM call.

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 09:16:44 +00:00
parent 5c0ca558c1
commit 3932f83147
22 changed files with 360 additions and 23 deletions

View File

@@ -0,0 +1,109 @@
using Nonemm.Core;
using Nonemm.Core.Country;
namespace Nonemm.Contests.Rules;
/// OK/OM DX. 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.
///
/// 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";
public string CabrilloName => "OK-OM-DX";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("County", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 4 },
new ExchangeField("Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Number),
];
/// An OK or OM station sends a county and no serial; everyone else the
/// other way round, so one of the two boxes is stepped over.
public bool SkipsField(ExchangeField field, CountryLookup? their) =>
field.Slot switch
{
ExchangeSlot.Section => their is not null && !IsOkOm(their.Entity.PrimaryPrefix),
ExchangeSlot.Exchange1 => their is not null && IsOkOm(their.Entity.PrimaryPrefix),
_ => false,
};
public IReadOnlyList<string> MultiplierNames => ["Countries", "Counties"];
public DupeScope DupeScope => DupeScope.PerBand;
public bool HasSerialNumbers => true;
public IReadOnlyList<ModeCategory> Modes => [mode];
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.
public int PointsFor(QsoContext qso)
{
if (qso.Qso.Call.IsMaritimeMobile)
{
return 5;
}
if (!IsOkOm(qso.Me.CountryPrefix) && IsOkOm(qso.CountryPrefix))
{
return 10;
}
int home = IsOkOm(qso.Me.CountryPrefix) ? 2 : 1;
return qso.IsSameCountry ? home : 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.
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso)
{
string band = qso.Band?.Name ?? "";
List<Multiplier> found = [];
if (qso.CountryPrefix.Length > 0)
{
found.Add(new Multiplier(1, qso.CountryPrefix, band));
}
string county = County(qso.Qso);
if (IsOkOm(qso.CountryPrefix) && county.Length > 0)
{
found.Add(new Multiplier(2, county, band));
}
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(IsOkOm(me.CountryPrefix) ? me.County : $"{qso.SentNumber:000}", 6),
],
[
new CabrilloField(qso.Call.Text, 13),
new CabrilloField(qso.ReceivedReport, 3),
new CabrilloField(County(qso).Length > 0 ? County(qso) : qso.Exchange1, 6),
]);
private static string County(Qso qso) => qso.Section.Trim().ToUpperInvariant();
private static bool IsOkOm(string countryPrefix) => countryPrefix is "OK" or "OM";
}