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