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:
Two players will take turns guessing a random number between 1 and 10. Answers will be typed into the command line.
- 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.
- 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.Stateexists, 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
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 ```