From 7d9bc05e9a6e0d2bba9b5109dbbb867baff6a276 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Mon, 31 Aug 2026 09:33:29 +0000 Subject: [PATCH] Add IOTA, the EU DX Contest, CQ 160 and Mexico RTTY Four more of the contests in the station's log. IOTA, CQ 160 and Mexico RTTY score every contact in it exactly as N1MM did. IOTA pays 15 points for a station that sends an island reference and 2 for one that does not, and counts each reference once per band and mode. CQ 160 is one band, so its states and countries count once each. Mexico RTTY and CQ 160 both take a state from one side of the contest and a serial or a zone from the other, in the columns N1MM keeps them in. The EU DX Contest is written from N1MM's class, including its list of what counts as Europe: the European Union, its outermost regions and its overseas territories, and nothing else. The station's own logs were made with the .udc file, which scores and counts two things differently; docs/unfinished.md says which. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD --- README.md | 2 +- docs/unfinished.md | 16 +-- src/Nonemm.Contests/ContestRegistry.cs | 4 + src/Nonemm.Contests/N1mmContestNames.cs | 3 + src/Nonemm.Contests/Rules/CqOneSixty.cs | 91 +++++++++++++++++ src/Nonemm.Contests/Rules/EuDxContest.cs | 97 +++++++++++++++++++ src/Nonemm.Contests/Rules/Iota.cs | 69 +++++++++++++ src/Nonemm.Contests/Rules/MexicoRtty.cs | 78 +++++++++++++++ .../Nonemm.Contests.Tests/CqOneSixtyTests.cs | 33 +++++++ .../Nonemm.Contests.Tests/EuDxContestTests.cs | 30 ++++++ tests/Nonemm.Contests.Tests/IotaTests.cs | 30 ++++++ .../Nonemm.Contests.Tests/MexicoRttyTests.cs | 25 +++++ 12 files changed, 471 insertions(+), 7 deletions(-) create mode 100644 src/Nonemm.Contests/Rules/CqOneSixty.cs create mode 100644 src/Nonemm.Contests/Rules/EuDxContest.cs create mode 100644 src/Nonemm.Contests/Rules/Iota.cs create mode 100644 src/Nonemm.Contests/Rules/MexicoRtty.cs create mode 100644 tests/Nonemm.Contests.Tests/CqOneSixtyTests.cs create mode 100644 tests/Nonemm.Contests.Tests/EuDxContestTests.cs create mode 100644 tests/Nonemm.Contests.Tests/IotaTests.cs create mode 100644 tests/Nonemm.Contests.Tests/MexicoRttyTests.cs diff --git a/README.md b/README.md index 90b32dd..07c9bf6 100644 --- a/README.md +++ b/README.md @@ -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, 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, Mexico RTTY, 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 | diff --git a/docs/unfinished.md b/docs/unfinished.md index 02b45af..8625d98 100644 --- a/docs/unfinished.md +++ b/docs/unfinished.md @@ -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.** Ten families are built in, plus whatever `.udc` files +**Contest coverage.** Fourteen 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,11 +123,15 @@ 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: 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. +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. + +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. ## Known rough edges diff --git a/src/Nonemm.Contests/ContestRegistry.cs b/src/Nonemm.Contests/ContestRegistry.cs index ef41de1..db62bfc 100644 --- a/src/Nonemm.Contests/ContestRegistry.cs +++ b/src/Nonemm.Contests/ContestRegistry.cs @@ -20,6 +20,10 @@ public sealed class ContestRegistry 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("IOTA", "RSGB Islands On The Air", [ModeCategory.Cw, ModeCategory.Phone], _ => new Iota()), + 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("DX", "General logging", [], _ => new GeneralLogging()), ]; diff --git a/src/Nonemm.Contests/N1mmContestNames.cs b/src/Nonemm.Contests/N1mmContestNames.cs index d0ffbab..dd8bd43 100644 --- a/src/Nonemm.Contests/N1mmContestNames.cs +++ b/src/Nonemm.Contests/N1mmContestNames.cs @@ -36,6 +36,9 @@ public static class N1mmContestNames ["OKOMDXS"] = ("OKOMDX", ModeCategory.Phone), ["OKOMDXC_DX"] = ("OKOMDX", ModeCategory.Cw), ["OKOMDXS_DX"] = ("OKOMDX", ModeCategory.Phone), + ["CQ160CW"] = ("CQ160", ModeCategory.Cw), + ["CQ160SSB"] = ("CQ160", ModeCategory.Phone), + ["EU_DXC"] = ("EUDXC", ModeCategory.Cw), }; public static bool TryResolve(string name, out string family, out ModeCategory mode) diff --git a/src/Nonemm.Contests/Rules/CqOneSixty.cs b/src/Nonemm.Contests/Rules/CqOneSixty.cs new file mode 100644 index 0000000..f981c1f --- /dev/null +++ b/src/Nonemm.Contests/Rules/CqOneSixty.cs @@ -0,0 +1,91 @@ +using Nonemm.Contests.Multipliers; +using Nonemm.Core; +using Nonemm.Core.Country; + +namespace Nonemm.Contests.Rules; + +/// The CQ 160 metre contest, CW and phone. One band, so nothing is counted per +/// band: the multipliers are the states, provinces and countries worked, and +/// the points are 2 inside your own country, 5 on your own continent and 10 +/// from another. +/// +/// A US or Canadian station sends a state or province, which N1MM keeps in the +/// section column; everyone else sends a CQ zone, which it keeps in the +/// exchange column. +public sealed class CqOneSixty : Contest +{ + private readonly ModeCategory mode; + + public CqOneSixty(ModeCategory mode) => this.mode = mode; + + public string Name => mode == ModeCategory.Cw ? "CQ160CW" : "CQ160SSB"; + + public string DisplayName => $"CQ World-Wide 160 Metre {ModeLabel()}"; + + public string CabrilloName => mode == ModeCategory.Cw ? "CQ-160-CW" : "CQ-160-SSB"; + + public IReadOnlyList ExchangeFieldsFor(StationInfo me) => + [ + new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), + new ExchangeField("S/P", ExchangeSlot.Section, ExchangeFieldKind.UsStateOrCanadianProvince), + new ExchangeField("Zone", ExchangeSlot.Exchange1, ExchangeFieldKind.CqZone), + ]; + + public bool SkipsField(ExchangeField field, CountryLookup? their) => field.Slot switch + { + ExchangeSlot.Section => their is not null && !IsUsOrCanadian(their.Entity.PrimaryPrefix), + ExchangeSlot.Exchange1 => their is not null && IsUsOrCanadian(their.Entity.PrimaryPrefix), + _ => false, + }; + + public IReadOnlyList MultiplierNames => ["States", "Countries"]; + + public DupeScope DupeScope => DupeScope.Once; + + public bool HasSerialNumbers => false; + + public IReadOnlyList Modes => [mode]; + + public string SentExchangeFor(StationInfo me) => + IsUsOrCanadian(me.CountryPrefix) + ? me.State.Length > 0 ? me.State : me.Province + : me.CqZone.ToString(); + + public int PointsFor(QsoContext qso) => + qso.IsSameCountry ? 2 : qso.IsSameContinent ? 5 : 10; + + public IReadOnlyList MultipliersFor(QsoContext qso) + { + List found = []; + string area = StatesAndProvinces.ArrlDxArea(qso.Qso.Section); + if (StatesAndProvinces.ArrlDxMultipliers.Contains(area)) + { + found.Add(new Multiplier(1, area, "")); + } + if (qso.CountryPrefix.Length > 0) + { + found.Add(new Multiplier(2, qso.CountryPrefix, "")); + } + 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 IsUsOrCanadian(string countryPrefix) => countryPrefix is "K" or "VE"; + + private string ModeLabel() => mode == ModeCategory.Cw ? "CW" : "SSB"; +} diff --git a/src/Nonemm.Contests/Rules/EuDxContest.cs b/src/Nonemm.Contests/Rules/EuDxContest.cs new file mode 100644 index 0000000..8f3e30f --- /dev/null +++ b/src/Nonemm.Contests/Rules/EuDxContest.cs @@ -0,0 +1,97 @@ +using Nonemm.Core; + +namespace Nonemm.Contests.Rules; + +/// The EU DX Contest. Its "Europe" is the European Union: the member states, +/// their outermost regions and their overseas territories, and nothing else. +/// The exchange is the country and district code of the other station. +/// +/// A contact with a station in that list is worth 10 points, or 2 inside your +/// own country. Everything else is 3 points on your own continent and 5 from +/// another. +public sealed class EuDxContest : Contest +{ + /// The member states, and the entities that are legally part of one: the + /// Azores and Madeira, the Canaries, Ceuta and Melilla, the French overseas + /// departments, Cyprus, and the Dutch and Danish territories. + private static readonly IReadOnlySet Union = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "9A", "9H", "CT", "CU", "DL", "EA", "EA6", "EI", "ES", "F", "HA", "I", "IS", "IT9", + "LX", "LY", "LZ", "OE", "OH", "OH0", "OJ0", "OK", "OM", "ON", "OX", "OZ", "PA", "S5", + "SM", "SP", "SV", "SV5", "SV9", "TK", "YL", "YO", + "5B", "CT3", "EA8", "EA9", "FO", "FP", "FY", "IG9", "IH9", "P4", + "PJ0", "PJ2", "PJ4", "PJ5", "PJ6", "PJ7", "PJ8", + }; + + public string Name => "EUDXC"; + + public string DisplayName => "EU DX Contest"; + + public string CabrilloName => "EUDXC"; + + public IReadOnlyList ExchangeFieldsFor(StationInfo me) => + [ + new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), + new ExchangeField("Code", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 5 }, + ]; + + public IReadOnlyList MultiplierNames => ["Codes", "Countries"]; + + public DupeScope DupeScope => DupeScope.PerBandAndMode; + + public bool HasSerialNumbers => false; + + public IReadOnlyList Modes => [ModeCategory.Cw, ModeCategory.Phone]; + + public string SentExchangeFor(StationInfo me) => ""; + + public int PointsFor(QsoContext qso) + { + if (IsUnion(qso.CountryPrefix)) + { + return qso.IsSameCountry ? 2 : 10; + } + if (!qso.IsSameContinent) + { + return 5; + } + return IsUnion(qso.Me.CountryPrefix) || !qso.IsSameCountry ? 3 : 2; + } + + /// The district code and the country each count once per band. Only a + /// station in the union sends a code; everyone else sends a CQ zone, which + /// counts for nothing. + public IReadOnlyList MultipliersFor(QsoContext qso) + { + string band = qso.Band?.Name ?? ""; + List found = []; + string code = qso.Qso.Section.Trim().ToUpperInvariant(); + if (IsUnion(qso.CountryPrefix) && code.Length > 0) + { + found.Add(new Multiplier(1, code, band)); + } + if (qso.CountryPrefix.Length > 0) + { + found.Add(new Multiplier(2, qso.CountryPrefix, 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(entry.SentExchange, 5), + ], + [ + new CabrilloField(qso.Call.Text, 13), + new CabrilloField(qso.ReceivedReport, 3), + new CabrilloField(qso.Section, 5), + ]); + + private static bool IsUnion(string countryPrefix) => Union.Contains(countryPrefix); +} diff --git a/src/Nonemm.Contests/Rules/Iota.cs b/src/Nonemm.Contests/Rules/Iota.cs new file mode 100644 index 0000000..474b050 --- /dev/null +++ b/src/Nonemm.Contests/Rules/Iota.cs @@ -0,0 +1,69 @@ +using Nonemm.Core; + +namespace Nonemm.Contests.Rules; + +/// RSGB Islands On The Air. A station on an island sends its IOTA reference +/// after the serial number and is worth 15 points; everyone else is worth 2. +/// Each reference counts once per band and mode. +/// +/// The operator's own island is not held anywhere, so the contest cannot yet +/// score a contact with a station on the same island, which N1MM counts as 5 +/// points. +public sealed class Iota : Contest +{ + public string Name => "IOTA"; + + public string DisplayName => "RSGB Islands On The Air"; + + public string CabrilloName => "RSGB-IOTA"; + + public IReadOnlyList ExchangeFieldsFor(StationInfo me) => + [ + new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), + new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number), + new ExchangeField("IOTA", ExchangeSlot.Section, ExchangeFieldKind.Text, IsRequired: false) + { + Width = 6, + }, + ]; + + public IReadOnlyList MultiplierNames => ["Islands"]; + + public DupeScope DupeScope => DupeScope.PerBandAndMode; + + public bool HasSerialNumbers => true; + + public IReadOnlyList Modes => [ModeCategory.Cw, ModeCategory.Phone]; + + public string SentExchangeFor(StationInfo me) => "001"; + + public int PointsFor(QsoContext qso) => Reference(qso.Qso).Length > 0 ? 15 : 2; + + public IReadOnlyList MultipliersFor(QsoContext qso) + { + string reference = Reference(qso.Qso); + return reference.Length == 0 + ? [] + : [new Multiplier(1, reference, $"{qso.Band?.Name}|{qso.ModeCategory}")]; + } + + 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(entry.SentExchange, 6), + ], + [ + new CabrilloField(qso.Call.Text, 13), + new CabrilloField(qso.ReceivedReport, 3), + new CabrilloField($"{qso.ReceivedNumber:000}", 4), + new CabrilloField(Reference(qso), 6), + ]); + + private static string Reference(Qso qso) => qso.Section.Trim().ToUpperInvariant(); +} diff --git a/src/Nonemm.Contests/Rules/MexicoRtty.cs b/src/Nonemm.Contests/Rules/MexicoRtty.cs new file mode 100644 index 0000000..5fc3421 --- /dev/null +++ b/src/Nonemm.Contests/Rules/MexicoRtty.cs @@ -0,0 +1,78 @@ +using Nonemm.Core; +using Nonemm.Core.Country; + +namespace Nonemm.Contests.Rules; + +/// The Mexico RTTY contest. A Mexican station sends its state, everyone else a +/// serial number. A contact with Mexico is worth 4 points, one inside your own +/// country 2, and anything else 3. The multipliers are the countries and the +/// Mexican states, each once per band. +public sealed class MexicoRtty : Contest +{ + public string Name => "XERTTY"; + + public string DisplayName => "Mexico RTTY Contest"; + + public string CabrilloName => "XE-RTTY"; + + public IReadOnlyList ExchangeFieldsFor(StationInfo me) => + [ + new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), + new ExchangeField("State", 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 && !IsMexican(their.Entity.PrimaryPrefix), + ExchangeSlot.Exchange1 => their is not null && IsMexican(their.Entity.PrimaryPrefix), + _ => false, + }; + + public IReadOnlyList MultiplierNames => ["Countries", "States"]; + + public DupeScope DupeScope => DupeScope.PerBand; + + public bool HasSerialNumbers => true; + + public IReadOnlyList Modes => [ModeCategory.Digital]; + + public string SentExchangeFor(StationInfo me) => IsMexican(me.CountryPrefix) ? me.State : "001"; + + public int PointsFor(QsoContext qso) => + IsMexican(qso.CountryPrefix) ? 4 : qso.IsSameCountry ? 2 : 3; + + public IReadOnlyList MultipliersFor(QsoContext qso) + { + string band = qso.Band?.Name ?? ""; + List found = []; + if (qso.CountryPrefix.Length > 0) + { + found.Add(new Multiplier(1, qso.CountryPrefix, band)); + } + string state = qso.Qso.Section.Trim().ToUpperInvariant(); + if (IsMexican(qso.CountryPrefix) && state.Length > 0) + { + found.Add(new Multiplier(2, state, 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(IsMexican(me.CountryPrefix) ? me.State : $"{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 IsMexican(string countryPrefix) => countryPrefix == "XE"; +} diff --git a/tests/Nonemm.Contests.Tests/CqOneSixtyTests.cs b/tests/Nonemm.Contests.Tests/CqOneSixtyTests.cs new file mode 100644 index 0000000..7327234 --- /dev/null +++ b/tests/Nonemm.Contests.Tests/CqOneSixtyTests.cs @@ -0,0 +1,33 @@ +using Nonemm.Contests.Rules; +using Nonemm.Core; + +namespace Nonemm.Contests.Tests; + +public class CqOneSixtyTests +{ + private static readonly Contest Ssb = new CqOneSixty(ModeCategory.Phone); + + private static ContestLog LogFor(StationInfo me) => new(Ssb, me, TestLog.CountryFile); + + private static Qso Contact(string call, string zone = "15", string stateOrProvince = "") => + TestLog.Contact(call, 1_830, mode: Modes.Lsb, exchange: zone, section: stateOrProvince); + + [Theory] + [InlineData("DL9XYZ", 2)] + [InlineData("IK2XYZ", 5)] + [InlineData("JA1XYZ", 10)] + public void PointsGoByCountryAndContinent(string call, int expected) => + Assert.Equal(expected, LogFor(TestLog.Germany).Judge(Contact(call)).Points); + + /// One band, so a country and a state each count once in the contest. + [Fact] + public void CountriesAndStatesEachCountOnce() + { + ContestLog log = LogFor(TestLog.Germany); + Assert.Equal( + [1, 2], + log.Judge(Contact("K1ABC", stateOrProvince: "CT")).NewMultipliers.Select(m => m.Index).ToList()); + log.Add(Contact("K1ABC", stateOrProvince: "CT")); + Assert.Empty(log.Judge(Contact("K2ABC", stateOrProvince: "CT")).NewMultipliers); + } +} diff --git a/tests/Nonemm.Contests.Tests/EuDxContestTests.cs b/tests/Nonemm.Contests.Tests/EuDxContestTests.cs new file mode 100644 index 0000000..e65408c --- /dev/null +++ b/tests/Nonemm.Contests.Tests/EuDxContestTests.cs @@ -0,0 +1,30 @@ +using Nonemm.Contests.Rules; +using Nonemm.Core; + +namespace Nonemm.Contests.Tests; + +public class EuDxContestTests +{ + private static ContestLog LogFor(StationInfo me) => new(new EuDxContest(), me, TestLog.CountryFile); + + private static Qso Contact(string call, string code) => + TestLog.Contact(call, mode: Modes.Usb, section: code); + + /// The contest's Europe is the European Union, so a Japanese contact is 5 + /// points and an Italian one 10. + [Theory] + [InlineData("IK2XYZ", "IT01", 10)] + [InlineData("DL9XYZ", "DL01", 2)] + [InlineData("JA1XYZ", "25", 5)] + public void PointsGoByTheUnionList(string call, string code, int expected) => + Assert.Equal(expected, LogFor(TestLog.Germany).Judge(Contact(call, code)).Points); + + /// A station outside the union sends a zone, which is no multiplier. + [Fact] + public void OnlyAUnionStationBringsACodeMultiplier() + { + ContestLog log = LogFor(TestLog.Germany); + Assert.Equal([1, 2], log.Judge(Contact("IK2XYZ", "IT01")).NewMultipliers.Select(m => m.Index).ToList()); + Assert.Equal([2], log.Judge(Contact("JA1XYZ", "25")).NewMultipliers.Select(m => m.Index).ToList()); + } +} diff --git a/tests/Nonemm.Contests.Tests/IotaTests.cs b/tests/Nonemm.Contests.Tests/IotaTests.cs new file mode 100644 index 0000000..d1887fa --- /dev/null +++ b/tests/Nonemm.Contests.Tests/IotaTests.cs @@ -0,0 +1,30 @@ +using Nonemm.Contests.Rules; +using Nonemm.Core; + +namespace Nonemm.Contests.Tests; + +public class IotaTests +{ + private static ContestLog LogFor() => new(new Iota(), TestLog.Germany, TestLog.CountryFile); + + private static Qso Contact(string call, string reference, double kilohertz = 14_025, Mode? mode = null) => + TestLog.Contact(call, kilohertz, mode: mode, section: reference); + + [Fact] + public void AnIslandStationIsWorthFifteenAndEveryoneElseTwo() + { + ContestLog log = LogFor(); + Assert.Equal(15, log.Judge(Contact("IK2XYZ", "EU025")).Points); + Assert.Equal(2, log.Judge(Contact("IK2XYZ", "")).Points); + } + + [Fact] + public void AReferenceCountsOncePerBandAndMode() + { + ContestLog log = LogFor(); + log.Add(Contact("IK2XYZ", "EU025")); + Assert.Empty(log.Judge(Contact("IK3XYZ", "EU025")).NewMultipliers); + Assert.Single(log.Judge(Contact("IK3XYZ", "EU025", mode: Modes.Usb)).NewMultipliers); + Assert.Single(log.Judge(Contact("IK3XYZ", "EU025", 7_025)).NewMultipliers); + } +} diff --git a/tests/Nonemm.Contests.Tests/MexicoRttyTests.cs b/tests/Nonemm.Contests.Tests/MexicoRttyTests.cs new file mode 100644 index 0000000..72bf564 --- /dev/null +++ b/tests/Nonemm.Contests.Tests/MexicoRttyTests.cs @@ -0,0 +1,25 @@ +using Nonemm.Contests.Rules; +using Nonemm.Core; + +namespace Nonemm.Contests.Tests; + +public class MexicoRttyTests +{ + private static ContestLog LogFor() => new(new MexicoRtty(), TestLog.Germany, TestLog.CountryFile); + + private static Qso Contact(string call, string number = "001", string state = "") => + TestLog.Contact(call, mode: Modes.Rtty, exchange: number, section: state); + + [Theory] + [InlineData("DL9XYZ", 2)] + [InlineData("IK2XYZ", 3)] + public void PointsGoByCountry(string call, int expected) => + Assert.Equal(expected, LogFor().Judge(Contact(call)).Points); + + [Fact] + public void CountriesCountForEveryoneAndStatesOnlyForMexico() + { + ContestLog log = LogFor(); + Assert.Equal([1], log.Judge(Contact("IK2XYZ")).NewMultipliers.Select(m => m.Index).ToList()); + } +}