3
\$\begingroup\$

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 ^ . ^
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

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.
\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.