This is the 2nd version of the Number Guessing Game which uses random numbers to generate the mystery number. (Make sure you check out how that first version works before you tackle this one). This new version of the game will give you a good overview of how to generate and use random numbers in a typical Elm application. You can play a demo of the game here:
https://gitcdn.xyz/repo/kittykatattack/randomNumberGame/master/index.html
A huge thanks to Petre Damoc who contributed all this new code and explained to me how it works. You can read his original code here, and the Reddit discussion about it here.
Here are the important new additions to the game:
First, there's a new Signal called newRandIntSignal that's added
as an input to the app in the Main.elm file.
app =
StartApp.start
{ init = init
, update = update
, view = view
, inputs = [Game.newRandIntSignal]
}In the Game.elm file, a new function called newRandIntSignal converts Time.timestamp into an Action.
newRandIntMB = Signal.mailbox ()
newRandIntSignal =
newRandIntMB.signal
|> Time.timestamp
|> Signal.map toActionThis is required because the timestamp is needed to generate a
seed for the random number generator.
Next, the timestamp is converted
into a random integer between 1 and 100, and a new action called NewMysteryInt runs.
toAction (t, _) =
let
(newInt, _) = generate (int 1 100) (initialSeed (truncate t))
in
NewMystery newIntThe NewMystery action in the update function supplies the new random
integer, newInt, and uses it to update the model's mysteryNumber.
NewMystery newInt ->
noFx { model | mysteryNumber = newInt}The noFx function is just a handy way to help de-clutter boilerplate code
if you want to return a model but don't need to run any effects.
noFx model = (model, Effects.none)The first random number is generated when the model is initialized,
by calling the newIntEffect Effect.
init : (Model, Effects Action)
init =
({ mysteryNumber = 50
, maxGuesses = 10
, guessesMade = 0
, gameState = Started
, inputValue = 0
}, newIntEffect)
newIntEffect =
Signal.send newRandIntMB.address ()
|> Task.map (always (EnterText ""))
|> Effects.taskIt can be a little tricky to understand how newIntEffect works.
Here's a description of how this works from the Reddit
thread:
The
newIntEffectis basically a task that will be run eventually by the runtime. It does not actually do anything in the code, it is only defined or described. As I said, this is tricky, especially if the primary experience is with an imperative language where you call things one after the other. In Elm you just declare things. You can play with the app and see it stop working once you comment out the port tasks lines. Those two lines are essential for the routing of the Tasks to the runtime. Only there do they have a chance to be executed.
Why does this do: Task.map (always (EnterText ""))?
The task produced by
Signal.sendneeds to get to the runtime and be executed. With current Effects library, this means that the result of the task needs to be of typeAction. After the task is executed in the runtime, this resulting action is sent back into the program BUT in the context of the send we are not interested in the result of that task. So, the main pattern I've seen so far is to just add anNoOpactionfor such cases BUT, in your case, I just re-purposedEnterText. It doesn't really matter ifEnterTextarrives before or afterNewMystery, the state of the model would be the same.
This new version of the game also includes some additional, more cosmetic, improvements to the code.
###Making a conditional statement more readable
In the first version of the Number Guessing Game, the checkGameState
function looked like this:
checkGameState model =
if model.guessesMade >= model.maxGuesses && model.inputValue /= model.mysteryNumber
then
Lost
else if model.guessesMade <= model.maxGuesses && model.inputValue == model.mysteryNumber
then
Won
else InProgressIt works, but it's a bit verbose and difficult to read. By using pattern matching with a tuple, you can use this much more concise code:
checkGameState model =
case
( model.guessesMade >= model.maxGuesses
, model.inputValue == model.mysteryNumber
)
of
(True, _) -> Lost
(_, True) -> Won
_ -> InProgressJust follow this same format if you have additional cases.
###Using the String.Interpolate package
This new code also uses the String.Interpolate package to help make
complex string concatenation much more readable. Here's how it's used
in the stateMessage function in the view.
stateMessage model =
let
stateToString =
if model.gameState == Started || model.gameState == InProgress
then interpolate
", Guess Number: {0} , Max Guesses: {1}"
[toString model.guessesMade , toString model.maxGuesses ]
else ""
in
interpolate
" Your guess: {0}, State: {1}, Mystery Number: {2} {3}"
[ toString model.inputValue
, toString model.gameState
, toString model.mysteryNumber
, stateToString]And that's it!