Logo

TicTacToe in Forth

After writing the Alternate Languages essay, I because increasingly interested in Forth. I've patched together a very simple version (if it gets simpler!) of TicTacToe in Forth. The program basically is highly defensive, and doesn't check to see if it can win, only if you can win. It won't even check whether it has won or not!

Playing the Game

Copy ttt.f into the Win32Forth directory and load up Win32Forth. Type "FLOAD TTT.F" and the program should correctly load, with various little messages appearing. Type "RUN" to start the game. To make a move, type the row and column followed by "TTTMOVE" (I didn't use "MOVE" since Forth already uses that). Here is a formatted output of a typical game:
1 1 tttmove
[X] [ ] [ ]
[ ] [ ] [ ]
[ ] [ ] [ ]
[X] [ ] [ ]
[ ] [O] [ ]
[ ] [ ] [ ]
3 1 tttmove
[X] [ ] [ ]
[ ] [O] [ ]
[X] [ ] [ ]
[X] [ ] [ ]
[O] [O] [ ]
[X] [ ] [ ]
1 3 tttmove
[X] [ ] [X]
[O] [O] [ ]
[X] [ ] [ ]
[X] [O] [X]
[O] [O] [ ]
[X] [ ] [ ]
3 2 tttmove
[X] [O] [X]
[O] [O] [ ]
[X] [X] [ ]
[X] [O] [X]
[O] [O] [ ]
[X] [X] [O]
2 3 tttmove
[X] [O] [X]
[O] [O] [X]
[X] [X] [O]
Game over. ok
This program was written using Win32Forth, but it should run under all Forth systems, since I made sure everything is capitalized (Win32Forth is not case sensitive, whereas others are).

Below is the code. I colour-coded everything to make it easier to see the various Forth syntax and semantics. I will break down the code, but you can download the code in its entirety here.

\ Program:      Tic-Tac-Toe
\ Author:       James.
\
\ Description:
\ This is a Forth implementation of TTT with very simple AI.
\ It happens to be my first Forth program, so please bear with
\ my coding. Written for Generation5:
\               http://library.advanced.org/18242/
Please make extra special note of that. It really is my first ever Forth program, I wrote it as I learnt new words! My use of the stack, excessive use of variables, lack of return stack usage, back looping techniques and more will probably make any Forth programmer sick. Sorry.
CR
CR .( TicTacToe - by James. )
CR .( Written for Generation5 [http://library.advanced.org/18242] )
CR .( Starting tictactoe...)
CR starts a new line, and .( ) is used to delimit text that is displayed upon reading the file. Therefore, this will be displayed when you FLOAD the file.
\ Constants
0 CONSTANT NOTHING
1 CONSTANT HUMAN
2 CONSTANT COMPUTER

\ Variables
VARIABLE LASTEMPTY              \ Last empty space in loop.
VARIABLE NUMENEMY               \ Number of enemies in the line.
VARIABLE HORIZONTAL             \ Check horizontal or vertical?
VARIABLE OPENSPACE              \ Last open space on board...bad AI.
VARIABLE MOVED                  \ Have we moved?
VARIABLE WON                    \ Has someone won the game?

\ Initializes the board.
CREATE BOARD                    \ Create the board!
9 CELLS ALLOT                   \ Allot 9 (3x3) cells.
Basically, after defining all of the constants and variables (which are all documented), I allot space for the board. The CREATE word returns the first usable memory address, and combined with a call to ALLOT, creates space for the board - a one-dimensional array. Forth does not distinguish between data types (as far as I know), therefore there is one fixed memory size. This is called a cell (32-bits in Win32Forth). Note that if you type "CELL" enter, the number 4 will be pushed on the stack - this stands for 4 bytes.
\ Various board functions.
: BOARDOFFSET ( n -- addr ) CELLS BOARD + ;
: SETBOARD ( ) BOARDOFFSET ! ;
: BOARDAT ( ) BOARDOFFSET @ ;
I was quite pleased with these functions that I came up with considering. Basically, BOARDOFFSET pops a number off the stack and pushes the memory address of the BOARD plus the offset. Therefore, to get the memory address of the 4th (2,2) cell, you would use "4 BOARDOFFSET". SETBOARD and BOARDAT utilize this by either setting or returning (I will use return to mean push on to the data stack) a value at that memory address. Note that SETBOARD requires an additional number on the stack - so to add a computer piece at the 4th cell you would use "COMPUTER 4 SETBOARD". Note COMPUTER is defined as a constant.
\ Draw the board
: DRAWBOARD ( -- )
   CR CR ." Board:" CR          \ Print message.
   3 3                          \ Put row/col on stack.
   0 ?DO CR DUP                 \ Outer loop (col)
      0 ?DO                     \ Inner loop (row)
        I J 3 * +               \ Calculate array index.
        DUP BOARDAT HUMAN = IF
          ." [X]" SPACE THEN
        DUP BOARDAT COMPUTER = IF
          ." [O]" SPACE  THEN
        BOARDAT NOTHING = IF
          ." [ ]" SPACE  THEN
        LOOP
   LOOP
   DROP CR ;
This is probably an inefficent word, but it does what I wanted it to! Basically, what I do is push the row and column size on to the stack to be used by the ?DO word which does the looping. I then calculate the cell offset by using the I and J words which return the indices of the inner and outer loops. The DUP (duplicate) word is used because using conditionals pops a value off the stack, but we need that value still, so I make a duplicate of it each time. The word outputs a bit of text according to whether the human has a piece, the computer, or none at all. Now on to my so-called Artificial Intelligence!
: COMPUTERMOVE ( )
   FALSE MOVED !                \ We haven't moved yet.

   \ Always try for centre IF possible.
   4 BOARDAT NOTHING = IF
      COMPUTER 4 SETBOARD
   ELSE
        -1 OPENSPACE !
        TRUE HORIZONTAL !       \ Check horizontal first.
        2 0 ?DO                 \ Repeat once.
        3 0 ?DO CR              \ Loop through cells.
           -1 LASTEMPTY !       \ Reinitialize lastempty.
           0  NUMENEMY  !       \ Ditto.
           3 0 ?DO              \ Loop through cells.
Here is a good break. Right, my AI works like this. If the centre is free, move there. If not, loop through the vertical and horizontal cells (doesn't check diagonals) to see if the player has two pieces in the row/column. If so, play to the unused place. Few lines control the looping. It is a little complicated because I loop through ALL the cells twice, once in a horizontal pass another in a vertical pass. The "2 0 ?DO" loop controls that. LASTEMPTY and NUMENEMY are reinitialized after every row/column pass.
              \ Are we checking horizontally or vertically?
              HORIZONTAL @ TRUE = IF
                 I J 3 * + DUP  \ Calculate horizontally.
              ELSE
                 J I 3 * + DUP  \ Calculate vertically.
              THEN
By simply swapping the I and J variables, I can calculate the cell offset using the same looping variables (since we're on a square board). If you have trouble understand postfix notation, draw a diagram of how the stack would look during the various stages of the calculation. See the Forth section of the Alternate Languages essay to see what I'm talking about.
              BOARDAT HUMAN = IF
                 1 NUMENEMY +! THEN
              DUP BOARDAT NOTHING = IF
                 LASTEMPTY ! ELSE DROP THEN
           LOOP
Ok, since the offset (see last code section - the DUPs) was duplicated when it was created, I still have a copy when I check to see whether the cell under scrutiny is a human piece. If the piece IS human, then increment NUMENEMY. Now, we only have one copy left on the stack, so an additional DUP is necessary, because if the piece is empty we load the value from the stack into LASTEMPTY. Now, continue with our row/column looping.
           \ If it DOESN'T EQUAL -1.
           LASTEMPTY @ -1 = 0= IF
              LASTEMPTY @ OPENSPACE ! THEN
This line takes a little explaining. Basically, since my AI is purely defensive, after every row/column has been scanned, I keep a record of any empty spaces. If no defensive moves are possible, it moves to the last open space that was scanned. If THAT is not possible, the game ends. Now, the "doesn't equal" part is a little tricky.

The @ retrieves the value in LASTEMPTY, which we then compare with -1, which we THEN compare with 0 (or FALSE!). So, think of it as "Is it false that the value stored in LASTEMPTY equals -1?" - circumlocution for you. If it doesn't, the value in LASTEMPTY is stored in OPENSPACE.

           NUMENEMY @ 2 = IF  \ Can we block this?
              LASTEMPTY @ -1 = 0= IF
                 COMPUTER LASTEMPTY @ SETBOARD TRUE MOVED ! THEN
           THEN
           NUMENEMY @ 3 = IF    \ Aw, dude, we lost.
              CR ." You won!"   \ Print message.
              TRUE WON !        \ Win flag.
              -1 OPENSPACE !    \ To print 'Game Over' message.
           THEN
        LOOP
Here is the defensive AI part! If the number of enemies equals 2, and there's an empty space, then move a piece there, and change the MOVED flag to true. We also check here to see if the player won (never check if we won). If it does, it sets various flags to help program control. Continue with our row/column looping...
        FALSE HORIZONTAL !      \ Now, check vertical.
        LOOP
Now that we've finished horizontal scanning, change to vertical, and start again.
        OPENSPACE @ -1 = IF
           CR ." Game over."
           TRUE WON !           \ No one has won, but end of game.
        ELSE
           MOVED @ FALSE = IF
              COMPUTER OPENSPACE @ SETBOARD THEN
        THEN
   THEN ;
If OPENSPACE equals -1, it either means that the board is full, or somebody won. In either case, we don't need to make any move, but should exit the game, so set the WIN flag (stops board being displayed after COMPUTERMOVE finished. If it doesn't equal -1, and we haven't moved, then add a piece there. That the end of the AI!
: TTTMOVE ( n1, n2 -- moves)
   1 - SWAP 1 - 3 * + DUP DUP
Ok, this took me a while to figure out how to code, so probably an explanation is worthwhile! Since the first cell is at the memory address "BOARD+0" I have to subtract 1 from both the row and column before multiplying them. I therefore subtract one from the column (first on stack) then swap then (row now on top of the stack), subtract one from that, THEN multiply by three and add them!
   8 > IF
     CR ." Bad row or column! Please re-enter... " DROP
   ELSE
      BOARDAT @ NOTHING = IF
         HUMAN SWAP SETBOARD
         DRAWBOARD
         COMPUTERMOVE
         WON @ FALSE = IF DRAWBOARD THEN
      ELSE
         CR ." Piece exists there. Please re-enter..."
         DROP
      THEN
   THEN ;
Basic error checking - if the row or column were out of range, then print an error message. Of course, it is possible to enter in an invalid row, but enter in a column so that no error message is displayed. If there isn't a piece at the position entered, set the board (notice we have to SWAP since HUMAN has to come second on the stack). The board is drawn, then the computer moves, then the board is drawn again if no one won.
: RUN ( -- )
   PAGE
   CR ." You are X, the computer is O. "
   CR ." To move, type [ROW] [COL] TTTMOVE "
   DRAWBOARD ;

CR .( Start the game by typing RUN...)
The rest is obvious. Enjoy.
  • Programs - Gen5 programs. ALL source code included.

  • Alternative Languages - SmallTalk, Forth, LISP, Scheme, Prolog!