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()); + } +}