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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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