WAE is two contests in one. The QSOs are scored like any other, except that a multiplier is worth four on 80 metres, three on 40 and two on the high bands, so Multiplier grew a weight and ScoreTally sums weights rather than counting. Europe works the rest of the world and the rest of the world works Europe; on RTTY that rule is dropped and everybody works everybody. The other half is QTC traffic: one station reads back contacts it has already made, ten to a series, and every line is a point for both stations. Those lines live in the log as rows of their own, exactly where N1MM puts them — Exchange1 says SQTC or RQTC, the reported contact's time, call and number go in the sent report, received report and sent number columns, and the series in misc text. So a log written by either program opens in the other with its traffic intact. A QTC row is not a worked station: Contest.IsContact says so, and ContestLog keeps such rows out of the dupe index and the multiplier index while still counting their points. Cabrillo writes them as QTC: records. Checked against eight real WAE logs rather than against my own reading of the rules. Points agree with N1MM on all eight, to the contact. Weighted multipliers agree on the six logs from 2022 on; the two older ones differ only where the country file has since changed its mind, and in one place on purpose: N1MM counts a callsign it cannot place as a multiplier with an empty value. That comparison turned up two bugs of ours. A KG4 call is Guantanamo Bay only when it is three or five characters long — KG4NE is, KG4IGC is an ordinary US call — and the country file cannot say so, so every program that reads it carries the rule. And Callsign is a record holding a list of modifiers, so the generated equality compared that list by reference and two parses of one call came out different. Entering QTCs while operating is not there yet; the window is next. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
190 lines
6.6 KiB
C#
190 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 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) || !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",
|
|
};
|
|
}
|