Let the published .udc files score the contests they are written for
I wrote SA 10 and OK/OM DX SSB in code after two failed attempts at the download. The download works — the page posts a nonce with the form — and the files say I got both contests wrong. SA 10 is deleted from the code. Its file scores the station's log to the contact, where the code had the wrong Cabrillo name and counted its multipliers once in the contest rather than once per mode. OK/OM DX now covers the CW running only, which is what N1MM's class is for and what N1MM's own documentation says: the SSB running has different rules and a pair of .udc files, one for each side. The CW rules go back to the class, so Czechia and Slovakia are one side of the contest again. With the SSB files in place both of the station's SSB logs score to the contact. Reading those files turned up four things in the .udc reader: a file whose extension is upper case was passed over, `Country` was not read as a multiplier source, `OtherCountry` was not a points condition, and a section multiplier counted the serial numbers the other side of a contest sends. All Asian stays in code — N1MM has a class for it after all — with the Cabrillo name and the band table it uses. 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, 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 |
|
| 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 (CW), YOTA, IOTA, EU DX Contest, CQ 160, ARRL 10M, REF, Ukrainian DX, Russian DX RTTY, RDAC, YO DX HF, Oceania DX, All Asian, 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 |
|
| 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 |
|
| 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 |
|
| 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
|
the contest rules score it, but there is no digital window: no decoding, no
|
||||||
transmitting, no interface to fldigi, MMTTY or similar.
|
transmitting, no interface to fldigi, MMTTY or similar.
|
||||||
|
|
||||||
**Contest coverage.** Twenty-six families are built in, plus whatever `.udc` files
|
**Contest coverage.** Twenty-five families are built in, plus whatever `.udc` files
|
||||||
are in the user-defined folder. N1MM ships well over a hundred. Opening a log
|
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
|
from a contest that is in neither place fails with the contest name, which is
|
||||||
the right answer but it is still a wall.
|
the right answer but it is still a wall.
|
||||||
@@ -136,10 +136,16 @@ What still differs, and why:
|
|||||||
multiplier, which is not what N1MM's own class works out, so the log looks
|
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
|
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.
|
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`
|
- **SA 10 and OK/OM DX on SSB** are not written in code at all: both are
|
||||||
file — so the points and multipliers are written from the published rules and
|
published as `.udc` files, and the files score the station's logs to the
|
||||||
from the station's log. Their Cabrillo names have not been checked against a
|
contact. N1MM's OK/OM DX class is the CW running only and says so; the SSB
|
||||||
sponsor's robot.
|
running has different rules and its own pair of files, one for each side of
|
||||||
|
the contest.
|
||||||
|
- **A `.udc` section multiplier** is counted for any exchange that is not all
|
||||||
|
digits. N1MM checks it against the section list the file names in
|
||||||
|
`MultWindowType`, and those lists are not held here, so a US state sent into
|
||||||
|
an OK/OM DX log counts here and not in N1MM. Three contacts across the two
|
||||||
|
logs.
|
||||||
- **The BARTG Sprint** stores the time of the contact in the exchange column,
|
- **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
|
which N1MM fills in by itself. Nothing here fills it in yet, so a log written
|
||||||
here carries the serial number alone.
|
here carries the serial number alone.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public sealed class ContestRegistry
|
|||||||
new("WAE", "Worked All Europe DX", [ModeCategory.Cw, ModeCategory.Phone, ModeCategory.Digital], m => new Wae(m)),
|
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("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("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("OKOMDX", "Czech and Slovak Republics DX", [ModeCategory.Cw], _ => new OkOmDx()),
|
||||||
new("YOTA", "YOTA Contest", [ModeCategory.Cw, ModeCategory.Phone], _ => new Yota()),
|
new("YOTA", "YOTA Contest", [ModeCategory.Cw, ModeCategory.Phone], _ => new Yota()),
|
||||||
new("IOTA", "RSGB Islands On The Air", [ModeCategory.Cw, ModeCategory.Phone], _ => new Iota()),
|
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("EUDXC", "EU DX Contest", [ModeCategory.Cw, ModeCategory.Phone], _ => new EuDxContest()),
|
||||||
@@ -35,7 +35,6 @@ public sealed class ContestRegistry
|
|||||||
new("FDREG1", "IARU Region 1 Field Day", [ModeCategory.Cw, ModeCategory.Phone], _ => new FieldDayRegionOne()),
|
new("FDREG1", "IARU Region 1 Field Day", [ModeCategory.Cw, ModeCategory.Phone], _ => new FieldDayRegionOne()),
|
||||||
new("SARTGRTTY", "SARTG World Wide RTTY", [ModeCategory.Digital], _ => new SartgRtty()),
|
new("SARTGRTTY", "SARTG World Wide RTTY", [ModeCategory.Digital], _ => new SartgRtty()),
|
||||||
new("ALLASIA", "All Asian DX", [ModeCategory.Cw, ModeCategory.Phone], m => new AllAsian(m)),
|
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()),
|
new("DX", "General logging", [], _ => new GeneralLogging()),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -60,15 +59,18 @@ public sealed class ContestRegistry
|
|||||||
return new ContestRegistry(byName);
|
return new ContestRegistry(byName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads every `.udc` file in the folder. A file that will not parse is
|
/// Reads every `.udc` file in the folder. Published files come with the
|
||||||
/// reported rather than silently left out.
|
/// extension in either case, and a file that will not parse is reported
|
||||||
|
/// rather than silently left out.
|
||||||
public static ContestRegistry FromFolder(string folder, out IReadOnlyList<string> problems)
|
public static ContestRegistry FromFolder(string folder, out IReadOnlyList<string> problems)
|
||||||
{
|
{
|
||||||
List<UdcFile> files = [];
|
List<UdcFile> files = [];
|
||||||
List<string> failures = [];
|
List<string> failures = [];
|
||||||
if (Directory.Exists(folder))
|
if (Directory.Exists(folder))
|
||||||
{
|
{
|
||||||
foreach (string path in Directory.EnumerateFiles(folder, "*.udc"))
|
IEnumerable<string> found = Directory.EnumerateFiles(folder)
|
||||||
|
.Where(f => Path.GetExtension(f).Equals(".udc", StringComparison.OrdinalIgnoreCase));
|
||||||
|
foreach (string path in found)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ namespace Nonemm.Contests;
|
|||||||
/// never plain `CQWW`. Our registry is keyed by the family, so a log written by
|
/// 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
|
/// N1MM has to have its contest name turned back into a family and a mode
|
||||||
/// before the rules can be built.
|
/// 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
|
public static class N1mmContestNames
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<string, (string Family, ModeCategory Mode)> Known =
|
private static readonly Dictionary<string, (string Family, ModeCategory Mode)> Known =
|
||||||
@@ -32,10 +28,6 @@ public static class N1mmContestNames
|
|||||||
["NAQPCW"] = ("NAQP", ModeCategory.Cw),
|
["NAQPCW"] = ("NAQP", ModeCategory.Cw),
|
||||||
["NAQPSSB"] = ("NAQP", ModeCategory.Phone),
|
["NAQPSSB"] = ("NAQP", ModeCategory.Phone),
|
||||||
["NAQPRTTY"] = ("NAQP", ModeCategory.Digital),
|
["NAQPRTTY"] = ("NAQP", ModeCategory.Digital),
|
||||||
["OKOMDXC"] = ("OKOMDX", ModeCategory.Cw),
|
|
||||||
["OKOMDXS"] = ("OKOMDX", ModeCategory.Phone),
|
|
||||||
["OKOMDXC_DX"] = ("OKOMDX", ModeCategory.Cw),
|
|
||||||
["OKOMDXS_DX"] = ("OKOMDX", ModeCategory.Phone),
|
|
||||||
["CQ160CW"] = ("CQ160", ModeCategory.Cw),
|
["CQ160CW"] = ("CQ160", ModeCategory.Cw),
|
||||||
["CQ160SSB"] = ("CQ160", ModeCategory.Phone),
|
["CQ160SSB"] = ("CQ160", ModeCategory.Phone),
|
||||||
["EU_DXC"] = ("EUDXC", ModeCategory.Cw),
|
["EU_DXC"] = ("EUDXC", ModeCategory.Cw),
|
||||||
@@ -46,7 +38,6 @@ public static class N1mmContestNames
|
|||||||
["BARTGSRTTY"] = ("BARTGRTTYS", ModeCategory.Digital),
|
["BARTGSRTTY"] = ("BARTGRTTYS", ModeCategory.Digital),
|
||||||
["ALLASIACW"] = ("ALLASIA", ModeCategory.Cw),
|
["ALLASIACW"] = ("ALLASIA", ModeCategory.Cw),
|
||||||
["ALLASIASSB"] = ("ALLASIA", ModeCategory.Phone),
|
["ALLASIASSB"] = ("ALLASIA", ModeCategory.Phone),
|
||||||
["SA10_DX"] = ("SA10", ModeCategory.Phone),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static bool TryResolve(string name, out string family, out ModeCategory mode)
|
public static bool TryResolve(string name, out string family, out ModeCategory mode)
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ namespace Nonemm.Contests.Rules;
|
|||||||
|
|
||||||
/// The JARL All Asian DX contest. Only contacts with Asia count for a station
|
/// 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
|
/// outside it. The exchange is the operator's age, the points go by band, and
|
||||||
/// each prefix worked counts once per band.
|
/// each prefix worked counts once per band. An Asian station scores three
|
||||||
|
/// times what a station outside Asia scores for the same contact.
|
||||||
public sealed class AllAsian : Contest
|
public sealed class AllAsian : Contest
|
||||||
{
|
{
|
||||||
private readonly ModeCategory mode;
|
private readonly ModeCategory mode;
|
||||||
@@ -15,7 +16,7 @@ public sealed class AllAsian : Contest
|
|||||||
|
|
||||||
public string DisplayName => $"All Asian DX {ModeLabel()}";
|
public string DisplayName => $"All Asian DX {ModeLabel()}";
|
||||||
|
|
||||||
public string CabrilloName => mode == ModeCategory.Cw ? "AA-CW" : "AA-SSB";
|
public string CabrilloName => mode == ModeCategory.Cw ? "AADX-CW" : "AADX-SSB";
|
||||||
|
|
||||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||||
[
|
[
|
||||||
@@ -33,7 +34,14 @@ public sealed class AllAsian : Contest
|
|||||||
|
|
||||||
public string SentExchangeFor(StationInfo me) => "";
|
public string SentExchangeFor(StationInfo me) => "";
|
||||||
|
|
||||||
public int PointsFor(QsoContext qso) => IsWorkable(qso) ? PointsOn(qso.Band) : 0;
|
public int PointsFor(QsoContext qso)
|
||||||
|
{
|
||||||
|
if (!IsWorkable(qso))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return PointsOn(qso.Band) * (qso.Me.Continent == "AS" ? 3 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso) =>
|
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso) =>
|
||||||
IsWorkable(qso) && qso.Qso.Call.WpxPrefix() is { } prefix
|
IsWorkable(qso) && qso.Qso.Call.WpxPrefix() is { } prefix
|
||||||
@@ -60,10 +68,11 @@ public sealed class AllAsian : Contest
|
|||||||
private static bool IsWorkable(QsoContext qso) =>
|
private static bool IsWorkable(QsoContext qso) =>
|
||||||
qso.Continent == "AS" || qso.Me.Continent == "AS";
|
qso.Continent == "AS" || qso.Me.Continent == "AS";
|
||||||
|
|
||||||
|
/// 160 metres is worth 3, 80 and 10 metres 2, and every other band 1.
|
||||||
private static int PointsOn(Band? band) => band?.Name switch
|
private static int PointsOn(Band? band) => band?.Name switch
|
||||||
{
|
{
|
||||||
"160M" or "80M" => 3,
|
"160M" => 3,
|
||||||
"40M" => 2,
|
"80M" or "10M" => 2,
|
||||||
null => 0,
|
null => 0,
|
||||||
_ => 1,
|
_ => 1,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,19 +3,19 @@ using Nonemm.Core.Country;
|
|||||||
|
|
||||||
namespace Nonemm.Contests.Rules;
|
namespace Nonemm.Contests.Rules;
|
||||||
|
|
||||||
/// OK/OM DX. An OK or OM station sends a county code and everyone else a
|
/// OK/OM DX, CW. 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
|
/// serial number, and which side the operator is on decides what a contact
|
||||||
/// scores.
|
/// scores and what it counts for.
|
||||||
|
///
|
||||||
|
/// The SSB running of this contest has its own rules and its own pair of `.udc`
|
||||||
|
/// files, `OKOMDXS` and `OKOMDXS_DX`, one for each side. N1MM says not to use
|
||||||
|
/// this contest for it, and neither should we.
|
||||||
///
|
///
|
||||||
/// The received exchange lands in the section column for an OK or OM station
|
/// 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
|
/// and in the exchange column for everyone else, which is where N1MM keeps
|
||||||
/// each of them.
|
/// each of them.
|
||||||
public sealed class OkOmDx : Contest
|
public sealed class OkOmDx : Contest
|
||||||
{
|
{
|
||||||
private readonly ModeCategory mode;
|
|
||||||
|
|
||||||
public OkOmDx(ModeCategory mode) => this.mode = mode;
|
|
||||||
|
|
||||||
public string Name => "OKOMDX";
|
public string Name => "OKOMDX";
|
||||||
|
|
||||||
public string DisplayName => "Czech and Slovak Republics DX";
|
public string DisplayName => "Czech and Slovak Republics DX";
|
||||||
@@ -39,52 +39,52 @@ public sealed class OkOmDx : Contest
|
|||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
public IReadOnlyList<string> MultiplierNames => ["Countries", "Counties"];
|
public IReadOnlyList<string> MultiplierNames => ["Mults"];
|
||||||
|
|
||||||
public DupeScope DupeScope => DupeScope.PerBand;
|
public DupeScope DupeScope => DupeScope.PerBand;
|
||||||
|
|
||||||
public bool HasSerialNumbers => true;
|
public bool HasSerialNumbers => true;
|
||||||
|
|
||||||
public IReadOnlyList<ModeCategory> Modes => [mode];
|
public IReadOnlyList<ModeCategory> Modes => [ModeCategory.Cw];
|
||||||
|
|
||||||
public string SentExchangeFor(StationInfo me) => IsOkOm(me.CountryPrefix) ? me.County : "001";
|
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
|
/// 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 —
|
/// 2 points for another OK or OM station, 3 for the rest of Europe and 5
|
||||||
/// Czechia and Slovakia are two countries, so they are 3 points to each
|
/// for another continent. Everyone else gets 10 points for an OK or OM
|
||||||
/// other. Everyone else gets 10 points for an OK or OM station, 1 at home,
|
/// station, 1 at home, 3 on their own continent and 5 from another.
|
||||||
/// 3 on their own continent and 5 from another.
|
|
||||||
public int PointsFor(QsoContext qso)
|
public int PointsFor(QsoContext qso)
|
||||||
{
|
{
|
||||||
if (qso.Qso.Call.IsMaritimeMobile)
|
if (qso.Qso.Call.IsMaritimeMobile)
|
||||||
{
|
{
|
||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
if (!IsOkOm(qso.Me.CountryPrefix) && IsOkOm(qso.CountryPrefix))
|
bool theirs = IsOkOm(qso.CountryPrefix);
|
||||||
|
if (IsOkOm(qso.Me.CountryPrefix))
|
||||||
|
{
|
||||||
|
return theirs ? 2 : qso.Continent == "EU" ? 3 : 5;
|
||||||
|
}
|
||||||
|
if (theirs)
|
||||||
{
|
{
|
||||||
return 10;
|
return 10;
|
||||||
}
|
}
|
||||||
int home = IsOkOm(qso.Me.CountryPrefix) ? 2 : 1;
|
return qso.IsSameCountry ? 1 : qso.IsSameContinent ? 3 : 5;
|
||||||
return qso.IsSameCountry ? home : qso.IsSameContinent ? 3 : 5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Both sides count the same two, once per band each: every country, and
|
/// Once per band: an OK or OM operator counts the prefixes of the stations
|
||||||
/// every OK or OM county. N1MM's own class counts prefixes for an OK or OM
|
/// it works, and everyone else counts the OK and OM counties.
|
||||||
/// operator, which was the rule before the counties came in.
|
|
||||||
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso)
|
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso)
|
||||||
{
|
{
|
||||||
string band = qso.Band?.Name ?? "";
|
string band = qso.Band?.Name ?? "";
|
||||||
List<Multiplier> found = [];
|
bool theirs = IsOkOm(qso.CountryPrefix);
|
||||||
if (qso.CountryPrefix.Length > 0)
|
if (IsOkOm(qso.Me.CountryPrefix))
|
||||||
{
|
{
|
||||||
found.Add(new Multiplier(1, qso.CountryPrefix, band));
|
return theirs || qso.Qso.Call.WpxPrefix() is not { } prefix
|
||||||
|
? []
|
||||||
|
: [new Multiplier(1, prefix, band)];
|
||||||
}
|
}
|
||||||
string county = County(qso.Qso);
|
string county = County(qso.Qso);
|
||||||
if (IsOkOm(qso.CountryPrefix) && county.Length > 0)
|
return theirs && county.Length > 0 ? [new Multiplier(1, county, band)] : [];
|
||||||
{
|
|
||||||
found.Add(new Multiplier(2, county, band));
|
|
||||||
}
|
|
||||||
return found;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int TotalScore(ScoreTally tally, ContestEntry entry) =>
|
public int TotalScore(ScoreTally tally, ContestEntry entry) =>
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
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),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,7 @@ public static class UdcCondition
|
|||||||
{
|
{
|
||||||
"" => false,
|
"" => false,
|
||||||
"MYCOUNTRY" => qso.IsSameCountry,
|
"MYCOUNTRY" => qso.IsSameCountry,
|
||||||
|
"OTHERCOUNTRY" => qso.CountryPrefix.Length > 0 && !qso.IsSameCountry,
|
||||||
"MYEXCHANGE" => Same(qso.Qso.Section, mySentExchange),
|
"MYEXCHANGE" => Same(qso.Qso.Section, mySentExchange),
|
||||||
"MYGRID" => SameGridField(qso.Qso.GridSquare, qso.Me.GridSquare),
|
"MYGRID" => SameGridField(qso.Qso.GridSquare, qso.Me.GridSquare),
|
||||||
"MYCQZONE" => qso.CqZone > 0 && qso.CqZone == qso.Me.CqZone,
|
"MYCQZONE" => qso.CqZone > 0 && qso.CqZone == qso.Me.CqZone,
|
||||||
|
|||||||
@@ -86,13 +86,13 @@ public sealed class UdcMultiplier
|
|||||||
|
|
||||||
private string? ValueFor(QsoContext qso) => source switch
|
private string? ValueFor(QsoContext qso) => source switch
|
||||||
{
|
{
|
||||||
"COUNTRYPREFIX" => qso.CountryPrefix,
|
"COUNTRYPREFIX" or "COUNTRY" => qso.CountryPrefix,
|
||||||
"EU_COUNTRY" or "AS_COUNTRY" or "NA_COUNTRY" or "SA_COUNTRY" or "AF_COUNTRY" or "OC_COUNTRY" =>
|
"EU_COUNTRY" or "AS_COUNTRY" or "NA_COUNTRY" or "SA_COUNTRY" or "AF_COUNTRY" or "OC_COUNTRY" =>
|
||||||
CountryOnContinent(qso, source[..2]),
|
CountryOnContinent(qso, source[..2]),
|
||||||
"WPXPREFIX" => qso.Qso.Call.WpxPrefix(),
|
"WPXPREFIX" => qso.Qso.Call.WpxPrefix(),
|
||||||
"2LPREFIX" => Truncate(qso.Qso.Call.Text, 2),
|
"2LPREFIX" => Truncate(qso.Qso.Call.Text, 2),
|
||||||
"LASTLETTER" => LastLetter(qso.Qso.Call.Text),
|
"LASTLETTER" => LastLetter(qso.Qso.Call.Text),
|
||||||
"SECTION" or "SECT" => Upper(qso.Qso.Section),
|
"SECTION" or "SECT" => SectionName(qso.Qso.Section),
|
||||||
"EXCHANGE" or "EXCH" => Upper(qso.Qso.Exchange1),
|
"EXCHANGE" or "EXCH" => Upper(qso.Qso.Exchange1),
|
||||||
"MISC" or "MISCTEXT" => Upper(qso.Qso.MiscText),
|
"MISC" or "MISCTEXT" => Upper(qso.Qso.MiscText),
|
||||||
"COMMENT" => Upper(qso.Qso.Comment),
|
"COMMENT" => Upper(qso.Qso.Comment),
|
||||||
@@ -127,6 +127,18 @@ public sealed class UdcMultiplier
|
|||||||
|
|
||||||
private static string Upper(string text) => text.Trim().ToUpperInvariant();
|
private static string Upper(string text) => text.Trim().ToUpperInvariant();
|
||||||
|
|
||||||
|
/// A section multiplier ignores a value that is all digits. N1MM checks the
|
||||||
|
/// received exchange against the section list the contest names, and a file
|
||||||
|
/// whose section column also carries the serial numbers the other side of
|
||||||
|
/// the contest sends — OK/OM DX SSB is one — would otherwise count every
|
||||||
|
/// serial number as a multiplier. The section lists themselves are not held
|
||||||
|
/// here.
|
||||||
|
private static string? SectionName(string text)
|
||||||
|
{
|
||||||
|
string section = Upper(text);
|
||||||
|
return section.Length > 0 && section.All(char.IsAsciiDigit) ? null : section;
|
||||||
|
}
|
||||||
|
|
||||||
private static string? LastLetter(string call)
|
private static string? LastLetter(string call)
|
||||||
{
|
{
|
||||||
string trimmed = call.Trim().ToUpperInvariant();
|
string trimmed = call.Trim().ToUpperInvariant();
|
||||||
|
|||||||
@@ -52,4 +52,21 @@ public class N1mmContestNamesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void HasFindsAContestByTheNameN1mmWrote() =>
|
public void HasFindsAContestByTheNameN1mmWrote() =>
|
||||||
Assert.True(Registry().Has("CQWPXRTTY"));
|
Assert.True(Registry().Has("CQWPXRTTY"));
|
||||||
|
|
||||||
|
/// Published `.udc` files come with the extension in either case, and a
|
||||||
|
/// case-sensitive file system will not match one glob for both.
|
||||||
|
[Fact]
|
||||||
|
public void AUdcFileIsReadWhateverTheCaseOfItsExtension()
|
||||||
|
{
|
||||||
|
string folder = Directory.CreateTempSubdirectory().FullName;
|
||||||
|
File.WriteAllText(Path.Combine(folder, "Loud.UDC"), "[Contest]\nName=LOUD\n");
|
||||||
|
File.WriteAllText(Path.Combine(folder, "quiet.udc"), "[Contest]\nName=QUIET\n");
|
||||||
|
|
||||||
|
ContestRegistry registry = ContestRegistry.FromFolder(folder, out IReadOnlyList<string> problems);
|
||||||
|
|
||||||
|
Assert.Empty(problems);
|
||||||
|
Assert.True(registry.Has("LOUD"));
|
||||||
|
Assert.True(registry.Has("QUIET"));
|
||||||
|
Directory.Delete(folder, recursive: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,20 +6,23 @@ namespace Nonemm.Contests.Tests;
|
|||||||
|
|
||||||
public class OkOmDxTests
|
public class OkOmDxTests
|
||||||
{
|
{
|
||||||
private static readonly Contest Ssb = new OkOmDx(ModeCategory.Phone);
|
private static readonly Contest Cw = new OkOmDx();
|
||||||
|
|
||||||
private static ContestLog LogFor(StationInfo me) => new(Ssb, me, TestLog.CountryFile);
|
private static ContestLog LogFor(StationInfo me) => new(Cw, me, TestLog.CountryFile);
|
||||||
|
|
||||||
private static Qso FromOkOm(string call, string county, double kilohertz = 14_025) =>
|
private static Qso FromOkOm(string call, string county, double kilohertz = 14_025) =>
|
||||||
TestLog.Contact(call, kilohertz, mode: Modes.Usb, section: county);
|
TestLog.Contact(call, kilohertz, section: county);
|
||||||
|
|
||||||
private static Qso FromDx(string call, string number, double kilohertz = 14_025) =>
|
private static Qso FromDx(string call, string number, double kilohertz = 14_025) =>
|
||||||
TestLog.Contact(call, kilohertz, mode: Modes.Usb, exchange: number);
|
TestLog.Contact(call, kilohertz, exchange: number);
|
||||||
|
|
||||||
|
/// The CW running counts Czechia and Slovakia as one side of the contest:
|
||||||
|
/// two points either way. The SSB running splits them, and it has its own
|
||||||
|
/// pair of .udc files.
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("OM3XYZ", 2)]
|
[InlineData("OM3XYZ", 2)]
|
||||||
[InlineData("OK1XYZ", 3)]
|
[InlineData("OK1XYZ", 2)]
|
||||||
public void AtHomeCzechiaAndSlovakiaAreTwoCountries(string call, int expected) =>
|
public void EitherSideOfTheContestIsTwoPointsAtHome(string call, int expected) =>
|
||||||
Assert.Equal(expected, LogFor(TestLog.Slovakia).Judge(FromOkOm(call, "NOV")).Points);
|
Assert.Equal(expected, LogFor(TestLog.Slovakia).Judge(FromOkOm(call, "NOV")).Points);
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -36,40 +39,39 @@ public class OkOmDxTests
|
|||||||
public void AStationOutsideScoresTenForAnOkOmContact(string call, int expected) =>
|
public void AStationOutsideScoresTenForAnOkOmContact(string call, int expected) =>
|
||||||
Assert.Equal(expected, LogFor(TestLog.Germany).Judge(FromOkOm(call, "NOV")).Points);
|
Assert.Equal(expected, LogFor(TestLog.Germany).Judge(FromOkOm(call, "NOV")).Points);
|
||||||
|
|
||||||
|
/// An OK or OM operator counts the prefixes it works and nothing for the
|
||||||
|
/// other side of the contest.
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CountriesAndCountiesBothCountOncePerBand()
|
public void AnOkOmOperatorCountsPrefixes()
|
||||||
{
|
{
|
||||||
ContestLog log = LogFor(TestLog.Slovakia);
|
ContestLog log = LogFor(TestLog.Slovakia);
|
||||||
Assert.Equal(
|
Assert.Equal("IK2", log.Judge(FromDx("IK2XYZ", "014")).NewMultipliers.Single().Value);
|
||||||
[1, 2],
|
Assert.Empty(log.Judge(FromOkOm("OM3XYZ", "NOV")).NewMultipliers);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A station outside counts the counties instead.
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AStationOutsideOkOmBringsItsCountryAndNoCounty() =>
|
public void EveryoneElseCountsCounties()
|
||||||
Assert.Equal(
|
{
|
||||||
[1],
|
ContestLog log = LogFor(TestLog.Germany);
|
||||||
LogFor(TestLog.Slovakia).Judge(FromDx("IK2XYZ", "014")).NewMultipliers
|
Assert.Equal("NOV", log.Judge(FromOkOm("OM3XYZ", "NOV")).NewMultipliers.Single().Value);
|
||||||
.Select(m => m.Index)
|
Assert.Empty(log.Judge(FromDx("IK2XYZ", "014")).NewMultipliers);
|
||||||
.ToList());
|
}
|
||||||
|
|
||||||
/// The entry window walks the county box for an OK or OM station and the
|
/// The entry window walks the county box for an OK or OM station and the
|
||||||
/// serial box for everyone else.
|
/// serial box for everyone else.
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OnlyOneOfTheTwoBoxesIsWalked()
|
public void OnlyOneOfTheTwoBoxesIsWalked()
|
||||||
{
|
{
|
||||||
IReadOnlyList<ExchangeField> fields = Ssb.ExchangeFieldsFor(TestLog.Slovakia);
|
IReadOnlyList<ExchangeField> fields = Cw.ExchangeFieldsFor(TestLog.Slovakia);
|
||||||
CountryLookup? okom = TestLog.CountryFile.Find("OM3XYZ");
|
CountryLookup? okom = TestLog.CountryFile.Find("OM3XYZ");
|
||||||
CountryLookup? dx = TestLog.CountryFile.Find("IK2XYZ");
|
CountryLookup? dx = TestLog.CountryFile.Find("IK2XYZ");
|
||||||
|
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
[ExchangeSlot.ReceivedReport, ExchangeSlot.Exchange1],
|
[ExchangeSlot.ReceivedReport, ExchangeSlot.Exchange1],
|
||||||
fields.Where(f => !Ssb.SkipsField(f, dx)).Select(f => f.Slot).ToList());
|
fields.Where(f => !Cw.SkipsField(f, dx)).Select(f => f.Slot).ToList());
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
[ExchangeSlot.ReceivedReport, ExchangeSlot.Section],
|
[ExchangeSlot.ReceivedReport, ExchangeSlot.Section],
|
||||||
fields.Where(f => !Ssb.SkipsField(f, okom)).Select(f => f.Slot).ToList());
|
fields.Where(f => !Cw.SkipsField(f, okom)).Select(f => f.Slot).ToList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,10 +28,12 @@ public class SmallContestTests
|
|||||||
.Judge(TestLog.Contact("K1ABC", mode: Modes.Rtty, section: "K1"))
|
.Judge(TestLog.Contact("K1ABC", mode: Modes.Rtty, section: "K1"))
|
||||||
.NewMultipliers.Select(m => m.Index).ToList());
|
.NewMultipliers.Select(m => m.Index).ToList());
|
||||||
|
|
||||||
|
/// 160 metres pays 3, 80 and 10 metres 2, and the rest 1.
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(14_150, 1)]
|
[InlineData(14_150, 1)]
|
||||||
[InlineData(7_050, 2)]
|
[InlineData(7_050, 1)]
|
||||||
[InlineData(3_650, 3)]
|
[InlineData(3_650, 2)]
|
||||||
|
[InlineData(1_830, 3)]
|
||||||
public void AllAsianScoresByBand(double kilohertz, int expected) =>
|
public void AllAsianScoresByBand(double kilohertz, int expected) =>
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
expected,
|
expected,
|
||||||
@@ -47,16 +49,6 @@ public class SmallContestTests
|
|||||||
.Judge(TestLog.Contact("IK2XYZ", mode: Modes.Usb, exchange: "46"))
|
.Judge(TestLog.Contact("IK2XYZ", mode: Modes.Usb, exchange: "46"))
|
||||||
.Points);
|
.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]
|
[Theory]
|
||||||
[InlineData("YO3XYZ", 8)]
|
[InlineData("YO3XYZ", 8)]
|
||||||
[InlineData("DL9XYZ", 1)]
|
[InlineData("DL9XYZ", 1)]
|
||||||
|
|||||||
@@ -261,4 +261,35 @@ public class UdcScoringTests
|
|||||||
Assert.True(Contest("ZoneType=IARU").UsesItuZones);
|
Assert.True(Contest("ZoneType=IARU").UsesItuZones);
|
||||||
Assert.False(Contest("ZoneType=CQ").UsesItuZones);
|
Assert.False(Contest("ZoneType=CQ").UsesItuZones);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The received exchange of a contest where one side sends a section and
|
||||||
|
/// the other a serial number lands in the same column, and only the
|
||||||
|
/// section counts. N1MM checks it against the contest's section list; the
|
||||||
|
/// lists are not held here, so an exchange that is all digits is taken for
|
||||||
|
/// a serial number.
|
||||||
|
[Fact]
|
||||||
|
public void ASerialNumberIsNoSectionMultiplier()
|
||||||
|
{
|
||||||
|
UserDefinedContest contest = Contest("MultSqlString=Section", "IsMultPer=1");
|
||||||
|
Assert.Empty(LogFor(contest).Judge(TestLog.Contact("IK2XYZ", section: "058")).NewMultipliers);
|
||||||
|
Assert.Single(LogFor(contest).Judge(TestLog.Contact("OM3XYZ", section: "NOV")).NewMultipliers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Country` is what a published file writes where the editor writes
|
||||||
|
/// `CountryPrefix`.
|
||||||
|
[Fact]
|
||||||
|
public void CountryIsTheSameSourceAsCountryPrefix() =>
|
||||||
|
Assert.Equal(
|
||||||
|
"I",
|
||||||
|
LogFor(Contest("MultSqlString=Country", "IsMultPer=4"))
|
||||||
|
.Judge(TestLog.Contact("IK2XYZ"))
|
||||||
|
.NewMultipliers.Single().Value);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OtherCountryIsEveryCountryButMine()
|
||||||
|
{
|
||||||
|
UserDefinedContest contest = Contest("PointsPerContact=MyCountry, 0, OtherCountry, 2");
|
||||||
|
Assert.Equal(0, Points(contest, TestLog.Contact("DL9XYZ")));
|
||||||
|
Assert.Equal(2, Points(contest, TestLog.Contact("JA1XYZ")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user