I've created a two-player number guessing game in Haskell. My main objective was to practice dealing with "state" in a purely functional language (such as player scores, whose turn it is, etc.).
Here are the rules:
Gameplay:
Two players will take turns guessing a random number between 1 and 10. Answers will be typed into the command line.
Scoring:
- If a player guesses the number correctly, they will be awarded 5 points
- If a player is within two (inclusive) from the answer, the player will be awarded 3 points.
- If a player is within three (inclusive) from the answer, they will be awarded 1 point.
- If a player is 7 or more points off, they will lose a point. The score may not be negative.
- All other offsets will result in zero points.
- The game will continue until the one of the players reaches 10 points.
Caveats:
- This is was not designed to be an exercise in enjoyable game design -- obviously the optimal solution is to always choose five, which doesn't make for a lot of excitement. :D
- I am aware that
Control.Monad.State
exists, but I want to practice tracking state without it. - I know that the "mutual recursion" is difficult to follow. I would love some suggestions for getting rid of that which do not involve nesting
if
statements.
import Data.Char
import System.Random
main = do
stdGen <- getStdGen
play 0 0 P1 stdGen
play :: Int -> Int -> Player -> StdGen -> IO ()
play p1Score p2Score player stdGen
| p1Score < 10 && p2Score < 10 = continueGame p1Score p2Score player stdGen
| otherwise = putStrLn $ show (determineWinner p1Score p2Score) ++ " wins!"
continueGame :: Int -> Int -> Player -> StdGen -> IO ()
continueGame p1Score p2Score player stdGen = do
putStr $ show player ++ "'s turn. Pick a number between 1 and 10: "
chosenNumber <- getLine
if isInteger chosenNumber
then do
let (randomNumber, newGen) = randomR (1, 10) stdGen :: (Int, StdGen)
putStrLn $ "The answer is " ++ show randomNumber
let pointsEarned = calcPointsEarned randomNumber (read chosenNumber)
let newP1Score = min (max (p1Score + calcPointsEarnedForPlayer player P1 pointsEarned) 0) 10
let newP2Score = min (max (p2Score + calcPointsEarnedForPlayer player P2 pointsEarned) 0) 10
putStrLn $ "P1 Score: " ++ show newP1Score
putStrLn $ "P2 Score: " ++ show newP2Score
play newP1Score newP2Score (changeTurn player) newGen
else do
putStrLn "The input must be an integer"
play p1Score p2Score player stdGen
data Player = P1 | P2 deriving (Show, Eq)
isInteger :: String -> Bool
isInteger = and . map isNumber
changeTurn :: Player -> Player
changeTurn player
| player == P1 = P2
| otherwise = P1
calcPointsEarned :: Int -> Int -> Int
calcPointsEarned actualAnswer chosenAnswer
| offset == 0 = 5
| offset <= 2 = 3
| offset <= 3 = 1
| offset >= 7 = (-1)
| otherwise = 0
where offset = abs $ chosenAnswer - actualAnswer
calcPointsEarnedForPlayer :: Player -> Player -> Int -> Int
calcPointsEarnedForPlayer actualTurn player pointsEarned
| actualTurn == player = pointsEarned
| otherwise = 0
determineWinner :: Int -> Int -> Player
determineWinner p1Score p2Score
| p1Score > p2Score = P1
| otherwise = P2
```