NCL 309: Winning the Game
In the previous lesson, we built a board, moved a selection around it, and placed Crosses into its cells.
We also built something else: a small piece of shared drawing code.
Each time we wanted to use it, we stored a label in #return, jumped to the drawing code, then jumped back through #return when it was finished.
It worked.
NCL has a better way.
Calling a subroutine
Here is the pattern we used in the previous lesson:
MOVE #return $after_draw
JUMP $draw_cell
$after_draw
The drawing code eventually returned with:
JUMP #return
We needed a different return label every time we used it. We also needed a register just to remember where the drawing code should return.
This pattern is common enough that NCL provides two instructions for it:
CALL $draw_cell
and:
RET
CALL jumps to another part of the program, much like JUMP. Unlike JUMP, it also remembers where execution came from.
RET returns to the instruction immediately following the most recent CALL.
Code designed to be called this way is called a subroutine.
Our drawing code can now end with:
$paint
D.COL #D.COL.WHITE #D.TXT.NORMAL
BEQ $paint_selected #selected 1
JUMP $paint_glyph
$paint_selected
D.COL #D.COL.WHITE #D.TXT.INVERT
$paint_glyph
D.CUR #x #y
D.CHR #glyph
RET
Whenever we need to draw a cell:
CALL $draw_cell
When $draw_cell reaches RET, execution continues immediately after that CALL.
We no longer need #return, or labels such as $after_old and $after_new.
There is another important difference: a subroutine can call another subroutine.
Each RET returns from the corresponding CALL. We do not have to manage those return destinations ourselves.
We'll take advantage of that shortly.
Adding the other player
So far, each cell has had two possible values:
| Value | Meaning |
|---|---|
0 |
Empty |
1 |
Cross |
Let's add Nought.
We could choose another positive number, but there is a more useful representation:
| Value | Meaning |
|---|---|
-1 |
Nought |
0 |
Empty |
1 |
Cross |
Add a glyph for Nought:
#cross "\uE573"
#nought "\uE5CB"
#empty "\u3000"
We'll also keep track of whose turn it is:
#player r9
Cross goes first:
MOVE #player 1
Instead of always placing 1 into a cell:
MOVE #cell4 1
place the current player:
MOVE #cell4 #player
After a successful move, we need to change 1 into -1, or -1 back into 1.
We already know enough arithmetic to do that:
MUL #player #player -1
If #player contains 1:
1 × -1 = -1
If it contains -1:
-1 × -1 = 1
One instruction alternates between the two players.
The drawing subroutine also needs to recognize both values. When it has copied the selected cell into #value, it can choose the appropriate glyph:
SMOVE #glyph #empty
BEQ $use_cross #value 1
BEQ $use_nought #value -1
JUMP $paint
$use_cross
SMOVE #glyph #cross
JUMP $paint
$use_nought
SMOVE #glyph #nought
The board now remembers which player owns each occupied cell.
That choice of 1 and -1 is about to become useful for more than drawing.
What makes a winning line?
Consider the top row of the board.
If Cross owns all three cells, their values are:
1 + 1 + 1 = 3
If Nought owns all three:
-1 + -1 + -1 = -3
Because an empty cell is 0, two Crosses and an empty cell produce:
1 + 1 + 0 = 2
Two Noughts and an empty cell produce:
-1 + -1 + 0 = -2
The sum of a line therefore tells us something about its contents.
| Sum | Meaning |
|---|---|
3 |
Cross owns the complete line |
2 |
Two Crosses and one empty cell |
1 |
The line favors Cross |
0 |
The line is numerically balanced |
-1 |
The line favors Nought |
-2 |
Two Noughts and one empty cell |
-3 |
Nought owns the complete line |
The values in the middle can describe several different arrangements. A sum of 1, for example, does not necessarily mean there is only one Cross.
For detecting a winner, however, the two extremes are unambiguous:
3 = Cross wins
-3 = Nought wins
Our board has eight possible winning lines:
| Line | Cells |
|---|---|
| Top row | 0, 1, 2 |
| Middle row | 3, 4, 5 |
| Bottom row | 6, 7, 8 |
| Left column | 0, 3, 6 |
| Middle column | 1, 4, 7 |
| Right column | 2, 5, 8 |
| Diagonal | 0, 4, 8 |
| Diagonal | 2, 4, 6 |
Before we check all eight, let's look at a few operations that will make the job easier.
Operations we can already build
The processor already knows enough to find negative values, absolute values, larger values, and smaller values.
We could build all of these operations ourselves using instructions we've already learned.
In fact, let's do exactly that before introducing anything new.
Negating a number
To negate a number is to change its sign.
We can do that by subtracting it from zero:
SUB #result 0 #value
If #value contains 5:
0 - 5 = -5
If it contains -5:
0 - -5 = 5
NCL provides NEG for this common operation:
NEG #result #value
It produces the same result.
That also gives us a clearer way to alternate players.
Instead of:
MUL #player #player -1
we can write:
NEG #player #player
Both perform the same calculation. NEG simply says more directly what we intend to do.
Finding an absolute value
Sometimes we care about the size of a number, but not whether it is positive or negative.
The absolute value of 5 is 5.
The absolute value of -5 is also 5.
We can build that operation ourselves:
MOVE #result #value
BGE $done #result 0
NEG #result #result
$done
Start by copying the value.
If it is already zero or positive, we are finished.
If it is negative, negate it.
NCL provides ABS for the same operation:
ABS #result #value
Choosing the larger value
Suppose we have two values, #a and #b, and want to keep whichever is larger.
Again, we can build that ourselves:
MOVE #result #a
BGE $done #a #b
MOVE #result #b
$done
Start by assuming #a is the larger value.
If it is greater than or equal to #b, that assumption was correct.
Otherwise, replace it with #b.
NCL provides MAX for this pattern:
MAX #result #a #b
Choosing the smaller value
Finding the smaller value is almost identical:
MOVE #result #a
BLE $done #a #b
MOVE #result #b
$done
NCL provides MIN for that operation:
MIN #result #a #b
None of these instructions gives the processor a fundamentally new ability. We could already produce the same results using arithmetic, comparisons, branches, and moves.
They give useful operations names and let us express them in a single instruction.
| Operation | Using what we already know | NCL instruction |
|---|---|---|
| Change the sign | Subtract from zero | NEG |
| Ignore the sign | Negate if negative | ABS |
| Choose the larger value | Compare, branch, move | MAX |
| Choose the smaller value | Compare, branch, move | MIN |
Now we have some convenient tools for examining the board.
Checking the eight lines
We only need one register for the current line:
#line r10
The top row is cells 0, 1, and 2:
$check_top
ADD #line #cell0 #cell1
ADD #line #line #cell2
RET
The other seven follow exactly the same pattern:
$check_middle
ADD #line #cell3 #cell4
ADD #line #line #cell5
RET
$check_bottom
ADD #line #cell6 #cell7
ADD #line #line #cell8
RET
$check_left
ADD #line #cell0 #cell3
ADD #line #line #cell6
RET
$check_center
ADD #line #cell1 #cell4
ADD #line #line #cell7
RET
$check_right
ADD #line #cell2 #cell5
ADD #line #line #cell8
RET
$check_diagonal1
ADD #line #cell0 #cell4
ADD #line #line #cell8
RET
$check_diagonal2
ADD #line #cell2 #cell4
ADD #line #line #cell6
RET
Each subroutine answers one question:
What is the sum of this line?
It leaves that answer in #line.
We could call all eight and test each result individually.
Instead, let's summarize them.
Finding the strongest line
A line can have a value anywhere from -3 through 3.
We care about two things:
- the highest line value on the board;
- the lowest line value on the board.
Add two more registers:
#highest r11
#lowest r12
Start them at the opposite extremes:
MOVE #highest -3
MOVE #lowest 3
Then check a line:
CALL $check_top
MAX #highest #highest #line
MIN #lowest #lowest #line
Suppose the top row has a value of 2.
MAX compares -3 with 2, so #highest becomes 2.
MIN compares 3 with 2, so #lowest also becomes 2.
Then we reuse #line for the next row:
CALL $check_middle
MAX #highest #highest #line
MIN #lowest #lowest #line
We do not need to remember every line value. Once a line has been compared with #highest and #lowest, its individual value can be discarded.
Put the complete operation into another subroutine:
$check_win
MOVE #highest -3
MOVE #lowest 3
CALL $check_top
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_middle
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_bottom
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_left
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_center
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_right
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_diagonal1
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_diagonal2
MAX #highest #highest #line
MIN #lowest #lowest #line
RET
Notice what is happening here.
Our game calls $check_win.
$check_win calls $check_top.
$check_top returns to $check_win.
Later, $check_win returns to the game.
This is something our hand-made return register from the previous lesson could not safely do. A second call would overwrite the return destination stored by the first.
CALL and RET keep track of those returns for us.
Who won?
After:
CALL $check_win
#highest contains the strongest result for Cross, while #lowest contains the strongest result for Nought.
For example:
#highest = 2
#lowest = -1
Cross has the stronger line.
Or:
#highest = 1
#lowest = -3
Nought has won.
The sign tells us which player a line favors. Its absolute value tells us its strength.
Add another working register:
#strength r14
First, remove the sign from Nought's strongest line:
ABS #strength #lowest
Then compare its magnitude with Cross's strongest line:
MAX #strength #highest #strength
Now #strength contains the strength of the strongest line belonging to either player.
If it reaches 3, somebody has won:
BNEQ $no_winner #strength 3
BEQ $cross_wins #highest 3
JUMP $nought_wins
$no_winner
We first ask:
Does either player have a line with strength
3?
Only then do we ask:
Which player has it?
The sign tells us who.
The magnitude tells us how strongly.
When the board is full
There is one more way the game can end.
Neither player may complete a line before all nine cells are occupied.
We could inspect every cell to find out whether the board is full, but we already know something simpler: every successful move fills exactly one previously empty cell.
Add a move counter:
#moves r13
Initialize it with the board:
MOVE #moves 0
Whenever a player successfully places a piece:
INC #moves
Do not increment it when the selected cell is already occupied.
After each successful move, the game can now follow this order:
INC #moves
CALL $check_win
ABS #strength #lowest
MAX #strength #highest #strength
BNEQ $no_winner #strength 3
BEQ $cross_wins #highest 3
JUMP $nought_wins
$no_winner
BEQ $draw #moves 9
NEG #player #player
JUMP $input
Checking for a winner comes before checking for a draw.
That matters on the ninth move: filling the last cell may also complete a winning line.
Only if nobody won do we ask whether all nine moves have been made.
Notice that we are now using NEG for the same operation we originally wrote as:
MUL #player #player -1
The arithmetic has not changed.
The new instruction expresses our intention more directly.
Finishing the game
The three endings can replace the controls at the bottom of the display.
For example:
$cross_wins
D.CUR 9 9
D.TXT "CROSS WINS! "
D.BLT
JUMP 0
$nought_wins
D.CUR 9 9
D.TXT "NOUGHT WINS! "
D.BLT
JUMP 0
$draw
D.CUR 9 9
D.TXT "DRAW! "
D.BLT
JUMP 0
JUMP 0 moves execution outside the program, ending it after the final screen has been drawn.
We now have a complete two-player game.
The board itself still contains nothing more complicated than nine integers:
-1 = Nought
0 = empty
1 = Cross
From those values, the program derives everything it needs to know.
It adds cells together to describe a line.
It uses MIN and MAX to summarize all eight lines.
It uses ABS when the magnitude matters more than the sign.
It uses NEG to move cleanly between the two sides of our signed representation.
And it uses CALL and RET to organize those operations into reusable subroutines.
None of those conveniences changed what the computer was capable of doing. We built each numerical operation from instructions we already knew, just as we built our own return mechanism in the previous lesson.
The new instructions let us express those familiar operations more directly.
The complete program
We've changed quite a lot since the previous lesson. Here is the complete two-player game assembled into one program.
The movement and drawing code should still look familiar. The important additions are #player, the move counter, the eight line-checking subroutines, and $check_win.
#selection r0
#column r1
#row r2
#x r3
#y r4
#draw_cell r6
#value r7
#selected r8
#player r9
#line r10
#highest r11
#lowest r12
#moves r13
#strength r14
#key s0
#glyph s1
#empty "\u3000"
#cross "\uE573"
#nought "\uE5CB"
#cell0 r20
#cell1 r21
#cell2 r22
#cell3 r23
#cell4 r24
#cell5 r25
#cell6 r26
#cell7 r27
#cell8 r28
-- Start the game.
MOVE #selection 0
MOVE #player 1
MOVE #moves 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
CALL $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
CALL $draw_cell
DEC #selection
MOVE #draw_cell #selection
MOVE #selected 1
CALL $draw_cell
JUMP $input
-- Move right.
$right
MOD #column #selection 3
BEQ $input #column 2
MOVE #draw_cell #selection
MOVE #selected 0
CALL $draw_cell
INC #selection
MOVE #draw_cell #selection
MOVE #selected 1
CALL $draw_cell
JUMP $input
-- Move up.
$up
BLT $input #selection 3
MOVE #draw_cell #selection
MOVE #selected 0
CALL $draw_cell
SUB #selection #selection 3
MOVE #draw_cell #selection
MOVE #selected 1
CALL $draw_cell
JUMP $input
-- Move down.
$down
BGE $input #selection 6
MOVE #draw_cell #selection
MOVE #selected 0
CALL $draw_cell
ADD #selection #selection 3
MOVE #draw_cell #selection
MOVE #selected 1
CALL $draw_cell
JUMP $input
-- Place the current player's piece.
$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 #player
JUMP $placed
$place1
BNEQ $input #cell1 0
MOVE #cell1 #player
JUMP $placed
$place2
BNEQ $input #cell2 0
MOVE #cell2 #player
JUMP $placed
$place3
BNEQ $input #cell3 0
MOVE #cell3 #player
JUMP $placed
$place4
BNEQ $input #cell4 0
MOVE #cell4 #player
JUMP $placed
$place5
BNEQ $input #cell5 0
MOVE #cell5 #player
JUMP $placed
$place6
BNEQ $input #cell6 0
MOVE #cell6 #player
JUMP $placed
$place7
BNEQ $input #cell7 0
MOVE #cell7 #player
JUMP $placed
$place8
BNEQ $input #cell8 0
MOVE #cell8 #player
-- Redraw the placed piece and check the game.
$placed
INC #moves
MOVE #draw_cell #selection
MOVE #selected 1
CALL $draw_cell
CALL $check_win
ABS #strength #lowest
MAX #strength #highest #strength
BNEQ $no_winner #strength 3
BEQ $cross_wins #highest 3
JUMP $nought_wins
$no_winner
BEQ $draw #moves 9
NEG #player #player
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 $choose_glyph
$draw1
MOVE #value #cell1
JUMP $choose_glyph
$draw2
MOVE #value #cell2
JUMP $choose_glyph
$draw3
MOVE #value #cell3
JUMP $choose_glyph
$draw4
MOVE #value #cell4
JUMP $choose_glyph
$draw5
MOVE #value #cell5
JUMP $choose_glyph
$draw6
MOVE #value #cell6
JUMP $choose_glyph
$draw7
MOVE #value #cell7
JUMP $choose_glyph
$draw8
MOVE #value #cell8
-- Choose the visible glyph.
$choose_glyph
SMOVE #glyph #empty
BEQ $use_cross #value 1
BEQ $use_nought #value -1
JUMP $paint
$use_cross
SMOVE #glyph #cross
JUMP $paint
$use_nought
SMOVE #glyph #nought
-- Find the Display position.
$paint
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.
D.COL #D.COL.WHITE #D.TXT.NORMAL
BEQ $paint_selected #selected 1
JUMP $paint_glyph
$paint_selected
D.COL #D.COL.WHITE #D.TXT.INVERT
$paint_glyph
D.CUR #x #y
D.CHR #glyph
RET
-- Check each winning line.
$check_top
ADD #line #cell0 #cell1
ADD #line #line #cell2
RET
$check_middle
ADD #line #cell3 #cell4
ADD #line #line #cell5
RET
$check_bottom
ADD #line #cell6 #cell7
ADD #line #line #cell8
RET
$check_left
ADD #line #cell0 #cell3
ADD #line #line #cell6
RET
$check_center
ADD #line #cell1 #cell4
ADD #line #line #cell7
RET
$check_right
ADD #line #cell2 #cell5
ADD #line #line #cell8
RET
$check_diagonal1
ADD #line #cell0 #cell4
ADD #line #line #cell8
RET
$check_diagonal2
ADD #line #cell2 #cell4
ADD #line #line #cell6
RET
-- Find the highest and lowest line values.
$check_win
MOVE #highest -3
MOVE #lowest 3
CALL $check_top
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_middle
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_bottom
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_left
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_center
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_right
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_diagonal1
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_diagonal2
MAX #highest #highest #line
MIN #lowest #lowest #line
RET
-- Finish the game.
$cross_wins
D.CUR 9 9
D.TXT "CROSS WINS! "
D.BLT
JUMP 0
$nought_wins
D.CUR 9 9
D.TXT "NOUGHT WINS! "
D.BLT
JUMP 0
$draw
D.CUR 9 9
D.TXT "DRAW! "
D.BLT
JUMP 0
$exit
JUMP 0
Try it
Run the game with another person and take turns placing pieces.
Try winning with a row, a column, and a diagonal. Then fill the board without either player completing a line and confirm that the game ends in a draw.
While playing, pick a moment and stop before making the next move. Look at the nine cell values and calculate the eight line sums yourself.
Which value should be in #highest?
Which should be in #lowest?
What should #strength contain?
In NCL 310: The Computer's Turn, we'll give Nought to the computer.
We've taught the program how to understand the board after a move. Next, we'll see what happens when it starts asking about moves that haven't happened yet.