NCL 308: Moving Around a Board

In the last lesson, we used one value in #selection to represent one of three choices.

This time, we'll expand the same idea to nine positions on a game board.

We're going to start building Noughts and Crosses.

The board has nine playable squares:

0 1 2
3 4 5
6 7 8

Just like the three choices in the last lesson, we can represent the current selection with one number.

This time:

  • 0 means the upper-left square;
  • 4 means the centre square;
  • 8 means the lower-right square.

By the end of this lesson, you'll be able to move around the board with the arrow keys and place crosses with Enter.

We won't have another player yet, and the program won't know how to win.

First, we need a board.


Drawing the board

Our game will look something like this:



           ╳│╳│╳
           ─┼─┼─
           ╳│╳│╳
           ─┼─┼─
           ╳│╳│╳

         ARROWS MOVE
         ENTER PLACE
         ESC TO EXIT

Of course, we won't begin with nine crosses. The playable squares will start empty.

Each position on the board occupies one complete physical Display tile.

NCS/e provides fullwidth versions of the characters in the Unicode 25XX range in its private E5XX range. The last two digits remain the same.

For example, the ordinary is U+2502. Its NCS/e fullwidth counterpart is U+E502.

We'll use fullwidth versions of , , and to build the board. The ordinary versions remain halfwidth and can still be used elsewhere on the same screen.

We also need a fullwidth empty space:

#empty "\u3000"

U+3000 is the ideographic space. Unlike an ordinary space, it occupies an entire Display tile.

Our board begins at (11, 2).

The playable squares therefore begin at these Display positions:

Square X Y
0 11 2
1 15 2
2 19 2
3 11 4
4 15 4
5 19 4
6 11 6
7 15 6
8 19 6

We can draw the board and the instructions once at the beginning of the program.

#empty "\u3000"

D.FIL " "

D.CUR 11 2
D.TXT #empty
D.TXT "\uE502"
D.TXT #empty
D.TXT "\uE502"
D.TXT #empty

D.CUR 11 3
D.TXT "\uE500\uE53C\uE500\uE53C\uE500"

D.CUR 11 4
D.TXT #empty
D.TXT "\uE502"
D.TXT #empty
D.TXT "\uE502"
D.TXT #empty

D.CUR 11 5
D.TXT "\uE500\uE53C\uE500\uE53C\uE500"

D.CUR 11 6
D.TXT #empty
D.TXT "\uE502"
D.TXT #empty
D.TXT "\uE502"
D.TXT #empty

D.CUR 9 8
D.TXT "ARROWS MOVE"

D.CUR 9 9
D.TXT "ENTER PLACE"

D.CUR 9 10
D.TXT "ESC TO EXIT"

D.BLT

The board itself is five physical tiles wide and five physical tiles tall.

Three of those rows and columns contain playable squares. The others contain the walls between them.

Now we need to turn a selection number from 0 through 8 into one of those Display positions.


Finding a square

We'll begin with these working registers:

#selection r0
#column r1
#row r2
#x r3
#y r4

Start with the upper-left square selected:

MOVE #selection 0

The board is three squares wide.

That means we can find the column with MOD:

MOD #column #selection 3

and the row with integer division:

DIV #row #selection 3

For example, suppose:

selection = 7

Then:

7 MOD 3 = 1
7 DIV 3 = 2

So square 7 is in column 1, row 2.

We can see the same pattern across the whole board:

Selection Column Row
0 0 0
1 1 0
2 2 0
3 0 1
4 1 1
5 2 1
6 0 2
7 1 2
8 2 2

The playable squares are separated by walls.

Horizontally, each playable square begins four logical character positions after the previous one:

MUL #x #column 4
ADD #x #x 11

Vertically, each playable row begins two Display rows after the previous one:

MUL #y #row 2
ADD #y #y 2

Together:

MOD #column #selection 3
DIV #row #selection 3

MUL #x #column 4
ADD #x #x 11

MUL #y #row 2
ADD #y #y 2

Now any selection from 0 through 8 can be turned into a Display position.

Selection Column Row X Y
0 0 0 11 2
2 2 0 19 2
4 1 1 15 4
7 1 2 15 6
8 2 2 19 6

We already know which square is selected.

Now we can show it.


Showing the selection

An empty square is normally drawn as:

#empty "\u3000"

To show that a square is selected, we'll draw the same character inverted.

D.COL #D.COL.WHITE #D.TXT.INVERT
D.CUR #x #y
D.CHR #empty
D.COL #D.COL.WHITE #D.TXT.NORMAL

The square itself hasn't changed.

It is still empty.

We are only drawing that empty square differently to show the player which position is selected.

This gives us the same distinction we used in the previous lesson:

#selection is what the program remembers.
The inverted square is how the Display shows it.

Now we can let the player move that selection.


Moving around the board

We'll read the Keyboard just as we did in the previous lessons.

#key s0

$input
SYS.AKEY #key

BSEQ $left #key "LEFT"
BSEQ $right #key "RIGHT"
BSEQ $up #key "UP"
BSEQ $down #key "DOWN"
BSEQ $exit #key "ESC"

JUMP $input

Each arrow key will move the selection by one square.

Before moving it, however, we need to make sure the requested square actually exists.

Moving left and right

We already know how to find the current column:

MOD #column #selection 3

If the column is 0, we're already at the left edge.

$left
MOD #column #selection 3
BEQ $input #column 0

Otherwise, the square immediately to the left is one selection number lower:

DEC #selection

Moving right works the same way.

If the column is already 2, there is nowhere farther right to go.

$right
MOD #column #selection 3
BEQ $input #column 2

INC #selection

Moving up and down

The top row contains selections 0, 1, and 2.

If the selection is less than 3, we can't move up.

Otherwise, moving up one row means subtracting 3:

$up
BLT $input #selection 3

SUB #selection #selection 3

The bottom row begins at selection 6.

$down
BGE $input #selection 6

ADD #selection #selection 3

This keeps #selection between 0 and 8.

But changing the number isn't enough.

We also need to move the visible selection.


Redrawing the old and new squares

When the selection moves, two squares need to change:

  1. the old square becomes normal;
  2. the new square becomes inverted.

This is the same pattern we used when updating fields in NCL 305: Updating the Screen.

Restore the old appearance, change the state, then draw the new appearance.

For now every square is empty, so restoring the old square is easy:

D.COL #D.COL.WHITE #D.TXT.NORMAL
D.CUR #x #y
D.CHR #empty

Then we change #selection, calculate its new coordinates, and draw the new square inverted.

For example, moving left could eventually look like:

$left
MOD #column #selection 3
BEQ $input #column 0

D.COL #D.COL.WHITE #D.TXT.NORMAL
D.CUR #x #y
D.CHR #empty

DEC #selection

MOD #column #selection 3
DIV #row #selection 3

MUL #x #column 4
ADD #x #x 11

MUL #y #row 2
ADD #y #y 2

D.COL #D.COL.WHITE #D.TXT.INVERT
D.CUR #x #y
D.CHR #empty
D.COL #D.COL.WHITE #D.TXT.NORMAL

JUMP $input

It works.

But we're already repeating quite a lot of code.

And we're about to make drawing a square more complicated.


Remembering the board

So far, every square has been empty.

Now let's let the player put something in one.

We'll use the fullwidth NCS/e version of :

#cross "\uE573"

The visible character isn't enough, though.

Suppose the player puts a cross in square 4, then moves to square 5.

The program still needs to remember that square 4 contains a cross.

There are nine independently changeable squares, so we'll give each one its own register.

We'll keep the board in r20 through r28, leaving the lower registers available for working values.

#cell0 r20
#cell1 r21
#cell2 r22
#cell3 r23
#cell4 r24
#cell5 r25
#cell6 r26
#cell7 r27
#cell8 r28

For now, each cell can contain one of two values:

Value Meaning
0 Empty
1 Cross

The registers begin at 0, so the board begins empty.

When the player places a cross in square 4, for example:

MOVE #cell4 1

The important distinction is:

1 is what the program remembers.
is how the Display shows it.

The board state is made from numbers.

The Display turns those numbers into something the player can see.


Drawing what belongs in a square

Our old movement code always restored a square with:

D.CHR #empty

That is no longer enough.

If the player moves away from a square containing a cross, we need to redraw the cross normally.

If the player moves onto a square containing a cross, we need to redraw the cross inverted.

So we'll make one section of the program responsible for drawing a square.

Several different parts of the program will need to use it.

There's one problem.

We know how to jump there:

JUMP $draw_cell

But after it has drawn the square, where should it go?


A destination in a register

So far, we've normally given JUMP its destination directly:

JUMP $input

A label represents a line in the program.

That line number is a value, and we can put it into an integer register.

#return r5

MOVE #return $continue

We can then use that register as the destination of a jump:

JUMP #return

This gives us a way to use the same drawing code from several places.

Before jumping to it, remember where execution should continue:

MOVE #return $after_draw
JUMP $draw_cell

$after_draw

Then the shared drawing code can finish with:

JUMP #return

Follow the execution:

  1. MOVE stores the location of $after_draw in #return.
  2. JUMP $draw_cell goes to the shared drawing code.
  3. $draw_cell does its work.
  4. JUMP #return goes back to $after_draw.

If another part of the program needs the same drawing code, it can store a different return destination first.

MOVE #return $after_other_draw
JUMP $draw_cell

$after_other_draw

The drawing code doesn't need to know which part of the program used it.

It only needs to know where to return.

Shared code

This technique works, but keeping track of return destinations ourselves will become increasingly inconvenient as programs grow.

We'll use it for this program.

There is a better way, and we'll meet it soon.


Looking up a cell

The drawing code needs to know which square we want to draw.

We'll give it another working register:

#draw_cell r6
#value r7

Before using the drawing code, put the desired square number in #draw_cell.

MOVE #draw_cell #selection

The drawing code can then find the corresponding board register.

$draw_cell

BEQ $draw0 #draw_cell 0
BEQ $draw1 #draw_cell 1
BEQ $draw2 #draw_cell 2
BEQ $draw3 #draw_cell 3
BEQ $draw4 #draw_cell 4
BEQ $draw5 #draw_cell 5
BEQ $draw6 #draw_cell 6
BEQ $draw7 #draw_cell 7
JUMP $draw8

Each branch copies the appropriate cell into #value.

$draw0
MOVE #value #cell0
JUMP $paint

$draw1
MOVE #value #cell1
JUMP $paint

$draw2
MOVE #value #cell2
JUMP $paint

$draw3
MOVE #value #cell3
JUMP $paint

$draw4
MOVE #value #cell4
JUMP $paint

$draw5
MOVE #value #cell5
JUMP $paint

$draw6
MOVE #value #cell6
JUMP $paint

$draw7
MOVE #value #cell7
JUMP $paint

$draw8
MOVE #value #cell8

Now #value tells the rest of the drawing code what that square contains.

We can choose its visible character:

#glyph s1

$paint
SMOVE #glyph #empty
BEQ $have_glyph #value 0
SMOVE #glyph #cross

$have_glyph

Then calculate the position from #draw_cell:

MOD #column #draw_cell 3
DIV #row #draw_cell 3

MUL #x #column 4
ADD #x #x 11

MUL #y #row 2
ADD #y #y 2

Finally, draw #glyph and return:

D.CUR #x #y
D.CHR #glyph

JUMP #return

One section of the program can now draw any of the nine squares.


Drawing the selection

The same cell may need to be drawn normally or inverted.

We'll give the drawing code one more input:

#selected r8

Before drawing a normal square:

MOVE #selected 0

Before drawing the selected square:

MOVE #selected 1

The drawing code can use that value to choose the style:

BEQ $normal #selected 0

D.COL #D.COL.WHITE #D.TXT.INVERT
JUMP $draw_glyph

$normal
D.COL #D.COL.WHITE #D.TXT.NORMAL

$draw_glyph
D.CUR #x #y
D.CHR #glyph

D.COL #D.COL.WHITE #D.TXT.NORMAL

JUMP #return

Now the responsibilities are nicely separated:

  • the board registers decide what is in the square;
  • #draw_cell decides which square we're drawing;
  • #selected decides how that square is shown.

The drawing code puts those pieces together.


Moving with the new drawing code

Now moving becomes much easier to read.

Before changing the selection, redraw the current square normally:

MOVE #draw_cell #selection
MOVE #selected 0
MOVE #return $after_old
JUMP $draw_cell

$after_old

Change the selection:

DEC #selection

Then draw the new square selected:

MOVE #draw_cell #selection
MOVE #selected 1
MOVE #return $after_new
JUMP $draw_cell

$after_new
JUMP $input

The same drawing code handles an empty square or a square containing a cross.

The movement code no longer needs to care.


Placing a cross

We can now add Enter to our Keyboard controls:

BSEQ $place #key "ENTER"

Just as when drawing a square, we need to find which board register corresponds to #selection.

$place

BEQ $place0 #selection 0
BEQ $place1 #selection 1
BEQ $place2 #selection 2
BEQ $place3 #selection 3
BEQ $place4 #selection 4
BEQ $place5 #selection 5
BEQ $place6 #selection 6
BEQ $place7 #selection 7
JUMP $place8

Each branch first checks whether its square is empty.

For example:

$place4
BNEQ $input #cell4 0
MOVE #cell4 1
JUMP $placed

If the cell isn't 0, it already contains something, so execution returns to $input without changing it.

Otherwise, the cell becomes 1.

The other squares follow the same pattern:

$place0
BNEQ $input #cell0 0
MOVE #cell0 1
JUMP $placed

$place1
BNEQ $input #cell1 0
MOVE #cell1 1
JUMP $placed

$place2
BNEQ $input #cell2 0
MOVE #cell2 1
JUMP $placed

$place3
BNEQ $input #cell3 0
MOVE #cell3 1
JUMP $placed

$place4
BNEQ $input #cell4 0
MOVE #cell4 1
JUMP $placed

$place5
BNEQ $input #cell5 0
MOVE #cell5 1
JUMP $placed

$place6
BNEQ $input #cell6 0
MOVE #cell6 1
JUMP $placed

$place7
BNEQ $input #cell7 0
MOVE #cell7 1
JUMP $placed

$place8
BNEQ $input #cell8 0
MOVE #cell8 1

After placing the cross, #selection hasn't changed.

We only need to redraw that square:

$placed
MOVE #draw_cell #selection
MOVE #selected 1
MOVE #return $after_place
JUMP $draw_cell

$after_place
JUMP $input

The drawing code reads the new board value and displays the cross, still inverted because the square is still selected.

The program changed its state first.

Then it redrew the part of the Display that represents that state.


The complete program

We've built the program a piece at a time. Here it is assembled into one complete script.

Almost everything here should now be familiar: the board is drawn once, #selection tracks the current square, the board registers remember placed crosses, and the shared drawing code turns that state into what appears on the Display.

#selection r0
#column r1
#row r2
#x r3
#y r4
#return r5
#draw_cell r6
#value r7
#selected r8

#key s0
#glyph s1

#empty "\u3000"
#cross "\uE573"

#cell0 r20
#cell1 r21
#cell2 r22
#cell3 r23
#cell4 r24
#cell5 r25
#cell6 r26
#cell7 r27
#cell8 r28


-- Start in the upper-left square.

MOVE #selection 0


-- Draw the board.

D.FIL " "

D.CUR 11 2
D.TXT #empty
D.TXT "\uE502"
D.TXT #empty
D.TXT "\uE502"
D.TXT #empty

D.CUR 11 3
D.TXT "\uE500\uE53C\uE500\uE53C\uE500"

D.CUR 11 4
D.TXT #empty
D.TXT "\uE502"
D.TXT #empty
D.TXT "\uE502"
D.TXT #empty

D.CUR 11 5
D.TXT "\uE500\uE53C\uE500\uE53C\uE500"

D.CUR 11 6
D.TXT #empty
D.TXT "\uE502"
D.TXT #empty
D.TXT "\uE502"
D.TXT #empty

D.CUR 9 8
D.TXT "ARROWS MOVE"

D.CUR 9 9
D.TXT "ENTER PLACE"

D.CUR 9 10
D.TXT "ESC TO EXIT"

D.BLT


-- Draw the initial selection.

MOVE #draw_cell #selection
MOVE #selected 1
MOVE #return $input
JUMP $draw_cell


-- Wait for input.

$input
SYS.AKEY #key

BSEQ $left #key "LEFT"
BSEQ $right #key "RIGHT"
BSEQ $up #key "UP"
BSEQ $down #key "DOWN"
BSEQ $place #key "ENTER"
BSEQ $exit #key "ESC"

JUMP $input


-- Move left.

$left
MOD #column #selection 3
BEQ $input #column 0

MOVE #draw_cell #selection
MOVE #selected 0
MOVE #return $after_left_old
JUMP $draw_cell

$after_left_old
DEC #selection

MOVE #draw_cell #selection
MOVE #selected 1
MOVE #return $after_left_new
JUMP $draw_cell

$after_left_new
JUMP $input


-- Move right.

$right
MOD #column #selection 3
BEQ $input #column 2

MOVE #draw_cell #selection
MOVE #selected 0
MOVE #return $after_right_old
JUMP $draw_cell

$after_right_old
INC #selection

MOVE #draw_cell #selection
MOVE #selected 1
MOVE #return $after_right_new
JUMP $draw_cell

$after_right_new
JUMP $input


-- Move up.

$up
BLT $input #selection 3

MOVE #draw_cell #selection
MOVE #selected 0
MOVE #return $after_up_old
JUMP $draw_cell

$after_up_old
SUB #selection #selection 3

MOVE #draw_cell #selection
MOVE #selected 1
MOVE #return $after_up_new
JUMP $draw_cell

$after_up_new
JUMP $input


-- Move down.

$down
BGE $input #selection 6

MOVE #draw_cell #selection
MOVE #selected 0
MOVE #return $after_down_old
JUMP $draw_cell

$after_down_old
ADD #selection #selection 3

MOVE #draw_cell #selection
MOVE #selected 1
MOVE #return $after_down_new
JUMP $draw_cell

$after_down_new
JUMP $input


-- Place a cross in the selected square.

$place
BEQ $place0 #selection 0
BEQ $place1 #selection 1
BEQ $place2 #selection 2
BEQ $place3 #selection 3
BEQ $place4 #selection 4
BEQ $place5 #selection 5
BEQ $place6 #selection 6
BEQ $place7 #selection 7
JUMP $place8

$place0
BNEQ $input #cell0 0
MOVE #cell0 1
JUMP $placed

$place1
BNEQ $input #cell1 0
MOVE #cell1 1
JUMP $placed

$place2
BNEQ $input #cell2 0
MOVE #cell2 1
JUMP $placed

$place3
BNEQ $input #cell3 0
MOVE #cell3 1
JUMP $placed

$place4
BNEQ $input #cell4 0
MOVE #cell4 1
JUMP $placed

$place5
BNEQ $input #cell5 0
MOVE #cell5 1
JUMP $placed

$place6
BNEQ $input #cell6 0
MOVE #cell6 1
JUMP $placed

$place7
BNEQ $input #cell7 0
MOVE #cell7 1
JUMP $placed

$place8
BNEQ $input #cell8 0
MOVE #cell8 1


-- Redraw the placed cross as selected.

$placed
MOVE #draw_cell #selection
MOVE #selected 1
MOVE #return $after_place
JUMP $draw_cell

$after_place
JUMP $input


-- Look up the requested board cell.

$draw_cell
BEQ $draw0 #draw_cell 0
BEQ $draw1 #draw_cell 1
BEQ $draw2 #draw_cell 2
BEQ $draw3 #draw_cell 3
BEQ $draw4 #draw_cell 4
BEQ $draw5 #draw_cell 5
BEQ $draw6 #draw_cell 6
BEQ $draw7 #draw_cell 7
JUMP $draw8

$draw0
MOVE #value #cell0
JUMP $paint

$draw1
MOVE #value #cell1
JUMP $paint

$draw2
MOVE #value #cell2
JUMP $paint

$draw3
MOVE #value #cell3
JUMP $paint

$draw4
MOVE #value #cell4
JUMP $paint

$draw5
MOVE #value #cell5
JUMP $paint

$draw6
MOVE #value #cell6
JUMP $paint

$draw7
MOVE #value #cell7
JUMP $paint

$draw8
MOVE #value #cell8


-- Choose the visible glyph.

$paint
SMOVE #glyph #empty
BEQ $have_glyph #value 0
SMOVE #glyph #cross

$have_glyph


-- Find the Display position.

MOD #column #draw_cell 3
DIV #row #draw_cell 3

MUL #x #column 4
ADD #x #x 11

MUL #y #row 2
ADD #y #y 2


-- Choose whether the square is selected.

BEQ $normal #selected 0

D.COL #D.COL.WHITE #D.TXT.INVERT
JUMP $draw_glyph

$normal
D.COL #D.COL.WHITE #D.TXT.NORMAL


-- Draw the square and return.

$draw_glyph
D.CUR #x #y
D.CHR #glyph

D.COL #D.COL.WHITE #D.TXT.NORMAL

JUMP #return


-- Leave the program.

$exit
JUMP 0

Try it

Move the selection to each edge of the board.

Try pressing the arrow key that would move beyond the edge. The selection should stay where it is.

Place several crosses and move away from them. Each cross should remain on the board.

Move back onto one of them. The cross should become inverted when selected and return to normal when you leave.

Press Enter on a square that already contains a cross. Nothing should change.

Finally, place three crosses in a row.

Nothing happens.

The program remembers that those three squares contain crosses, but as far as it currently knows, they are simply three cells containing 1.

It doesn't yet know that this combination means something.


In NCL 309: Winning the Game, we'll teach the program how to recognize the eight winning lines.

As the program grows, we'll also revisit the shared drawing code we built here. Manually remembering where it should return works, but NCL has a much better way to do it.