Files
Nonemm/src/Nonemm.Contests/Rules/Wae.cs
T
erikbrockoandClaude Opus 5 3932f83147 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
2026-08-31 09:16:44 +00:00

189 lines
6.6 KiB
C#

using Nonemm.Core;
namespace Nonemm.Contests.Rules;
/// Worked All Europe DX, CW, SSB and RTTY. Europe works the rest of the world
/// and the rest of the world works Europe; on RTTY everybody works everybody.
///
/// Two things make it unlike the other contests here. A multiplier is worth
/// more on the low bands — four times on 80 metres, three on 40, twice on 20,
/// 15 and 10 — and the contest is half about QTC traffic: a station reports
/// contacts it has already made to the station it is working, and each line
/// reported is a point for both of them. Those lines live in the log as rows of
/// their own; see `WaeQtc`.
public sealed class Wae : Contest
{
/// The countries whose call areas count separately, so W1 and W2 are two
/// multipliers rather than one.
private static readonly IReadOnlySet<string> CallAreaCountries =
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"K", "VE", "VK", "ZL", "ZS", "JA", "PY", "BY",
};
/// Asiatic Russia is split by call area too, and the multiplier is written
/// `UA` and the call area digit whatever the prefix on the air, so RM0W and
/// UA0AB are one multiplier.
private static readonly IReadOnlySet<string> AsiaticRussia =
new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "UA8", "UA9", "UA0" };
private readonly ModeCategory mode;
public Wae(ModeCategory mode) => this.mode = mode;
public string Name => mode switch
{
ModeCategory.Phone => "WAESSB",
ModeCategory.Digital => "WAERTTY",
_ => "WAECW",
};
public string DisplayName => $"Worked All Europe DX {ModeLabel()}";
public string CabrilloName => mode switch
{
ModeCategory.Phone => "WAE-SSB",
ModeCategory.Digital => "WAE-RTTY",
_ => "WAE-CW",
};
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
];
public IReadOnlyList<string> MultiplierNames => ["Mults"];
public DupeScope DupeScope => DupeScope.PerBand;
public bool HasSerialNumbers => true;
public IReadOnlyList<ModeCategory> Modes => [mode];
public bool HasQtcTraffic => true;
public string SentExchangeFor(StationInfo me) => "001";
/// A QTC line counts, and so does every contact on a contest band. The
/// WARC bands are not in this contest, so a contact there is worth nothing.
public int PointsFor(QsoContext qso)
{
if (!IsWorkable(qso))
{
return 0;
}
return WaeQtc.IsQtc(qso.Qso) || WeightFor(qso.Band) > 0 ? 1 : 0;
}
public bool IsContact(Qso qso) => !WaeQtc.IsQtc(qso);
/// One multiplier per band, worth more the lower the band. For a station in
/// Europe it is the other station's country or call area; for a station
/// outside it, the European country worked.
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso)
{
int weight = WeightFor(qso.Band);
if (weight == 0 || !IsWorkable(qso))
{
return [];
}
return [new Multiplier(1, MultiplierValue(qso), qso.Band?.Name ?? "", weight)];
}
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
/// being reported.
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me, ContestEntry entry)
{
if (WaeQtc.From(qso) is not { } qtc)
{
return new CabrilloExchange(
[
new CabrilloField(me.Callsign, 13),
new CabrilloField(qso.SentReport, 3),
new CabrilloField($"{qso.SentNumber:0000}", 4),
],
[
new CabrilloField(qso.Call.Text, 13),
new CabrilloField(qso.ReceivedReport, 3),
new CabrilloField($"{qso.ReceivedNumber:0000}", 4),
]);
}
string receiver = qtc.IsSent ? qtc.Station.Text : me.Callsign;
string sender = qtc.IsSent ? me.Callsign : qtc.Station.Text;
return new CabrilloExchange(
[
new CabrilloField(receiver, 13),
new CabrilloField(qtc.SeriesText, 10),
new CabrilloField(sender, 13),
],
[
new CabrilloField(qtc.TimeUtc, 4),
new CabrilloField(qtc.Call.Text, 13),
new CabrilloField($"{qtc.Number:0000}", 4),
]);
}
/// On CW and SSB the contest is Europe against the rest of the world, so a
/// contact with your own side of that line is worth nothing. RTTY drops the
/// rule and everybody works everybody.
public bool IsWorkable(QsoContext qso)
{
if (mode == ModeCategory.Digital)
{
return true;
}
return qso.Me.Continent == "EU" ? qso.Continent != "EU" : qso.Continent == "EU";
}
/// 80 metres is worth four, 40 three, and 20, 15 and 10 two. Everything
/// else is not in the contest.
public static int WeightFor(Band? band) => band?.Name switch
{
"80M" => 4,
"40M" => 3,
"20M" or "15M" or "10M" => 2,
_ => 0,
};
private string MultiplierValue(QsoContext qso)
{
string prefix = qso.CountryPrefix;
if (prefix.Length == 0)
{
return "";
}
// outside Europe on CW and SSB, only European countries are worked and
// the country alone is the multiplier
if (mode != ModeCategory.Digital && qso.Me.Continent != "EU")
{
return prefix;
}
if (AsiaticRussia.Contains(prefix))
{
return $"UA{CallAreaDigit(qso) ?? '9'}";
}
return CallAreaCountries.Contains(prefix) ? CallArea(qso) : prefix;
}
/// The country prefix with the call area digit on the end: W1AW is `K1`.
private static string CallArea(QsoContext qso) =>
CallAreaDigit(qso) is { } digit ? $"{qso.CountryPrefix}{digit}" : qso.CountryPrefix;
private static char? CallAreaDigit(QsoContext qso)
{
string? wpx = qso.Qso.Call.WpxPrefix();
return wpx is { Length: > 0 } && char.IsAsciiDigit(wpx[^1]) ? wpx[^1] : null;
}
private string ModeLabel() => mode switch
{
ModeCategory.Cw => "CW",
ModeCategory.Phone => "SSB",
_ => "RTTY",
};
}