Permalink
Browse files

Starting chess-tools project.

This initial code creates some simple board structures and has the early
foundations for creating lookup tables for rapid move enumeration.
Nothing specific to any particular variant of chess yet.
  • Loading branch information...
0 parents commit 1ef0d972bb0c7ae2f11f08b310a95c64aa943f4b @malcolmt committed Oct 30, 2011
Showing with 450 additions and 0 deletions.
  1. +2 −0 .gitignore
  2. +36 −0 TODO.otl
  3. +130 −0 src/ChessTools/Board.hs
  4. +97 −0 src/ChessTools/Board/Internal.hs
  5. +36 −0 test/Suite.hs
  6. +149 −0 test/TestBoard.hs
@@ -0,0 +1,2 @@
+html
+
@@ -0,0 +1,36 @@
+[_] Boards
+ [_] piece lookup tables
+ [_] generic
+ [_] regular chess
+ [_] king
+ [_] queen
+ [_] rook
+ [_] bishop
+ [_] knight
+ [_] pawn
+ [_] Shogi
+ [_] Xiangqi
+ : May be tricky because of the "knight" move and blocking.
+ [X] tests
+[_] Games
+ [_] data structure for holding games and nested comments
+ [_] move legality checking
+ [_] move iterator
+ [_] showing a board position
+ [_] getting options from a particular position (variation choices)
+[_] PGN
+ [_] read string & convert to game
+ [_] take game object and convert to PGN
+ [_] arbitrary starting position (FEN parsing)
+ [_] Chess 960 games
+[_] Packaging
+ [_] Setup,hs
+ [_] chess-tools.cabal
+[_] Maybe one day...
+ [_] UCI interface for western chess
+ [_] USI interface for shogi
+ [_] Graphical board display
+ [_] Western
+ [_] Shogi
+ [_] Xiangqi
+
@@ -0,0 +1,130 @@
+{- | General routines and data structures to support generating moves in a
+ chess-like game.
+
+ Unless you are implementing support for a new style of game, you probably
+ won't use this module directly. More specific and useful functions are in
+ modules such as 'ChessTools.Board.Western' (for western chess, for example).
+ -}
+
+-- XXX: Document that there are two kinds of arrays in play here: the board
+-- array, representing a position, and the lookup tables. They are of different
+-- sizes. Make sure they are different types as well.
+
+module ChessTools.Board (
+ -- * Board representations
+ -- $board_arrays
+ BoardSize(..)
+ , boardArraySize
+ , Square(..)
+ , squareToIndex
+ , indexToSquare
+
+ -- * Creating lookup tables
+ -- $lookup_creation
+ , LookupTable
+ , CoveringIndexList(..)
+ , repIndexList
+
+ -- * Existing lookup creation functions
+ -- $tables
+ , fileTable
+ , rankTable
+ , squareTable
+
+ -- * Using lookup tables
+ , fetch
+
+) where
+
+
+import Data.Array ((!))
+import Data.List (groupBy, sort)
+
+import ChessTools.Board.Internal
+
+-- $board_arrays
+-- There are two types of array-like structures in this module. The
+-- 'BoardArray' is a representation of a specific position in a single game. It
+-- represents the state of play at a particular moment. Any references to
+-- /"board"/ in this documentatin is talking about board arrays.
+--
+-- The other type of useful data structure are lookup arrays. They are designed
+-- to assist rapid computation of various quantities concerning the
+-- relationship between two squares. Functions for working with lookup arrays
+-- are documented further down.
+
+-- | Computes the size of the index array for a board of the given dimensions.
+boardArraySize :: BoardSize -> Int
+boardArraySize s@(BoardSize _ v vBuf) = rowLength s * (v + 2 * vBuf)
+
+-- $lookup_creation
+-- Creating lookup tables for the various piece types in a game is a once-off
+-- effort that is done before any play starts. The typical process involves
+-- creating a valid 'BoardSize' and 'CoveringIndexList' and repeatedly using
+-- those to create the necessary 'LookupTable' items.
+--
+-- For example, in a program that needs to create moves for a western chess
+-- game, the setup process would look like
+--
+-- @
+-- boardSize = BoardSize 8 8 2
+-- coveringList = repIndexList boardSize
+--
+-- slidingTable = makeSlidingTable boardSize coveringList
+-- where makeSlidingTable bs cl = ...
+-- knightTable = makeKnightTable boardSize coveringList
+-- where makeKnightTable bs cl = ...
+-- @
+--
+-- This slightly verbose creation process is necessary only because creating
+-- the 'CoveringIndexList' each time is time consuming. Since it remains
+-- constant for a given 'BoardSize' and the board size doesn't change for a
+-- particular sort of game, they can be computed once and reused.
+
+-- | Return the 'LookupTable' value for a pair of indexes. The first index is
+-- the \"/from/\" location, the second is the \"/to/\" location.
+fetch :: LookupTable -> Int -> Int -> Int
+fetch (LookupTable off arr) s1 s2 = arr ! (off + s1 - s2)
+
+-- | Returns a list of representative 'Square' pairs that cover all the lookup
+-- table index values. On a square board, there are @(2x-1)^2@ possible index
+-- values and @x^4@ different 'Square' pairs, so the representative set is much
+-- smaller than iterating over all possible pair combinations when creating
+-- lookup tables.
+--
+-- This function is very expensive on larger board sizes: we compute all
+-- candidates before filtering out duplicates in the current implementation.
+-- However, it is typically only executed once and then the results reused, so
+-- the cost is a negligible contribution to the total runtime in practice.
+repIndexList :: BoardSize -> CoveringIndexList
+repIndexList s@(BoardSize h v _) = CL $ map head $ groupBy compFirst $ sort l
+ where l = [(d1 - d2, (s1, s2)) | (d1, s1) <- squares, (d2, s2) <- squares]
+ compFirst x y = fst x == fst y
+ squares = zip (map (squareToIndex s) sqs) sqs
+ sqs = [Square (x, y) | x <- [0 .. h - 1], y <- [0 .. v - 1]]
+
+
+-- $tables
+-- Some useful lookup arrays that are commonly required across most chess
+-- variants (more specific tables are created in the particular variant
+-- files). Each of these take a 'CoveringIndexList' parameter so that it only
+-- has to be generated once (see documentation of 'repIndexList' for the
+-- algorithmic complexity description).
+
+-- | File separation between two squares.
+fileTable :: BoardSize -> CoveringIndexList -> LookupTable
+fileTable b cl = distanceTableWith f b cl
+ where f (Square s1) (Square s2) = abs (fst s1 - fst s2)
+
+-- | Rank separation between two squares.
+rankTable :: BoardSize -> CoveringIndexList -> LookupTable
+rankTable b cl = distanceTableWith f b cl
+ where f (Square s1) (Square s2) = abs (snd s1 - snd s2)
+
+-- | Square diagonal distance between two squares (this is using the chessboard
+-- metric, not the Euclidean one).
+squareTable :: BoardSize -> CoveringIndexList -> LookupTable
+squareTable b cl = distanceTableWith f b cl
+ where f (Square s1) (Square s2) =
+ max (abs (fst s1 - fst s2)) (abs (snd s1 - snd s2))
+
@@ -0,0 +1,97 @@
+{- | Semi-private internal implementation features used by 'ChessTools.Board'.
+
+ Most things in here are only useful in the public module. However, in case
+ of unanticipated needs, it is possible to use these directly.
+ -}
+module ChessTools.Board.Internal
+where
+
+import Data.Array
+
+-- | A structure encapsulting the useful parts of a board size. This won't
+-- change for a particular sort of game (such as western chess or Shogi or
+-- Xianqi).
+--
+-- The row and column length values are obvious. The vertical buffer size is
+-- used internally for creating various data structures and depends on the
+-- pieces being used in the game (hence must be provided by the library user).
+-- It is the maximum distance any jumping piece could move off the top or
+-- bottom of the board. In western-style chess, this will be 2, since a knight
+-- can jump that far from either the 1st or 8th ranks.
+data BoardSize = BoardSize {
+ boardHorizSize :: Int, -- ^ Row length.
+ boardVertSize :: Int, -- ^ Column length.
+ boardVertBuffer :: Int -- ^ Vertical buffer size for
+ -- jumping pieces.
+ }
+
+-- | The coordinates of a cell on the board.
+--
+-- 'Square' coordinates are 0-based, with the column preceeding the rank So the
+-- first row of a western chess board is (0, 0) (the /a1/ square) to (0, 7),
+-- with the /h8/ square having coordinates (7, 7).
+--
+-- Note that the order of coordinates in a `Square` is independent of any
+-- traditional external representation of a game's cells. Thus, whilst Shogi
+-- games are recorded with the rank before the column (for example, \"/7f/\"),
+-- this module and its callers still use column-first ordering.
+newtype Square = Square (Int, Int) deriving (Show, Eq, Ord)
+
+-- | A rapid lookup (/O(1)/) data structure for computing various values based
+-- on two squares on the board. These could be distances between the squares in
+-- some form (file or rank separation) or whether some kind of piece can move
+-- between those two squares (using 0 values for invalid moves).
+data LookupTable = LookupTable Int (Array Int Int)
+
+-- | Used to hold a representative set of squares when computing 'LookupTable'
+-- results. Create one with 'repIndexList' and use it in all lookup table
+-- creation functions.
+newtype CoveringIndexList = CL [(Int, (Square, Square))]
+
+-- | Convert a 'Square' to an index into a board array. The index is the same
+-- for all board arrays associated with a given 'BoardSize'.
+squareToIndex :: BoardSize -> Square -> Int
+squareToIndex s (Square (x, y))
+ | x < 0 || y < 0 || x >= h || y >= v = 0
+ | otherwise = (y + vBuf) * rowLength s + leftBuf s + x
+ where BoardSize h v vBuf = s
+
+-- | Convert a board array index to a 'Square'. This is the inverse of
+-- 'squareToIndex'.
+indexToSquare :: BoardSize -> Int -> Square
+indexToSquare s idx = Square (x, y)
+ where rl = rowLength s
+ idx' = idx - rl * boardVertBuffer s
+ (y, x') = idx' `divMod` rl
+ x = x' - leftBuf s
+
+-- | The length of a single (virtual) row in the board array. This is wider
+-- than the board row length due to the buffer space at each end of the row.
+rowLength :: BoardSize -> Int
+rowLength s = 2 * boardHorizSize s - 1
+
+-- | The amount of buffer space at the start of each virtual row in the board
+-- array before the board data proper begins.
+leftBuf :: BoardSize -> Int
+leftBuf s = boardHorizSize s `div` 2
+
+-- | Given two square indices, @sq1@ and @sq2@, the value
+-- @(constOffset + sq1 - sq2)@ is always inside a lookup table's bounds.
+constOffset :: BoardSize -> Int
+constOffset s = ur - ll
+ where BoardSize h v _ = s
+ ur = squareToIndex s $ Square (h - 1, v - 1)
+ ll = squareToIndex s $ Square (0, 0)
+
+
+-- | Utility function for computing the file, rank and square distance tables.
+-- In each case, the computations are almost identical, differing only by the
+-- type of calculation performed on the two squares. That calculation function
+-- is passed in as the first argument to 'distanceTableWith'.
+distanceTableWith :: (Square -> Square -> Int) -> BoardSize ->
+ CoveringIndexList -> LookupTable
+distanceTableWith cmp b (CL cs) =
+ LookupTable offset (array (0, 2 * offset) $ map f cs)
+ where offset = constOffset b
+ f (d, (sq1, sq2)) = (offset + d, cmp sq1 sq2)
+
@@ -0,0 +1,36 @@
+{-
+ - Runs all the tests and provides a nice summary output, using the
+ - test-framework library.
+ -
+ - Useful during development. During deployment situations, "cabal test" is
+ - going to be more appropriate.
+ -}
+
+import Test.Framework (defaultMain, testGroup)
+import Test.Framework.Providers.QuickCheck2 (testProperty)
+
+import TestBoard
+
+
+main = defaultMain tests
+
+tests = [
+ testGroup "Board arrays" [
+ testProperty "index to square" prop_index_to_square_inverse,
+ testProperty "square to index" prop_square_to_index_inverse,
+ testProperty "indices increase" prop_index_increases_with_square,
+ testProperty "array size" prop_board_array_size
+ ],
+ testGroup "Lookup arrays" [
+ testProperty "valid lookup indices" prop_lookup_index_is_positive,
+ testProperty "repIndexList represents"
+ prop_repIndexList_is_representative,
+ testProperty "file lookup 1" prop_check_file_distance_1,
+ testProperty "file lookup 2" prop_check_file_distance_2,
+ testProperty "rank lookup 1" prop_check_rank_distance_1,
+ testProperty "rank lookup 2" prop_check_rank_distance_2,
+ testProperty "square lookup 1" prop_check_square_distance_1,
+ testProperty "square lookup 2" prop_check_square_distance_2
+ ]
+ ]
+
Oops, something went wrong.

0 comments on commit 1ef0d97

Please sign in to comment.