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 reach 10 points
(1) 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 😀
(2) I am aware that
Control.Monad.State exists, but I want to practice tracking state without it
(3) 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 ```