From 1deb6813e34884edea923451c56f1b01176a073e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Bernardo=20Galkin?= Date: Tue, 3 May 2011 21:06:08 -0300 Subject: [PATCH] refactoring, documentation --- src/tictactoe/ai.clj | 54 +++++++++ src/tictactoe/core.clj | 206 ++++++++++++++++++++++++++--------- src/tictactoe/util.clj | 66 ----------- test/tictactoe/test/core.clj | 27 +++-- 4 files changed, 222 insertions(+), 131 deletions(-) create mode 100644 src/tictactoe/ai.clj delete mode 100644 src/tictactoe/util.clj diff --git a/src/tictactoe/ai.clj b/src/tictactoe/ai.clj new file mode 100644 index 0000000..729207b --- /dev/null +++ b/src/tictactoe/ai.clj @@ -0,0 +1,54 @@ +(ns tictactoe.ai + "Implement AI to play a game. Generate a tree of candidate moves and decide + which one is the best for the computer to play") + +; --------------- +; Tree Generation +; --------------- + +(defn game-tree + "Generate a tree of posible next boards starting with a given board position. + Each node in the tree will have a board and a set of children representing + possible positions for the next move. The tree is genarated lazily. + make-move is a function that given a board returns all posible next boards" + [board make-move] + {:node board + :children (map #(game-tree % make-move) + (make-move board))}) + +; --------------- +; Tree Evaluation +; --------------- + +(declare minimize) + +(defn maximize [evaluator tree] + (if (seq (:children tree)) + (apply max + (map #(minimize evaluator %) + (:children tree))) + (evaluator (:node tree)))) + +(defn minimize [evaluator tree] + (if (seq (:children tree)) + (apply min + (map #(maximize evaluator %) + (:children tree))) + (evaluator (:node tree)))) + +(defn evaluator + "Dynamic evaluation of a game tree. Returns a number representing how good + the root position is. Uses minimax algorithm" + [static-evaluator] + (fn [tree] + (minimize static-evaluator tree))) + +(defn best-move + "Get the best computer move for the given game tree. + static-evaluator evaluates single positions, without looking at the tree, and + returning a number" + [tree static-evaluator] + (:node (apply max-key + (evaluator static-evaluator) + (:children tree)))) + diff --git a/src/tictactoe/core.clj b/src/tictactoe/core.clj index 7037fbd..e960172 100644 --- a/src/tictactoe/core.clj +++ b/src/tictactoe/core.clj @@ -1,17 +1,33 @@ -(ns tictactoe.core) - -(defn make-board [x-cells o-cells] +(ns tictactoe.core + (:require [tictactoe.ai :as ai])) + +; ----------------- +; Board abstraction +; ----------------- +(defn make-board + "Main game board abstraction. Creates a board given x marked cells + and o marked cells. Cells will be identified by integers starting + with 0 at top lef and ending with 8 at bottom right. + The board is immutable" + [x-cells o-cells] {:x (set x-cells) :o (set o-cells)}) -(def x-cells :x) -(def o-cells :o) +(def ^{:doc "Get board cells marked with x"} x-cells :x) +(def ^{:doc "Get board cells marked with o"} o-cells :o) -(def all-cells (apply sorted-set (range 9))) +(def ^{:doc "Sorted set of all cell identifiers, ordered from top left to bottom right"} + all-cells + (apply sorted-set (range 9))) -(defn empty-cells [board] +(defn empty-cells + "Given a board, return a set of all empty cells" + [board] (clojure.set/difference all-cells (x-cells board) (o-cells board))) -(defn mark [board cell] +(defn mark + "Mark a cell in the board. Cell is marked according to the corresponding player turn. + Cell should be an integer 0 <= cell < 9. Returns a new board with the given cell marked" + [board cell] (assert (contains? (empty-cells board) cell)) (let [turn (fn [board] (if (> (count (x-cells board)) @@ -24,62 +40,150 @@ t (conj (t board) cell)))) -(def win-cells - (let [row1 #{0 1 2} - row2 #{3 4 5} - row3 #{6 7 8} - col1 #{0 3 6} - col2 #{1 4 7} - col3 #{2 5 8} - dia1 #{0 4 8} - dia2 #{2 4 6}] +; ---------- +; Find winner +; ---------- + +(def ^{:doc "All sets of winning cells"} + win-cells + (let [row1 #{0 1 2} row2 #{3 4 5} row3 #{6 7 8} + col1 #{0 3 6} col2 #{1 4 7} col3 #{2 5 8} + dia1 #{0 4 8} dia2 #{2 4 6}] [row1 row2 row3 col1 col2 col3 dia1 dia2])) -(defn won? [cells] +(defn won? + "Return not nil if the sequence of marked cells represent a winner board" + [cells] (some #(every? cells %) win-cells)) -(defn winner [board] +(defn winner + "Return a keyword (:x or :o) representing the winner or nil if nobody wins in the board" + [board] (cond (won? (:x board)) :x (won? (:o board)) :o)) -(defn plays [board] - (if (winner board) - [] - (map (partial mark board) (empty-cells board)))) - -(defn prune [n {:keys [node children]}] - (if (= n 0) - {:node node :children []} - {:node node :children (map (partial prune (dec n)) children)})) - -(defn game-tree [board generator] - {:node board - :children (map #(game-tree % generator) (generator board))}) +; --------- +; Tic-tac-toe +; --------- -(defn evaluate-static-position [position] - (case (winner position) +(defn evaluate-static-position + "Trivial static evaluation of a board. Not really evaluating anything, it just + detects winners" + [board] + (case (winner board) :x 1 :o -1 0)) -(declare minimize) - -(defn maximize [tree] - (if (seq (:children tree)) - (apply max (map minimize (:children tree))) - (evaluate-static-position (:node tree)))) - -(defn minimize [tree] - (if (seq (:children tree)) - (apply min (map maximize (:children tree))) - (evaluate-static-position (:node tree)))) +(defn plays + "Return a lazy seq of all posible boards obtained by making one move from the given + board. The move will be done by the player with the current turn" + [board] + (if (winner board) + [] + (map (partial mark board) (empty-cells board)))) -(defn evaluate [tree] - (minimize tree)) +; -------------- +; Board printing +; -------------- -(defn best-play [position] - (let [tree (game-tree position plays) - children (:children tree)] - (:node (apply max-key evaluate children)))) +(defn cell-string + "Print a given cell of the board, using the right player symbol" + [board cell] + (cond + (contains? (:x board) cell) " x " + (contains? (:o board) cell) " o " + :else " ")) + +(defn print-board + "Print the given board to stdout" + [board] + (let [cells (map #(cell-string board %) all-cells) + rows (partition 3 cells) + str-rows (map #(apply str (interpose "|" %)) rows) + row-sep "-----------"] + (doseq [row (interpose row-sep str-rows)] + (println row)))) + +; ------- +; Helpers +; ------- + +(defmacro board + "Create a board by drawing it. Pass to the macro 9 arguments, each one being + one of the symbols x, o - + + (board - x - + o - x + o - -) + + It doesn't do any checks" + [& cells] + (let [marks (map vector cells all-cells) + x (map second (filter #(= 'x (first %)) marks)) + o (map second (filter #(= 'o (first %)) marks))] + `(make-board (vector ~@x) (vector ~@o)))) + +(def symbol->cell + {'n 1 's 7 'e 5 + 'w 3 'c 4 'o 4 + 'ne 2 'nw 0 'se 8 'sw 6}) + +(defmacro mark# + "Mark a cell in a board with the current player turn. + sym could be any of + n s e w c o ne nw se sw + + c and o represent the center cell, the rest are the corresponding cardinal positions" + [board sym] + `(mark ~board ~(symbol->cell sym))) + +(def initial-board + (board - - - + - - - + - - -)) + +(defn best-tictactoe-move + "Find the best computer move for the given board position" + [board] + (ai/best-move (ai/game-tree board plays) evaluate-static-position)) + +; ------- +; Game UI +; ------- + +(defn play + "Find best computer move and print it. Return new board" + [board] + (doto (best-tictactoe-move board) + print-board)) + +(defn player-move + "Ask the player to move and return the new board" + [board] + (prn) + (println "Your move: ") + (mark board (symbol->cell (read)))) + +(defn ended? + "True if there are no more empty cells in the board" + [board] + (empty? (empty-cells board))) + +(defn driver + "Drive the UI asking the player and making the computer make moves" + [board] + (let [my-play (play board)] + (cond + (winner my-play) (println "I win") + (ended? my-play) (println "Draw") + :else (let [your-play (player-move my-play)] + (cond + (winner your-play) (println "You win") + (ended? your-play) (println "Draw") + :else (driver your-play)))))) + +(defn -main [] + (driver initial-board)) diff --git a/src/tictactoe/util.clj b/src/tictactoe/util.clj deleted file mode 100644 index 7e56341..0000000 --- a/src/tictactoe/util.clj +++ /dev/null @@ -1,66 +0,0 @@ -(ns tictactoe.util - (:gen-class) - (:use tictactoe.core)) - -(defn cell-char [board cell] - (cond - (contains? (:x board) cell) " x " - (contains? (:o board) cell) " o " - :else " ")) - -(defn print-board [board] - (let [cells (map #(cell-char board %) all-cells) - rows (partition 3 cells) - str-rows (map #(apply str (interpose "|" %)) rows) - row-sep "-----------"] - (doseq [row (interpose row-sep str-rows)] - (println row)))) - -(defn play [position] - (doto (best-play position) - print-board)) - -(def symbol->cell - {'n 1 's 7 'e 5 - 'w 3 'c 4 'o 4 - 'ne 2 'nw 0 'se 8 'sw 6}) - -(defn ask-player [position] - (prn) - (println "Su jugada: ") - (mark position (symbol->cell (read)))) - -(defn ended? [position] - (empty? (empty-cells position))) - -(defn driver [position] - (let [my-play (play position)] - (cond - (winner my-play) (println "Yo gano") - (ended? my-play) (println "Empate") - :else (let [your-play (ask-player my-play)] - (cond - (winner your-play) (println "Usted gana") - (ended? your-play) (println "Empate") - :else (driver your-play)))))) - -(defmacro board [& cells] - (let [marks (map vector cells all-cells) - x (map second (filter #(= 'x (first %)) marks)) - o (map second (filter #(= 'o (first %)) marks))] - `(make-board (vector ~@x) (vector ~@o)))) - -(defmacro mark# [board sym] - `(mark ~board ~(symbol->cell sym))) - -(def initial-board - (board - - - - - - - - - - -)) - -(defn count-tree [t] - (apply + 1 (map count-tree (:children t)))) - -(defn -main [] - (driver initial-board)) - diff --git a/test/tictactoe/test/core.clj b/test/tictactoe/test/core.clj index 0fe7eb3..cbac986 100644 --- a/test/tictactoe/test/core.clj +++ b/test/tictactoe/test/core.clj @@ -1,6 +1,5 @@ (ns tictactoe.test.core - (:use [tictactoe.core] - [tictactoe.util]) + (:use tictactoe.core tictactoe.ai) (:use [clojure.test])) (deftest mark-test @@ -50,30 +49,30 @@ o x o))))) (deftest play-test - (is (= (best-play (board - x x - - o - - - - o)) + (is (= (best-tictactoe-move (board - x x + - o - + - - o)) (board x x x - o - - - o))) - (is (= (best-play (board - - o - - x o - x - -)) + (is (= (best-tictactoe-move (board - - o + - x o + x - -)) (board - - o - x o x - x))) - (is (= (best-play (board - - - - - x o - - - -)) + (is (= (best-tictactoe-move (board - - - + - x o + - - -)) (board - - - - x o - - x))) - (is (= (best-play (board - - o - - x o - x - -)) + (is (= (best-tictactoe-move (board - - o + - x o + x - -)) (board - - o - x o x - x))))