Skip to content

Commit 62864be

Browse files
Implement game to show ReaderT pattern storing mutable state in env (#193)
* Implement game to show ReaderT pattern storing mutable state in env * Don't run the program when testing on CI
1 parent fb48546 commit 62864be

File tree

6 files changed

+259
-0
lines changed

6 files changed

+259
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ Running a web-compatible recipe:
126126
| | :heavy_check_mark: | [NumbersReactHooks](recipes/NumbersReactHooks) | A React port of the ["Random - Numbers" Elm Example](https://elm-lang.org/examples/numbers). |
127127
| | :heavy_check_mark: | [PositionsHalogenHooks](recipes/PositionsHalogenHooks) | A Halogen port of the ["Random - Positions" Elm Example](https://elm-lang.org/examples/positions). |
128128
| | :heavy_check_mark: | [PositionsReactHooks](recipes/PositionsReactHooks) | A React port of the ["Random - Positions" Elm Example](https://elm-lang.org/examples/positions). |
129+
| :heavy_check_mark: | | [RandomNumberGameNode](recipes/RandomNumberGameNode) | This recipe shows how to build a "guess the random number" game using a custom `AppM` monad via the `ReaderT` design pattern and `Aff`, storing the game state in a mutable variable via a `Ref`. |
129130
| :heavy_check_mark: | | [ReadPrintFileContentsNode](recipes/ReadPrintFileContentsNode) | Reads a file's contents and prints it to the console. |
130131
| | :heavy_check_mark: | [RoutingHashHalogenClassic](recipes/RoutingHashHalogenClassic) | This recipe shows how to use `purescript-routing` to do client-side hash-based routing in a Halogen-based single-page application (SPA). |
131132
| | :heavy_check_mark: | [RoutingHashLog](recipes/RoutingHashLog) | This recipe demonstrates hash-based routing with `purescript-routing`. No web framework is used. |
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/bower_components/
2+
/node_modules/
3+
/.pulp-cache/
4+
/output/
5+
/generated-docs/
6+
/.psc-package/
7+
/.psc*
8+
/.purs*
9+
/.psa*
10+
/.spago
11+
/web-dist/
12+
/prod-dist/
13+
/prod/
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# RandomNumberGameNode
2+
3+
This recipe shows how to build a "guess the random number" game using a custom `AppM` monad via the `ReaderT` design pattern and `Aff`, storing the game state in a mutable variable via a `Ref`.
4+
5+
## Expected Behavior:
6+
7+
### Node.js
8+
9+
Example output:
10+
```
11+
Guess the random number. It's between 1 and 10. You have 5 guesses. Enter 'q' to quit. Invalid inputs will not count against you.
12+
13+
Prevous guesses: []
14+
Guess a number between 1 - 10: al
15+
'al' is not an integer. Try again (Use 'q' to quit).
16+
17+
Prevous guesses: []
18+
Guess a number between 1 - 10: 52.9
19+
'52.9' is not an integer. Try again (Use 'q' to quit).
20+
21+
Prevous guesses: []
22+
Guess a number between 1 - 10: 52
23+
52 was not between 1 and 10. Try again.
24+
25+
Prevous guesses: []
26+
Guess a number between 1 - 10: 5
27+
28+
Prevous guesses: [5]
29+
Guess a number between 1 - 10: 2
30+
31+
Prevous guesses: [5,2]
32+
Guess a number between 1 - 10: 4
33+
34+
Prevous guesses: [5,2,4]
35+
Guess a number between 1 - 10: 3
36+
37+
Prevous guesses: [5,2,4,3]
38+
Guess a number between 1 - 10: 5
39+
You already guessed 5 previously. Please guess a different number.
40+
41+
Prevous guesses: [5,2,4,3]
42+
Guess a number between 1 - 10: 0
43+
0 was not between 1 and 10. Try again.
44+
45+
Prevous guesses: [5,2,4,3]
46+
Guess a number between 1 - 10: 9
47+
Player lost! The answer was 6
48+
Guesses made: [5,2,4,3,9]
49+
```
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
This file just indicates that the node backend is supported.
2+
It is used for CI and autogeneration purposes.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{ name = "RandomNumberGameNode"
2+
, dependencies =
3+
[ "aff"
4+
, "console"
5+
, "effect"
6+
, "interpolate"
7+
, "node-readline"
8+
, "psci-support"
9+
, "random"
10+
, "transformers"
11+
]
12+
, packages = ../../packages.dhall
13+
, sources = [ "recipes/RandomNumberGameNode/src/**/*.purs" ]
14+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
module RandomNumberGameNode.Main where
2+
3+
import Prelude
4+
5+
import Control.Monad.Reader (class MonadAsk, ReaderT, ask, asks, runReaderT)
6+
import Data.Array (elem, snoc)
7+
import Data.Either (Either(..))
8+
import Data.Int (fromString)
9+
import Data.Interpolate (i)
10+
import Data.Maybe (Maybe(..))
11+
import Effect (Effect)
12+
import Effect.Aff (Aff, makeAff, nonCanceler, runAff_)
13+
import Effect.Aff.Class (class MonadAff, liftAff)
14+
import Effect.Class (class MonadEffect, liftEffect)
15+
import Effect.Class.Console (log)
16+
import Effect.Random (randomInt)
17+
import Effect.Ref (Ref)
18+
import Effect.Ref as Ref
19+
import Node.ReadLine (Interface, close, createConsoleInterface, noCompletion)
20+
import Node.ReadLine as NRL
21+
import Type.Equality (class TypeEquals, from)
22+
23+
main :: Effect Unit
24+
main = do
25+
-- get/create environment values
26+
interface <- createConsoleInterface noCompletion
27+
randomNumber <- randomInt 1 10
28+
remainingGuesses <- Ref.new 4
29+
previousGuesses <- Ref.new []
30+
31+
let env = { randomNumber, remainingGuesses, previousGuesses, interface }
32+
33+
-- now run our game using custom monad
34+
runAff_
35+
(\_ -> close interface)
36+
(runAppM env game)
37+
38+
-- All the impure API our monad needs to support to run the program
39+
-- Normally, this would be broken up into more type classes for higher
40+
-- granularity.
41+
class (Monad m) <= GameCapabilities m where
42+
notifyUser :: String -> m Unit
43+
getUserInput :: String -> m String
44+
45+
getRandomNumber :: m Int
46+
47+
getRemainingGuesses :: m Int
48+
49+
getPreviousGuesses :: m (Array Int)
50+
51+
storeGuess :: Int -> m Unit
52+
53+
-- Game logic using only the above capabilities
54+
game :: forall m.
55+
GameCapabilities m =>
56+
m Unit
57+
game = do
58+
remaining <- getRemainingGuesses
59+
notifyUser $ i "Guess the random number. It's between 1 and 10. You have "
60+
(remaining + 1) " guesses. Enter 'q' to quit. \
61+
\Invalid inputs will not count against you."
62+
63+
result <- gameLoop
64+
65+
case result of
66+
PlayerWins correctAnswer remainingGuesses -> do
67+
notifyUser $ i "Player won! The answer was " correctAnswer
68+
notifyUser $ i "Player guessed the random number with " remainingGuesses
69+
" guesses remaining."
70+
PlayerLoses correctAnswer allGuessesMade -> do
71+
notifyUser $ i "Player lost! The answer was " correctAnswer
72+
notifyUser $ i "Guesses made: " (show allGuessesMade)
73+
PlayerQuits -> do
74+
notifyUser "Player quit the game. Bye!"
75+
76+
-- This function is normally NOT stack-safe because of its recursive nature.
77+
-- However, as long as the base/foundational monad is `Aff`, which is always
78+
-- stack-safe, we don't have to worry about this concern.
79+
gameLoop :: forall m. GameCapabilities m => m GameResult
80+
gameLoop = do
81+
prev <- getPreviousGuesses
82+
notifyUser $ i "\n\
83+
\Prevous guesses: " (show prev)
84+
nextInput <- getUserInput "Guess a number between 1 - 10: "
85+
case nextInput of
86+
"q" -> pure PlayerQuits
87+
x -> case fromString x of
88+
Nothing -> do
89+
notifyUser $ i "'" x "' is not an integer. Try again (Use 'q' to quit)."
90+
gameLoop
91+
Just guess
92+
| not (between 1 10 guess) -> do
93+
notifyUser $ i guess " was not between 1 and 10. Try again."
94+
gameLoop
95+
| elem guess prev -> do
96+
notifyUser $ i "You already guessed " guess " previously. \
97+
\Please guess a different number."
98+
gameLoop
99+
| otherwise -> do
100+
answer <- getRandomNumber
101+
if (guess == answer) then do
102+
remaining <- getRemainingGuesses
103+
pure $ PlayerWins guess remaining
104+
else do
105+
remaining <- getRemainingGuesses
106+
if remaining <= 0 then do
107+
pure $ PlayerLoses answer (prev `snoc` guess)
108+
else do
109+
storeGuess guess
110+
gameLoop
111+
112+
data GameResult
113+
= PlayerWins Int Int
114+
| PlayerLoses Int (Array Int)
115+
| PlayerQuits
116+
117+
-- ReaderT Design Pattern
118+
119+
type Environment = { randomNumber :: Int
120+
, interface :: Interface
121+
, remainingGuesses :: Ref Int
122+
, previousGuesses :: Ref (Array Int)
123+
}
124+
125+
newtype AppM a = AppM (ReaderT Environment Aff a)
126+
127+
-- Given an Environment value, we can convert an `AppM` computation
128+
-- into an `Aff` computation.
129+
runAppM :: Environment -> AppM ~> Aff
130+
runAppM env (AppM m) = runReaderT m env
131+
132+
derive newtype instance functorAppM :: Functor AppM
133+
derive newtype instance applicativeAppM :: Applicative AppM
134+
derive newtype instance applyAppM :: Apply AppM
135+
derive newtype instance bindAppM :: Bind AppM
136+
derive newtype instance monadAppM :: Monad AppM
137+
derive newtype instance monadEffectAppM :: MonadEffect AppM
138+
derive newtype instance monadAffAppM :: MonadAff AppM
139+
140+
-- Since Environment is a type alias, we need to use `TypeEquals`
141+
-- to make this work without newtyping Envirnoment.
142+
-- This limitation will likely be removed in a future PS release
143+
instance monadAskAppM :: TypeEquals e Environment => MonadAsk e AppM where
144+
ask = AppM $ asks from
145+
146+
-------------------------
147+
148+
instance gameCapabilitiesAppM :: GameCapabilities AppM where
149+
notifyUser :: String -> AppM Unit
150+
notifyUser = log
151+
152+
getUserInput :: String -> AppM String
153+
getUserInput prompt = do
154+
iface <- asks _.interface
155+
-- Note: `purescript-node-readline-aff` isn't in the package set,
156+
-- so we'll reimplement `question` in this file.
157+
liftAff $ question prompt iface
158+
159+
getRandomNumber :: AppM Int
160+
getRandomNumber = asks _.randomNumber
161+
162+
getRemainingGuesses :: AppM Int
163+
getRemainingGuesses = asks _.remainingGuesses >>= (liftEffect <<< Ref.read)
164+
165+
getPreviousGuesses :: AppM (Array Int)
166+
getPreviousGuesses = asks _.previousGuesses >>= (liftEffect <<< Ref.read)
167+
168+
storeGuess :: Int -> AppM Unit
169+
storeGuess guess = do
170+
rec <- ask
171+
liftEffect do
172+
Ref.modify_ (_ - 1) rec.remainingGuesses
173+
Ref.modify_ (\a -> a `snoc` guess) rec.previousGuesses
174+
175+
-- Provides an Aff wrapper around Node.Readline.question
176+
question :: String -> Interface -> Aff String
177+
question prompt iface =
178+
makeAff \runCallback -> do
179+
NRL.question prompt (\value -> runCallback (Right value)) iface
180+
pure nonCanceler

0 commit comments

Comments
 (0)