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:
@@ -41,7 +41,7 @@ scorer: red for a dupe, green for a new multiplier, blue for points.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Contests | CQ WW (CW, SSB, RTTY), CQ WPX (CW, SSB, RTTY), WAE (CW, SSB, RTTY), ARRL DX, IARU HF, Sweepstakes, RTTY Roundup, NAQP, general logging, and user-defined `.udc` contests |
|
||||
| Contests | CQ WW (CW, SSB, RTTY), CQ WPX (CW, SSB, RTTY), WAE (CW, SSB, RTTY), ARRL DX, IARU HF, Sweepstakes, RTTY Roundup, NAQP, OK/OM DX, YOTA, general logging, and user-defined `.udc` contests |
|
||||
| Log | N1MM `.s3db`, Cabrillo 3.0 out, ADIF in and out |
|
||||
| While typing | dupe check, multiplier check, points, country and zone from the country file, exchange filled from a call history file |
|
||||
| Windows | entry, log, check, bandmap, available mults and Qs, score summary, telnet |
|
||||
|
||||
@@ -92,7 +92,7 @@ which is too much work per spot for the little it would add.
|
||||
the contest rules score it, but there is no digital window: no decoding, no
|
||||
transmitting, no interface to fldigi, MMTTY or similar.
|
||||
|
||||
**Contest coverage.** Eight families are built in, plus whatever `.udc` files
|
||||
**Contest coverage.** Ten families are built in, plus whatever `.udc` files
|
||||
are in the user-defined folder. N1MM ships well over a hundred. Opening a log
|
||||
from a contest that is in neither place fails with the contest name, which is
|
||||
the right answer but it is still a wall.
|
||||
@@ -117,12 +117,17 @@ default shape. The session, off-time and band-change settings
|
||||
as one long session.
|
||||
|
||||
N1MM's `.udc` format has no way to say that the exchange itself differs by the
|
||||
other station's country, as the OK/OM DX contest asks a district from OK and OM
|
||||
stations and a serial number from everyone else. That one needs code, as it
|
||||
does in N1MM, and `Contest.SkipsField` is where it goes. A contest scored by
|
||||
what this station sends — YOTA counts the sent exchange — needs code too, and
|
||||
reads `QsoContext.Entry`, which carries the entry categories and the sent
|
||||
exchange.
|
||||
other station's country. OK/OM DX is written in code for that reason — it asks
|
||||
a county from OK and OM stations and a serial number from everyone else — and
|
||||
`Contest.SkipsField` steps over the box that does not apply. The same is true
|
||||
of REF, ARRL 10M, the Ukrainian DX contest and the Russian DX contest, which
|
||||
are not written yet.
|
||||
|
||||
What is left in the station's own logs, in the order it works them: IOTA, EU
|
||||
DXC, CQ 160, XE RTTY, REF, SARTG, BARTG, Field Day Region 1, Oceania, All
|
||||
Asian, RDAC and ARRL 10M. Everything but IOTA, REF and ARRL 10M is an exchange
|
||||
we already store — a serial, an age, a zone or a district — so a `.udc` file
|
||||
covers it.
|
||||
|
||||
## Known rough edges
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ public sealed partial class EntryWindow
|
||||
{
|
||||
return;
|
||||
}
|
||||
CabrilloWriter writer = new(Logging.Contest, Logging.Me);
|
||||
CabrilloWriter writer = new(Logging.Contest, Logging.Me, Logging.Instance.Entry);
|
||||
CabrilloHeader header = new()
|
||||
{
|
||||
Contest = Logging.Contest.CabrilloName,
|
||||
|
||||
@@ -70,6 +70,8 @@ public interface Contest
|
||||
/// the whole score by the power category the operator entered in.
|
||||
int TotalScore(ScoreTally tally, ContestEntry entry);
|
||||
|
||||
/// The exchange columns of this contact's Cabrillo line.
|
||||
CabrilloExchange CabrilloExchange(Qso qso, StationInfo me);
|
||||
/// The exchange columns of this contact's Cabrillo line. The entry is here
|
||||
/// because the sent side can be what the operator typed into the contest
|
||||
/// setup rather than anything the station record holds.
|
||||
CabrilloExchange CabrilloExchange(Qso qso, StationInfo me, ContestEntry entry);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ public sealed class ContestRegistry
|
||||
new("WAE", "Worked All Europe DX", [ModeCategory.Cw, ModeCategory.Phone, ModeCategory.Digital], m => new Wae(m)),
|
||||
new("ARRLRTTY", "ARRL RTTY Roundup", [ModeCategory.Digital], _ => new RttyRoundup()),
|
||||
new("NAQP", "North American QSO Party", [ModeCategory.Cw, ModeCategory.Phone, ModeCategory.Digital], m => new NorthAmericanQsoParty(m)),
|
||||
new("OKOMDX", "Czech and Slovak Republics DX", [ModeCategory.Cw, ModeCategory.Phone], m => new OkOmDx(m)),
|
||||
new("YOTA", "YOTA Contest", [ModeCategory.Cw, ModeCategory.Phone], _ => new Yota()),
|
||||
new("DX", "General logging", [], _ => new GeneralLogging()),
|
||||
];
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@ namespace Nonemm.Contests;
|
||||
/// never plain `CQWW`. Our registry is keyed by the family, so a log written by
|
||||
/// N1MM has to have its contest name turned back into a family and a mode
|
||||
/// before the rules can be built.
|
||||
///
|
||||
/// The `.udc` files people use for OK/OM DX name it per mode as well, and one
|
||||
/// file per side of the contest — `OKOMDXS_DX` is the one a station outside OK
|
||||
/// and OM runs. Both sides are the same rules here, so both names resolve to
|
||||
/// the same contest.
|
||||
public static class N1mmContestNames
|
||||
{
|
||||
private static readonly Dictionary<string, (string Family, ModeCategory Mode)> Known =
|
||||
@@ -27,6 +32,10 @@ public static class N1mmContestNames
|
||||
["NAQPCW"] = ("NAQP", ModeCategory.Cw),
|
||||
["NAQPSSB"] = ("NAQP", ModeCategory.Phone),
|
||||
["NAQPRTTY"] = ("NAQP", ModeCategory.Digital),
|
||||
["OKOMDXC"] = ("OKOMDX", ModeCategory.Cw),
|
||||
["OKOMDXS"] = ("OKOMDX", ModeCategory.Phone),
|
||||
["OKOMDXC_DX"] = ("OKOMDX", ModeCategory.Cw),
|
||||
["OKOMDXS_DX"] = ("OKOMDX", ModeCategory.Phone),
|
||||
};
|
||||
|
||||
public static bool TryResolve(string name, out string family, out ModeCategory mode)
|
||||
|
||||
@@ -64,7 +64,7 @@ public sealed class ArrlDx : Contest
|
||||
|
||||
public int TotalScore(ScoreTally tally, ContestEntry entry) => tally.Points * tally.TotalMultipliers;
|
||||
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me) =>
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me, ContestEntry entry) =>
|
||||
new(
|
||||
[
|
||||
new CabrilloField(me.Callsign, 13),
|
||||
|
||||
@@ -113,7 +113,7 @@ public sealed class CqWorldWide : Contest
|
||||
|
||||
public int TotalScore(ScoreTally tally, ContestEntry entry) => tally.Points * tally.TotalMultipliers;
|
||||
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me) => IsRtty
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me, ContestEntry entry) => IsRtty
|
||||
? new CabrilloExchange(
|
||||
[
|
||||
new CabrilloField(me.Callsign, 13),
|
||||
|
||||
@@ -76,7 +76,7 @@ public sealed class CqWpx : Contest
|
||||
|
||||
public int TotalScore(ScoreTally tally, ContestEntry entry) => tally.Points * tally.TotalMultipliers;
|
||||
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me) =>
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me, ContestEntry entry) =>
|
||||
new(
|
||||
[
|
||||
new CabrilloField(me.Callsign, 13),
|
||||
|
||||
@@ -37,7 +37,7 @@ public sealed class GeneralLogging : Contest
|
||||
|
||||
public int TotalScore(ScoreTally tally, ContestEntry entry) => tally.Qsos;
|
||||
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me) =>
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me, ContestEntry entry) =>
|
||||
new(
|
||||
[new CabrilloField(me.Callsign, 13), new CabrilloField(qso.SentReport, 3)],
|
||||
[new CabrilloField(qso.Call.Text, 13), new CabrilloField(qso.ReceivedReport, 3)]);
|
||||
|
||||
@@ -60,7 +60,7 @@ public sealed class IaruHf : Contest
|
||||
|
||||
public int TotalScore(ScoreTally tally, ContestEntry entry) => tally.Points * tally.TotalMultipliers;
|
||||
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me) =>
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me, ContestEntry entry) =>
|
||||
new(
|
||||
[
|
||||
new CabrilloField(me.Callsign, 13),
|
||||
|
||||
@@ -56,7 +56,7 @@ public sealed class NorthAmericanQsoParty : Contest
|
||||
|
||||
public int TotalScore(ScoreTally tally, ContestEntry entry) => tally.Points * tally.TotalMultipliers;
|
||||
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me) =>
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me, ContestEntry entry) =>
|
||||
new(
|
||||
[
|
||||
new CabrilloField(me.Callsign, 13),
|
||||
|
||||
109
src/Nonemm.Contests/Rules/OkOmDx.cs
Normal file
109
src/Nonemm.Contests/Rules/OkOmDx.cs
Normal 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";
|
||||
}
|
||||
@@ -46,7 +46,7 @@ public sealed class RttyRoundup : Contest
|
||||
|
||||
public int TotalScore(ScoreTally tally, ContestEntry entry) => tally.Points * tally.TotalMultipliers;
|
||||
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me) =>
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me, ContestEntry entry) =>
|
||||
new(
|
||||
[
|
||||
new CabrilloField(me.Callsign, 13),
|
||||
|
||||
@@ -48,7 +48,7 @@ public sealed class Sweepstakes : Contest
|
||||
|
||||
/// Sweepstakes puts the callsign after the serial number and precedence
|
||||
/// rather than first, which is why the writer takes the whole column list.
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me) =>
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me, ContestEntry entry) =>
|
||||
new(
|
||||
[
|
||||
new CabrilloField($"{qso.SentNumber}", 4),
|
||||
|
||||
@@ -96,7 +96,7 @@ public sealed class Wae : Contest
|
||||
/// 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)
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me, ContestEntry entry)
|
||||
{
|
||||
if (WaeQtc.From(qso) is not { } qtc)
|
||||
{
|
||||
|
||||
70
src/Nonemm.Contests/Rules/Yota.cs
Normal file
70
src/Nonemm.Contests/Rules/Yota.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using Nonemm.Core;
|
||||
|
||||
namespace Nonemm.Contests.Rules;
|
||||
|
||||
/// The YOTA contest. The exchange is the operator's age, a contact with a
|
||||
/// young operator is worth more, and every age worked on a band is a
|
||||
/// multiplier.
|
||||
///
|
||||
/// N1MM has no class for this one — it is logged there through a `.udc` file,
|
||||
/// which is why the contest name is plain `YOTA`.
|
||||
public sealed class Yota : Contest
|
||||
{
|
||||
public string Name => "YOTA";
|
||||
|
||||
public string DisplayName => "YOTA Contest";
|
||||
|
||||
public string CabrilloName => "YOTA";
|
||||
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("Age", ExchangeSlot.Exchange1, ExchangeFieldKind.Number) { Width = 3 },
|
||||
];
|
||||
|
||||
public IReadOnlyList<string> MultiplierNames => ["Ages"];
|
||||
|
||||
public DupeScope DupeScope => DupeScope.PerBandAndMode;
|
||||
|
||||
public bool HasSerialNumbers => false;
|
||||
|
||||
public IReadOnlyList<ModeCategory> Modes => [ModeCategory.Cw, ModeCategory.Phone];
|
||||
|
||||
/// The operator's own age, which the contest setup asks for.
|
||||
public string SentExchangeFor(StationInfo me) => "";
|
||||
|
||||
/// A contact with an operator of 25 or under is worth 10 to 13 points by
|
||||
/// age, whatever the continent. Everyone else is worth 1 point on your own
|
||||
/// continent and 3 from another.
|
||||
public int PointsFor(QsoContext qso) => Age(qso) switch
|
||||
{
|
||||
null or > 25 => qso.IsSameContinent ? 1 : 3,
|
||||
<= 12 => 13,
|
||||
<= 16 => 12,
|
||||
<= 21 => 11,
|
||||
_ => 10,
|
||||
};
|
||||
|
||||
/// Each age worked on a band, whatever the mode.
|
||||
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso) =>
|
||||
Age(qso) is { } age ? [new Multiplier(1, age.ToString(), qso.Band?.Name ?? "")] : [];
|
||||
|
||||
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(entry.SentExchange, 3),
|
||||
],
|
||||
[
|
||||
new CabrilloField(qso.Call.Text, 13),
|
||||
new CabrilloField(qso.ReceivedReport, 3),
|
||||
new CabrilloField(qso.Exchange1, 3),
|
||||
]);
|
||||
|
||||
private static int? Age(QsoContext qso) =>
|
||||
int.TryParse(qso.Qso.Exchange1.Trim(), out int age) && age > 0 ? age : null;
|
||||
}
|
||||
@@ -115,7 +115,7 @@ public sealed class UserDefinedContest : Contest
|
||||
return (int)Math.Round(total * pointsMultiplier.ForEntry(entry));
|
||||
}
|
||||
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me) =>
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me, ContestEntry entry) =>
|
||||
new(
|
||||
[
|
||||
new CabrilloField(me.Callsign, 13),
|
||||
|
||||
@@ -11,11 +11,13 @@ public sealed class CabrilloWriter
|
||||
{
|
||||
private readonly Contest contest;
|
||||
private readonly StationInfo me;
|
||||
private readonly ContestEntry entry;
|
||||
|
||||
public CabrilloWriter(Contest contest, StationInfo me)
|
||||
public CabrilloWriter(Contest contest, StationInfo me, ContestEntry? entry = null)
|
||||
{
|
||||
this.contest = contest;
|
||||
this.me = me;
|
||||
this.entry = entry ?? new ContestEntry();
|
||||
}
|
||||
|
||||
public string Write(CabrilloHeader header, IEnumerable<Qso> qsos)
|
||||
@@ -62,7 +64,7 @@ public sealed class CabrilloWriter
|
||||
|
||||
public string QsoLine(Qso qso)
|
||||
{
|
||||
CabrilloExchange exchange = contest.CabrilloExchange(qso, me);
|
||||
CabrilloExchange exchange = contest.CabrilloExchange(qso, me, entry);
|
||||
StringBuilder line = new(contest.IsContact(qso) ? "QSO: " : "QTC: ");
|
||||
line.Append(CabrilloBands.Designator(qso.Frequency).PadLeft(5)).Append(' ');
|
||||
line.Append(qso.Mode.CabrilloCode.PadRight(2)).Append(' ');
|
||||
|
||||
75
tests/Nonemm.Contests.Tests/OkOmDxTests.cs
Normal file
75
tests/Nonemm.Contests.Tests/OkOmDxTests.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using Nonemm.Contests.Rules;
|
||||
using Nonemm.Core;
|
||||
using Nonemm.Core.Country;
|
||||
|
||||
namespace Nonemm.Contests.Tests;
|
||||
|
||||
public class OkOmDxTests
|
||||
{
|
||||
private static readonly Contest Ssb = new OkOmDx(ModeCategory.Phone);
|
||||
|
||||
private static ContestLog LogFor(StationInfo me) => new(Ssb, me, TestLog.CountryFile);
|
||||
|
||||
private static Qso FromOkOm(string call, string county, double kilohertz = 14_025) =>
|
||||
TestLog.Contact(call, kilohertz, mode: Modes.Usb, section: county);
|
||||
|
||||
private static Qso FromDx(string call, string number, double kilohertz = 14_025) =>
|
||||
TestLog.Contact(call, kilohertz, mode: Modes.Usb, exchange: number);
|
||||
|
||||
[Theory]
|
||||
[InlineData("OM3XYZ", 2)]
|
||||
[InlineData("OK1XYZ", 3)]
|
||||
public void AtHomeCzechiaAndSlovakiaAreTwoCountries(string call, int expected) =>
|
||||
Assert.Equal(expected, LogFor(TestLog.Slovakia).Judge(FromOkOm(call, "NOV")).Points);
|
||||
|
||||
[Theory]
|
||||
[InlineData("IK2XYZ", 3)]
|
||||
[InlineData("JA1XYZ", 5)]
|
||||
public void TheRestOfTheWorldIsScoredByContinent(string call, int expected) =>
|
||||
Assert.Equal(expected, LogFor(TestLog.Slovakia).Judge(FromDx(call, "014")).Points);
|
||||
|
||||
[Theory]
|
||||
[InlineData("OM3XYZ", 10)]
|
||||
[InlineData("DL9XYZ", 1)]
|
||||
[InlineData("IK2XYZ", 3)]
|
||||
[InlineData("JA1XYZ", 5)]
|
||||
public void AStationOutsideScoresTenForAnOkOmContact(string call, int expected) =>
|
||||
Assert.Equal(expected, LogFor(TestLog.Germany).Judge(FromOkOm(call, "NOV")).Points);
|
||||
|
||||
[Fact]
|
||||
public void CountriesAndCountiesBothCountOncePerBand()
|
||||
{
|
||||
ContestLog log = LogFor(TestLog.Slovakia);
|
||||
Assert.Equal(
|
||||
[1, 2],
|
||||
log.Judge(FromOkOm("OM3XYZ", "NOV")).NewMultipliers.Select(m => m.Index).ToList());
|
||||
log.Add(FromOkOm("OM3XYZ", "NOV"));
|
||||
Assert.Empty(log.Judge(FromOkOm("OM4XYZ", "NOV")).NewMultipliers);
|
||||
Assert.Single(log.Judge(FromOkOm("OM4XYZ", "PAR")).NewMultipliers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AStationOutsideOkOmBringsItsCountryAndNoCounty() =>
|
||||
Assert.Equal(
|
||||
[1],
|
||||
LogFor(TestLog.Slovakia).Judge(FromDx("IK2XYZ", "014")).NewMultipliers
|
||||
.Select(m => m.Index)
|
||||
.ToList());
|
||||
|
||||
/// The entry window walks the county box for an OK or OM station and the
|
||||
/// serial box for everyone else.
|
||||
[Fact]
|
||||
public void OnlyOneOfTheTwoBoxesIsWalked()
|
||||
{
|
||||
IReadOnlyList<ExchangeField> fields = Ssb.ExchangeFieldsFor(TestLog.Slovakia);
|
||||
CountryLookup? okom = TestLog.CountryFile.Find("OM3XYZ");
|
||||
CountryLookup? dx = TestLog.CountryFile.Find("IK2XYZ");
|
||||
|
||||
Assert.Equal(
|
||||
[ExchangeSlot.ReceivedReport, ExchangeSlot.Exchange1],
|
||||
fields.Where(f => !Ssb.SkipsField(f, dx)).Select(f => f.Slot).ToList());
|
||||
Assert.Equal(
|
||||
[ExchangeSlot.ReceivedReport, ExchangeSlot.Section],
|
||||
fields.Where(f => !Ssb.SkipsField(f, okom)).Select(f => f.Slot).ToList());
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,10 @@ public static class TestLog
|
||||
PY,PP;
|
||||
Asiatic Russia: 17: 30: AS: 55.88: -84.10: -7.0: UA9:
|
||||
RM8,RM9,RM0,UA8,UA9,UA0,R8,R9,R0;
|
||||
Slovak Republic: 15: 28: EU: 48.67: -19.50: -1.0: OM:
|
||||
OM;
|
||||
Czech Republic: 15: 28: EU: 49.80: -15.47: -1.0: OK:
|
||||
OK,OL;
|
||||
""";
|
||||
|
||||
public static readonly CountryFile CountryFile = CountryFile.Parse(Countries);
|
||||
@@ -46,6 +50,16 @@ public static class TestLog
|
||||
ArrlSection = "CT",
|
||||
};
|
||||
|
||||
public static readonly StationInfo Slovakia = new()
|
||||
{
|
||||
Callsign = "OM5M",
|
||||
CqZone = 15,
|
||||
ItuZone = 28,
|
||||
Continent = "EU",
|
||||
CountryPrefix = "OM",
|
||||
County = "BRA",
|
||||
};
|
||||
|
||||
public static Qso Contact(
|
||||
string call,
|
||||
double kilohertz = 14_025,
|
||||
|
||||
49
tests/Nonemm.Contests.Tests/YotaTests.cs
Normal file
49
tests/Nonemm.Contests.Tests/YotaTests.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Nonemm.Contests.Rules;
|
||||
using Nonemm.Core;
|
||||
|
||||
namespace Nonemm.Contests.Tests;
|
||||
|
||||
public class YotaTests
|
||||
{
|
||||
private static ContestLog LogFor(StationInfo me) => new(new Yota(), me, TestLog.CountryFile);
|
||||
|
||||
private static Qso Contact(string call, string age, double kilohertz = 14_025, Mode? mode = null) =>
|
||||
TestLog.Contact(call, kilohertz, mode: mode, exchange: age);
|
||||
|
||||
[Theory]
|
||||
[InlineData("9", 13)]
|
||||
[InlineData("12", 13)]
|
||||
[InlineData("13", 12)]
|
||||
[InlineData("16", 12)]
|
||||
[InlineData("17", 11)]
|
||||
[InlineData("21", 11)]
|
||||
[InlineData("22", 10)]
|
||||
[InlineData("25", 10)]
|
||||
[InlineData("26", 1)]
|
||||
public void AYoungOperatorIsWorthMore(string age, int expected) =>
|
||||
Assert.Equal(expected, LogFor(TestLog.Germany).Judge(Contact("IK2XYZ", age)).Points);
|
||||
|
||||
/// The age pays the same from anywhere; only the contacts with everyone
|
||||
/// else are scored by continent.
|
||||
[Fact]
|
||||
public void AgeBeatsTheContinent()
|
||||
{
|
||||
ContestLog log = LogFor(TestLog.Germany);
|
||||
Assert.Equal(13, log.Judge(Contact("JA1XYZ", "11")).Points);
|
||||
Assert.Equal(3, log.Judge(Contact("JA1XYZ", "44")).Points);
|
||||
Assert.Equal(1, log.Judge(Contact("IK2XYZ", "44")).Points);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnAgeCountsOncePerBandWhateverTheMode()
|
||||
{
|
||||
ContestLog log = LogFor(TestLog.Germany);
|
||||
log.Add(Contact("IK2XYZ", "19"));
|
||||
Assert.Empty(log.Judge(Contact("DL9XYZ", "19", mode: Modes.Usb)).NewMultipliers);
|
||||
Assert.Single(log.Judge(Contact("DL9XYZ", "19", 7_025)).NewMultipliers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnExchangeThatIsNoAgeBringsNoMultiplier() =>
|
||||
Assert.Empty(LogFor(TestLog.Germany).Judge(Contact("IK2XYZ", "")).NewMultipliers);
|
||||
}
|
||||
Reference in New Issue
Block a user