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 CallAreaCountries = new HashSet(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 AsiaticRussia = new HashSet(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 ExchangeFieldsFor(StationInfo me) => [ new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number), ]; public IReadOnlyList MultiplierNames => ["Mults"]; public DupeScope DupeScope => DupeScope.PerBand; public bool HasSerialNumbers => true; public IReadOnlyList 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 MultipliersFor(QsoContext qso) { int weight = WeightFor(qso.Band); if (weight == 0 || !IsWorkable(qso) || !qso.Qso.Call.CountsForEntity) { return []; } string value = MultiplierValue(qso); return value.Length == 0 ? [] : [new Multiplier(1, value, qso.Band?.Name ?? "", weight)]; } public int TotalScore(ScoreTally tally) => 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) { 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", }; }