Tonλ's blog May the λ be with you

PIH - ch9 - Interactive programs - exercises

by @ardumont on

Here are the exercises from the chapter 9 of the book Programming in Haskell. As there were some compatibilities issues between the content of the book and my realities, We will only concentrate the efforts on the nim one.

Anyway, enough chit chat, here is the exercise:

Nim is a game that is played on a board comprising five numbered rows of stars, which is initially set up as follows:

1:∗∗∗∗∗

2:∗∗∗∗

3:∗∗∗

4:∗∗

5:∗

Two players take it in turn to remove one or more stars from the end of a single row. The winner is the player who removes the last star or stars from the board. Implement the game of nim in Haskell.

Hint: represent the board as a list comprising the number of stars remaining on each row, with the initial board being [5, 4, 3, 2, 1].

We will begin simply by representing a board following the recommandations.

type Board = [Int]

I'm creating a utility function to create a board:

makeBoard :: Int -> Board
makeBoard n = [n, n-1..1]

For example:

*Ch9> makeBoard 5
[5,4,3,2,1]

Now we can compute the stars to display on board:

computeStars :: Board -> [(Int, String)]
computeStars b = zip [0..(length b - 1)] (map (flip replicate '*') b)

Example:

*Ch9> computeStars board
[(0,"*****"),(1,"****"),(2,"***"),(3,"**"),(4,"*")]

Now, we can show the board:

showBoard :: [(Int, String)] -> IO ()
showBoard b = mapM_ putStrLn [ show x ++ ": " ++ s | (x, s) <- b ]

Possible output:

*Ch9> showBoard $ computeStars board
0: *****
1: ****
2: ***
3: **
4: *

We will also need some utility function to deal silently with error. That is if we want to remove more stars than exists, then we no longer have stars.

wrap :: Int -> Int
wrap n = if n >= 0 then n else 0
*Ch9> wrap 0
0
*Ch9> wrap (-1)
0

Same idea for dealing with stars.

wrapStars :: Int -> Int -> Int
wrapStars r l | r < 0 = 0
              | l <= r = l
              | otherwise = r

Output:

*Ch9> wrapStars 10 12
10
*Ch9> wrapStars 13 12
12
*Ch9> wrapStars (-1) 12
0

Now we need some function to help in removing stars. Simply, I split at the index r and retrieve the head of the second list (which then correponds to the value we want to decrement). Then we update the new value by decrementing the value by n. We wrap the result to return 0 at the worst case scenario. Then we recreate the list with the new value.

remove :: Int -> Int -> Board -> Board
remove r n b = let (h, (v:t)) = splitAt r b
                   ns = wrap (v - n) in
               h ++ (ns:t)

Output:

*Ch9> remove 0 2 board
[3,4,3,2,1]
*Ch9> remove 0 10 board
[0,4,3,2,1]
*Ch9> remove 0 5 board
[0,4,3,2,1]

The game is finished if the board no longer has stars. As the board is represented by a list of int, we simply compute the sum of the list. It the rest is zero, there is no more stars.

win :: Board -> Bool
win = (== 0) . sum

Output:

*Ch9> win [4,4,3,2,1]
False
*Ch9> win [0,0,0,0,0]
True
turn :: Int -> Board -> IO Board
turn p b = do showBoard $ computeStars b
              putStrLn $ "Player " ++ (show p) ++ ", on which row do you want to remove stars?"
              x <- getLine
              let r = read x in
                do putStrLn "How many stars?"
                   s <- getLine
                   let n = wrapStars (read s) (length b) in
                     return $ remove r n b

Output:

*Ch9> turn 1 board
*****
****
***
**
*
Player 1, what stars do you want to remove?
1
[5,3,3,2,1]

Here is a small function to compute the next player:

nextplayer :: Int -> Int
nextplayer p = ((p+1) `mod` 2)

Output:

*Ch9> nextplayer 1
0
*Ch9> nextplayer 0
1

The main function which, given a player and a board, launches the game:

game :: Int -> Board -> IO ()
game p b = do nb <- turn p b
              if win nb
                then putStrLn $ "p" ++ (show p) ++ " won!"
                else game (nextplayer p) nb

A small utility function to setup the game regarding the size of the board and which player starts:

setupGame :: IO (Int, Int)
setupGame = do putStrLn "What size for the board?"
               n <- getLine
               let size = read n in
                 do putStrLn "What player first? (0 or 1)"
                    p <- getLine
                    let player = read p in
                      return (size, player)

At last, the main function which launches the game:

main :: IO ()
main = do (size, player) <- setupGame
          game player (makeBoard size)

A sample run:

*Ch9> main
What size for the board?
3
What player first? (0 or 1)
1
0: ***
1: **
2: *
Player 1, on which row do you want to remove stars?
0
How many stars?
3
0:
1: **
2: *
Player 0, on which row do you want to remove stars?
1
How many stars?
1
0:
1: *
2: *
Player 1, on which row do you want to remove stars?
0
How many stars?
1
0:
1: *
2: *
Player 0, on which row do you want to remove stars?
2
How many stars?
1
0:
1: *
2:
Player 1, on which row do you want to remove stars?
1
How many stars?
1
p1 won!

As usual, here is the complete source.

Latest posts