Reading the source again after this was tried turned up several things it had
wrong or left out.
The TU message went out only when reading a series out. N1MM sends it whichever
way the traffic went, and lets it name the series: {QTC} in it stands for the
header, so TU {QTC} 73 goes out as TU QTC 3/10 73.
The RX Ready button was hidden while sending and sent nothing on CW. It is
N1MM's QRV button: reading out it says R U QRV and keys QRV?, taking down it
says QRV and keys QRV.
The fields were asked for again with shift and Enter, which was made up. N1MM
uses the number keys, and writes them under the lines: 1, 2, 3 and 4 send the
time, call, serial or header of the last line again while reading out, and
shift with 1, 2 or 3 asks for that field while taking down.
The buttons now carry N1MM's names for the direction the traffic is going —
R U QRV, Snd Hdr, Snd1 to Snd10, Exit reading out; QRV, Hdr Agn, Agn1 to Agn10,
Exit taking down — and the Cfm column and Clear are hidden while reading out,
as N1MM hides them. The separate Send Hdr button is gone: N1MM has one button
there that sends the header one way and asks for it the other.
The log window gained N1MM's QTC? column for a contest that carries QTC
traffic. It holds the series, so a QTC row can be told from a contact; before
this a QTC row read as an ordinary one with odd values in the report columns.
Driven under Xvfb against a fake cwdaemon. Reading out: QRV?, QTC 3/2,
2319 DL7GGG 03, then 2319 and QTC 3/2 from the number keys, and
TU QTC 3/2 73 on Exit. Taking down: QRV, AGN TIME, AGN CALL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
192 lines
6.7 KiB
C#
192 lines
6.7 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) || !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",
|
|
};
|
|
}
|