Add the twelve contests left in the station's log

REF, the Ukrainian DX contest, the BARTG Sprint, ARRL 10M, Russian DX RTTY,
RDAC, YO DX HF, Oceania DX, IARU Region 1 Field Day, SARTG RTTY, All Asian and
SA 10. Every contest name in the log now opens, and every one of these but
Field Day scores its log contact for contact as N1MM did.

Most are written from N1MM's own class and checked against the log. Three
things the log settled that the class does not say plainly: ARRL 10M counts its
multipliers once per mode rather than once, the BARTG Sprint counts a continent
once in the contest while its countries and call areas count per band, and RDAC
counts nothing but the districts and scores nothing for a station that sends
none.

All Asian and SA 10 have no class in N1MM, which logs them from a .udc file, so
those two are written from the published rules and the log. docs/unfinished.md
lists what is still not settled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
2026-08-31 09:43:03 +00:00
parent 7d9bc05e9a
commit 264036f0f2
24 changed files with 1208 additions and 11 deletions

View File

@@ -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, OK/OM DX, YOTA, IOTA, EU DX Contest, CQ 160, Mexico RTTY, 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, IOTA, EU DX Contest, CQ 160, ARRL 10M, REF, Ukrainian DX, Russian DX RTTY, RDAC, YO DX HF, Oceania DX, All Asian, SA 10, SARTG RTTY, BARTG Sprint, Mexico RTTY, IARU Region 1 Field Day, 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 |

View File

@@ -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.** Fourteen families are built in, plus whatever `.udc` files
**Contest coverage.** Twenty-six 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.
@@ -123,15 +123,26 @@ a county from OK and OM stations and a serial number from everyone else — and
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: REF, the
Ukrainian DX contest, SA-10, BARTG Sprint, ARRL 10M, Field Day Region 1, the
Russian DX contest, RDAC, All Asian, YO HF DX and Oceania.
Every contest in the station's own logs can now be opened, and all but five of
its 78 contest instances score contact for contact as N1MM did.
The EU DX Contest is written from N1MM's own class. The station's logs were
made with the `EU_DXC.udc` file instead, which scores a contact inside your own
country 1 point where the class scores 2, and counts a district code once in
the contest where the class counts it once per band. Those are the only
differences left in those two logs.
What still differs, and why:
- **The EU DX Contest** is written from N1MM's own class. The station's logs
were made with the `EU_DXC.udc` file instead, which scores a contact inside
your own country 1 point where the class scores 2, and counts a district code
once in the contest where the class counts it once per band.
- **Field Day Region 1**: every contact in that log holds 1 point and no
multiplier, which is not what N1MM's own class works out, so the log looks
like it was never scored. The rules here follow the class: 5 points for a
portable station, 2 for one in Region 1 at home and 3 from outside it.
- **All Asian and SA 10** have no class in N1MM — it logs them from a `.udc`
file — so the points and multipliers are written from the published rules and
from the station's log. Their Cabrillo names have not been checked against a
sponsor's robot.
- **The BARTG Sprint** stores the time of the contact in the exchange column,
which N1MM fills in by itself. Nothing here fills it in yet, so a log written
here carries the serial number alone.
## Known rough edges

View File

@@ -24,6 +24,18 @@ public sealed class ContestRegistry
new("EUDXC", "EU DX Contest", [ModeCategory.Cw, ModeCategory.Phone], _ => new EuDxContest()),
new("CQ160", "CQ World-Wide 160 Metre", [ModeCategory.Cw, ModeCategory.Phone], m => new CqOneSixty(m)),
new("XERTTY", "Mexico RTTY Contest", [ModeCategory.Digital], _ => new MexicoRtty()),
new("REF", "French REF DX contest", [ModeCategory.Cw, ModeCategory.Phone], m => new FrenchRef(m)),
new("UKRAINDX", "Ukrainian DX", [ModeCategory.Cw, ModeCategory.Phone], _ => new UkrainianDx()),
new("BARTGRTTYS", "BARTG RTTY Sprint", [ModeCategory.Digital], _ => new BartgSprint()),
new("ARRL10M", "ARRL 10 Metre Contest", [ModeCategory.Cw, ModeCategory.Phone], _ => new Arrl10Metre()),
new("RUSDXRTTY", "Russian DX RTTY", [ModeCategory.Digital], _ => new RussianDxRtty()),
new("RDAC", "Russian District Award Contest", [ModeCategory.Cw, ModeCategory.Phone], _ => new RussianDistrictAward()),
new("YOHFDX", "YO DX HF", [ModeCategory.Cw, ModeCategory.Phone], _ => new YoDxHf()),
new("OCEANIA", "Oceania DX", [ModeCategory.Cw, ModeCategory.Phone], m => new OceaniaDx(m)),
new("FDREG1", "IARU Region 1 Field Day", [ModeCategory.Cw, ModeCategory.Phone], _ => new FieldDayRegionOne()),
new("SARTGRTTY", "SARTG World Wide RTTY", [ModeCategory.Digital], _ => new SartgRtty()),
new("ALLASIA", "All Asian DX", [ModeCategory.Cw, ModeCategory.Phone], m => new AllAsian(m)),
new("SA10", "SA 10 Metre Contest", [ModeCategory.Cw, ModeCategory.Phone], _ => new SouthAmerica10()),
new("DX", "General logging", [], _ => new GeneralLogging()),
];

View File

@@ -39,6 +39,14 @@ public static class N1mmContestNames
["CQ160CW"] = ("CQ160", ModeCategory.Cw),
["CQ160SSB"] = ("CQ160", ModeCategory.Phone),
["EU_DXC"] = ("EUDXC", ModeCategory.Cw),
["REFCW"] = ("REF", ModeCategory.Cw),
["REFSSB"] = ("REF", ModeCategory.Phone),
["OCEANIACW"] = ("OCEANIA", ModeCategory.Cw),
["OCEANIASSB"] = ("OCEANIA", ModeCategory.Phone),
["BARTGSRTTY"] = ("BARTGRTTYS", ModeCategory.Digital),
["ALLASIACW"] = ("ALLASIA", ModeCategory.Cw),
["ALLASIASSB"] = ("ALLASIA", ModeCategory.Phone),
["SA10_DX"] = ("SA10", ModeCategory.Phone),
};
public static bool TryResolve(string name, out string family, out ModeCategory mode)

View File

@@ -0,0 +1,72 @@
using Nonemm.Core;
namespace Nonemm.Contests.Rules;
/// The JARL All Asian DX contest. Only contacts with Asia count for a station
/// outside it. The exchange is the operator's age, the points go by band, and
/// each prefix worked counts once per band.
public sealed class AllAsian : Contest
{
private readonly ModeCategory mode;
public AllAsian(ModeCategory mode) => this.mode = mode;
public string Name => mode == ModeCategory.Cw ? "ALLASIACW" : "ALLASIASSB";
public string DisplayName => $"All Asian DX {ModeLabel()}";
public string CabrilloName => mode == ModeCategory.Cw ? "AA-CW" : "AA-SSB";
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 => ["Prefixes"];
public DupeScope DupeScope => DupeScope.PerBand;
public bool HasSerialNumbers => false;
public IReadOnlyList<ModeCategory> Modes => [mode];
public string SentExchangeFor(StationInfo me) => "";
public int PointsFor(QsoContext qso) => IsWorkable(qso) ? PointsOn(qso.Band) : 0;
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso) =>
IsWorkable(qso) && qso.Qso.Call.WpxPrefix() is { } prefix
? [new Multiplier(1, prefix, 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),
]);
/// One side of every contact has to be in Asia.
private static bool IsWorkable(QsoContext qso) =>
qso.Continent == "AS" || qso.Me.Continent == "AS";
private static int PointsOn(Band? band) => band?.Name switch
{
"160M" or "80M" => 3,
"40M" => 2,
null => 0,
_ => 1,
};
private string ModeLabel() => mode == ModeCategory.Cw ? "CW" : "SSB";
}

View File

@@ -0,0 +1,79 @@
using Nonemm.Contests.Multipliers;
using Nonemm.Core;
using Nonemm.Core.Country;
namespace Nonemm.Contests.Rules;
/// The ARRL 10 metre contest. A CW contact is 4 points and a phone contact 2.
/// US, Canadian and Mexican stations send a state or province, everyone else a
/// serial number, and both the states and the countries are multipliers, each
/// counted once per mode: the contest is one band, and CW and phone count
/// separately.
public sealed class Arrl10Metre : Contest
{
public string Name => "ARRL10M";
public string DisplayName => "ARRL 10 Metre Contest";
public string CabrilloName => "ARRL-10";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("S/P", ExchangeSlot.Section, ExchangeFieldKind.UsStateOrCanadianProvince),
new ExchangeField("Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Number),
];
public bool SkipsField(ExchangeField field, CountryLookup? their) => field.Slot switch
{
ExchangeSlot.Section => their is not null && !SendsState(their.Entity.PrimaryPrefix),
ExchangeSlot.Exchange1 => their is not null && SendsState(their.Entity.PrimaryPrefix),
_ => false,
};
public IReadOnlyList<string> MultiplierNames => ["States", "Countries"];
public DupeScope DupeScope => DupeScope.PerMode;
public bool HasSerialNumbers => true;
public IReadOnlyList<ModeCategory> Modes => [ModeCategory.Cw, ModeCategory.Phone];
public string SentExchangeFor(StationInfo me) =>
SendsState(me.CountryPrefix)
? me.State.Length > 0 ? me.State : me.Province
: "001";
public int PointsFor(QsoContext qso) => qso.ModeCategory == ModeCategory.Cw ? 4 : 2;
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso)
{
string mode = qso.ModeCategory.ToString();
string area = StatesAndProvinces.ArrlDxArea(qso.Qso.Section);
if (StatesAndProvinces.ArrlDxMultipliers.Contains(area))
{
return [new Multiplier(1, area, mode)];
}
return qso.CountryPrefix.Length == 0 ? [] : [new Multiplier(2, qso.CountryPrefix, mode)];
}
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(SentExchangeFor(me), 6),
],
[
new CabrilloField(qso.Call.Text, 13),
new CabrilloField(qso.ReceivedReport, 3),
new CabrilloField(qso.Section.Length > 0 ? qso.Section : qso.Exchange1, 6),
]);
/// The states and provinces of the US, Canada and Mexico are the exchange;
/// the rest of the world sends a number.
private static bool SendsState(string countryPrefix) => countryPrefix is "K" or "VE" or "XE";
}

View File

@@ -0,0 +1,71 @@
using Nonemm.Core;
namespace Nonemm.Contests.Rules;
/// The BARTG RTTY Sprint. Every contact is one point; the multipliers are the
/// countries and the US and Canadian call areas once per band, and the
/// continents once in the contest.
///
/// The exchange is a serial number. N1MM also writes the time of the contact
/// into the exchange column, which the Cabrillo line carries.
public sealed class BartgSprint : Contest
{
public string Name => "BARTGRTTYS";
public string DisplayName => "BARTG RTTY Sprint";
public string CabrilloName => "BARTG-SPRINT";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[
new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
];
public IReadOnlyList<string> MultiplierNames => ["Countries", "Areas", "Continents"];
public DupeScope DupeScope => DupeScope.PerBand;
public bool HasSerialNumbers => true;
public IReadOnlyList<ModeCategory> Modes => [ModeCategory.Digital];
public string SentExchangeFor(StationInfo me) => "001";
public int PointsFor(QsoContext qso) => 1;
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 area = qso.Qso.Section.Trim().ToUpperInvariant();
if (area.Length > 0)
{
found.Add(new Multiplier(2, area, band));
}
if (qso.Continent.Length > 0)
{
found.Add(new Multiplier(3, qso.Continent, ""));
}
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.SentNumber:000}", 4),
new CabrilloField(qso.TimestampUtc.ToString("HHmm"), 4),
],
[
new CabrilloField(qso.Call.Text, 13),
new CabrilloField($"{qso.ReceivedNumber:000}", 4),
new CabrilloField(qso.Exchange1, 4),
]);
}

View File

@@ -0,0 +1,72 @@
using Nonemm.Core;
namespace Nonemm.Contests.Rules;
/// The IARU Region 1 Field Day. A portable station is worth 5 points, a station
/// in Region 1 operating from home 2, and one outside Region 1 3. The exchange
/// is a serial number and the countries are multipliers, once per band.
public sealed class FieldDayRegionOne : Contest
{
public string Name => "FDREG1";
public string DisplayName => "IARU Region 1 Field Day";
public string CabrilloName => "FIELDDAY-REGION-1";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
];
public IReadOnlyList<string> MultiplierNames => ["Countries"];
public DupeScope DupeScope => DupeScope.PerBandAndMode;
public bool HasSerialNumbers => true;
public IReadOnlyList<ModeCategory> Modes => [ModeCategory.Cw, ModeCategory.Phone];
public string SentExchangeFor(StationInfo me) => "001";
public int PointsFor(QsoContext qso)
{
if (IsPortable(qso.Qso.Call))
{
return 5;
}
return IsRegionOne(qso.Continent) ? 2 : 3;
}
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso) =>
qso.CountryPrefix.Length == 0
? []
: [new Multiplier(1, qso.CountryPrefix, 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($"{qso.SentNumber:000}", 4),
],
[
new CabrilloField(qso.Call.Text, 13),
new CabrilloField(qso.ReceivedReport, 3),
new CabrilloField($"{qso.ReceivedNumber:000}", 4),
]);
/// A field day station signs portable, mobile or /A.
private static bool IsPortable(Callsign call) =>
call.Text.EndsWith("/P", StringComparison.OrdinalIgnoreCase)
|| call.Text.EndsWith("/M", StringComparison.OrdinalIgnoreCase)
|| call.Text.EndsWith("/A", StringComparison.OrdinalIgnoreCase);
/// Region 1 is Europe, Africa and the Middle East; the country file names
/// the first two, and Asia is split between regions, which the continent
/// alone cannot say.
private static bool IsRegionOne(string continent) => continent is "EU" or "AF";
}

View File

@@ -0,0 +1,104 @@
using Nonemm.Core;
using Nonemm.Core.Country;
namespace Nonemm.Contests.Rules;
/// The French REF DX contest, CW and phone. A French station sends its
/// department, everyone else a serial number. For a station outside France a
/// contact is 1 point on its own continent and 3 from another, and the
/// multipliers are the French departments and the French entities overseas.
public sealed class FrenchRef : Contest
{
/// France and everything the contest counts as French: Corsica, the
/// overseas departments and territories, and the Antarctic bases.
private static readonly IReadOnlySet<string> French = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"F", "TK", "FG", "FH", "FJ", "FM", "FP", "FS", "FW", "FY", "FK", "TX0", "FO", "FR",
"FT5X", "FT5Y", "FT5Z", "FT5W", "FT8X", "FT8Y", "FT8Z", "FT8W", "CE9",
};
private readonly ModeCategory mode;
public FrenchRef(ModeCategory mode) => this.mode = mode;
public string Name => mode == ModeCategory.Cw ? "REFCW" : "REFSSB";
public string DisplayName => $"French REF DX contest {ModeLabel()}";
public string CabrilloName => mode == ModeCategory.Cw ? "REF-CW" : "REF-SSB";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Dept", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 3 },
new ExchangeField("Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Number),
];
public bool SkipsField(ExchangeField field, CountryLookup? their) => field.Slot switch
{
ExchangeSlot.Section => their is not null && their.Entity.PrimaryPrefix != "F",
ExchangeSlot.Exchange1 => their is not null && their.Entity.PrimaryPrefix == "F",
_ => false,
};
public IReadOnlyList<string> MultiplierNames => ["Mults"];
public DupeScope DupeScope => DupeScope.PerBand;
public bool HasSerialNumbers => true;
public IReadOnlyList<ModeCategory> Modes => [mode];
public string SentExchangeFor(StationInfo me) =>
me.CountryPrefix == "F" ? me.County : "001";
/// A French operator gets 6 points for another French station on its own
/// continent and 15 from another, and 1 or 2 points for everyone else.
public int PointsFor(QsoContext qso)
{
if (IsFrench(qso.Me.CountryPrefix))
{
return IsFrench(qso.CountryPrefix)
? qso.IsSameContinent ? 6 : 15
: qso.IsSameContinent ? 1 : 2;
}
return qso.IsSameContinent ? 1 : 3;
}
/// A French operator counts countries; everyone else counts the French
/// departments and the French entities overseas, each once per band.
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso)
{
string band = qso.Band?.Name ?? "";
if (IsFrench(qso.Me.CountryPrefix))
{
return qso.CountryPrefix.Length == 0 ? [] : [new Multiplier(1, qso.CountryPrefix, band)];
}
if (qso.CountryPrefix == "F")
{
string department = qso.Qso.Section.Trim().ToUpperInvariant();
return department.Length == 0 ? [] : [new Multiplier(1, department, band)];
}
return IsFrench(qso.CountryPrefix) ? [new Multiplier(1, qso.CountryPrefix, band)] : [];
}
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(me.CountryPrefix == "F" ? me.County : $"{qso.SentNumber:000}", 6),
],
[
new CabrilloField(qso.Call.Text, 13),
new CabrilloField(qso.ReceivedReport, 3),
new CabrilloField(qso.Section.Length > 0 ? qso.Section : qso.Exchange1, 6),
]);
private static bool IsFrench(string countryPrefix) => French.Contains(countryPrefix);
private string ModeLabel() => mode == ModeCategory.Cw ? "CW" : "SSB";
}

View File

@@ -0,0 +1,75 @@
using Nonemm.Core;
namespace Nonemm.Contests.Rules;
/// The Oceania DX contest, CW and phone. Only contacts with Oceania count for
/// a station outside it, and they are worth more the lower the band: 20 points
/// on 160 metres, 10 on 80, 5 on 40 and 1 on the higher bands. Each prefix
/// worked counts once per band.
public sealed class OceaniaDx : Contest
{
private readonly ModeCategory mode;
public OceaniaDx(ModeCategory mode) => this.mode = mode;
public string Name => mode == ModeCategory.Cw ? "OceaniaCW" : "OceaniaSSB";
public string DisplayName => $"Oceania DX {ModeLabel()}";
public string CabrilloName => mode == ModeCategory.Cw ? "Oceania-DX-CW" : "Oceania-DX-SSB";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
];
public IReadOnlyList<string> MultiplierNames => ["Prefixes"];
public DupeScope DupeScope => DupeScope.PerBand;
public bool HasSerialNumbers => true;
public IReadOnlyList<ModeCategory> Modes => [mode];
public string SentExchangeFor(StationInfo me) => "001";
public int PointsFor(QsoContext qso) =>
IsWorkable(qso) ? PointsOn(qso.Band) : 0;
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso) =>
IsWorkable(qso) && qso.Qso.Call.WpxPrefix() is { } prefix
? [new Multiplier(1, prefix, 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($"{qso.SentNumber:000}", 4),
],
[
new CabrilloField(qso.Call.Text, 13),
new CabrilloField(qso.ReceivedReport, 3),
new CabrilloField($"{qso.ReceivedNumber:000}", 4),
]);
/// One side of every contact has to be in Oceania.
private static bool IsWorkable(QsoContext qso) =>
qso.Continent == "OC" || qso.Me.Continent == "OC";
private static int PointsOn(Band? band) => band?.Name switch
{
"160M" => 20,
"80M" => 10,
"40M" => 5,
null => 0,
_ => 1,
};
private string ModeLabel() => mode == ModeCategory.Cw ? "CW" : "SSB";
}

View File

@@ -0,0 +1,62 @@
using Nonemm.Core;
namespace Nonemm.Contests.Rules;
/// The Russian District Award contest. Only a station that sends a district
/// code counts: it is worth 10 points and its district is a multiplier once
/// per band. Everyone else is worth nothing.
public sealed class RussianDistrictAward : Contest
{
public string Name => "RDAC";
public string DisplayName => "Russian District Award Contest";
public string CabrilloName => "RDAC";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("RDA", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 5 },
];
/// N1MM names a country multiplier for this contest and never counts one,
/// and the score summary shows both, so the districts stay in the second
/// column here too.
public IReadOnlyList<string> MultiplierNames => ["Countries", "Districts"];
public DupeScope DupeScope => DupeScope.PerBandAndMode;
public bool HasSerialNumbers => false;
public IReadOnlyList<ModeCategory> Modes => [ModeCategory.Cw, ModeCategory.Phone];
public string SentExchangeFor(StationInfo me) => me.County;
public int PointsFor(QsoContext qso) => District(qso.Qso).Length > 0 ? 10 : 0;
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso)
{
string district = District(qso.Qso);
return district.Length == 0
? []
: [new Multiplier(2, district, 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(me.County, 6),
],
[
new CabrilloField(qso.Call.Text, 13),
new CabrilloField(qso.ReceivedReport, 3),
new CabrilloField(District(qso), 6),
]);
private static string District(Qso qso) => qso.Section.Trim().ToUpperInvariant();
}

View File

@@ -0,0 +1,71 @@
using Nonemm.Core;
using Nonemm.Core.Country;
namespace Nonemm.Contests.Rules;
/// The Russian DX RTTY contest. A Russian station sends its oblast, everyone
/// else their CQ zone. A contact on your own continent is 5 points and one
/// from another 10, and the countries and the oblasts are multipliers, each
/// once per band.
public sealed class RussianDxRtty : Contest
{
public string Name => "RUSDXRTTY";
public string DisplayName => "Russian DX RTTY";
public string CabrilloName => "RADIO-WW-RTTY";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Oblast/Zone", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 3 },
];
public IReadOnlyList<string> MultiplierNames => ["Countries", "Oblasts"];
public DupeScope DupeScope => DupeScope.PerBand;
public bool HasSerialNumbers => false;
public IReadOnlyList<ModeCategory> Modes => [ModeCategory.Digital];
public string SentExchangeFor(StationInfo me) =>
IsRussian(me.CountryPrefix) ? me.County : me.CqZone.ToString();
public int PointsFor(QsoContext qso) => qso.IsSameContinent ? 5 : 10;
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 oblast = qso.Qso.Section.Trim().ToUpperInvariant();
if (IsRussian(qso.CountryPrefix) && oblast.Length > 0)
{
found.Add(new Multiplier(2, oblast, 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(SentExchangeFor(me), 6),
],
[
new CabrilloField(qso.Call.Text, 13),
new CabrilloField(qso.ReceivedReport, 3),
new CabrilloField(qso.Section, 6),
]);
private static bool IsRussian(string countryPrefix) =>
countryPrefix is "UA" or "UA2" or "UA9";
}

View File

@@ -0,0 +1,66 @@
using Nonemm.Core;
namespace Nonemm.Contests.Rules;
/// The SARTG World Wide RTTY contest. A contact inside your own country is 5
/// points, one on your own continent 10 and one from another 15. The countries
/// count once per band, and so do the US, Canadian and Japanese call areas.
public sealed class SartgRtty : Contest
{
public string Name => "SARTGRTTY";
public string DisplayName => "SARTG World Wide RTTY";
public string CabrilloName => "SARTG-RTTY";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
];
public IReadOnlyList<string> MultiplierNames => ["Countries", "Areas"];
public DupeScope DupeScope => DupeScope.PerBand;
public bool HasSerialNumbers => true;
public IReadOnlyList<ModeCategory> Modes => [ModeCategory.Digital];
public string SentExchangeFor(StationInfo me) => "001";
public int PointsFor(QsoContext qso) =>
qso.IsSameCountry ? 5 : qso.IsSameContinent ? 10 : 15;
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 area = qso.Qso.Section.Trim().ToUpperInvariant();
if (area.Length > 0)
{
found.Add(new Multiplier(2, area, 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($"{qso.SentNumber:000}", 4),
],
[
new CabrilloField(qso.Call.Text, 13),
new CabrilloField(qso.ReceivedReport, 3),
new CabrilloField($"{qso.ReceivedNumber:000}", 4),
]);
}

View File

@@ -0,0 +1,67 @@
using Nonemm.Core;
namespace Nonemm.Contests.Rules;
/// The SA 10 metre contest. A South American station is worth 4 points and
/// everyone else 2. The exchange is a CQ zone, and both the prefixes and the
/// zones are multipliers. One band, so nothing is counted per band.
///
/// N1MM logs this one through a `.udc` file, so the Cabrillo name here is the
/// contest name and has not been checked against the sponsor's robot.
public sealed class SouthAmerica10 : Contest
{
public string Name => "SA10";
public string DisplayName => "SA 10 Metre Contest";
public string CabrilloName => "SA10";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Zone", ExchangeSlot.Section, ExchangeFieldKind.CqZone),
];
public IReadOnlyList<string> MultiplierNames => ["Prefixes", "Zones"];
public DupeScope DupeScope => DupeScope.PerMode;
public bool HasSerialNumbers => false;
public IReadOnlyList<ModeCategory> Modes => [ModeCategory.Cw, ModeCategory.Phone];
public string SentExchangeFor(StationInfo me) => me.CqZone.ToString();
public int PointsFor(QsoContext qso) => qso.Continent == "SA" ? 4 : 2;
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso)
{
List<Multiplier> found = [];
if (qso.Qso.Call.WpxPrefix() is { } prefix)
{
found.Add(new Multiplier(1, prefix, ""));
}
string zone = qso.Qso.Section.Trim();
if (zone.Length > 0)
{
found.Add(new Multiplier(2, zone, ""));
}
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(me.CqZone.ToString(), 3),
],
[
new CabrilloField(qso.Call.Text, 13),
new CabrilloField(qso.ReceivedReport, 3),
new CabrilloField(qso.Section, 3),
]);
}

View File

@@ -0,0 +1,89 @@
using Nonemm.Core;
using Nonemm.Core.Country;
namespace Nonemm.Contests.Rules;
/// The Ukrainian DX contest. A Ukrainian station sends its oblast, everyone
/// else a serial number, and a contact with Ukraine is worth 10 points to a
/// station outside it. The multipliers are the countries and the oblasts, each
/// once per band.
public sealed class UkrainianDx : Contest
{
public string Name => "UKRAINDX";
public string DisplayName => "Ukrainian DX";
public string CabrilloName => "UKRAINIAN-DX";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Oblast", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 3 },
new ExchangeField("Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Number),
];
public bool SkipsField(ExchangeField field, CountryLookup? their) => field.Slot switch
{
ExchangeSlot.Section => their is not null && !IsUkrainian(their.Entity.PrimaryPrefix),
ExchangeSlot.Exchange1 => their is not null && IsUkrainian(their.Entity.PrimaryPrefix),
_ => false,
};
public IReadOnlyList<string> MultiplierNames => ["Countries", "Oblasts"];
public DupeScope DupeScope => DupeScope.PerBandAndMode;
public bool HasSerialNumbers => true;
public IReadOnlyList<ModeCategory> Modes => [ModeCategory.Cw, ModeCategory.Phone];
public string SentExchangeFor(StationInfo me) =>
IsUkrainian(me.CountryPrefix) ? me.County : "001";
public int PointsFor(QsoContext qso)
{
if (IsUkrainian(qso.CountryPrefix))
{
return IsUkrainian(qso.Me.CountryPrefix) ? 1 : 10;
}
if (qso.IsSameCountry)
{
return 1;
}
return qso.IsSameContinent ? 2 : 3;
}
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 oblast = qso.Qso.Section.Trim().ToUpperInvariant();
if (IsUkrainian(qso.CountryPrefix) && oblast.Length > 0)
{
found.Add(new Multiplier(2, oblast, 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(SentExchangeFor(me), 6),
],
[
new CabrilloField(qso.Call.Text, 13),
new CabrilloField(qso.ReceivedReport, 3),
new CabrilloField(qso.Section.Length > 0 ? qso.Section : qso.Exchange1, 6),
]);
private static bool IsUkrainian(string countryPrefix) => countryPrefix == "UR";
}

View File

@@ -0,0 +1,82 @@
using Nonemm.Core;
using Nonemm.Core.Country;
namespace Nonemm.Contests.Rules;
/// The Romanian YO DX HF contest. A Romanian station sends its county and is
/// worth 8 points; everyone else is 1 point inside your own country, 2 on your
/// continent and 4 from another. The countries and the Romanian counties are
/// multipliers, each once per band.
public sealed class YoDxHf : Contest
{
public string Name => "YOHFDX";
public string DisplayName => "YO DX HF";
public string CabrilloName => "YO-DX-HF";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("County", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 3 },
new ExchangeField("Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Number),
];
public bool SkipsField(ExchangeField field, CountryLookup? their) => field.Slot switch
{
ExchangeSlot.Section => their is not null && their.Entity.PrimaryPrefix != "YO",
ExchangeSlot.Exchange1 => their is not null && their.Entity.PrimaryPrefix == "YO",
_ => false,
};
public IReadOnlyList<string> MultiplierNames => ["Countries", "Counties"];
public DupeScope DupeScope => DupeScope.PerBandAndMode;
public bool HasSerialNumbers => true;
public IReadOnlyList<ModeCategory> Modes => [ModeCategory.Cw, ModeCategory.Phone];
public string SentExchangeFor(StationInfo me) => me.CountryPrefix == "YO" ? me.County : "001";
public int PointsFor(QsoContext qso)
{
if (qso.CountryPrefix == "YO")
{
return 8;
}
return qso.IsSameCountry ? 1 : qso.IsSameContinent ? 2 : 4;
}
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 = qso.Qso.Section.Trim().ToUpperInvariant();
if (qso.CountryPrefix == "YO" && 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(SentExchangeFor(me), 6),
],
[
new CabrilloField(qso.Call.Text, 13),
new CabrilloField(qso.ReceivedReport, 3),
new CabrilloField(qso.Section.Length > 0 ? qso.Section : qso.Exchange1, 6),
]);
}

View File

@@ -0,0 +1,38 @@
using Nonemm.Contests.Rules;
using Nonemm.Core;
namespace Nonemm.Contests.Tests;
public class Arrl10MetreTests
{
private static ContestLog LogFor() => new(new Arrl10Metre(), TestLog.Germany, TestLog.CountryFile);
private static Qso Contact(string call, Mode mode, string state = "", string number = "") =>
TestLog.Contact(call, 28_025, mode: mode, section: state, exchange: number);
[Fact]
public void CwIsWorthFourAndPhoneTwo()
{
ContestLog log = LogFor();
Assert.Equal(4, log.Judge(Contact("K1ABC", Modes.Cw, state: "CT")).Points);
Assert.Equal(2, log.Judge(Contact("K1ABC", Modes.Usb, state: "CT")).Points);
}
/// The contest is one band and counts its multipliers once per mode.
[Fact]
public void AMultiplierCountsOncePerMode()
{
ContestLog log = LogFor();
log.Add(Contact("K1ABC", Modes.Cw, state: "CT"));
Assert.Empty(log.Judge(Contact("K2ABC", Modes.Cw, state: "CT")).NewMultipliers);
Assert.Single(log.Judge(Contact("K2ABC", Modes.Usb, state: "CT")).NewMultipliers);
}
[Fact]
public void AStationWithNoStateBringsItsCountry() =>
Assert.Equal(
[2],
LogFor().Judge(Contact("JA1XYZ", Modes.Cw, number: "042")).NewMultipliers
.Select(m => m.Index)
.ToList());
}

View File

@@ -0,0 +1,30 @@
using Nonemm.Contests.Rules;
using Nonemm.Core;
namespace Nonemm.Contests.Tests;
public class FrenchRefTests
{
private static ContestLog LogFor() =>
new(new FrenchRef(ModeCategory.Phone), TestLog.Germany, TestLog.CountryFile);
private static Qso Contact(string call, string department = "", string number = "") =>
TestLog.Contact(call, mode: Modes.Usb, section: department, exchange: number);
[Theory]
[InlineData("F5XYZ", 1)]
[InlineData("IK2XYZ", 1)]
[InlineData("JA1XYZ", 3)]
public void AStationOutsideFranceScoresByContinent(string call, int expected) =>
Assert.Equal(expected, LogFor().Judge(Contact(call, department: "03")).Points);
/// A French department is the multiplier, and so is a French entity
/// overseas, but an ordinary DX contact is neither.
[Fact]
public void OnlyFranceBringsAMultiplier()
{
ContestLog log = LogFor();
Assert.Equal("03", log.Judge(Contact("F5XYZ", department: "03")).NewMultipliers.Single().Value);
Assert.Empty(log.Judge(Contact("JA1XYZ", number: "001")).NewMultipliers);
}
}

View File

@@ -47,7 +47,7 @@ public class N1mmContestNamesTests
[Fact]
public void AContestNeitherProgramKnowsIsStillAnError() =>
Assert.Throws<KeyNotFoundException>(() => Registry().Create("BARTGSRTTY", ModeCategory.Digital));
Assert.Throws<KeyNotFoundException>(() => Registry().Create("NOSUCHCONTEST", ModeCategory.Digital));
[Fact]
public void HasFindsAContestByTheNameN1mmWrote() =>

View File

@@ -0,0 +1,29 @@
using Nonemm.Contests.Rules;
using Nonemm.Core;
namespace Nonemm.Contests.Tests;
public class OceaniaDxTests
{
private static ContestLog LogFor() =>
new(new OceaniaDx(ModeCategory.Phone), TestLog.Germany, TestLog.CountryFile);
[Theory]
[InlineData(1_830, 20)]
[InlineData(3_650, 10)]
[InlineData(7_050, 5)]
[InlineData(14_150, 1)]
public void TheLowerTheBandTheMorePoints(double kilohertz, int expected) =>
Assert.Equal(
expected,
LogFor().Judge(TestLog.Contact("VK4XYZ", kilohertz, mode: Modes.Usb)).Points);
/// One side of the contact has to be in Oceania.
[Fact]
public void AContactOutsideOceaniaScoresNothing()
{
Verdict verdict = LogFor().Judge(TestLog.Contact("IK2XYZ", mode: Modes.Usb));
Assert.Equal(0, verdict.Points);
Assert.Empty(verdict.NewMultipliers);
}
}

View File

@@ -0,0 +1,27 @@
using Nonemm.Contests.Rules;
using Nonemm.Core;
namespace Nonemm.Contests.Tests;
public class RussianDistrictAwardTests
{
private static ContestLog LogFor() =>
new(new RussianDistrictAward(), TestLog.Germany, TestLog.CountryFile);
/// Only a station that sends a district counts.
[Fact]
public void ADistrictIsTenPointsAndAMultiplier()
{
Verdict verdict = LogFor().Judge(TestLog.Contact("UA3XYZ", section: "MA12"));
Assert.Equal(10, verdict.Points);
Assert.Equal("MA12", verdict.NewMultipliers.Single().Value);
}
[Fact]
public void AStationWithNoDistrictScoresNothing()
{
Verdict verdict = LogFor().Judge(TestLog.Contact("IK2XYZ"));
Assert.Equal(0, verdict.Points);
Assert.Empty(verdict.NewMultipliers);
}
}

View File

@@ -0,0 +1,97 @@
using Nonemm.Contests.Rules;
using Nonemm.Core;
namespace Nonemm.Contests.Tests;
/// The contests whose rules are one table each: what a contact scores and
/// where its multiplier comes from.
public class SmallContestTests
{
private static ContestLog LogFor(Contest contest) =>
new(contest, TestLog.Germany, TestLog.CountryFile);
[Theory]
[InlineData("DL9XYZ", 5)]
[InlineData("IK2XYZ", 10)]
[InlineData("JA1XYZ", 15)]
public void SartgScoresByCountryAndContinent(string call, int expected) =>
Assert.Equal(
expected,
LogFor(new SartgRtty()).Judge(TestLog.Contact(call, mode: Modes.Rtty)).Points);
/// The SARTG call area is a second multiplier next to the country.
[Fact]
public void SartgCountsCallAreasAsWellAsCountries() =>
Assert.Equal(
[1, 2],
LogFor(new SartgRtty())
.Judge(TestLog.Contact("K1ABC", mode: Modes.Rtty, section: "K1"))
.NewMultipliers.Select(m => m.Index).ToList());
[Theory]
[InlineData(14_150, 1)]
[InlineData(7_050, 2)]
[InlineData(3_650, 3)]
public void AllAsianScoresByBand(double kilohertz, int expected) =>
Assert.Equal(
expected,
LogFor(new AllAsian(ModeCategory.Phone))
.Judge(TestLog.Contact("JA1XYZ", kilohertz, mode: Modes.Usb, exchange: "46"))
.Points);
[Fact]
public void AllAsianCountsOnlyAsia() =>
Assert.Equal(
0,
LogFor(new AllAsian(ModeCategory.Phone))
.Judge(TestLog.Contact("IK2XYZ", mode: Modes.Usb, exchange: "46"))
.Points);
[Theory]
[InlineData("PY2XYZ", 4)]
[InlineData("IK2XYZ", 2)]
public void SouthAmericaTenPaysMoreForSouthAmerica(string call, int expected) =>
Assert.Equal(
expected,
LogFor(new SouthAmerica10())
.Judge(TestLog.Contact(call, 28_400, mode: Modes.Usb, section: "11"))
.Points);
[Theory]
[InlineData("YO3XYZ", 8)]
[InlineData("DL9XYZ", 1)]
[InlineData("IK2XYZ", 2)]
[InlineData("JA1XYZ", 4)]
public void YoDxHfPaysEightForRomania(string call, int expected) =>
Assert.Equal(expected, LogFor(new YoDxHf()).Judge(TestLog.Contact(call, section: "AR")).Points);
[Theory]
[InlineData("IK2XYZ", 5)]
[InlineData("JA1XYZ", 10)]
public void RussianDxRttyScoresByContinent(string call, int expected) =>
Assert.Equal(
expected,
LogFor(new RussianDxRtty()).Judge(TestLog.Contact(call, mode: Modes.Rtty, section: "14")).Points);
[Theory]
[InlineData("IK2XYZ/P", 5)]
[InlineData("IK2XYZ", 2)]
[InlineData("JA1XYZ", 3)]
public void FieldDayPaysMostForAPortableStation(string call, int expected) =>
Assert.Equal(
expected,
LogFor(new FieldDayRegionOne()).Judge(TestLog.Contact(call, number: 22)).Points);
/// Every contact is one point, and the continent counts once in the whole
/// contest rather than once per band.
[Fact]
public void BartgCountsAContinentOnce()
{
ContestLog log = LogFor(new BartgSprint());
Assert.Equal(1, log.Judge(TestLog.Contact("IK2XYZ", mode: Modes.Rtty)).Points);
log.Add(TestLog.Contact("IK2XYZ", mode: Modes.Rtty));
Assert.DoesNotContain(
log.Judge(TestLog.Contact("F5XYZ", 7_040, mode: Modes.Rtty)).NewMultipliers,
m => m.Index == 3);
}
}

View File

@@ -26,6 +26,16 @@ public static class TestLog
OM;
Czech Republic: 15: 28: EU: 49.80: -15.47: -1.0: OK:
OK,OL;
France: 14: 27: EU: 46.00: 2.00: -1.0: F:
F;
Ukraine: 16: 29: EU: 50.00: -30.00: -2.0: UR:
UR,US,UT,UY,UZ,EM,EO;
European Russia: 16: 29: EU: 55.75: -37.62: -3.0: UA:
UA,RA,RN,RV,RW,RX,RZ,R3;
Romania: 20: 28: EU: 45.50: -25.00: -2.0: YO:
YO,YP,YQ,YR;
Australia: 30: 59: OC: -23.70: -133.87: -10.0: VK:
VK,AX;
""";
public static readonly CountryFile CountryFile = CountryFile.Parse(Countries);

View File

@@ -0,0 +1,25 @@
using Nonemm.Contests.Rules;
using Nonemm.Core;
namespace Nonemm.Contests.Tests;
public class UkrainianDxTests
{
private static ContestLog LogFor() => new(new UkrainianDx(), TestLog.Germany, TestLog.CountryFile);
[Theory]
[InlineData("UR5XYZ", 10)]
[InlineData("DL9XYZ", 1)]
[InlineData("IK2XYZ", 2)]
[InlineData("JA1XYZ", 3)]
public void UkraineIsWorthTen(string call, int expected) =>
Assert.Equal(expected, LogFor().Judge(TestLog.Contact(call, section: "DO")).Points);
[Fact]
public void OnlyUkraineBringsAnOblast() =>
Assert.Equal(
[1],
LogFor().Judge(TestLog.Contact("IK2XYZ", section: "DO")).NewMultipliers
.Select(m => m.Index)
.ToList());
}