Skip to content

Example: a tic tac toe game

cosmos-lang edited this page Jun 27, 2023 · 4 revisions

As an example, we'll implement a simple tic-tac-toe game with user input.

How to represent the board

A few choices are,

  1. An array.
  2. A list.
  3. 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.

Drawing the Board

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

//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.

The main functions

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 and throw 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

Adding UI

One may want to expand the program to use a graphical user-interface, such as by using the Space framework.

Exploring logic programming further

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.

Clone this wiki locally