# Tic-Tac-Toe game with AI

I started learning Racket recently and decide to revisit the problem that introduced me to computer programming: making a Tic-Tac-Toe game with AI.

Here's the code:

#lang racket
(require math/array)

;; PLAYER DEFINITION
(define empty 'EMPTY)
(define draw 'DRAW)
(define player1 'PLAYER_ONE)
(define player2 'PLAYER_TWO)

(define (oponent player)
(cond [(eq? player player1) player2]
[(eq? player player2) player1]
[else empty]))

;; BOARD DEFINITION

;; Position looks like '(x . y)
(define make-position cons)
(define get-x car)
(define get-y cdr)

(define board-indexes '(0 1 2))

(define (make-board)
(make-vector 9 empty))

(define (board-position pos)
;; Used internally to convert 2D position to 1D
(+ (get-y pos) (* (get-x pos) 3)))

(define (board-ref board pos)
(vector-ref board (board-position pos)))

(define (board-set! board pos value)
(vector-set! board (board-position pos) value))

;; Game looks like '(board . player)
(define get-player cdr)
(define get-board car)
(define make-game cons)

;; RULES

;; A line is a list with 3 positions
(define (make-line n f)
(foldl (lambda (k l)
(cons (f n k) l))
(list)
board-indexes))

(define (make-column n) (make-line n make-position))
(define (make-row n) (make-line n (lambda (a b) (make-position b a))))

(define win-lines
;; List with the lines that are relevant for determining wins
(foldl (lambda (n l)
(cons (make-row n) (cons (make-column n) l)))
(list
(make-line #f (lambda (_ n) ;; Diagonal one
(make-position n n)))
(make-line #f (lambda (_ n) ;; Diagonal two
(make-position n (- 2 n)))))
board-indexes))

(define (check-line board line)
;; Checks if any player won in a given line
(apply
(lambda (a b c)
(if (and (eq? a b) (eq? b c) (not (eq? a empty)))
a
#f))
(map(lambda (pos)
(board-ref board pos))
line)))

(define (who-won board)
;; Checks if one of the players already won
(foldl (lambda (line old)
(or
old
(check-line board line)))
#f
win-lines))

(define (is-over board)
(not (vector-member empty board)))

(define (score board)
;; Returns the player who won, false if the game isn't over
(or (who-won board) (if (is-over board) draw #f)))

;; PLAYER INTERFACE
(define all-cells
;; List with all '(x y) cells
(foldl (lambda (i l)
(append l (map (lambda (j) (cons i j)) board-indexes)))
(list)
board-indexes))

(define (try-play game pos fn)
;; Temporarily applies a play on the 'pos' cell and runs 'fn' on the resulting game
(let ([board (get-board game)]
[player (get-player game)])
(board-set! board pos player)
(define return (fn (make-game board (oponent player))))
(board-set! board pos empty)
return))

(define (fold-plays game selector)
;; Handy method to choose from all possible play options
(foldl (lambda (pos current)
(let ([board (get-board game)]
[player (get-player game)])
(if (eq? empty (board-ref board pos))
(try-play game pos (lambda (game)
(selector game current pos)))
current)))
#f
all-cells))

(define (play game pos)
;; Returns the game state updated after playing on position pos
(let ([new-board (vector-copy (get-board game))]
[player (get-player game)])
(cond [(not (eq? (board-ref new-board pos) empty)) (error 'BAD_PLAY)]
[else
(board-set! new-board pos player)
(make-game new-board (oponent player))])))

(define (ai-play game)
;; Artificial inteligente returns the ideal position to play on
(define (predict game)
(or (score (get-board game)) (try-play game (ai-play game) predict)))
(let ([player (get-player game)])
;; Keep track of best '(score . position), but only return the position
(cdr (fold-plays game (lambda (game best pos)
(cond [(not best) (cons (predict game) pos)]
[(eq? (car best) player) best]
[(eq? (predict game) player) (cons (predict game) pos)]
[(eq? (car best) draw) best]
[else (cons (predict game) pos)]))))))

(define (show game)
;; Displays current state of the game
(for ([y board-indexes])
(for ([x board-indexes])
(print (board-ref (get-board game) (make-position x y))))
(println '()))
(print "PLAYNG: ")
(print (get-player game))
(println '_)
(println '_))

;; Simple example of how to play, needs a better interface
(define board (make-board))
(define game (make-game board player1))
(set! game (play game '(0 . 0)))
(set! game (play game (ai-play game)))
(show game)


I would appreciate some feedback on the following topics mostly:

• Clarity: Is the code simple and easy to understand/extend/modify?
• Performance: Are there any simple changes to make it faster, without sacrificing the previous point by much?
• General Tips: Is there anything that I'm doing which a Scheme programmer usually wouldn't?

Some topics I don't care much about:

• Interface: I know it's bad at the moment, I'll improve it later ^ . ^

Modularity

All the definitions are in the same lexical scope. For example all-cells is in the same lexical scope as fold-plays even though the only place all-cells is referenced is within fold-plays. In addition try-play is defined between them. Organizing the code:

(define (fold-plays game selector)
(define all-cells...))


Would improve the structure and probably improve readability.

Naming

Names such as fold-plays mix game level logic of plays with an implementation detail of folding. This reflects a general intermingling of abstraction layers and that's probably related to the limited modularity in the code.

Other

• first and rest are more typical in Racket than car and cdr.
• A function that takes a board and a move and produces a new board would be more typical in Racket than mutating the board with Vector-set!. math/array` does not appear to be used.