NCL 306: Changing a Setting
In NCL 305, we changed part of an existing screen without redrawing everything around it.
Now we'll let the user make that change.
We'll build a small room temperature control:
ROOM CONTROL
TEMPERATURE: 21 °C
(69 °F)
↑ / ↓ ADJUST
ESC TO EXIT
The Up and Down arrow keys will change the temperature.
When the setting changes, we'll update only the temperature fields.
The temperature setting
The program needs to remember the current temperature.
We'll store it in an integer register:
#temperature r0
MOVE #temperature 21
This is the value the user will actually control.
We'll also need somewhere to calculate the Fahrenheit temperature:
#fahrenheit r1
Unlike #temperature, we don't need to give #fahrenheit an initial value.
We'll calculate it whenever we update the Display.
Drawing the screen
Most of our interface never changes.
We can draw all of that once:
D.FIL " "
D.CUR 8 1
D.TXT "ROOM CONTROL"
D.CUR 5 4
D.TXT "TEMPERATURE:"
D.CUR 8 8
D.TXT "↑ / ↓ ADJUST"
D.CUR 9 9
D.TXT "ESC TO EXIT"
The temperature itself will be drawn separately.
That gives us two kinds of Display contents:
- text that stays the same;
- fields that change while the program is running.
We don't need to redraw the first kind every time the second kind changes.
Calculating Fahrenheit
Our program stores the temperature in Celsius, but we'd also like to show the Fahrenheit equivalent.
Converting Celsius to Fahrenheit
Fahrenheit can be calculated from Celsius with:
°F = (°C × 9 ÷ 5) + 32
Our program will perform the same calculation one operation at a time. Because NCL uses integer arithmetic here, any fractional part is discarded.
We can calculate it in #fahrenheit:
MUL #fahrenheit #temperature 9
DIV #fahrenheit #fahrenheit 5
ADD #fahrenheit #fahrenheit 32
Notice that we aren't storing two independent temperature settings.
#temperature is the setting.
#fahrenheit is calculated from it.
If #temperature contains 21, the calculation leaves 69 in #fahrenheit.
Drawing the temperature
Let's put the Celsius temperature beside the label:
TEMPERATURE: 21 °C
and the Fahrenheit conversion underneath it in brackets:
(69 °F)
We can reserve enough room for each field and use the restoration technique from the previous lesson.
First, make a short restore string:
#restore s0
SMOVE #restore ""
SPADR #restore #restore 8 " "
SPADR #restore #restore 16 "\b"
It contains eight spaces followed by eight backspaces.
Writing it restores an eight-character field, then moves the cursor back to the beginning of that field.
Now the Celsius field can be drawn with:
D.CUR 18 4
D.CHR #restore
D.CHR #temperature
D.CHR " °C"
and the Fahrenheit field with:
D.CUR 18 5
D.CHR #restore
D.CHR "("
D.CHR #fahrenheit
D.CHR " °F)"
The surrounding screen doesn't need to change.
Updating both values
We'll put the calculation and the two field updates together:
$draw
MUL #fahrenheit #temperature 9
DIV #fahrenheit #fahrenheit 5
ADD #fahrenheit #fahrenheit 32
D.CUR 18 4
D.CHR #restore
D.CHR #temperature
D.CHR " °C"
D.CUR 18 5
D.CHR #restore
D.CHR "("
D.CHR #fahrenheit
D.CHR " °F)"
JUMP $input
Every time we reach $draw:
- calculate Fahrenheit from the current Celsius setting;
- restore the Celsius field;
- draw the new Celsius value;
- restore the Fahrenheit field;
- draw the new Fahrenheit value;
- return to
$input.
This is the same small-field update from NCL 305, now driven by a value that can change while the program is running.
Waiting for a key
We'll need another string register for the Keyboard:
#key s1
Then wait for input:
$input
SYS.AKEY #key
The keys we're interested in produce these values:
| Key | Value |
|---|---|
| ↑ | "UP" |
| ↓ | "DOWN" |
| Escape | "ESC" |
We can send each one to the appropriate part of the program:
BSEQ $up #key "UP"
BSEQ $down #key "DOWN"
BSEQ $exit #key "ESC"
JUMP $input
Any other key reaches JUMP $input and is ignored.
Turning the temperature up
Let's allow settings from 15 °C through 30 °C.
When Up is pressed, first check whether we're already at the maximum:
$up
BGE $input #temperature 30
If #temperature is already 30, execution returns to $input.
Nothing changes, so there is nothing to redraw.
Otherwise:
INC #temperature
JUMP $draw
changes the setting and updates the two temperature fields.
Together:
$up
BGE $input #temperature 30
INC #temperature
JUMP $draw
Turning the temperature down
Down works the same way in the other direction:
$down
BLE $input #temperature 15
DEC #temperature
JUMP $draw
If the setting is already 15, the branch rejects the change.
Otherwise, DEC changes the stored temperature and $draw updates the Display.
The important part is that the check happens before the value changes.
#temperature therefore never contains a setting outside our allowed range.
Putting it together
Here's the complete program:
#temperature r0
#fahrenheit r1
#restore s0
#key s1
MOVE #temperature 21
-- Build the field restoration string.
SMOVE #restore ""
SPADR #restore #restore 8 " "
SPADR #restore #restore 16 "\b"
-- Draw the parts of the screen that don't change.
D.FIL " "
D.CUR 8 1
D.TXT "ROOM CONTROL"
D.CUR 5 4
D.TXT "TEMPERATURE:"
D.CUR 8 8
D.TXT "↑ / ↓ ADJUST"
D.CUR 9 9
D.TXT "ESC TO EXIT"
D.BLT
-- Draw the initial temperature.
JUMP $draw
$input
SYS.AKEY #key
BSEQ $up #key "UP"
BSEQ $down #key "DOWN"
BSEQ $exit #key "ESC"
-- Ignore any other key.
JUMP $input
$up
BGE $input #temperature 30
INC #temperature
JUMP $draw
$down
BLE $input #temperature 15
DEC #temperature
$draw
MUL #fahrenheit #temperature 9
DIV #fahrenheit #fahrenheit 5
ADD #fahrenheit #fahrenheit 32
D.CUR 18 4
D.CHR #restore
D.CHR #temperature
D.CHR " °C"
D.CUR 18 5
D.CHR #restore
D.CHR "("
D.CHR #fahrenheit
D.CHR " °F)"
JUMP $input
$exit
Run it and press Up and Down.
The Celsius setting changes by one degree at a time, and the Fahrenheit value underneath is recalculated to match.
Press Up repeatedly until the temperature reaches 30 °C. Further Up presses do nothing.
Do the same with Down at 15 °C.
Press Escape when you're finished.
One setting, two displayed values
There's an important distinction in this program.
When the user presses Up, we only change:
#temperature
We don't separately change #fahrenheit.
Instead:
INC #temperature
JUMP $draw
takes us to code that calculates Fahrenheit again from the new temperature.
So if the setting changes:
21 °C
to:
22 °C
the program derives the new Fahrenheit value:
(71 °F)
from the current state.
There is only one temperature setting for the two displayed values to represent.
The interaction loop
Follow the program from $input.
It waits here:
SYS.AKEY #key
until a key arrives.
Suppose the user presses Up.
Execution goes to $up, checks the upper limit, changes #temperature, and jumps to $draw.
$draw calculates Fahrenheit and updates the two fields.
Then:
JUMP $input
returns to the Keyboard.
The program repeats the same pattern:
- wait for input;
- decide what the input means;
- change the program's state;
- update the Display;
- wait again.
Nothing special is happening while the program waits.
You can still walk through its instructions in order. When you reach SYS.AKEY, your finger simply stops there until somebody presses a key.
Try it
Change the starting temperature:
MOVE #temperature 21
Try 15, 25, or 30.
The Fahrenheit value should be correct as soon as the program starts.
Then change the allowed range.
For example, make the controls stop at 18 °C and 25 °C.
Finally, try changing the amount adjusted by each key press.
Instead of:
INC #temperature
and:
DEC #temperature
try:
ADD #temperature #temperature 2
and:
SUB #temperature #temperature 2
Watch what happens near the limits. Does our existing check still guarantee that #temperature stays between them?
Our program now has a complete interaction loop:
- wait for input;
- decide what the input means;
- change its state;
- update the Display;
- wait again.
So far, each key has directly performed an action: Up increases the temperature, and Down decreases it.
In NCL 307: Making a Selection, we'll give the user a selection of choices, let them move between those choices, and decide what happens when they make one.
We'll also use everything we've learned so far to build our first game.