-
Notifications
You must be signed in to change notification settings - Fork 1
Example: a tic tac toe game
As an example, we'll implement a simple tic-tac-toe game with user input.
A few choices are,
- An array.
- A list.
- An array of arrays, etc.
We choose a single array with get/set relations to modify and access.
Since this is a game played on a 3x3 board, the choice won't heavily impact the performance, and you can change later by changing its get/set relations.
range=math.range
_board={' ',' ',' ',' ',' ',' ',' ',' ',' '}
rel set(o,o2,x,y,e)
i=x+(y*3)
table.set(o,i,e,o2)
rel get(o,x,y,e)
i=x+(y*3)
e=o[i]
rel empty(board,x,y)
get(board,x,y,' ')
As we use a single array, a calculation is made to convert coordinates like x=0, y=1 to the element numbered 3 in the array.
If you get the string ' '
, naturally it means the board is empty.
write=io.write
rel draw(board)
init i=0
while(i<3)
init j=0
write('|')
while(j<3)
tile=get(board,j,i)
write(tile)
write('|')
next j=j+1
write('\n')
next i=i+1
The board is drawn in a typical procedural fashion using while-statements, so that it draws tiles and columns from 0 to 2. Tiles might be either ' ', 'X' or 'O'.
The idiom write=io.write
gets the relation write
directly from module io
.
rel full(board)
for(x in board)
x!=' '
rel win(board,piece)
case
some(col in range(0,2))
get(board,0,col,piece)
get(board,1,col,piece)
get(board,2,col,piece)
case
some(line in range(0,2))
get(board,line,0,piece)
get(board,line,1,piece)
get(board,line,2,piece)
It's easy to see if the board is full (aka. every square has been filled by a piece) by checking that it has no empty tile.
We may also see if a board is won by checking that a full line or column has been filled. We are back to declarative programming and we may read the whole as,
It's a win if either,
There's _some_ column such that every tile in it has a piece,
There's _some_ line such that every tile in it has a piece,
The same is true for full
,
It's a full board when,
Every tile is not empty.
This leaves checking diagonals.
case //check the 0-2, 1-1, 2-2 diagonal
for(pos in range(0,2))
get(board,pos,pos,piece)
case //check the remaining
get(board,2,0,piece)
get(board,1,1,piece)
get(board,0,2,piece)
piece!=' '
We may end with the declaration that piece!=' '
. That is, we'll not declare a win for a player if the piece found is the empty tile.
However, in this example we call win with an argument for piece, so that in any case it won't spot the empty tile.
//ai
//_(board,x,y)
rel leftmost(x)
x=0 or x=1 or x=2
rel _default(board,x,y)
leftmost(x) and leftmost(y)
empty(board,x,y)
function _player(board,x,y)
print('> input line:')
y=math.stringToNumber(io.read())-1
print('> input column:')
x=math.stringToNumber(io.read())-1
if(not empty(board,x,y))//print([x,y])
throw('invalid move')
Here we have the chance to explore AI programming, although on a basic level.
the AI interface
We decide that an AI function or predicate may look at a board
and decides on a x,y
position which is its move. That's the AI's interface. Any function or predicate with board and position as parameters may thus be an AI.
the simplest AI
Our default AI is maybe the simplest. It's actually simply a declarative logic-style definition of what a move is and it works because this is a declarative logic language.
x is either 0, 1 or 2.
y is either 0, 1 or 2.
The position x,y is empty.
It states that x/y are either 0, 1 or 2 and that it's possible to move there. It'll therefore pick the first available move.
This is of course not the greatest AI, and the programmer is invited to improve on it.
a random AI
An AI that picks an available move randomly. A simple random number generator is provided by math.random
.
an optimal AI
An optimal AI could be done by looking at all possible choices one by one and picking an optimal move, i.e. one that wins the game, or draws if that's not possible. It's possible to do this in a tic-tac-toe game, but it's too resource-intensive for a more complex on. It's not necessarily good to make such an AI as it'll give the player no chance to win.
the player
The player is asked what move to make. The player inputs a move in typical 1-indexing, and this is converted to 0-indexing for the program.
rel move(board, p, board2)
p.ai(board, x, y)
set(board,board2,x,y,p.piece)
function game(board,p1,p2,turn)
if(win(board,'X'))
io.writeln('X won!')
elseif(win(board,'O'))
io.writeln('O won!')
elseif(full(board))
io.writeln('Draw!')
else
io.writeln('')
io.writeln(str('turn '+turn))
move(board,p1,board2)
draw(board2)
game(board2,p2,p1,turn+1)
From now on, we proceed with the main function of the game.
It's important to declare a function
whenever,
- Procedural code is used.
io
andthrow
is procedural code. - It's the main function of a procedural program, and thus doesn't need relational behaviour (i.e. the game function).
In addition, we avoid any complications that may arise from the use of relational negation or relational if-else. Although, if we're using conditionals procedurally as we're doing, it's already not relational code.
On the other hand, we managed to contain the procedural parts to a few functions: game which is essentially our main function and the player. This is the classical approach to making a declarative program, as even if you're making a declarative program, a lot of code you have to use might be procedural. That is, you make most of the program code declarative but call it through a procedural main function that uses I/O, etc. so as to interface the program with the user.
p1={
piece='X'
ai=_default
}
p2={
piece='O'
ai=_player//_default
}
board=_board
game(board,p1,p2,1)
Finally, we define the players.
You may see the full code at the URL: https://pastebin.com/n1ByALVd
One may want to expand the program to use a graphical user-interface, such as by using the Space framework.
If we want to explore logic programming, one may set both AIs to _default.
rel pause()
print('> ')
c::ioread(x)
...
p2={
piece='O'
ai=_default
}
...
game(board,p1,p2,1)
pause()
false //the program will fail and search other possible moves
Simply adding false
to the end will make the program fail, thus search for other ways the game could've gone. The AIs should extensively go through every possible game of tic-tac-toe.
In the URL, we've made even this impossible and the program will simply fail, as we've added the line.
once move(board,p1,board2)
move
is correctly set to run once regardless of the non-deterministic AI in order to better work as a procedural function.