Files
Nonemm/tests/Nonemm.Contests.Tests/BandChangeRulesTests.cs
ericek111 6f2f39768c Show the other computers, and apply the ten-minute rule
Three pieces on top of the link:

**The network status window**, on the entry window's Window menu. A row
per station, this computer included the way N1MM shows it: number,
address, operator, band, mode, run, transmit, pass frequency, the last
message type, how long ago it arrived, the counts each way and the echo
round trip. A station that broadcast the wrong version is named under the
table in red — that is the one fault where everything looks connected
and nothing arrives. The box at the bottom sends a line of chat, and
Echo asks every station whether it is there.

**Where this station is** goes out once a second when it has changed.
The frequency, the mode and run have a dozen places they can change from
— the radio moving, a band button, a QSY typed into the callsign box —
so it is read and compared rather than announced from each of them.

**The ten-minute rule.** `BandChangeRules` already counted changes and
the stay on a band, and the entry window already showed the countdown,
but only a user-defined contest carried a rule, so every built-in
contest allowed anything. `ForCategory` gives a multi-operator entry
with one or two transmitters a ten-minute stay, which is N1MM's fallback
in `ContestInstance.BandChangeTimerDuration` for every contest that does
not name its own. Its per-contest table is a few hundred cases in a
decompiled hash switch and is not repeated.

Config ▸ Edit Networked-Computer Names now covers both networks: the
12060 broadcast to other programs and the 12070 link, with the station
number, the port, the version to claim and stations named by address for
a network where a broadcast does not reach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 15:26:42 +00:00

154 lines
5.4 KiB
C#

using Nonemm.Core;
namespace Nonemm.Contests.Tests;
/// The `…BandChange…` settings: what counts as a change, and over what stretch
/// of time the changes are counted.
public class BandChangeRulesTests
{
private static readonly DateTime Start = new(2026, 5, 30, 12, 0, 0, DateTimeKind.Utc);
private static Qso At(int minutes, double kilohertz, Mode? mode = null, int seconds = 0) =>
TestLog.Contact("OM3XYZ", kilohertz, mode: mode) with
{
TimestampUtc = Start.AddMinutes(minutes).AddSeconds(seconds),
};
/// N1MM's fallback, which is the ten-minute rule: a multi-operator entry
/// with one transmitter has to stay ten minutes on a band, and nothing caps
/// how many times it may move.
[Theory]
[InlineData("MULTI-ONE", "ONE", 10)]
[InlineData("MULTI-OP", "ONE", 10)]
[InlineData("MULTI-TWO", "TWO", 10)]
[InlineData("MULTI-OP", "TWO", 10)]
[InlineData("MULTI-OP", "UNLIMITED", 0)]
[InlineData("SINGLE-OP", "ONE", 0)]
public void TheCategoryAloneSaysHowLongAStationStaysOnABand(
string operatorCategory,
string transmitters,
int minutes)
{
BandChangeRules rules = BandChangeRules.ForCategory(new ContestEntry
{
OperatorCategory = operatorCategory,
TransmitterCategory = transmitters,
});
Assert.Equal(TimeSpan.FromMinutes(minutes), rules.MinimumStay);
Assert.False(rules.IsCounted);
}
/// A contest that says nothing about band changes still gets the rule for
/// the category, which is what N1MM does.
[Fact]
public void AContestThatSaysNothingStillGetsTheTenMinuteRule()
{
Contest contest = new Rules.CqWorldWide(ModeCategory.Cw);
BandChangeRules rules = contest.BandChangesFor(new ContestEntry
{
OperatorCategory = "MULTI-ONE",
TransmitterCategory = "ONE",
});
Assert.True(rules.HasStayTimer);
Assert.Equal(TimeSpan.FromMinutes(10), rules.MinimumStay);
}
[Fact]
public void ASingleOperatorEntryHasNoBandChangeRule()
{
Contest contest = new Rules.CqWorldWide(ModeCategory.Cw);
Assert.Equal(
BandChangeRules.None,
contest.BandChangesFor(new ContestEntry { OperatorCategory = "SINGLE-OP" }));
}
[Fact]
public void EachMoveToAnotherBandIsOneChange() =>
Assert.Equal(
2,
new BandChangeRules(8).CountedIn(
[At(0, 14_025), At(5, 7_040), At(10, 7_045), At(15, 14_030)],
Start.AddMinutes(20)));
/// The count is per clock hour unless the file says otherwise, so a change
/// made in the hour before does not count against this one.
[Fact]
public void ChangesMadeInAnEarlierHourAreNotCounted() =>
Assert.Equal(
1,
new BandChangeRules(8).CountedIn(
[At(0, 14_025), At(5, 7_040), At(65, 14_025)],
Start.AddMinutes(70)));
[Fact]
public void CountingPerContestKeepsEveryChange() =>
Assert.Equal(
2,
new BandChangeRules(8, PerContest: true).CountedIn(
[At(0, 14_025), At(5, 7_040), At(65, 14_025)],
Start.AddMinutes(70)));
/// `CountBandChangesPerPeriod` counts the hour up to now rather than the
/// clock hour.
[Fact]
public void CountingPerPeriodLooksBackAnHour() =>
Assert.Equal(
1,
new BandChangeRules(8, PerPeriod: true).CountedIn(
[At(0, 14_025), At(5, 7_040), At(65, 14_025)],
Start.AddMinutes(70)));
/// `CountBandOrModeChange` counts a move to another mode on the same band.
[Fact]
public void ModeChangesCountWhenTheContestSaysSo() =>
Assert.Equal(
1,
new BandChangeRules(8, CountsModeChanges: true).CountedIn(
[At(0, 14_025), At(5, 14_250, Modes.Usb)],
Start.AddMinutes(10)));
/// The stay runs from the minute after the contact that changed band, so a
/// change at 12:05:40 with a ten-minute stay has five minutes left at
/// 12:11. N1MM starts it the same way.
[Fact]
public void TheStayCountsDownFromTheMinuteAfterTheChange()
{
BandChangeRules rules = new(8, MinimumStay: TimeSpan.FromMinutes(10));
Assert.Equal(
TimeSpan.FromMinutes(5),
rules.StayLeft([At(0, 14_025), At(5, 7_040, seconds: 40)], Start.AddMinutes(11)));
}
[Fact]
public void TheStayIsUpOnceTheTimeHasPassed() =>
Assert.Equal(
TimeSpan.Zero,
new BandChangeRules(8, MinimumStay: TimeSpan.FromMinutes(10)).StayLeft(
[At(0, 14_025), At(5, 7_040)],
Start.AddMinutes(30)));
/// A log that has stayed on one band has nothing to count down.
[Fact]
public void NothingCountsDownUntilTheStationChangesBand() =>
Assert.Equal(
TimeSpan.Zero,
new BandChangeRules(8, MinimumStay: TimeSpan.FromMinutes(10)).StayLeft(
[At(0, 14_025), At(5, 14_030)],
Start.AddMinutes(6)));
[Fact]
public void AContestThatAsksForNoStayHasNoTimer()
{
Assert.False(new BandChangeRules(8).HasStayTimer);
Assert.True(new BandChangeRules(8, MinimumStay: TimeSpan.FromMinutes(10)).HasStayTimer);
}
[Fact]
public void AContestWithNoLimitCountsNothing() => Assert.False(BandChangeRules.None.IsCounted);
}