I whipped this up last night and was wondering if anyone had any thoughts/comments on it; for example, on:
- Code organisation
- Coding style
- Other improvements
- Semantic errors
(ns tictactoe.core)
(comment "
Author: Liam Goodacre
Date: 03/06/12
This module allows the creation and progression of tictactoe games.
To create a game, use the make-game function:
(make-game)
A game is a map with the following data:
:play-state :playing | :player1 | :player2 | :draw
:level-state [:player1|:player2|nil]
:scores {:player1 Int, :player2 Int, :draw Int}
:turn :player1 | :player2 | :none
If the game isn't over, play-state is :playing.
If play-state is :player1 or :player2, this indicates that that player won.
If play-state is :draw, then the game completed with no winner.
level-state is a vector or nine elements.
An empty cell has the value of nil.
If a player uses a cell, then the cell has that player's id.
E.g., if a player1 uses an empty cell, that cell now has the value :player1
scores is a map from ids to integers.
It includes score values for draws and for the two players.
When the game is over, turn is :none.
Otherwise, it describes which player's turn it is.
When a new game is created, turn is randomised
between the two players.
To play a turn, use the play-turn command.
For example, assuming 'g' is a predefined game:
(play-turn g 2)
Will produce a new game state with play progressed by one move,
in which the current player used cell 2.
Calling new-game clears the level data of a game and
randomises the initial player turn. Scores are the
only preserved datum.
The following are game analysis functions:
get-play-state - what state of play are we in?
get-turn - whos turn is it?
get-scores - get the scores
get-level-state - get the state of the level
playing? - are we still playing?
game-over? - is the game over?
valid-move? - is this a valid move?
")
(declare ;; Public Functions
; Game Transforming
make-game
new-game
play-turn
; Game Analysing
get-play-state
get-turn
get-scores
get-level-state
playing?
game-over?
valid-move?
; Extra
get-game-grid)
(declare ;; Private Functions
^:private
; Transforming
apply-move
next-turn
recalculate-state
; Analysis
calculate-winner
try-combination)
;; Utility functions
(defmacro ^:private def-
" A private version of def. "
[& info] `(def ^:private ~@info))
(defn- within-range
" Determines if a value lies within an inclusive range. "
[n x] (fn [v] (and (>= v n) (<= v x))))
;; Initial data for making new games
(def- players [:player1, :player2])
(def- initial-play-state :playing)
(def- initial-level-state (vec (repeat 9 nil)))
(def- initial-scores {:player1 0, :player2 0, :draw 0})
(defn- random-turn [] (players (int (rand 2))))
(def- winning-combinations [[1 2 3][4 5 6][7 8 9][1 5 9][3 5 7][1 4 7][2 5 8][3 6 9]])
;; Public Transforming Functions
(defn make-game []
" Creates a new game object. "
{ :play-state initial-play-state
:level-state initial-level-state
:scores initial-scores
:turn (random-turn) })
(defn new-game [g]
" Sets up a game object for the next play. "
(assoc (make-game) :scores (get-scores g)))
(defn play-turn [game move]
" Progresses game-play by one move if possible. "
(if (and (playing? game) (valid-move? game move))
(-> game
(apply-move move)
(recalculate-state)
(next-turn))
game))
;; Private Transforming Functions
(defn- apply-move [game move]
" Progresses game-play by one move. "
(assoc game
:level-state
(assoc (get-level-state game)
move
(get-turn game))))
(defn- next-turn [game]
" Updates which player should play next.
If no player is currently active, then
do not switch. "
(assoc game
:turn
(case (get-turn game)
:none :none
:player1 :player2
:player2 :player1)))
(defn- recalculate-state [game]
" Calculate if there is a winner. If so update
the play-state, turn, and scores; to reflect
the result. "
(let [winner (calculate-winner game)
scores (get-scores game)]
(if (= winner :none)
game
(-> game
(assoc :play-state winner, :turn :none)
(update-in [:scores winner] inc)))))
;; Private Analysis Functions
(defn- calculate-winner [game]
" Calculates if there is a winner. "
(let [state (get-level-state game)
matching (partial try-combination state)]
(if (some matching winning-combinations)
(get-turn game)
(if (some nil? state)
:none
:draw))))
(defn- try-combination [state [one two three]]
" Calculates if a winning combination has occurred. "
(let [lookup (comp (partial nth state) dec)
player (lookup one)]
(and
(not= player nil)
(= player (lookup two) (lookup three)))))
;; Public Analysis Functions
(def get-play-state :play-state)
(def get-turn :turn)
(def get-scores :scores)
(def get-level-state :level-state)
(defn playing? [game] (= :playing (get-play-state game)))
(def game-over? (comp not playing?))
(defn valid-move? [game move]
(and ((within-range 0 8) move)
(nil? (nth (get-level-state game) move))))
;; Public Extra Functions
(defn get-game-grid [g]
" Builds a simple string representation
of the current level state. "
(clojure.string/join "\n" (map vec (partition 3 (get-level-state g)))))