NCL 310: The Computer's Turn
Our game is complete, but it still needs two people to play it.
Let's give Nought to the computer.
Cross will continue to use the arrow keys and Enter. After each successful Cross move, the computer will inspect the board, choose an empty cell, and place a Nought of its own.
It won't choose randomly.
We'll give it a simple strategy:
- If Nought can win immediately, take the winning move.
- If Cross can win on the next move, block it.
- Otherwise, choose from a fixed list of preferred cells.
The first two decisions sound like they might require a new way to analyze the board.
They don't.
Opportunities and dangers
In the previous lesson, we gave each board cell a signed value:
| Value | Cell |
|---|---|
-1 |
Nought |
0 |
Empty |
1 |
Cross |
We then added the three cells in each winning line.
A line containing two Noughts and one empty cell has a sum of:
-1 + -1 + 0 = -2
If Nought plays in that empty cell, it wins.
A line containing two Crosses and one empty cell has a sum of:
1 + 1 + 0 = 2
If Nought doesn't play in that empty cell, Cross may win on its next turn.
Those values now have a practical meaning:
| Line value | Meaning |
|---|---|
-2 |
An opportunity for Nought |
2 |
A danger from Cross |
The computer already knows how to calculate the value of every line.
What it needs to discover is where to play.
A deliberately imperfect opponent
Noughts and Crosses is a solved game. With perfect play, neither player needs to lose.
The computer we build here will not play perfectly. It can recognize an immediate opportunity to win, recognize when Cross must be blocked, and otherwise choose from a fixed list of preferred positions.
It does not plan several moves ahead or recognize every strategy available to either player.
That's intentional. Our goal is to build an opponent whose decisions we can understand using the tools we've learned so far.
Looking at the board another way
In the previous lesson, we looked at each winning line and asked which cells belonged to it.
The top row contains cells 0, 1, and 2.
The left column contains cells 0, 3, and 6.
The first diagonal contains cells 0, 4, and 8.
For the computer's turn, we'll turn that relationship around.
Cell 0 belongs to the top row, left column, and first diagonal.
Cell 1 belongs to the top row and middle column.
The center belongs to four different lines.
| Cell | Lines |
|---|---|
| 0 | Top row, left column, diagonal 1 |
| 1 | Top row, middle column |
| 2 | Top row, right column, diagonal 2 |
| 3 | Middle row, left column |
| 4 | Middle row, middle column, diagonal 1, diagonal 2 |
| 5 | Middle row, right column |
| 6 | Bottom row, left column, diagonal 2 |
| 7 | Bottom row, middle column |
| 8 | Bottom row, right column, diagonal 1 |
This gives us a different question to ask:
Among the lines passing through this empty cell, what are the highest and lowest line values?
We already know how to answer that.
Checking one cell
In the previous lesson, $check_win used these registers:
#highest r11
#lowest r12
They summarized all eight lines on the board.
During the computer's search, we don't need the old values stored in them. We can use the same registers to summarize the lines through one cell.
A register doesn't permanently belong to one particular idea. It holds whatever value the program needs at that moment.
For example, cell 0 belongs to three lines:
$check_cell0
MOVE #highest -3
MOVE #lowest 3
CALL $check_top
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_left
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_diagonal1
MAX #highest #highest #line
MIN #lowest #lowest #line
RET
After CALL $check_cell0:
#highestcontains the highest-valued line through cell 0;#lowestcontains the lowest-valued line through cell 0.
If cell 0 is empty and #lowest is -2, Nought can win by playing there.
If #highest is 2, Cross can win there.
The center works the same way, but belongs to four lines:
$check_cell4
MOVE #highest -3
MOVE #lowest 3
CALL $check_middle
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_center
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
The other cells use the same pattern with the lines that pass through them.
Notice what is happening with CALL and RET.
The computer's turn can call $check_cell4.
$check_cell4 can call $check_middle.
$check_middle returns to $check_cell4.
When all four lines have been checked, $check_cell4 returns to the computer's turn.
We don't need to manage any of those return destinations ourselves.
Remembering what we find
The computer will inspect every empty cell once.
While it does, it needs to remember two things:
- a cell where Nought can win;
- a cell where Cross must be blocked.
We'll use two registers:
#opportunity r15
#danger r16
At the beginning of the computer's turn:
MOVE #opportunity -1
MOVE #danger -1
Our board cells are numbered from 0 through 8, so -1 means that nothing has been found yet.
Scanning the board
Start with cell 0.
If it is occupied, skip it:
BNEQ $scan1 #cell0 0
Otherwise, inspect the lines that pass through it:
CALL $check_cell0
If the lowest line is -2, this cell is an opportunity:
BNEQ $danger0 #lowest -2
BNEQ $danger0 #opportunity -1
MOVE #opportunity 0
The second branch means that we only store the cell if #opportunity is still -1.
Once we've found an opportunity, we keep the first one.
Now check for danger:
$danger0
BNEQ $scan1 #highest 2
BNEQ $scan1 #danger -1
MOVE #danger 0
A cell can even be both.
It might complete a Nought line while also blocking a Cross line. We don't stop after finding one result, because we want the scan to describe everything important about that cell.
Then we continue with cell 1:
$scan1
BNEQ $scan2 #cell1 0
CALL $check_cell1
BNEQ $danger1 #lowest -2
BNEQ $danger1 #opportunity -1
MOVE #opportunity 1
$danger1
BNEQ $scan2 #highest 2
BNEQ $scan2 #danger -1
MOVE #danger 1
$scan2
The same pattern continues through all nine cells.
By the end of the scan, the computer has surveyed the complete board.
Choosing what matters most
The computer may find neither result:
#opportunity = -1
#danger = -1
It may find a danger:
#opportunity = -1
#danger = 6
Or it may find both:
#opportunity = 2
#danger = 6
If Nought can win immediately, blocking Cross no longer matters.
So opportunity comes first:
BEQ $no_opportunity #opportunity -1
MOVE #selection #opportunity
JUMP $place
$no_opportunity
Then danger:
BEQ $no_danger #danger -1
MOVE #selection #danger
JUMP $place
$no_danger
Notice that the computer doesn't need a separate register for its final choice.
#selection already means a selected board cell.
The keyboard changes it with the arrow keys.
The computer changes it after inspecting the board.
Either way, the rest of the program receives the same thing: a cell number.
When nothing is urgent
If the computer finds neither an opportunity nor a danger, it still needs to choose something.
We could build a more elaborate strategy.
For this opponent, a fixed preference is enough:
- Center
- Corners
- Edges
Because every board cell already has its own register, we can directly ask whether each preferred position is empty:
BEQ $choose4 #cell4 0
BEQ $choose0 #cell0 0
BEQ $choose2 #cell2 0
BEQ $choose6 #cell6 0
BEQ $choose8 #cell8 0
BEQ $choose1 #cell1 0
BEQ $choose3 #cell3 0
BEQ $choose5 #cell5 0
JUMP $choose7
Each destination puts its cell number into #selection:
$choose0
MOVE #selection 0
JUMP $place
$choose1
MOVE #selection 1
JUMP $place
...
$choose8
MOVE #selection 8
JUMP $place
No matter how the computer chooses a move, it ends with the same information the keyboard gives us:
Place the current player in
#selection.
We already have almost everything needed to do that.
One placement mechanism
In the previous lesson, #player contained:
1 = Cross
-1 = Nought
We could remove that register now that Cross and Nought have different controllers.
Instead, we'll make it more useful.
#player will mean:
The value to store in the next successfully selected cell.
When Cross presses Enter, #player is 1.
When the computer chooses a cell, #player is -1.
The code that actually places a piece doesn't need to know who chose the move:
$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 destination checks that the cell is empty and stores #player:
$place4
BNEQ $occupied #cell4 0
MOVE #cell4 #player
JUMP $placed
The same code places both symbols.
The person and the computer choose their moves differently.
Once a move has been chosen, however, placing it is the same operation.
After a move
Every successful move also has the same consequences.
We increment the move counter:
INC #moves
We redraw the selected cell:
CALL $draw_selected
And we inspect the board:
CALL $check_win
ABS #strength #lowest
MAX #strength #highest #strength
If #strength is 3, someone has won.
Otherwise, if all nine moves have been played, the game is a draw.
If the game continues, change players:
NEG #player #player
Now the sign of #player tells us what happens next.
If it is 1, Cross needs a move from the keyboard.
If it is -1, Nought needs a move from the computer:
BEQ $input #player 1
JUMP $computer_turn
The two players share the board, the drawing code, the placement code, and the win detection.
Only choosing a move is different.
An occupied cell
There is one small difference between the two players.
The keyboard can select an occupied cell and press Enter.
The computer never deliberately chooses an occupied cell.
Our shared placement code still checks:
BNEQ $occupied #cell4 0
If the selected cell is occupied, we arrive at:
$occupied
JUMP $input
For Cross, that simply ignores the attempted move and waits for another key.
The computer should never arrive there because its scan and preference list only choose empty cells.
The complete game
We've been building this program since NCL 308: Moving Around a Board.
It now contains keyboard input, persistent board state, drawing, reusable subroutines, nested calls, arithmetic analysis, shared game logic, and a computer opponent.
Here is the complete program.
-- Noughts and Crosses
-- Cross is controlled by the keyboard.
-- Nought is controlled by the computer.
#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
#opportunity r15
#danger r16
#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 with an empty board.
-- Cross moves first.
MOVE #selection 0
MOVE #player 1
MOVE #moves 0
MOVE #cell0 0
MOVE #cell1 0
MOVE #cell2 0
MOVE #cell3 0
MOVE #cell4 0
MOVE #cell5 0
MOVE #cell6 0
MOVE #cell7 0
MOVE #cell8 0
-- Draw the board.
D.PALRST
D.COL #D.COL.WHITE #D.TXT.NORMAL
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
CALL $draw_selected
-- Wait for Cross to move.
$input
SYS.AKEY #key
BSEQ $move_left #key "LEFT"
BSEQ $move_right #key "RIGHT"
BSEQ $move_up #key "UP"
BSEQ $move_down #key "DOWN"
BSEQ $place #key "ENTER"
BSEQ $exit #key "ESC"
JUMP $input
-- Move the selection.
$move_left
MOD #column #selection 3
BEQ $input #column 0
CALL $draw_normal
DEC #selection
CALL $draw_selected
JUMP $input
$move_right
MOD #column #selection 3
BEQ $input #column 2
CALL $draw_normal
INC #selection
CALL $draw_selected
JUMP $input
$move_up
BLT $input #selection 3
CALL $draw_normal
SUB #selection #selection 3
CALL $draw_selected
JUMP $input
$move_down
BGE $input #selection 6
CALL $draw_normal
ADD #selection #selection 3
CALL $draw_selected
JUMP $input
-- Place the current player in the selected cell.
$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 $occupied #cell0 0
MOVE #cell0 #player
JUMP $placed
$place1
BNEQ $occupied #cell1 0
MOVE #cell1 #player
JUMP $placed
$place2
BNEQ $occupied #cell2 0
MOVE #cell2 #player
JUMP $placed
$place3
BNEQ $occupied #cell3 0
MOVE #cell3 #player
JUMP $placed
$place4
BNEQ $occupied #cell4 0
MOVE #cell4 #player
JUMP $placed
$place5
BNEQ $occupied #cell5 0
MOVE #cell5 #player
JUMP $placed
$place6
BNEQ $occupied #cell6 0
MOVE #cell6 #player
JUMP $placed
$place7
BNEQ $occupied #cell7 0
MOVE #cell7 #player
JUMP $placed
$place8
BNEQ $occupied #cell8 0
MOVE #cell8 #player
JUMP $placed
$occupied
JUMP $input
-- Redraw the move and inspect the board.
$placed
INC #moves
CALL $draw_selected
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
BEQ $input #player 1
JUMP $computer_turn
-- Nought's turn.
$computer_turn
-- Remove the selection while the computer thinks.
CALL $draw_normal
-- -1 means that nothing has been found yet.
MOVE #opportunity -1
MOVE #danger -1
-- Inspect cell 0.
BNEQ $scan1 #cell0 0
CALL $check_cell0
BNEQ $danger0 #lowest -2
BNEQ $danger0 #opportunity -1
MOVE #opportunity 0
$danger0
BNEQ $scan1 #highest 2
BNEQ $scan1 #danger -1
MOVE #danger 0
-- Inspect cell 1.
$scan1
BNEQ $scan2 #cell1 0
CALL $check_cell1
BNEQ $danger1 #lowest -2
BNEQ $danger1 #opportunity -1
MOVE #opportunity 1
$danger1
BNEQ $scan2 #highest 2
BNEQ $scan2 #danger -1
MOVE #danger 1
-- Inspect cell 2.
$scan2
BNEQ $scan3 #cell2 0
CALL $check_cell2
BNEQ $danger2 #lowest -2
BNEQ $danger2 #opportunity -1
MOVE #opportunity 2
$danger2
BNEQ $scan3 #highest 2
BNEQ $scan3 #danger -1
MOVE #danger 2
-- Inspect cell 3.
$scan3
BNEQ $scan4 #cell3 0
CALL $check_cell3
BNEQ $danger3 #lowest -2
BNEQ $danger3 #opportunity -1
MOVE #opportunity 3
$danger3
BNEQ $scan4 #highest 2
BNEQ $scan4 #danger -1
MOVE #danger 3
-- Inspect cell 4.
$scan4
BNEQ $scan5 #cell4 0
CALL $check_cell4
BNEQ $danger4 #lowest -2
BNEQ $danger4 #opportunity -1
MOVE #opportunity 4
$danger4
BNEQ $scan5 #highest 2
BNEQ $scan5 #danger -1
MOVE #danger 4
-- Inspect cell 5.
$scan5
BNEQ $scan6 #cell5 0
CALL $check_cell5
BNEQ $danger5 #lowest -2
BNEQ $danger5 #opportunity -1
MOVE #opportunity 5
$danger5
BNEQ $scan6 #highest 2
BNEQ $scan6 #danger -1
MOVE #danger 5
-- Inspect cell 6.
$scan6
BNEQ $scan7 #cell6 0
CALL $check_cell6
BNEQ $danger6 #lowest -2
BNEQ $danger6 #opportunity -1
MOVE #opportunity 6
$danger6
BNEQ $scan7 #highest 2
BNEQ $scan7 #danger -1
MOVE #danger 6
-- Inspect cell 7.
$scan7
BNEQ $scan8 #cell7 0
CALL $check_cell7
BNEQ $danger7 #lowest -2
BNEQ $danger7 #opportunity -1
MOVE #opportunity 7
$danger7
BNEQ $scan8 #highest 2
BNEQ $scan8 #danger -1
MOVE #danger 7
-- Inspect cell 8.
$scan8
BNEQ $choose_move #cell8 0
CALL $check_cell8
BNEQ $danger8 #lowest -2
BNEQ $danger8 #opportunity -1
MOVE #opportunity 8
$danger8
BNEQ $choose_move #highest 2
BNEQ $choose_move #danger -1
MOVE #danger 8
-- Prefer a winning move, then a blocking move.
$choose_move
BEQ $no_opportunity #opportunity -1
MOVE #selection #opportunity
JUMP $place
$no_opportunity
BEQ $no_danger #danger -1
MOVE #selection #danger
JUMP $place
-- Nothing is urgent.
-- Prefer the center, then corners, then edges.
$no_danger
BEQ $choose4 #cell4 0
BEQ $choose0 #cell0 0
BEQ $choose2 #cell2 0
BEQ $choose6 #cell6 0
BEQ $choose8 #cell8 0
BEQ $choose1 #cell1 0
BEQ $choose3 #cell3 0
BEQ $choose5 #cell5 0
JUMP $choose7
$choose0
MOVE #selection 0
JUMP $place
$choose1
MOVE #selection 1
JUMP $place
$choose2
MOVE #selection 2
JUMP $place
$choose3
MOVE #selection 3
JUMP $place
$choose4
MOVE #selection 4
JUMP $place
$choose5
MOVE #selection 5
JUMP $place
$choose6
MOVE #selection 6
JUMP $place
$choose7
MOVE #selection 7
JUMP $place
$choose8
MOVE #selection 8
JUMP $place
-- Draw the current selection normally.
$draw_normal
MOVE #draw_cell #selection
MOVE #selected 0
CALL $draw_cell
RET
-- Draw the current selection inverted.
$draw_selected
MOVE #draw_cell #selection
MOVE #selected 1
CALL $draw_cell
RET
-- Read a board cell and draw its visible glyph.
$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 glyph stored in this cell.
$paint
SMOVE #glyph #empty
BEQ $use_cross #value 1
BEQ $use_nought #value -1
JUMP $paint_position
$use_cross
SMOVE #glyph #cross
JUMP $paint_position
$use_nought
SMOVE #glyph #nought
-- Convert the cell number into screen coordinates.
$paint_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
-- Draw normally or inverted.
BEQ $paint_selected #selected 1
D.COL #D.COL.WHITE #D.TXT.NORMAL
JUMP $paint_glyph
$paint_selected
D.COL #D.COL.WHITE #D.TXT.INVERT
$paint_glyph
D.CUR #x #y
D.CHR #glyph
D.COL #D.COL.WHITE #D.TXT.NORMAL
RET
-- Calculate the eight winning lines.
$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 strongest line for either player.
$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
-- Find the strongest lines passing through cell 0.
$check_cell0
MOVE #highest -3
MOVE #lowest 3
CALL $check_top
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_left
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_diagonal1
MAX #highest #highest #line
MIN #lowest #lowest #line
RET
-- Find the strongest lines passing through cell 1.
$check_cell1
MOVE #highest -3
MOVE #lowest 3
CALL $check_top
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_center
MAX #highest #highest #line
MIN #lowest #lowest #line
RET
-- Find the strongest lines passing through cell 2.
$check_cell2
MOVE #highest -3
MOVE #lowest 3
CALL $check_top
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_right
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_diagonal2
MAX #highest #highest #line
MIN #lowest #lowest #line
RET
-- Find the strongest lines passing through cell 3.
$check_cell3
MOVE #highest -3
MOVE #lowest 3
CALL $check_middle
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_left
MAX #highest #highest #line
MIN #lowest #lowest #line
RET
-- Find the strongest lines passing through cell 4.
$check_cell4
MOVE #highest -3
MOVE #lowest 3
CALL $check_middle
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_center
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
-- Find the strongest lines passing through cell 5.
$check_cell5
MOVE #highest -3
MOVE #lowest 3
CALL $check_middle
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_right
MAX #highest #highest #line
MIN #lowest #lowest #line
RET
-- Find the strongest lines passing through cell 6.
$check_cell6
MOVE #highest -3
MOVE #lowest 3
CALL $check_bottom
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_left
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_diagonal2
MAX #highest #highest #line
MIN #lowest #lowest #line
RET
-- Find the strongest lines passing through cell 7.
$check_cell7
MOVE #highest -3
MOVE #lowest 3
CALL $check_bottom
MAX #highest #highest #line
MIN #lowest #lowest #line
CALL $check_center
MAX #highest #highest #line
MIN #lowest #lowest #line
RET
-- Find the strongest lines passing through cell 8.
$check_cell8
MOVE #highest -3
MOVE #lowest 3
CALL $check_bottom
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
RET
-- Game endings.
$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
Play several games against the computer.
Let it get two Noughts in a line and confirm that it takes the winning move.
Create two Crosses in a line and confirm that it blocks you.
Watch what it does when neither player has an immediate opportunity or danger. Once you know its preference order, you should often be able to predict its move.
Then try to beat it.
The computer understands immediate opportunities and dangers, but it doesn't plan several moves into the future.
If you find a position it handles poorly, walk through the program's decision.
What did its scan see?
What did you notice that its algorithm never asked about?
We've spent the 300-level teaching the computer to interact with us. Along the way, a simple keyboard-controlled board grew into a program with persistent state, reusable subroutines, nested calls, and decisions based on that state.
In the next section, we'll take a closer look at those structures and how they can help us organize larger NCL programs.