Stack the calls that answer a CQ, the way N1MM does
A CQ is answered by more than one station. N1MM lets the operator put the
others on a stack and work them one after another; this is that, with N1MM's
keys and macros.
Ctrl+Alt+G and {STACKANOTHER} put the call being typed on the stack and clear
the boxes. Alt+G brings the top call back. Alt+D drops it. {SOCALLSTACK} both
stacks and unstacks in one key: it swaps the typed call for the top one, stacks
it when the stack is empty, and takes the top call when the boxes are.
{LOGTHENPOP} and {LOGTHENNEXT} log the contact and bring the next caller in.
{CLRSTACK} empties it.
The rules are N1MM's, and CallStack and the entry actions hold them without a
window, so they are unit-tested: stacking only while running, nothing shorter
than two characters, no dupe, no frequency typed into the callsign box, a call
already on the stack moved rather than repeated, and a multiplier to the front.
{SOCALLSTACK} takes the stacked call off before it puts the typed one on, so a
multiplier just typed cannot come straight back.
Two places differ. N1MM refuses your own call only while the stack is empty;
it is never a station to work, so it is refused either way here. And the order
is multipliers first in every mode: N1MM's digital window offers first-in and
last-in as well, and there is no digital window here.
The stack window opens by itself with the first call and closes with the last,
or Window > Call Stack opens it. It does not take the keyboard when it opens,
because the operator is typing the next call. The next call out is ringed and
each is coloured by what working it would bring, as in every other window.
OperatingPosition.JudgeCall now answers what a call alone is worth, which the
stack window and the check window both need; it was private to the check
window before.
Driven under Xvfb: four calls stacked with Ctrl+Alt+G and listed in the window,
Alt+G bringing the top one into the boxes, Alt+D dropping the next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
49
README.md
49
README.md
@@ -132,7 +132,8 @@ macros that are filled in:
|
|||||||
|
|
||||||
The action macros run rather than standing for text: `{WIPE}`, `{LOG}`,
|
The action macros run rather than standing for text: `{WIPE}`, `{LOG}`,
|
||||||
`{RUN}`, `{S&P}`, `{SPACE}`, `{SPOTME}`, `{STOPTX}`, `{FREQUP}`, `{FREQDN}`,
|
`{RUN}`, `{S&P}`, `{SPACE}`, `{SPOTME}`, `{STOPTX}`, `{FREQUP}`, `{FREQDN}`,
|
||||||
`{PGUP}`, `{PGDN}`, `{JUMPRX}`, `{JUMPRXTX}`, `{CTRLF1}` to `{CTRLF12}` to send
|
`{PGUP}`, `{PGDN}`, `{JUMPRX}`, `{JUMPRXTX}`, `{STACKANOTHER}`, `{SOCALLSTACK}`,
|
||||||
|
`{LOGTHENPOP}`, `{LOGTHENNEXT}`, `{CLRSTACK}`, `{CTRLF1}` to `{CTRLF12}` to send
|
||||||
on the other radio, `{TELNET sh/dx}` to put a command on the cluster, and
|
on the other radio, `{TELNET sh/dx}` to put a command on the cluster, and
|
||||||
`{OTRSP TX2}` to send one to the SO2R box. An action runs before the message
|
`{OTRSP TX2}` to send one to the SO2R box. An action runs before the message
|
||||||
goes out, unless it stands after `{END}`, and then it runs once the message has
|
goes out, unless it stands after `{END}`, and then it runs once the message has
|
||||||
@@ -145,8 +146,8 @@ How far `{FREQUP}` and the rest move the radio is `FrequencyStepHertz` and
|
|||||||
for the same two numbers in its Configurer.
|
for the same two numbers in its Configurer.
|
||||||
|
|
||||||
The rest of N1MM's action macros — the CAT families, audio, rotators, stereo,
|
The rest of N1MM's action macros — the CAT families, audio, rotators, stereo,
|
||||||
call stacking, the digital TNC macros — are read and passed over, so a message
|
the digital TNC macros — are read and passed over, so a message that holds one
|
||||||
that holds one still sends the right characters.
|
still sends the right characters.
|
||||||
|
|
||||||
### ESM — Enter sends the message
|
### ESM — Enter sends the message
|
||||||
|
|
||||||
@@ -204,6 +205,41 @@ Ctrl+? switches between them, and F1 puts you into run mode, as in N1MM.
|
|||||||
Two lights sit beside them: the left one is green while a radio is answering,
|
Two lights sit beside them: the left one is green while a radio is answering,
|
||||||
the right one red while something is going out on the air.
|
the right one red while something is going out on the air.
|
||||||
|
|
||||||
|
### Call stacking
|
||||||
|
|
||||||
|
A CQ is answered by more than one station at a time. Rather than ask the others
|
||||||
|
to call again, put them on the call stack and work them one after another. This
|
||||||
|
is N1MM's single-operator call stacking, with N1MM's keys.
|
||||||
|
|
||||||
|
| Key | Macro | What it does |
|
||||||
|
|---|---|---|
|
||||||
|
| Ctrl+Alt+G | `{STACKANOTHER}` | puts the call being typed on the stack and clears the boxes, so the next caller can be copied |
|
||||||
|
| Alt+G | | the call on top of the stack replaces what is typed |
|
||||||
|
| Alt+D | | drops the call on top without working it |
|
||||||
|
| | `{SOCALLSTACK}` | one key that both stacks and unstacks: it swaps the call being typed for the one on top, stacks it when the stack is empty, and takes the top call when the boxes are |
|
||||||
|
| | `{LOGTHENPOP}`, `{LOGTHENNEXT}` | logs the contact and brings the next caller into the boxes in one key |
|
||||||
|
| | `{CLRSTACK}` | empties the stack |
|
||||||
|
|
||||||
|
Stacking only works while running: a station you are chasing is worked now, not
|
||||||
|
later. A dupe is not stacked, nor is your own call, nor a frequency typed into
|
||||||
|
the callsign box. `{SOCALLSTACK}` takes the stacked call off before it puts the
|
||||||
|
typed one on, so a multiplier you have just typed cannot come straight back into
|
||||||
|
the boxes.
|
||||||
|
|
||||||
|
A call that would bring a multiplier goes to the front of the stack and the rest
|
||||||
|
to the back, which is the order N1MM uses on CW and phone. N1MM's digital window
|
||||||
|
also offers plain first-in-first-out and last-in-first-out; there is no digital
|
||||||
|
window here, so every mode gets that one order.
|
||||||
|
|
||||||
|
The stack window opens by itself when the first call goes on and closes when the
|
||||||
|
last one comes off, or **Window ▸ Call Stack** opens it. The next call out is
|
||||||
|
ringed, and each is coloured by what working it would bring, as everywhere else.
|
||||||
|
Double-click a call to bring it to the front, right-click to drop it.
|
||||||
|
|
||||||
|
What is not here: N1MM's ESM "next call key" setting, which picks the function
|
||||||
|
key ESM sends when a contact is logged with calls still stacked, and passing
|
||||||
|
stacked calls to the other stations of a multi-operator entry.
|
||||||
|
|
||||||
### The buttons and the title bar
|
### The buttons and the title bar
|
||||||
|
|
||||||
The twelve function keys sit in two rows of six, with the row of actions under
|
The twelve function keys sit in two rows of six, with the row of actions under
|
||||||
@@ -214,9 +250,10 @@ them. Each row fills the width, and the first button of the action row takes the
|
|||||||
The action row is N1MM's: Esc: Stop, Wipe, Log It, Edit, Spot It and QRZ, each
|
The action row is N1MM's: Esc: Stop, Wipe, Log It, Edit, Spot It and QRZ, each
|
||||||
doing what its key does. QRZ opens the callsign's page in a browser.
|
doing what its key does. QRZ opens the callsign's page in a browser.
|
||||||
|
|
||||||
N1MM's other two buttons are not there. Mark puts a `Busy@` marker on your own
|
N1MM's other two buttons are not there. Both put something on your own bandmap
|
||||||
bandmap at the frequency you are on, which needs a bandmap entry that is not a
|
rather than on the cluster: Mark leaves a `Busy@` marker at the frequency you
|
||||||
station. Store is call stacking, which is not here either.
|
are on, which needs a bandmap entry that is not a station, and Store puts the
|
||||||
|
call being typed there so you can come back to it.
|
||||||
|
|
||||||
The title bar says what N1MM's says: the frequency, the mode, whether the
|
The title bar says what N1MM's says: the frequency, the mode, whether the
|
||||||
frequency came from a radio or was typed, and which radio the window belongs to
|
frequency came from a radio or was typed, and which radio the window belongs to
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ when a download brings back something unreadable.
|
|||||||
**Not every action macro is acted on.** The text macros and the action macros
|
**Not every action macro is acted on.** The text macros and the action macros
|
||||||
that this program can carry out are listed in the README. What is read and
|
that this program can carry out are listed in the README. What is read and
|
||||||
passed over: the `{CAT…}` and radio-hex families, the audio and rotator macros,
|
passed over: the `{CAT…}` and radio-hex families, the audio and rotator macros,
|
||||||
`{STEREOON}` and `{STEREOOFF}`, the call-stacking macros, `{CONDJUMP}`,
|
`{STEREOON}` and `{STEREOOFF}`, `{CONDJUMP}`,
|
||||||
`{QSYCQ}`, `{FORCELOG}`, `{SwapContests}`, the digital TNC macros (`{ENTER}`,
|
`{QSYCQ}`, `{FORCELOG}`, `{SwapContests}`, the digital TNC macros (`{ENTER}`,
|
||||||
`{ESC}`, `{CTRL-A}`…), and the wav-directory macros, which wait on voice keying.
|
`{ESC}`, `{CTRL-A}`…), and the wav-directory macros, which wait on voice keying.
|
||||||
A message holding any of them still sends the right characters.
|
A message holding any of them still sends the right characters.
|
||||||
|
|||||||
17
src/Nonemm.App/Windows/CallStackWindow.axaml
Normal file
17
src/Nonemm.App/Windows/CallStackWindow.axaml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<local:RefreshableWindow xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="using:Nonemm.App.Windows"
|
||||||
|
x:Class="Nonemm.App.Windows.CallStackWindow"
|
||||||
|
Title="Call stack" Width="180" Height="220"
|
||||||
|
ShowActivated="False">
|
||||||
|
<DockPanel Margin="4">
|
||||||
|
<Button DockPanel.Dock="Bottom" Name="ClearButton" Content="Clear stack" FontSize="11"
|
||||||
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
|
||||||
|
Margin="0,4,0,0" Click="OnClear" />
|
||||||
|
<TextBlock DockPanel.Dock="Bottom" FontSize="10" Opacity="0.7" Margin="2,4,0,0"
|
||||||
|
Text="double-click to move up, right-click to drop" TextWrapping="Wrap" />
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Name="Calls" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</DockPanel>
|
||||||
|
</local:RefreshableWindow>
|
||||||
87
src/Nonemm.App/Windows/CallStackWindow.axaml.cs
Normal file
87
src/Nonemm.App/Windows/CallStackWindow.axaml.cs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Nonemm.Session;
|
||||||
|
|
||||||
|
namespace Nonemm.App.Windows;
|
||||||
|
|
||||||
|
/// The calls waiting to be worked, next one at the top and ringed. Each is
|
||||||
|
/// coloured by what working it would bring, the way every other window colours
|
||||||
|
/// a station. Double-click one to bring it to the top, right-click to drop it.
|
||||||
|
public sealed partial class CallStackWindow : RefreshableWindow
|
||||||
|
{
|
||||||
|
private readonly AppSession session;
|
||||||
|
private readonly int radioNumber;
|
||||||
|
|
||||||
|
public CallStackWindow(AppSession session, int radioNumber = 1)
|
||||||
|
{
|
||||||
|
this.session = session;
|
||||||
|
this.radioNumber = radioNumber;
|
||||||
|
InitializeComponent();
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private OperatingPosition? Radio =>
|
||||||
|
session.Positions.FirstOrDefault(p => p.RadioNumber == radioNumber);
|
||||||
|
|
||||||
|
public override void Refresh()
|
||||||
|
{
|
||||||
|
Calls.Children.Clear();
|
||||||
|
if (Radio is not { } position)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ClearButton.IsEnabled = !position.Stack.IsEmpty;
|
||||||
|
foreach (string call in position.Stack.Calls)
|
||||||
|
{
|
||||||
|
Calls.Children.Add(Row(position, call, isNext: call == position.Stack.Top));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Control Row(OperatingPosition position, string call, bool isNext)
|
||||||
|
{
|
||||||
|
TextBlock text = new()
|
||||||
|
{
|
||||||
|
Text = call,
|
||||||
|
FontFamily = new FontFamily("monospace"),
|
||||||
|
FontSize = 14,
|
||||||
|
Foreground = Verdicts.Colour(position.JudgeCall(call)),
|
||||||
|
};
|
||||||
|
Border row = new()
|
||||||
|
{
|
||||||
|
CornerRadius = new CornerRadius(9),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
BorderBrush = isNext ? text.Foreground : Brushes.Transparent,
|
||||||
|
Padding = new Thickness(6, 1),
|
||||||
|
Margin = new Thickness(0, 0, 0, 2),
|
||||||
|
Cursor = new Cursor(StandardCursorType.Hand),
|
||||||
|
Child = text,
|
||||||
|
};
|
||||||
|
row.PointerPressed += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.GetCurrentPoint(row).Properties.IsRightButtonPressed)
|
||||||
|
{
|
||||||
|
position.Stack.Remove(call);
|
||||||
|
position.Session.NotifyChanged();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
row.DoubleTapped += (_, _) =>
|
||||||
|
{
|
||||||
|
position.Stack.MoveToTop(call);
|
||||||
|
position.Session.NotifyChanged();
|
||||||
|
};
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClear(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (Radio is not { } position)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
position.Stack.Clear();
|
||||||
|
position.Session.NotifyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -211,9 +211,85 @@ public sealed partial class EntryWindow
|
|||||||
case MessageCommand.Otrsp:
|
case MessageCommand.Otrsp:
|
||||||
_ = session.Box?.SendCommandAsync(action.Argument);
|
_ = session.Box?.SendCommandAsync(action.Argument);
|
||||||
break;
|
break;
|
||||||
|
case MessageCommand.StackAnother:
|
||||||
|
StackAnother();
|
||||||
|
break;
|
||||||
|
case MessageCommand.SwapWithStack:
|
||||||
|
SwapWithStack();
|
||||||
|
break;
|
||||||
|
case MessageCommand.LogThenPop:
|
||||||
|
LogThenPop();
|
||||||
|
break;
|
||||||
|
case MessageCommand.ClearStack:
|
||||||
|
Logging.Stack.Clear();
|
||||||
|
ShowCallStack();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// N1MM's {STACKANOTHER} and Ctrl+Alt+G.
|
||||||
|
private void StackAnother()
|
||||||
|
{
|
||||||
|
if (Logging is null || !Logging.StackAnother())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SyncBoxes();
|
||||||
|
boxes[0].Focus();
|
||||||
|
ShowCallStack();
|
||||||
|
Status($"{Logging.Stack.Count} stacked");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// N1MM's Alt+G.
|
||||||
|
private void GrabStacked()
|
||||||
|
{
|
||||||
|
if (Logging is null || !Logging.GrabStacked())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SyncBoxes();
|
||||||
|
boxes[0].Focus();
|
||||||
|
ShowCallStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// N1MM's Alt+D: the call on top of the stack is dropped without being
|
||||||
|
/// worked.
|
||||||
|
private void DropStacked()
|
||||||
|
{
|
||||||
|
if (Logging is null || Logging.Stack.IsEmpty)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Status($"{Logging.Stack.Next()} dropped");
|
||||||
|
ShowCallStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// N1MM's {SOCALLSTACK}.
|
||||||
|
private void SwapWithStack()
|
||||||
|
{
|
||||||
|
if (Logging is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Logging.SwapWithStack();
|
||||||
|
SyncBoxes();
|
||||||
|
boxes[0].Focus();
|
||||||
|
ShowCallStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// N1MM's {LOGTHENPOP} and {LOGTHENNEXT}: one key logs the contact and
|
||||||
|
/// brings the next caller into the boxes, so a run does not stop between
|
||||||
|
/// callers.
|
||||||
|
private void LogThenPop()
|
||||||
|
{
|
||||||
|
if (Logging is null || !Logging.IsRunning)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LogContact();
|
||||||
|
GrabStacked();
|
||||||
|
}
|
||||||
|
|
||||||
private void Step(int hertz)
|
private void Step(int hertz)
|
||||||
{
|
{
|
||||||
if (Logging is null)
|
if (Logging is null)
|
||||||
|
|||||||
@@ -353,6 +353,28 @@ public sealed partial class EntryWindow
|
|||||||
|
|
||||||
private void OnShowLog(object? sender, RoutedEventArgs e) => Show(() => new LogWindow(session));
|
private void OnShowLog(object? sender, RoutedEventArgs e) => Show(() => new LogWindow(session));
|
||||||
|
|
||||||
|
private void OnShowCallStack(object? sender, RoutedEventArgs e) =>
|
||||||
|
Show(() => new CallStackWindow(session, radioNumber));
|
||||||
|
|
||||||
|
/// N1MM opens the stack window when the first call goes on it and closes it
|
||||||
|
/// when the last one comes off.
|
||||||
|
private void ShowCallStack()
|
||||||
|
{
|
||||||
|
if (Logging is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Logging.Stack.IsEmpty)
|
||||||
|
{
|
||||||
|
if (openWindows.TryGetValue(typeof(CallStackWindow), out Window? open))
|
||||||
|
{
|
||||||
|
open.Close();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Show(() => new CallStackWindow(session, radioNumber), activate: false).Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnShowCheck(object? sender, RoutedEventArgs e) => Show(() => new CheckWindow(session));
|
private void OnShowCheck(object? sender, RoutedEventArgs e) => Show(() => new CheckWindow(session));
|
||||||
|
|
||||||
private void OnShowBandmap(object? sender, RoutedEventArgs e) => Show(() => new BandmapWindow(session, Tune));
|
private void OnShowBandmap(object? sender, RoutedEventArgs e) => Show(() => new BandmapWindow(session, Tune));
|
||||||
@@ -587,11 +609,16 @@ public sealed partial class EntryWindow
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The one window of that kind, opened if it was not open already.
|
/// The one window of that kind, opened if it was not open already.
|
||||||
private T Show<T>(Func<T> create) where T : Window
|
/// `activate` is false for a window that opens on its own while the
|
||||||
|
/// operator is typing, so the keyboard stays in the entry boxes.
|
||||||
|
private T Show<T>(Func<T> create, bool activate = true) where T : Window
|
||||||
{
|
{
|
||||||
if (openWindows.TryGetValue(typeof(T), out Window? existing))
|
if (openWindows.TryGetValue(typeof(T), out Window? existing))
|
||||||
|
{
|
||||||
|
if (activate)
|
||||||
{
|
{
|
||||||
existing.Activate();
|
existing.Activate();
|
||||||
|
}
|
||||||
return (T)existing;
|
return (T)existing;
|
||||||
}
|
}
|
||||||
T window = create();
|
T window = create();
|
||||||
|
|||||||
@@ -115,6 +115,7 @@
|
|||||||
<MenuItem Header="Wi_ndow">
|
<MenuItem Header="Wi_ndow">
|
||||||
<MenuItem Header="Available Mult's and Q's" Click="OnShowAvailable" />
|
<MenuItem Header="Available Mult's and Q's" Click="OnShowAvailable" />
|
||||||
<MenuItem Header="_Bandmap" Click="OnShowBandmap" />
|
<MenuItem Header="_Bandmap" Click="OnShowBandmap" />
|
||||||
|
<MenuItem Header="Call Stack" Click="OnShowCallStack" />
|
||||||
<MenuItem Header="Check" Click="OnShowCheck" />
|
<MenuItem Header="Check" Click="OnShowCheck" />
|
||||||
<MenuItem Header="Log" Click="OnShowLog" InputGesture="Ctrl+L" />
|
<MenuItem Header="Log" Click="OnShowLog" InputGesture="Ctrl+L" />
|
||||||
<MenuItem Header="Score Summary" Click="OnShowScore" />
|
<MenuItem Header="Score Summary" Click="OnShowScore" />
|
||||||
|
|||||||
@@ -418,6 +418,19 @@ public sealed partial class EntryWindow : Window
|
|||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
OnSpotIt(this, new RoutedEventArgs());
|
OnSpotIt(this, new RoutedEventArgs());
|
||||||
break;
|
break;
|
||||||
|
case Key.G when e.KeyModifiers.HasFlag(KeyModifiers.Control)
|
||||||
|
&& e.KeyModifiers.HasFlag(KeyModifiers.Alt):
|
||||||
|
e.Handled = true;
|
||||||
|
StackAnother();
|
||||||
|
break;
|
||||||
|
case Key.G when e.KeyModifiers.HasFlag(KeyModifiers.Alt):
|
||||||
|
e.Handled = true;
|
||||||
|
GrabStacked();
|
||||||
|
break;
|
||||||
|
case Key.D when e.KeyModifiers.HasFlag(KeyModifiers.Alt):
|
||||||
|
e.Handled = true;
|
||||||
|
DropStacked();
|
||||||
|
break;
|
||||||
case Key.OemQuestion when e.KeyModifiers.HasFlag(KeyModifiers.Control):
|
case Key.OemQuestion when e.KeyModifiers.HasFlag(KeyModifiers.Control):
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
ToggleRun();
|
ToggleRun();
|
||||||
|
|||||||
76
src/Nonemm.Session/CallStack.cs
Normal file
76
src/Nonemm.Session/CallStack.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
namespace Nonemm.Session;
|
||||||
|
|
||||||
|
/// The calls waiting to be worked while running. A station calling CQ hears
|
||||||
|
/// more than one answer, and this holds the ones it has not got to yet, so the
|
||||||
|
/// operator takes them one at a time instead of asking each to call again.
|
||||||
|
/// N1MM's single-operator call stacking.
|
||||||
|
///
|
||||||
|
/// A call that would bring a multiplier goes to the front and the rest to the
|
||||||
|
/// back, which is the order N1MM uses on CW and phone. N1MM's digital window
|
||||||
|
/// offers plain first-in-first-out and last-in-first-out as well; there is no
|
||||||
|
/// digital window here, so every mode gets that one order.
|
||||||
|
public sealed class CallStack
|
||||||
|
{
|
||||||
|
/// N1MM stacks nothing shorter than this.
|
||||||
|
public const int ShortestCall = 2;
|
||||||
|
|
||||||
|
private readonly List<string> calls = [];
|
||||||
|
|
||||||
|
/// The calls in the order they come off, so the front is next.
|
||||||
|
public IReadOnlyList<string> Calls => calls;
|
||||||
|
|
||||||
|
public int Count => calls.Count;
|
||||||
|
|
||||||
|
public bool IsEmpty => calls.Count == 0;
|
||||||
|
|
||||||
|
/// The call that comes off next, or empty when the stack is.
|
||||||
|
public string Top => calls.Count > 0 ? calls[0] : "";
|
||||||
|
|
||||||
|
/// Puts a call on the stack. A call already there is moved rather than
|
||||||
|
/// repeated, and a multiplier goes to the front.
|
||||||
|
public void Add(string call, bool isMultiplier)
|
||||||
|
{
|
||||||
|
string wanted = call.Trim().ToUpperInvariant();
|
||||||
|
if (wanted.Length < ShortestCall)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Remove(wanted);
|
||||||
|
if (isMultiplier)
|
||||||
|
{
|
||||||
|
calls.Insert(0, wanted);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
calls.Add(wanted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes the next call off, or returns empty when there is nothing on the
|
||||||
|
/// stack.
|
||||||
|
public string Next()
|
||||||
|
{
|
||||||
|
if (calls.Count == 0)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
string call = calls[0];
|
||||||
|
calls.RemoveAt(0);
|
||||||
|
return call;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(string call) =>
|
||||||
|
calls.RemoveAll(c => string.Equals(c, call.Trim(), StringComparison.OrdinalIgnoreCase)) > 0;
|
||||||
|
|
||||||
|
/// Puts a call already on the stack at the front, which is what
|
||||||
|
/// double-clicking it in N1MM's window does.
|
||||||
|
public void MoveToTop(string call)
|
||||||
|
{
|
||||||
|
if (Remove(call))
|
||||||
|
{
|
||||||
|
calls.Insert(0, call.Trim().ToUpperInvariant());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear() => calls.Clear();
|
||||||
|
}
|
||||||
@@ -90,22 +90,8 @@ public sealed class CheckWindowSources
|
|||||||
}
|
}
|
||||||
|
|
||||||
private CheckCandidate Judged(PartialMatch match) =>
|
private CheckCandidate Judged(PartialMatch match) =>
|
||||||
new(match.Call, match.Quality, VerdictFor(match.Call));
|
new(match.Call, match.Quality, session.JudgeCall(match.Call));
|
||||||
|
|
||||||
/// What logging this call on the current band and mode would do.
|
|
||||||
private Verdict? VerdictFor(string call)
|
|
||||||
{
|
|
||||||
Qso candidate = new()
|
|
||||||
{
|
|
||||||
Id = "",
|
|
||||||
TimestampUtc = DateTime.UtcNow,
|
|
||||||
Call = Callsign.Parse(call),
|
|
||||||
Frequency = session.Frequency,
|
|
||||||
Mode = session.Mode,
|
|
||||||
ContestName = session.Contest.Name,
|
|
||||||
};
|
|
||||||
return session.Log.Judge(candidate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IReadOnlyList<string> LoggedCalls() =>
|
private IReadOnlyList<string> LoggedCalls() =>
|
||||||
session.Log.Qsos.Select(q => q.Call.Text).Distinct(StringComparer.Ordinal).ToList();
|
session.Log.Qsos.Select(q => q.Call.Text).Distinct(StringComparer.Ordinal).ToList();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
namespace Nonemm.Session;
|
namespace Nonemm.Session;
|
||||||
|
|
||||||
/// What an action macro in a function key message does. N1MM's action macros
|
/// What an action macro in a function key message does. N1MM's action macros
|
||||||
/// that this program has nothing to do — the CAT families, audio, rotators,
|
/// that this program has nothing to do — the CAT families, audio, rotators —
|
||||||
/// call stacking — are not here; they are read and passed over.
|
/// are not here; they are read and passed over.
|
||||||
public enum MessageCommand
|
public enum MessageCommand
|
||||||
{
|
{
|
||||||
/// Clears the entry boxes, as Escape and N1MM's `{WIPE}` do.
|
/// Clears the entry boxes, as Escape and N1MM's `{WIPE}` do.
|
||||||
@@ -51,4 +51,19 @@ public enum MessageCommand
|
|||||||
|
|
||||||
/// Sends its argument to the SO2R box.
|
/// Sends its argument to the SO2R box.
|
||||||
Otrsp,
|
Otrsp,
|
||||||
|
|
||||||
|
/// Puts the call being typed on the call stack and clears the entry, so
|
||||||
|
/// the next caller can be copied.
|
||||||
|
StackAnother,
|
||||||
|
|
||||||
|
/// Swaps the call being typed for the one on top of the call stack, or
|
||||||
|
/// stacks it when the stack is empty, or takes the top call when the box
|
||||||
|
/// is.
|
||||||
|
SwapWithStack,
|
||||||
|
|
||||||
|
/// Logs the contact and takes the next call off the stack.
|
||||||
|
LogThenPop,
|
||||||
|
|
||||||
|
/// Empties the call stack.
|
||||||
|
ClearStack,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,11 @@ public sealed record MessagePlan(
|
|||||||
"STOPTX" => new MessageAction(MessageCommand.StopSending),
|
"STOPTX" => new MessageAction(MessageCommand.StopSending),
|
||||||
"JUMPRX" => new MessageAction(MessageCommand.JumpToOtherRadio),
|
"JUMPRX" => new MessageAction(MessageCommand.JumpToOtherRadio),
|
||||||
"JUMPRXTX" => new MessageAction(MessageCommand.JumpToOtherRadioWithTransmit),
|
"JUMPRXTX" => new MessageAction(MessageCommand.JumpToOtherRadioWithTransmit),
|
||||||
|
"STACKANOTHER" => new MessageAction(MessageCommand.StackAnother),
|
||||||
|
"SOCALLSTACK" => new MessageAction(MessageCommand.SwapWithStack),
|
||||||
|
// N1MM runs the same code for both
|
||||||
|
"LOGTHENPOP" or "LOGTHENNEXT" => new MessageAction(MessageCommand.LogThenPop),
|
||||||
|
"CLRSTACK" => new MessageAction(MessageCommand.ClearStack),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,105 @@ public sealed class OperatingPosition
|
|||||||
/// The frequency typed into the callsign box, if that is what it holds.
|
/// The frequency typed into the callsign box, if that is what it holds.
|
||||||
public Frequency? PendingQsy() => FrequencyEntry.Parse(Entry.Call);
|
public Frequency? PendingQsy() => FrequencyEntry.Parse(Entry.Call);
|
||||||
|
|
||||||
|
/// What working this call on the band and mode we are on would bring,
|
||||||
|
/// judged from the call alone. A call that has not been worked has sent no
|
||||||
|
/// exchange, so a contest scored on what the other station sends can be
|
||||||
|
/// judged no further than this.
|
||||||
|
public Verdict? JudgeCall(string call)
|
||||||
|
{
|
||||||
|
string wanted = call.Trim();
|
||||||
|
return wanted.Length == 0
|
||||||
|
? null
|
||||||
|
: Log.Judge(new Qso
|
||||||
|
{
|
||||||
|
Id = "",
|
||||||
|
TimestampUtc = DateTime.UtcNow,
|
||||||
|
Call = Callsign.Parse(wanted),
|
||||||
|
Frequency = Frequency,
|
||||||
|
Mode = Mode,
|
||||||
|
ContestName = Contest.Name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The calls answering our CQ that have not been worked yet.
|
||||||
|
public CallStack Stack { get; } = new();
|
||||||
|
|
||||||
|
/// N1MM's {STACKANOTHER} and Ctrl+Alt+G: the call being typed goes on the
|
||||||
|
/// stack and the entry is cleared, so the next caller can be copied. Only
|
||||||
|
/// while running, and a dupe is not stacked. False when nothing was
|
||||||
|
/// stacked.
|
||||||
|
public bool StackAnother()
|
||||||
|
{
|
||||||
|
if (!IsRunning || !IsWorthStacking(Entry.Call.Trim()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Verdict? worth = Verdict();
|
||||||
|
if (worth is null || worth.IsDupe)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Stack.Add(Entry.Call.Trim(), worth.IsNewMultiplier);
|
||||||
|
Wipe();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// N1MM's Alt+G: the call on top of the stack replaces what is typed.
|
||||||
|
/// False when the stack is empty, and then the entry is left alone.
|
||||||
|
public bool GrabStacked()
|
||||||
|
{
|
||||||
|
if (Stack.IsEmpty)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
string call = Stack.Next();
|
||||||
|
Wipe();
|
||||||
|
Entry.Call = call;
|
||||||
|
Entry.FocusOn(0);
|
||||||
|
Session.NotifyChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// N1MM's {SOCALLSTACK}: one key both stacks and unstacks. With a call
|
||||||
|
/// typed and the stack holding one, the two are swapped — the stacked call
|
||||||
|
/// comes off first, so the typed call cannot come straight back. With the
|
||||||
|
/// box empty it takes the next call off; with the stack empty it puts the
|
||||||
|
/// typed call on. Only while running, and a dupe or a frequency is left
|
||||||
|
/// alone. False when there was nothing to do, which is what N1MM reports
|
||||||
|
/// back to ESM.
|
||||||
|
public bool SwapWithStack()
|
||||||
|
{
|
||||||
|
if (!IsRunning)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
string call = Entry.Call.Trim();
|
||||||
|
if (call.Length == 0)
|
||||||
|
{
|
||||||
|
return GrabStacked();
|
||||||
|
}
|
||||||
|
if (!IsWorthStacking(call) || Verdict() is not { IsDupe: false } worth)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool isMultiplier = worth.IsNewMultiplier;
|
||||||
|
string next = Stack.Next();
|
||||||
|
Stack.Add(call, isMultiplier);
|
||||||
|
Wipe();
|
||||||
|
Entry.Call = next;
|
||||||
|
Entry.FocusOn(0);
|
||||||
|
Session.NotifyChanged();
|
||||||
|
return next.Length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A frequency typed into the callsign box is a band change, not a call,
|
||||||
|
/// and our own callsign is never stacked. N1MM refuses its own call only
|
||||||
|
/// while the stack is empty.
|
||||||
|
private bool IsWorthStacking(string call) =>
|
||||||
|
call.Length >= CallStack.ShortestCall
|
||||||
|
&& !call.All(c => char.IsAsciiDigit(c) || c == '.')
|
||||||
|
&& !string.Equals(call, Me.Callsign, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
/// Logs what is typed and clears the entry. Throws when the entry is not
|
/// Logs what is typed and clears the entry. Throws when the entry is not
|
||||||
/// complete, so a caller that has not checked cannot lose an exchange.
|
/// complete, so a caller that has not checked cannot lose an exchange.
|
||||||
public Qso LogContact()
|
public Qso LogContact()
|
||||||
|
|||||||
84
tests/Nonemm.Session.Tests/CallStackTests.cs
Normal file
84
tests/Nonemm.Session.Tests/CallStackTests.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
namespace Nonemm.Session.Tests;
|
||||||
|
|
||||||
|
public class CallStackTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ACallThatIsNotAMultiplierGoesToTheBack()
|
||||||
|
{
|
||||||
|
CallStack stack = new();
|
||||||
|
|
||||||
|
stack.Add("DL1ABC", isMultiplier: false);
|
||||||
|
stack.Add("G4ABC", isMultiplier: false);
|
||||||
|
|
||||||
|
Assert.Equal(["DL1ABC", "G4ABC"], stack.Calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AMultiplierGoesToTheFront()
|
||||||
|
{
|
||||||
|
CallStack stack = new();
|
||||||
|
stack.Add("DL1ABC", isMultiplier: false);
|
||||||
|
stack.Add("G4ABC", isMultiplier: false);
|
||||||
|
|
||||||
|
stack.Add("JA1XYZ", isMultiplier: true);
|
||||||
|
|
||||||
|
Assert.Equal(["JA1XYZ", "DL1ABC", "G4ABC"], stack.Calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ACallAlreadyOnTheStackIsMovedRatherThanRepeated()
|
||||||
|
{
|
||||||
|
CallStack stack = new();
|
||||||
|
stack.Add("DL1ABC", isMultiplier: false);
|
||||||
|
stack.Add("G4ABC", isMultiplier: false);
|
||||||
|
|
||||||
|
stack.Add("DL1ABC", isMultiplier: true);
|
||||||
|
|
||||||
|
Assert.Equal(["DL1ABC", "G4ABC"], stack.Calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ACallTooShortToBeOneIsNotStacked()
|
||||||
|
{
|
||||||
|
CallStack stack = new();
|
||||||
|
|
||||||
|
stack.Add("D", isMultiplier: false);
|
||||||
|
|
||||||
|
Assert.True(stack.IsEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TheCallsComeOffTheFront()
|
||||||
|
{
|
||||||
|
CallStack stack = new();
|
||||||
|
stack.Add("DL1ABC", isMultiplier: false);
|
||||||
|
stack.Add("G4ABC", isMultiplier: false);
|
||||||
|
|
||||||
|
Assert.Equal("DL1ABC", stack.Next());
|
||||||
|
Assert.Equal("G4ABC", stack.Next());
|
||||||
|
Assert.Equal("", stack.Next());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MoveToTopBringsACallForward()
|
||||||
|
{
|
||||||
|
CallStack stack = new();
|
||||||
|
stack.Add("DL1ABC", isMultiplier: false);
|
||||||
|
stack.Add("G4ABC", isMultiplier: false);
|
||||||
|
|
||||||
|
stack.MoveToTop("G4ABC");
|
||||||
|
|
||||||
|
Assert.Equal(["G4ABC", "DL1ABC"], stack.Calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MoveToTopLeavesACallThatIsNotThereAlone()
|
||||||
|
{
|
||||||
|
CallStack stack = new();
|
||||||
|
stack.Add("DL1ABC", isMultiplier: false);
|
||||||
|
|
||||||
|
stack.MoveToTop("G4ABC");
|
||||||
|
|
||||||
|
Assert.Equal(["DL1ABC"], stack.Calls);
|
||||||
|
}
|
||||||
|
}
|
||||||
190
tests/Nonemm.Session.Tests/CallStackingTests.cs
Normal file
190
tests/Nonemm.Session.Tests/CallStackingTests.cs
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
using Nonemm.Contests;
|
||||||
|
using Nonemm.Contests.Rules;
|
||||||
|
using Nonemm.Core;
|
||||||
|
using Nonemm.Core.Country;
|
||||||
|
using Nonemm.Storage;
|
||||||
|
|
||||||
|
namespace Nonemm.Session.Tests;
|
||||||
|
|
||||||
|
/// What the stacking keys do to the entry: N1MM's Ctrl+Alt+G, Alt+G and
|
||||||
|
/// {SOCALLSTACK}.
|
||||||
|
public class CallStackingTests
|
||||||
|
{
|
||||||
|
private const string Countries = """
|
||||||
|
Germany: 14: 28: EU: 51.00: -10.00: -1.0: DL:
|
||||||
|
DL,DK,DJ;
|
||||||
|
Japan: 25: 45: AS: 36.40: -138.38: -9.0: JA:
|
||||||
|
JA,JH,JR;
|
||||||
|
""";
|
||||||
|
|
||||||
|
private static readonly StationInfo Me = new()
|
||||||
|
{
|
||||||
|
Callsign = "DL1ABC",
|
||||||
|
CqZone = 14,
|
||||||
|
ItuZone = 28,
|
||||||
|
Continent = "EU",
|
||||||
|
CountryPrefix = "DL",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static OperatingPosition Running()
|
||||||
|
{
|
||||||
|
FakeLogStore store = new();
|
||||||
|
ContestInstance instance = store.AddContest(new ContestInstance
|
||||||
|
{
|
||||||
|
ContestNumber = 0,
|
||||||
|
ContestName = "CQWW",
|
||||||
|
});
|
||||||
|
ContestSession session = new(
|
||||||
|
store,
|
||||||
|
new CqWorldWide(ModeCategory.Cw),
|
||||||
|
instance,
|
||||||
|
Me,
|
||||||
|
CountryFile.Parse(Countries));
|
||||||
|
return new OperatingPosition(session) { IsRunning = true };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Type(OperatingPosition position, string call, string zone = "25")
|
||||||
|
{
|
||||||
|
position.Entry.Call = call;
|
||||||
|
position.Entry.Set(ExchangeSlot.ReceivedReport, "599");
|
||||||
|
position.Entry.Set(ExchangeSlot.Zone, zone);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StackAnotherPutsTheCallOnTheStackAndClearsTheEntry()
|
||||||
|
{
|
||||||
|
OperatingPosition position = Running();
|
||||||
|
Type(position, "JA1XYZ");
|
||||||
|
|
||||||
|
Assert.True(position.StackAnother());
|
||||||
|
Assert.Equal(["JA1XYZ"], position.Stack.Calls);
|
||||||
|
Assert.Equal("", position.Entry.Call);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NothingIsStackedWhileSearching()
|
||||||
|
{
|
||||||
|
OperatingPosition position = Running();
|
||||||
|
position.IsRunning = false;
|
||||||
|
Type(position, "JA1XYZ");
|
||||||
|
|
||||||
|
Assert.False(position.StackAnother());
|
||||||
|
Assert.True(position.Stack.IsEmpty);
|
||||||
|
Assert.Equal("JA1XYZ", position.Entry.Call);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ADupeIsNotStacked()
|
||||||
|
{
|
||||||
|
OperatingPosition position = Running();
|
||||||
|
Type(position, "JA1XYZ");
|
||||||
|
position.LogContact();
|
||||||
|
Type(position, "JA1XYZ");
|
||||||
|
|
||||||
|
Assert.False(position.StackAnother());
|
||||||
|
Assert.True(position.Stack.IsEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AFrequencyTypedIntoTheCallsignBoxIsNotStacked()
|
||||||
|
{
|
||||||
|
OperatingPosition position = Running();
|
||||||
|
position.Entry.Call = "14025";
|
||||||
|
|
||||||
|
Assert.False(position.StackAnother());
|
||||||
|
Assert.True(position.Stack.IsEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// N1MM refuses your own call only while the stack is empty. It is never
|
||||||
|
/// a station to work, so it is refused either way here.
|
||||||
|
[Fact]
|
||||||
|
public void OurOwnCallIsNotStacked()
|
||||||
|
{
|
||||||
|
OperatingPosition position = Running();
|
||||||
|
position.Stack.Add("JA1XYZ", isMultiplier: false);
|
||||||
|
Type(position, "DL1ABC", "14");
|
||||||
|
|
||||||
|
Assert.False(position.StackAnother());
|
||||||
|
Assert.Equal(["JA1XYZ"], position.Stack.Calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GrabbingPutsTheTopCallInTheBox()
|
||||||
|
{
|
||||||
|
OperatingPosition position = Running();
|
||||||
|
position.Stack.Add("JA1XYZ", isMultiplier: false);
|
||||||
|
position.Stack.Add("G4ABC", isMultiplier: false);
|
||||||
|
|
||||||
|
Assert.True(position.GrabStacked());
|
||||||
|
Assert.Equal("JA1XYZ", position.Entry.Call);
|
||||||
|
Assert.Equal(["G4ABC"], position.Stack.Calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GrabbingFromAnEmptyStackLeavesTheEntryAlone()
|
||||||
|
{
|
||||||
|
OperatingPosition position = Running();
|
||||||
|
Type(position, "JA1XYZ");
|
||||||
|
|
||||||
|
Assert.False(position.GrabStacked());
|
||||||
|
Assert.Equal("JA1XYZ", position.Entry.Call);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SwappingExchangesTheTypedCallForTheStackedOne()
|
||||||
|
{
|
||||||
|
OperatingPosition position = Running();
|
||||||
|
position.Stack.Add("G4ABC", isMultiplier: false);
|
||||||
|
Type(position, "JA1XYZ");
|
||||||
|
|
||||||
|
Assert.True(position.SwapWithStack());
|
||||||
|
Assert.Equal("G4ABC", position.Entry.Call);
|
||||||
|
Assert.Equal(["JA1XYZ"], position.Stack.Calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The stacked call comes off before the typed one goes on, so a typed
|
||||||
|
/// multiplier cannot come straight back into the box.
|
||||||
|
[Fact]
|
||||||
|
public void SwappingTakesTheStackedCallEvenWhenTheTypedOneIsAMultiplier()
|
||||||
|
{
|
||||||
|
OperatingPosition position = Running();
|
||||||
|
position.Stack.Add("G4ABC", isMultiplier: false);
|
||||||
|
Type(position, "JA1XYZ");
|
||||||
|
|
||||||
|
position.SwapWithStack();
|
||||||
|
|
||||||
|
Assert.Equal("G4ABC", position.Entry.Call);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SwappingWithAnEmptyStackJustStacksTheTypedCall()
|
||||||
|
{
|
||||||
|
OperatingPosition position = Running();
|
||||||
|
Type(position, "JA1XYZ");
|
||||||
|
|
||||||
|
Assert.False(position.SwapWithStack());
|
||||||
|
Assert.Equal(["JA1XYZ"], position.Stack.Calls);
|
||||||
|
Assert.Equal("", position.Entry.Call);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SwappingWithAnEmptyBoxTakesTheNextCall()
|
||||||
|
{
|
||||||
|
OperatingPosition position = Running();
|
||||||
|
position.Stack.Add("G4ABC", isMultiplier: false);
|
||||||
|
|
||||||
|
Assert.True(position.SwapWithStack());
|
||||||
|
Assert.Equal("G4ABC", position.Entry.Call);
|
||||||
|
Assert.True(position.Stack.IsEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SwappingWithNothingTypedAndNothingStackedDoesNothing()
|
||||||
|
{
|
||||||
|
OperatingPosition position = Running();
|
||||||
|
|
||||||
|
Assert.False(position.SwapWithStack());
|
||||||
|
Assert.True(position.Stack.IsEmpty);
|
||||||
|
Assert.Equal("", position.Entry.Call);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,6 +83,24 @@ public class MessagePlanTests
|
|||||||
Assert.Equal([new MessageAction(MessageCommand.SendOnOtherRadio, "9")], plan.Before);
|
Assert.Equal([new MessageAction(MessageCommand.SendOnOtherRadio, "9")], plan.Before);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TheCallStackingMacrosAreRead()
|
||||||
|
{
|
||||||
|
MessagePlan plan = MessagePlan.Read(
|
||||||
|
"{STACKANOTHER}{SOCALLSTACK}{LOGTHENPOP}{LOGTHENNEXT}{CLRSTACK}", Session());
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
new MessageAction(MessageCommand.StackAnother),
|
||||||
|
new MessageAction(MessageCommand.SwapWithStack),
|
||||||
|
new MessageAction(MessageCommand.LogThenPop),
|
||||||
|
new MessageAction(MessageCommand.LogThenPop),
|
||||||
|
new MessageAction(MessageCommand.ClearStack),
|
||||||
|
],
|
||||||
|
plan.Before);
|
||||||
|
Assert.Equal("", plan.Text);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AMacroThisProgramDoesNotActOnIsPassedOver()
|
public void AMacroThisProgramDoesNotActOnIsPassedOver()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user