Two player random number guessing game
I'd contend that State is pure and functional, but I think translating your current code to use State is an excellent exercise so I'll leave that up to you.
The first thing I'd address is making your types do more of the bookkeeping. Well designed types lend themselves to correct-by-construction solutions.
data Player = P1 | P2 deriving Show

data Game = Game { turn :: Player, p1 :: Int, p2 :: Int } deriving Show

Prefer pattern matching to equality testing, it is frequently more terse. Decreased line noise often means increased readability.
changeTurn :: Player -&gt; Player
changeTurn P1 = P2
changeTurn P2 = P1

Your determineWinner function has a (currently unreachable) logic error. If both player's scores are equal then it prefers handing victory to player two. This may not matter in your code as written, but if your code changes or you begin property testing or some other unforeseeable future event comes to pass, it could begin mattering unexpectedly. Handling ties is “morally” the right thing to do.
Also as-is it isn't really determining a winner by the rules of the game, only which player's score is higher.
winner :: Game -&gt; Maybe Player
winner (Game _ p1 p2) =
    case (max p1 p2 &gt;= 10, p1 &gt; p2, p2 &gt; p1) of
      (True, True, False) -&gt; Just P1
      (True, False, True) -&gt; Just P2
      (_, _ , _)          -&gt; Nothing

Don't validate and then parse, parse and allow for failure. If your validation code is separate from your parsing code you risk them drifting out of sync and causing errors. In this case, use Text.Read.readMaybe from base and leave out your isInteger function entirely.
It's a good idea to separate as much of your pure game logic from IO actions as possible. It's easier to test, easier to understand, and enables you to reuse functionality you otherwise couldn't.
updateRound :: Int -&gt; Game -&gt; Game
updateRound n (Game P1 p1 p2) = Game P2 (boundScore $ p1 + n) p2
updateRound n (Game P2 p1 p2) = Game P1 p1 (boundScore $ p2 + n)

clamp :: Ord a =&gt; a -&gt; a -&gt; a -&gt; a
clamp lo val hi = lo `max` val `min` hi

boundScore :: Int -&gt; Int
boundScore n = clamp 0 n 10

This also obviates the need for changeTurn.
It's also usually handy to separate your display logic from your control logic, even if both are IO actions. It might be useful to you if you make use of the REPL while developing, your types often shouldn't include multiple copies of the same information (e.g., two numbers and their difference) as that carries a risk of the values getting out of sync. Those derived values might be all you want to see while working though.
displayGame :: Game -&gt; IO ()
displayGame (Game _ p1 p2) = do
    putStrLn $ &quot;P1 Score: &quot; ++ show p1
    putStrLn $ &quot;P2 Score: &quot; ++ show p2

That said, it's good to separate your control logic from your sources of input and output also. It makes your program testable without needing to muck about with piping stdin and stdout. There's also a very elegant transformation when you decide to begin using State, but I'll leave that to you to figure out.
gameRound :: Int -&gt; Int -&gt; Game -&gt; (Maybe Player, Game)
gameRound guess answer game =
  let
    score = calcPointsEarned guess answer
    nextGame = updateRound score game
  in
    (winner nextGame, nextGame)

receiveGuess :: Player -&gt; IO Int
receiveGuess player = do
    putStr $ show player ++ &quot;'s turn. Pick a number between 1 and 10: &quot;
    input &lt;- getLine
    case readMaybe input of
      Nothing -&gt; do
        putStrLn &quot;The input must be an integer&quot;
        receiveGuess player
      Just guess -&gt; pure guess

play :: Game -&gt; IO ()
play game = do
    answer &lt;- randomRIO (1, 10)
    guess &lt;- receiveGuess (turn game)
    putStrLn $ &quot;The answer is &quot; ++ show answer
    let (mPlayer, nextGame) = gameRound guess answer game
    displayGame nextGame
    case mPlayer of
      Just player -&gt; putStrLn $ show player ++ &quot; wins!&quot;
      Nothing -&gt; play nextGame