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