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:

  1. calculate Fahrenheit from the current Celsius setting;
  2. restore the Celsius field;
  3. draw the new Celsius value;
  4. restore the Fahrenheit field;
  5. draw the new Fahrenheit value;
  6. 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:

  1. wait for input;
  2. decide what the input means;
  3. change the program's state;
  4. update the Display;
  5. 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:

  1. wait for input;
  2. decide what the input means;
  3. change its state;
  4. update the Display;
  5. 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.