# Fox & Geese in Haskell: Part 2

filed in Games, Software Development on Jun.27, 2011

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}

switchPlayer

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,

destinationIsEmpty,

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.

Jan 22nd, 2012 on 3:17 pm

in order for it to work with the latest release, you should change the “gameInWindow” function of the main for the new “play”.

replacing the main by:

dis = InWindow “Fox & Geese” windowSize windowPos

main = play

dis

white

10

(newGameState ((fromIntegral (fst windowSize)), (fromIntegral (snd windowSize))))

drawGame

handleInputEvent

gameStep

Jan 22nd, 2012 on 3:18 pm

Also, cool game!

Jan 23rd, 2012 on 10:35 am

That’s great, thanks. I need to find some time to update this and finish the series.