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:
0means the upper-left square;4means the centre square;8means 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:
#selectionis 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:
- the old square becomes normal;
- 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:
1is 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:
MOVEstores the location of$after_drawin#return.JUMP $draw_cellgoes to the shared drawing code.$draw_celldoes its work.JUMP #returngoes 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_celldecides which square we're drawing;#selecteddecides 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.