In my journey to learn Clojure I recently decided that I would like to write a chess engine, which is kind of funny because I don't really know chess either. ;-) My goals are to learn Clojure and chess, and write something that is fairly easy to understand.
Currently I'm working on board representation and basic movement. There are many ways to represent a chess board, but I decided to represent the board as a list of game pieces because I would like to see how that choice affects the implementation in a Lisp dialect.
Everything is still early in the development, but I'd like to get some feedback on what I've started.
Here is my board representation.
(ns chess.state)
(defn make-board
"Creates a chess board in the initial configuration."
[]
'({:type :rook :color :black :rank 8 :file 1 }
{:type :knight :color :black :rank 8 :file 2 }
{:type :bishop :color :black :rank 8 :file 3 }
{:type :queen :color :black :rank 8 :file 4 }
{:type :king :color :black :rank 8 :file 5 }
{:type :bishop :color :black :rank 8 :file 6 }
{:type :knight :color :black :rank 8 :file 7 }
{:type :rook :color :black :rank 8 :file 8 }
{:type :pawn :color :black :rank 7 :file 1 }
{:type :pawn :color :black :rank 7 :file 2 }
{:type :pawn :color :black :rank 7 :file 3 }
{:type :pawn :color :black :rank 7 :file 4 }
{:type :pawn :color :black :rank 7 :file 5 }
{:type :pawn :color :black :rank 7 :file 6 }
{:type :pawn :color :black :rank 7 :file 7 }
{:type :pawn :color :black :rank 7 :file 8 }
{:type :pawn :color :white :rank 2 :file 1 }
{:type :pawn :color :white :rank 2 :file 2 }
{:type :pawn :color :white :rank 2 :file 3 }
{:type :pawn :color :white :rank 2 :file 4 }
{:type :pawn :color :white :rank 2 :file 5 }
{:type :pawn :color :white :rank 2 :file 6 }
{:type :pawn :color :white :rank 2 :file 7 }
{:type :pawn :color :white :rank 2 :file 8 }
{:type :rook :color :white :rank 1 :file 1 }
{:type :knight :color :white :rank 1 :file 2 }
{:type :bishop :color :white :rank 1 :file 3 }
{:type :queen :color :white :rank 1 :file 4 }
{:type :king :color :white :rank 1 :file 5 }
{:type :bishop :color :white :rank 1 :file 6 }
{:type :knight :color :white :rank 1 :file 7 }
{:type :rook :color :white :rank 1 :file 8 }))
Here are some common movement functions that I'm using for calculating the movement of each piece.
(ns chess.movement
(:require [chess.state :refer :all]
[clojure.math.numeric-tower :as math]))
(defn on-board?
"Determines if a position is on the board."
[[rank file :as position]]
(and (>= rank 1)
(<= rank 8)
(>= file 1)
(<= file 8)))
(defn same-rank?
"Determines if two positions are in the same rank."
[[start-rank start-file :as position] [dest-rank dest-file :as destination]]
(and (= dest-rank start-rank) (not= dest-file start-file)))
(defn same-file?
"Determines if two positions are in the same file."
[[start-rank start-file :as position] [dest-rank dest-file :as destination]]
(and (= dest-file start-file) (not= dest-rank start-rank)))
(defn diagonal?
"Determines if two positions are diagonal from each other. We need this for things like determining whether a pawn movement is a capture, etc."
[[start-rank start-file :as position] [dest-rank dest-file :as destination]]
(= (math/abs (- start-rank dest-rank))
(math/abs (- start-file dest-file))))
(defn occupied?
"Determines if a location on the board is occupied by a piece. If the color actual
parameter is provided then the piece at that location must be that color. We need to know this
to help determine the ability to move, capture, etc."
([board [rank file :as position]]
(some #(and (= rank (% :rank)) (= file (% :file))) board))
([board [rank file :as position] color]
(some #(and (= rank (% :rank)) (= file (% :file)) (= color (% :color))) board)))
(defn opponent-color
"Returns the opponent's color."
[color]
(cond
(= color :black) :white
(= color :white) :black))
(defn remove-pieces
"Removes pieces from the board at the specified locations."
[board position & remaining-positions]
(let [positions (into #{position} remaining-positions)
matching-position? #(positions [(% :rank) (% :file)])]
(remove matching-position? board)))
(defn positions-between
"Generates a list of the positions between start and end. If the
start and end positions aren't in the same rank, file, or diagonal
from each other then returns an empty list."
[[start-rank start-file :as start] [end-rank end-file :as end]]
(if (or (same-rank? start end)
(same-file? start end)
(diagonal? start end))
(let [next-rank (cond (= start-rank end-rank) identity
(< start-rank end-rank) inc
(> start-rank end-rank) dec)
next-file (cond (= start-file end-file) identity
(< start-file end-file) inc
(> start-file end-file) dec)]
(loop [positions '()
[current-rank current-file :as current-position] [(next-rank start-rank) (next-file start-file)]]
(if (= current-position end)
positions
(recur (conj positions current-position) [(next-rank current-rank) (next-file current-file)]))))
'()))
(defn movement-blocked?
"Move is blocked if the destination is occupied by the player's own piece, or
if any position between the start and destination contains a piece. Note that
this function does not work for Pawns (yet) because a pawn is blocked when
moving forward if the destination is occupied by any piece."
[board [rank file :as position] [dest-rank dest-file :as destination] color]
(or (occupied? board destination color)
(some (into #{} (positions-between position destination))
(map (fn [p] [(p :rank) (p :file)]) board))))
Here is my code for deciding which positions a rook can move to on the board.
(ns chess.rook-movement
(:require [chess.movement :refer :all]
[chess.state :refer :all]))
(defn valid-rook-move?
"Determines if a rook can make a move from one position to another on a board."
[board [start-rank start-file :as position] [dest-rank dest-file :as destination] color]
(and (on-board? destination)
(or (same-rank? position destination)
(same-file? position destination))
(not (movement-blocked? board position destination color))))
(defn rook-destinations
"Returns a lazy sequence of positions (tuples containing rank and file) that a rook can move to."
[board [rank file :as position] color]
(let [possible-dests (concat (map (fn [f] [rank f]) (range 1 9))
(map (fn [r] [r file]) (range 1 9)))]
(filter #(valid-rook-move? board position % color) possible-dests)))
Please comment! I'm looking for ways to make the code more idiomatic, readable, organized better, more efficient, etc.