Files
Nonemm/tests/Nonemm.Contests.Tests/WaeTests.cs
T
erikbrockoandClaude Opus 5 2079bf3f59 Paint a WAE station by the traffic left with it, as N1MM does
N1MM has four colours for a station in WAE rather than the usual three, and
the manual and the RTTY tutorial both spell them out. Red is a new
multiplier, as everywhere else. Green is a new station with QTC traffic
still to exchange, which takes the more-than-one-multiplier colour because
WAE has no second multiplier to use it. Black is a station already worked
that still has traffic left — worth working for the QTC points alone — and
it is a colour of its own, N1MM's "Color of a QTC callsign in the WAE
contest". Grey is a dupe with nothing left, the same as in other contests.

The verdict carries how much traffic is left with the station, so the
callsign box, the check window, the bandmap and everything else that colours
a station say the same thing. The contest works the number out: ten with a
station on the other side of the contest, none with one on our own
continent, and none once the ten have gone. Every other contest answers
nought. The log counts the QTC rows per station as they are indexed rather
than counting them again for each station judged, because the bandmap judges
every spot it holds on every pass.

The verdict line says it in words as well, so a dupe reads `dupe · 7 QTCs
left` rather than `dupe`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 20:03:32 +00:00

275 lines
9.7 KiB
C#

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);
}
/// A callsign the country file cannot place still counts, with an empty
/// value, so the log scores the same here as in N1MM. All the unplaceable
/// calls worked on a band share the one multiplier between them.
[Fact]
public void ACallWithNoCountryCountsOncePerBand()
{
ContestLog log = LogFor(Cw, TestLog.Germany);
Assert.Equal("", Assert.Single(log.Judge(On("D1M", 14_025)).NewMultipliers).Value);
log.Add(On("D1M", 14_025));
Assert.Empty(log.Judge(On("D2X", 14_025)).NewMultipliers);
}
/// N1MM's score window gives contacts, traffic sent and traffic received a
/// row of their own on every band, and the multipliers in a band row carry
/// that band's weight: four on 80, three on 40, two on 20, 15 and 10.
[Fact]
public void TheScoreSplitsByBandAndByKindOfRow()
{
ContestLog log = LogFor(Cw, TestLog.Germany);
log.Add(On("JA1XYZ", 3_525));
log.Add(On("K1ABC", 14_025));
log.Add(new WaeQtc(
Callsign.Parse("JA1XYZ"), IsSent: false, 1, 10, "1435", Callsign.Parse("W1AW"), 42)
.ApplyTo(On("JA1XYZ", 3_525)));
Assert.Equal(4, log.Rows[new ScoreKey("80M", WaeQtc.Contact)].TotalMultipliers);
Assert.Equal(1, log.Rows[new ScoreKey("80M", WaeQtc.Contact)].Points);
Assert.Equal(2, log.Rows[new ScoreKey("20M", WaeQtc.Contact)].TotalMultipliers);
ScoreTally traffic = log.Rows[new ScoreKey("80M", WaeQtc.Received)];
Assert.Equal(1, traffic.Qsos);
Assert.Equal(1, traffic.Points);
Assert.Equal(0, traffic.TotalMultipliers);
Assert.Equal(log.Tally.Points, log.Rows.Values.Sum(r => r.Points));
Assert.Equal(log.Tally.TotalMultipliers, log.Rows.Values.Sum(r => r.TotalMultipliers));
}
[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);
}
/// N1MM paints a station by what is left to exchange with it as well as by
/// what it scores, so the verdict carries how much traffic is left: ten
/// with a station on the other side of the contest, none with one on our
/// own, and none once the ten have gone.
[Fact]
public void TheVerdictSaysHowMuchTrafficIsLeft()
{
ContestLog log = LogFor(Cw, TestLog.Germany);
Assert.Equal(10, log.Judge(On("JA1XYZ", 14_025)).TrafficLeft);
Assert.Equal(0, log.Judge(On("OM5M", 14_025)).TrafficLeft);
}
[Fact]
public void TrafficRunsOutAfterTenLines()
{
ContestLog log = LogFor(Cw, TestLog.Germany);
log.Add(On("JA1XYZ", 14_025));
for (int line = 1; line <= 10; line++)
{
log.Add(new WaeQtc(
Callsign.Parse("JA1XYZ"), IsSent: false, 1, 10, "1435", Callsign.Parse("W1AW"), line)
.ApplyTo(On("JA1XYZ", 14_025)));
}
Verdict verdict = log.Judge(On("JA1XYZ", 14_025));
Assert.True(verdict.IsDupe);
Assert.Equal(0, verdict.TrafficLeft);
}
/// A station already worked is still worth working while it has traffic
/// left, which is the state N1MM paints in a colour of its own.
[Fact]
public void ADupeWithTrafficLeftSaysSo()
{
ContestLog log = LogFor(Cw, TestLog.Germany);
log.Add(On("JA1XYZ", 14_025));
Verdict verdict = log.Judge(On("JA1XYZ", 14_025));
Assert.True(verdict.IsDupe);
Assert.Equal(10, verdict.TrafficLeft);
}
/// On RTTY the line is between continents rather than between Europe and
/// the rest, so two European stations still cannot exchange traffic.
[Fact]
public void OnRttyTrafficNeedsTwoContinents()
{
ContestLog log = LogFor(Rtty, TestLog.Germany);
Assert.Equal(10, log.Judge(On("JA1XYZ", 14_080, Modes.Rtty)).TrafficLeft);
Assert.Equal(0, log.Judge(On("OM5M", 14_080, Modes.Rtty)).TrafficLeft);
}
/// Every other contest has no traffic at all.
[Fact]
public void AContestWithoutTrafficLeavesTheNumberAtNought()
{
ContestLog log = new(new CqWorldWide(ModeCategory.Cw), TestLog.Germany, TestLog.CountryFile);
Assert.Equal(0, log.Judge(TestLog.Contact("JA1XYZ", zone: 25)).TrafficLeft);
}
/// ADIF has no QTC record, so the export writes contacts alone. N1MM says
/// the same in its manual: "the ADIF file will not contain QTCs, only
/// QSOs".
[Fact]
public void TheContactsAreTheRowsWithoutTheTraffic()
{
ContestLog log = LogFor(Rtty, TestLog.Germany);
log.Add(On("JA1XYZ", 14_080, Modes.Rtty));
log.Add(new WaeQtc(
Callsign.Parse("JA1XYZ"), IsSent: true, 1, 10, "1435", Callsign.Parse("DL9XYZ"), 42)
.ApplyTo(On("JA1XYZ", 14_080, Modes.Rtty)));
Assert.Equal(2, log.Qsos.Count);
Qso only = Assert.Single(log.Contacts);
Assert.Equal(WaeQtc.Contact, only.Exchange1);
}
[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)));
}