An Arimaa Library for Haskell.

Table of Contents

1 MODULE Arimaa

Arimaa is an abstract strategy game for two players.

See http://arimaa.com/arimaa/

{-# LANGUAGE TypeSynonymInstances, OverlappingInstances #-}

import Data.Array
import Data.Char
import qualified Data.Map as Map
import qualified Data.String.Utils as DSU
import Control.Monad.State

2 The Pieces

data Animal = R | C | D | H | M | E 
     deriving (Ord, Eq, Show)
data Color = Gold | Silver deriving (Show, Eq)
type Colored a = (Color, a)
type Piece = Colored Animal

showPiece (Gold, a)   = show a
showPiece (Silver, a) = map toLower $ show a


gold   a = (Gold, a)
silver a = (Silver, a)

g = gold
s = silver

3 The Board

3.1 squares

files = ['a'..'h']
ranks = [1..8]
type Square = (Char, Int)

showSquare (x, y) = x : show y

To avoid bounds-checking when we look at neighboring pieces, we can surround the board with a one-cell-thick border. Thus any square next to a piece on the board will either be empty, contain a wall, or contain a piece.

As a special case, I'm putting "trap" in here, even though it evaluates to the same thing as Empty.

data Content = Empty | Wall | Trap | A Piece
instance Show Content where
    show c = case c of
               Empty     -> " "
               Trap      -> "x"
               Wall      -> "#"
               A (Gold, piece)   -> show piece
               A (Silver, piece) -> map toLower $ show piece

3.2 empty board

Here we build an empty board, with a border around the edge.

type BoardShape =  Array Square Content
data Board = Board BoardShape
emptyBoard :: Board
emptyBoard = Board $ array fullBounds
           $ [ (sq, initSquare sq) | sq <- range fullBounds ]

fullBounds = ((westOf 'a', 0), (eastOf 'h', 9))
coreBounds = (('a', 1), ('h',8))

This may come in handy for traps later on:

initSquare :: Square -> Content
initSquare sq@(x, y)
  | (x < 'a') = Wall
  | (x > 'h') = Wall
  | (y < 1)   = Wall
  | (y > 8)   = Wall
  | isTrap sq = Trap
  | otherwise = Empty

eastOf file = chr $ (ord file) + 1
westOf file = chr $ (ord file) - 1

3.3 the Board type

Here's a handy function to show the board, as per the standard ASCII notation defined at http://arimaa.com/arimaa/learn/notation.html

render :: Board -> String
render (Board br) = DSU.join "\n" $ map row [9,8.. -1]
    where row y | y == 9 || y== 0  = " +-----------------+"
                | y == -1          = "   a b c d e f g h  \n"
                | otherwise        = show y ++ "| " ++ cells y ++ " |"
          cells y = DSU.join " " $ map (\x -> show $ br ! (x, y)) files

instance Show (Board) where 
   show = render

3.4 traps

traps :: [Square]
traps = [('c',3), ('c',6), ('f',3), ('f',6)]

isTrap :: Square -> Bool
isTrap ('c', 3) = True
isTrap ('c', 6) = True
isTrap ('f', 3) = True
isTrap ('f', 6) = True
isTrap others = False

4 navigating the board

data Direction = North | South | East | West
  deriving (Eq)
North @: (x, y) = (x, y + 1)
South @: (x, y) = (x, y - 1)
East  @: (x, y) = (eastOf x, y)
West  @: (x, y) = (westOf x, y)

test: East @ ('d',3) == ('e', 3)

instance (Show) Direction where
    show North = "n"
    show South = "s"
    show East  = "e"
    show West  = "w"
(>>:) :: Square -> Square -> Direction
(x1, y1) >>: (x2, y2) = diff (ord x1 - ord x2, y1-y2)
   where diff ( 1,  0) = West
         diff (-1,  0) = East
         diff ( 0,  1) = North
         diff ( 0, -1) = South
         diff _        = error ">>: only works for 2 numbers" 

cardinals :: [Direction]
cardinals = [North, East, South, West]

neighborSquares :: Square -> [(Direction, Square)]
neighborSquares sq = [(dir, dir @: sq) | dir <- cardinals ]

rev :: Direction -> Direction
rev North = South
rev East  = West
rev South = North
rev West  = East

5 Step and step parser:

Again, from the standard notation:

data Step
   = Put Piece Square
   | Kill Piece Square
   | Move Piece Square Direction
   | Resign
   | Takeback
   deriving (Eq)

instance (Show) Step where
    show step = case step of
        Put  p s   -> sh p s
        Kill p s   -> sh p s ++ "x"
        Move p s d -> sh p s ++ show d
        Resign     -> "resign"
        Takeback   -> "takeback"
        where sh p s = showPiece p ++ showSquare s

Parser is pretty simple:

parseStep :: String -> Step
parseStep s@(p:x:y:end)
  | s == "takeback" = Takeback
  | s == "resign"   = Resign 
  | end == ""       = Put  pp (x, py)
  | end == "x"      = Kill pp (x, py)
  | otherwise       = Move pp (x, py) pd
  where pp = parsePiece p
        py = parseRank y
        pd = parseDirection $ head end


parsePiece :: Char -> Piece
parsePiece ch = case ch of 
  -- gold pieces --
  'E' -> g E
  'M' -> g M
  'H' -> g H
  'D' -> g D
  'C' -> g C
  'R' -> g R
  -- silver pieces --
  'e' -> s E
  'm' -> s M
  'h' -> s H
  'd' -> s D
  'c' -> s C
  'r' -> s R

parseRank :: Char -> Int
parseRank ch = case ch of
  '1' -> 1
  '2' -> 2
  '3' -> 3
  '4' -> 4
  '5' -> 5
  '6' -> 6
  '7' -> 7
  '8' -> 8

parseDirection :: Char -> Direction
parseDirection ch = case ch of
  'n' -> North
  's' -> South
  'e' -> East
  'w' -> West

6 setting up pieces

So, now I want to actually apply the moves to the board.

I couldn't figure out how to convince the type system to let me make Board an instance of Functor, but that's probably okay, because as I visit the squares I actually need to know both the coordinates and the contents. So:

apply :: (Board -> Square -> Content) -> Board -> Board
apply f br = Board $ array fullBounds $ contents
             where contents = [(sq, f br sq) | sq <- range fullBounds]

Let's see how to define the popular 99of9 opening for gold. (see http://arimaa.com/arimaa/mwiki/index.php/Setup_Positions )

steps :: String -> [Step]
steps s = map parseStep $ DSU.split " " s

open99of9 = steps "Ra1 Rb1 Rc1 Cd1 Ce1 Rf1 Rg1 Rh1 Ra2 Hb2 Dc2 Md2 Ee2 Df2 Hg2 Rh2"

So now:

(//=) :: Board -> [(Square, Content)] -> Board
(Board br) //= changes = Board $ br // changes

step :: Step -> Board -> Board
step st br = case st of
    Put  p sq     -> br //= [(sq, A p)]    
    Kill p sq     -> todo
    Move p sq d   -> todo
    Resign        -> todo
    Takeback      -> todo
  where
    todo = br

doSteps :: [Step] -> Board -> Board
doSteps []      br = br
doSteps (s :ss) br = doSteps ss $ step s br

How about the same for silver?

mirror :: Step -> Step
mirror (Put (Gold, p) (x, y)) = Put (Silver, p) (x, 9-y)
mirror other = other

board99of9 = doSteps (gold ++ silver) emptyBoard
     where gold   = open99of9
           silver = map mirror gold

7 board queries

type PieceAt = (Piece, Square)

contents br = [(sq, content br sq) | sq <- range coreBounds]

content sq (Board br) = br ! sq
neighbors :: Square -> Board -> [(Direction, PieceAt)]
neighbors square br = [(dir, (pieceAt sq br, sq))
                         | (dir, sq)  <- neighborSquares square
                         , isPiece $ content sq br]

Enemitos = lesser enemies (spanglish ftw!)

enemitos :: PieceAt -> Board -> [(Direction, PieceAt)]
enemitos ((c, a), sq) br = [n | n@(dir, ((c2, a2), _)) <- neighbors sq br 
                              , a2 < a  && c2 == opp c]

isPiece :: Content -> Bool
isPiece (A _) = True
isPiece _     = False

toPiece :: Content -> Piece
toPiece (A p) = p
toPiece _     = error "not a piece!"

pieceAt :: Square -> Board -> Piece
pieceAt sq br  = toPiece $ content sq br

pieceSquares br = [sq | sq <- range coreBounds,  isPiece $ content sq br]

isEmpty sq br = case content sq br of
                   Empty -> True
                   Trap  -> True
                   _     -> False

army :: Color -> Board -> [PieceAt]
army c br =  [(pieceAt sq br, sq) | sq <- pieceSquares br
                                  , colorOf (pieceAt sq br) == c]

colorOf :: (Color, a) -> Color
colorOf = fst


isColor :: Color -> (Color, a) -> Bool
isColor c = \x -> colorOf x == c

8 Movement rules.

8.1 summary

validSteps :: Board -> Color -> [Step] -> [Step]
validSteps b c ss = [] -- TODO

validPushes + validPulls + validSteps

emcTestBoard = doSteps (steps "Ed3 me3 Ce4") emptyBoard
emcE = head $ army Gold emcTestBoard
emcM = head $ army Silver emcTestBoard 
emcC = head $ tail $ army Gold emcTestBoard

Which looks like this:

 +-----------------+
8|                 |
7|                 |
6|     x     x     |
5|                 |
4|         C       |
3|     x E m x     |
2|                 |
1|                 |
 +-----------------+
   a b c d e f g h

8.2 1. Pieces move in cardinal directions, except that rabbits cannot move backward.

8.2.1 forward and backward:

fore :: Color -> Direction
fore Gold   = North
fore Silver = South

back :: Color -> Direction
back = fore . opp

opp Gold = Silver
opp Silver = Gold

8.2.2 valid empty squares

emptyDirs :: PieceAt -> Board -> [Direction]
emptyDirs (p, sq) br = [dir | dir <- potentialDirs p, isEmpty (dir @: sq) br]

test: [North, East, West] = emptyDirs (('b', 5), (g R)) emptyBoard test: [South, East, West] = emptyDirs (('b', 5), (s R)) emptyBoard test: [South, East] == emptyDirs (('a', 5), (s R)) emptyBoard

potentialDirs :: Piece -> [Direction]
potentialDirs (color, pc) =
   case pc of 
      R  -> (foreAndSides color) 
      _  -> (back color) : (foreAndSides color)
   where foreAndSides color = fore color : [East, West]

8.2.3 valid single-step moves:

validMoves :: PieceAt -> Board -> [[Step]]
validMoves pcAt@(p, sq) br = [[Move p sq d] | d <- emptyDirs pcAt br ]

8.3 2. Pieces can push or pull weaker enemy pieces provided there are >= 2 steps left.

-- >>> validPushes emcE emcTestBoard
-- [[me3s,Ed3e],[me3e,Ed3e]]
-- >>> validPulls emcE emcTestBoard
-- [[Ed3s,me3w],[Ed3n,me3w],[Ed3w,me3w]]

To push, move the enemy piece, then move your piece where the enemy started.

push :: PieceAt -> Direction -> Direction -> Board -> [Step]
push (pc, sq) pcDir enDir br = [Move en enSq enDir, Move pc sq pcDir]
   where en = pieceAt enSq br
         enSq = pcDir @: sq

piece p1 can push adjacent enemy p2 if p1 > p2 and p2 has adjacent empty square

validPushes :: PieceAt -> Board -> [[Step]]
validPushes pcAt@((c, a), sq) br = 
   [push pcAt pcDir enDir br
       | (pcDir, en) <- enemitos pcAt br
       , enDir <- emptyDirs en br ]

To pull, move your piece, then move the enemy to where your piece started.

pull :: PieceAt -> Direction -> Direction -> Board -> [Step]
pull (pc, sq) pcDir enDir br = [Move pc sq pcDir, Move en enSq enDelta]
   where en = pieceAt enSq br
         enSq = enDir @: sq
         enDelta = rev enDir

piece p1 can pull adjacent enemy p2 if p1 > p2 and p1 has adjacent empty square

validPulls :: PieceAt -> Board -> [[Step]]
validPulls pcAt@((c, a), sq) br = 
   [pull pcAt pcDir enDir br
       | (enDir, en) <- enemitos pcAt br
       , pcDir <- emptyDirs pcAt br ]

8.4 3. A piece is frozen when adjacent to a stronger enemy, unless it is also adjacent to another friendly piece.

-- | is the piece frozen?
-- >>> frozen emcE emcTestBoard
-- False
-- >>> frozen emcM emcTestBoard
-- True
-- >>>  frozen emcC emcTestBoard
-- False
frozen pcAt br | hasFriend pcAt br = False
               | otherwise = hasThreat pcAt br
hasFriend :: PieceAt -> Board -> Bool
hasFriend ((c, _), sq) br = any (\(p, s) -> isColor c p) $ map snd $ neighbors sq br

hasThreat :: PieceAt -> Board -> Bool
hasThreat ((c, a), sq) br = any biggerEnemy $ map snd $ neighbors sq br
   where biggerEnemy ((c2,  a2), _) = (a2 > a) && (not $ c2 == c)

8.5 4. A piece is killed when placed on a trap, unless it is also adjacent to a friendly piece.

trapped :: PieceAt -> Board -> Bool
trapped (p, sq) br = isTrap sq && (not $ hasFriend (p, sq) br)

9 the Play State Monad

type Move = (Color, [Step])
data PlayState = PlayState {
  board     :: Board,
  moveNum   :: Int,
  toMove    :: Color,
  stepsLeft :: Int,
  history   :: [Move],
  future    :: [Move]
} deriving Show

initState = PlayState {
  board     = emptyBoard,
  moveNum   = 1,
  toMove    = Gold,
  stepsLeft = 4,
  history   = [],
  future    = []
}