Score Worked All Europe, QTC traffic and all

WAE is two contests in one. The QSOs are scored like any other, except that a
multiplier is worth four on 80 metres, three on 40 and two on the high bands, so
Multiplier grew a weight and ScoreTally sums weights rather than counting.
Europe works the rest of the world and the rest of the world works Europe; on
RTTY that rule is dropped and everybody works everybody.

The other half is QTC traffic: one station reads back contacts it has already
made, ten to a series, and every line is a point for both stations. Those lines
live in the log as rows of their own, exactly where N1MM puts them — Exchange1
says SQTC or RQTC, the reported contact's time, call and number go in the sent
report, received report and sent number columns, and the series in misc text.
So a log written by either program opens in the other with its traffic intact.
A QTC row is not a worked station: Contest.IsContact says so, and ContestLog
keeps such rows out of the dupe index and the multiplier index while still
counting their points. Cabrillo writes them as QTC: records.

Checked against eight real WAE logs rather than against my own reading of the
rules. Points agree with N1MM on all eight, to the contact. Weighted
multipliers agree on the six logs from 2022 on; the two older ones differ only
where the country file has since changed its mind, and in one place on purpose:
N1MM counts a callsign it cannot place as a multiplier with an empty value.

That comparison turned up two bugs of ours. A KG4 call is Guantanamo Bay only
when it is three or five characters long — KG4NE is, KG4IGC is an ordinary US
call — and the country file cannot say so, so every program that reads it
carries the rule. And Callsign is a record holding a list of modifiers, so the
generated equality compared that list by reference and two parses of one call
came out different.

Entering QTCs while operating is not there yet; the window is next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 23:48:47 +00:00
parent 86c01f246f
commit 8961677354
19 changed files with 533 additions and 13 deletions

View File

@@ -20,6 +20,8 @@ public static class TestLog
JA,JH,JR,7K;
Brazil: 11: 15: SA: -10.00: 55.00: 3.0: PY:
PY,PP;
Asiatic Russia: 17: 30: AS: 55.88: -84.10: -7.0: UA9:
RM8,RM9,RM0,UA8,UA9,UA0,R8,R9,R0;
""";
public static readonly CountryFile CountryFile = CountryFile.Parse(Countries);

View File

@@ -0,0 +1,164 @@
using Nonemm.Contests.Rules;
using Nonemm.Core;
namespace Nonemm.Contests.Tests;
public class WaeTests
{
private static readonly Wae Cw = new(ModeCategory.Cw);
private static readonly Wae Rtty = new(ModeCategory.Digital);
private static ContestLog LogFor(Contest contest, StationInfo me) =>
new(contest, me, TestLog.CountryFile);
private static Qso On(string call, double kilohertz, Mode? mode = null) =>
TestLog.Contact(call, kilohertz: kilohertz, mode: mode ?? Modes.Cw, exchange: WaeQtc.Contact);
[Fact]
public void EveryContactOnAContestBandIsOnePoint()
{
ContestLog log = LogFor(Cw, TestLog.Germany);
Assert.Equal(1, log.Judge(On("JA1XYZ", 14_025)).Points);
}
/// WAE does not run on the WARC bands.
[Fact]
public void AContactOnAWarcBandScoresNothing()
{
ContestLog log = LogFor(Cw, TestLog.Germany);
Assert.Equal(0, log.Judge(On("JA1XYZ", 18_080)).Points);
}
[Fact]
public void EuropeDoesNotWorkEuropeOnCw()
{
ContestLog log = LogFor(Cw, TestLog.Germany);
Verdict verdict = log.Judge(On("IK2XYZ", 14_025));
Assert.Equal(0, verdict.Points);
Assert.Empty(verdict.NewMultipliers);
}
[Fact]
public void OnRttyEverybodyWorksEverybody()
{
ContestLog log = LogFor(Rtty, TestLog.Germany);
Verdict verdict = log.Judge(On("IK2XYZ", 14_080, Modes.Rtty));
Assert.Equal(1, verdict.Points);
Assert.Single(verdict.NewMultipliers);
}
/// A multiplier is worth four on 80 metres, three on 40 and two higher up.
[Theory]
[InlineData(3_530, 4)]
[InlineData(7_030, 3)]
[InlineData(14_030, 2)]
[InlineData(21_030, 2)]
[InlineData(28_030, 2)]
public void AMultiplierIsWorthMoreOnTheLowBands(double kilohertz, int weight)
{
ContestLog log = LogFor(Cw, TestLog.Germany);
Multiplier multiplier = Assert.Single(log.Judge(On("JA1XYZ", kilohertz)).NewMultipliers);
Assert.Equal(weight, multiplier.Weight);
Assert.Equal("JA1", multiplier.Value);
}
[Fact]
public void TheWeightGoesIntoTheScore()
{
ContestLog log = LogFor(Cw, TestLog.Germany);
log.Add(On("JA1XYZ", 3_530));
Assert.Equal(1, log.Tally.Points);
Assert.Equal(4, log.Tally.TotalMultipliers);
Assert.Equal(4, log.TotalScore);
}
/// The countries with call areas count each area separately.
[Theory]
[InlineData("K1ABC", "K1")]
[InlineData("W6XYZ", "K6")]
[InlineData("JA1XYZ", "JA1")]
[InlineData("PY2ABC", "PY2")]
public void ACallAreaIsAMultiplierOfItsOwn(string call, string expected)
{
ContestLog log = LogFor(Cw, TestLog.Germany);
Multiplier multiplier = Assert.Single(log.Judge(On(call, 14_025)).NewMultipliers);
Assert.Equal(expected, multiplier.Value);
}
/// Asiatic Russia goes by the call area digit whatever the prefix on the
/// air, so RM0W and UA0AB are the same multiplier.
[Theory]
[InlineData("RM0W")]
[InlineData("UA0AB")]
public void AsiaticRussiaCountsByCallArea(string call)
{
ContestLog log = LogFor(Cw, TestLog.Germany);
Multiplier multiplier = Assert.Single(log.Judge(On(call, 14_025)).NewMultipliers);
Assert.Equal("UA0", multiplier.Value);
}
/// N1MM counts a callsign it cannot place as a multiplier with an empty
/// value, which inflates the score. A call with no country is no
/// multiplier here.
[Fact]
public void ACallWithNoCountryIsNoMultiplier()
{
ContestLog log = LogFor(Cw, TestLog.Germany);
Assert.Empty(log.Judge(On("D1M", 14_025)).NewMultipliers);
}
[Fact]
public void AQtcIsAPointAndNothingElse()
{
ContestLog log = LogFor(Rtty, TestLog.Germany);
Qso qtc = new WaeQtc(
Callsign.Parse("JA1XYZ"), IsSent: true, 1, 10, "1435", Callsign.Parse("DL9XYZ"), 42)
.ApplyTo(On("JA1XYZ", 14_080, Modes.Rtty));
Verdict verdict = log.Judge(qtc);
Assert.Equal(1, verdict.Points);
Assert.Empty(verdict.NewMultipliers);
}
/// The station is worked once and then reported to ten times over; those
/// rows must not read as dupes of the contact.
[Fact]
public void QtcTrafficWithAStationAlreadyWorkedIsNotADupe()
{
ContestLog log = LogFor(Rtty, TestLog.Germany);
log.Add(On("JA1XYZ", 14_080, Modes.Rtty));
Qso qtc = new WaeQtc(
Callsign.Parse("JA1XYZ"), IsSent: true, 1, 10, "1435", Callsign.Parse("DL9XYZ"), 42)
.ApplyTo(On("JA1XYZ", 14_080, Modes.Rtty));
Assert.False(log.Judge(qtc).IsDupe);
log.Add(qtc);
Assert.Equal(2, log.Tally.Points);
Assert.Equal(2, log.Tally.TotalMultipliers);
}
[Fact]
public void AQtcGoesIntoTheLogTheWayN1mmKeepsIt()
{
WaeQtc qtc = new(
Callsign.Parse("H2X"), IsSent: true, 9, 10, "0107", Callsign.Parse("OH1SIC"), 7);
Qso row = qtc.ApplyTo(On("H2X", 14_099, Modes.Rtty));
Assert.Equal("SQTC", row.Exchange1);
Assert.Equal("9/10", row.MiscText);
Assert.Equal("0107", row.SentReport);
Assert.Equal("OH1SIC", row.ReceivedReport);
Assert.Equal(7, row.SentNumber);
Assert.Equal(qtc, WaeQtc.From(row));
}
[Fact]
public void AnOrdinaryContactIsNotAQtc() =>
Assert.Null(WaeQtc.From(On("JA1XYZ", 14_025)));
}

View File

@@ -69,4 +69,12 @@ public class CallsignTests
[InlineData("")]
public void ATextThatCouldNotBeACallIsNot(string text) =>
Assert.False(Callsign.Parse(text).IsPlausible);
[Fact]
public void TwoParsesOfOneCallAreTheSameCallsign()
{
Assert.Equal(Callsign.Parse("OH1SIC"), Callsign.Parse("OH1SIC"));
Assert.Equal(Callsign.Parse("KH9/N8BJQ"), Callsign.Parse("KH9/N8BJQ"));
Assert.NotEqual(Callsign.Parse("OH1SIC"), Callsign.Parse("OH1SID"));
}
}