Last time we looked at how the board is represented in the little Fox & Geese game I wrote in Haskell. This time, I’ll cover the machinery that makes the game go: moving pieces, jumping, and validating moves. The code for this part is available in my GitHub repo under tag v0.1.2.

To simplify the game mechanic functions, we’ll carry the game state around in a State monad called GameStateM.

type GameStateM a = State GameState a

This is created by the input functions that we’ll see later.

The first thing we want is a high-level function that can be used to move a piece. This is easy to describe in plain English: if the move from space A to space B is valid, move the piece on the board and remove any piece that was jumped, then it’s the next player’s turn. It is almost as easy to write in Haskell:

movePiece :: Position -> Position -> GameStateM ()
movePiece from to = do
  (valid, jumped) <- isMoveValid from to
  when valid $ do
    oldBoard <- gets board
    let newBoard = jumpPiece jumped . move $ oldBoard
    modify $ \game -> game {board = newBoard}
  selectPosition Nothing

We’ll look at isMoveValid soon. When the move is valid, this gets the board from the current state, makes a new board with the pieces moved, then puts the new board back in the state. The function gets board maps the projection board into the state, so it is equivalent to get >>= return . board. We put the new board back into the state with modify, which takes a function that modifies the current state. So modify f is equivalent to get >>= put . f. We’ll look at switchPlayer and selectPosition in the part about input.

Now, let’s define move and jumpPiece.

move board’ = maybe board’ (boardAfterMove board’)
                    (getPiece’ from board’)

boardAfterMove board’ piece = Map.insert to piece
                            . Map.delete from $ board’

In move we first try to get the moving piece from the board. If it existed, we return the board with the piece moved. To do that, we delete the piece from the old position and insert it at the new one.

Next, we take a leap.

– jumpPiece :: Maybe Position -> Board -> Board
jumpPiece Nothing = id
jumpPiece (Just pos) = Map.delete pos

Notice that these are partial functions. If the position that was jumped is Nothing, i.e. no jump, then don’t modify the board. Otherwise, delete the piece that was jumped.

Now the hard part: checking if the move is valid. We have a number of things to check here, so let’s break it down. A move is valid if:

  • The destination is on the board
  • The destination space is empty
  • If the piece that is moving is a goose, then it must move forward
  • The spaces have to be adjacent or a jump
  • A jump can only occur if the destination is two spaces away from the start along a line, and the moving piece is a fox, and the space jumped had a goose, and the first two rules still apply

Whew, that’s a lot to check. Let’s start.

isMoveValid :: Position -> Position -> GameStateM (Bool, Maybe Position)
isMoveValid from@(fx, fy) to@(tx, ty) = do
  Just movingPiece <- gets $ getPiece from
  destinationIsEmpty <- (not . Map.member to) <$> gets board
  isJump <- isGoose . jumpedPos $ movingPiece
  valid <- return $ and [onBoard,
                         movingPiece == Fox || movingForward,
                         isAdjacent || isJump]

  return (valid, jumpedPos movingPiece)

We’ll need to know what piece is moving in a few places, so get it here. It also helps to get what we need from the state here, so we can define helper functions which aren’t in the state monad. Next, we check if the destination is empty with Map.member. Remember, we don’t store empty spaces in the map. We use a couple of helper functions to check if the move is a jump, then we combine all of our rules in a call to and. Finally, the result of the function is if the move is valid and which space was jumped, if any.

Let’s dive into these helper functions.

dx = tx – fx
dy = ty – fy
onBoard = to `elem` validPositions
isAdjacent = to `elem` adjacentPositions from
movingForward = dy >= 0

– isGoose :: Maybe Position -> GameStateM Bool
isGoose Nothing = return False
isGoose (Just pos) = (Just Goose ==) <$> (gets $ getPiece pos)

These are all very simple definitions. We need dx and dy in a lot of places, so they’re defined here. onBoard and isAdjacent use two definitions from part 1. The function isGoose checks if a jumped space has a goose. How do we calculate the jumped space? We know that it has to be either two positions up, down, or sideways, and if (x + y) is even then we can go diagonally also.

– jumpedPos :: Side -> Maybe Position
jumpedPos Fox
    – Straight up or down
    | dx == 0 && abs dy == 2
        = Just (fx, fy + signum dy)
    – Sideways
    | dy == 0 && abs dx == 2
        = Just (fx + signum dx, fy)
    – Diagonally
    | even (fx + fy) && abs dx == 2 && abs dy == 2
        = Just (fx + signum dx, fy + signum dy)
jumpedPos _ = Nothing

If the jumping piece isn’t a fox, or the move isn’t a jump, return Nothing.

This is the bare minimum of mechanics for a working fox and geese game. It’s actually not complete, because we don’t allow multiple jumps, and we don’t force a player to take a jump if he can. I will add those later and write a new post.

Next time, we’ll see how to make our game interact with the world, with input and drawing. The source code is available on GitHub, so please fork it and make it better.