Level 4 - The game!
In memory, as you may know, the player opens two cards, one after another, and if they match they stay open. If they do not match, both cards are closed again. This repeats until all cards on the board are open. Before we start implementing the game logic, let's clean up a bit.
4.1 Housekeeping part one
Our deck of cards is a list of Cards and we will be passing them around in our program.
Therefore, instead of having to write List Card everywhere, we want to be able to write Deck.
Also, in the game we will be matching pairs of cards with each other, and will need some way to distinguish between two cards with the same image.
We will do this by saying that a card can be either in group A or in group B. Use a custom type to achieve this, and add it as a field in our Card type.
With this we can check if two cards are of one pair by comparing their id and group fields!
Task:
- Create a type alias for our deck of cards.
- Create a custom type for representing the group of a card.
- Add
groupas a field in ourCardtype
4.2 Housekeeping part two
By now our Main.elm file is getting quite big, so we should probably do something about that.
It is common in Elm projects to have the application's model and associated types in their own file(s), so let's try that.
Task:
- Move all types and type aliases to the file
Model.elm- See Note on modules below
- To use our types in
Main.elmwe also need to import them. This is done in the same way as we import theHtmlmodule (import Html exposing (..))
In addition to this, let's pretend we're famous TV chefs and cheat a little bit. We have prepared a module DeckGenerator that can be used to generate a deck of cards.
Use this by importing DeckGenerator in Main.elm and using the DeckGenerator.static value as model's initial value.
Note on modules
A module's name must match its filename. For example:
- Filename:
GameLogic.elm-> module name:GameLogic- Filename:
Models/User.elm-> module:Models.UserYou must also be explicit with what your module exposes to the public by listing them along with the module declaration. For type aliases and functions you just list their names, but for custom types you have two choices:
- exposing only the type (often called opaque types)
- exposing both the type and its constructors.
When exposing only the type you list its name, but when you expose its constructors too you add
(..)after the type name.Example:
module MyModule exposing (MyTypeAlias, myFunction, MyCustomType(..), MyOpaqueCustomType) type alias MyTypeAlias = { name : String } myFunction : Int -> Int myFunction number = number * 2 type MyCustomType = Foo | Bar Int type MyOpaqueCustomType = MyOpaqueCustomType { name : String }
4.3 Game logic!
Our game implementation will have three states:
Choosing- the player chooses the first cardMatching- the player chooses the second card to match with the firstGameOver- all cards are matched and the player has won
The game logic will flow like this:
- When the player chooses the first card he is in the
Choosingstate:- Set all unmatched cards to
Closed - Set the chosen/clicked card to
Open - Go to
Matchingstate
- Set all unmatched cards to
- In the
Matchingstate, the player chooses his second card:- If it matches the first card, then set the two cards to
Matched. If the two cards do not match, set the clicked card toOpen.
- If it matches the first card, then set the two cards to
- If all cards are
Matched, then go toGameOverstate, else go toChoosingstate
The Model of our program should now change from consisting of just a Deck to being a GameState.
We also need a function that can handle the three different GameStates.
It should have the signature updateCardClick : Card -> GameState -> GameState.
Task:
- Implement the three game states as a custom type called
GameState - Implement the
updateCardClick : Card -> GameState -> GameStatefunction - Update your
updateandviewfunctions to accommodate for the new shape of our model - Now take a minute and pat yourself on the back for making an awesome game in Elm!
Hint:
The GameOver state does not need any extra data, but Choosing needs a Deck (the deck we are choosing from), and Matching needs both a Card (the card we are trying to match with) and a Deck (the deck we are choosing from).
When implementing updateCardClick there are a couple of things that will help:
- You can create a function
closeUnmatched : Deck -> Deckthat, as its name implies, sets all cards that are notMatchedtoClosed - You can use the built-in function
List.all : (a -> Bool) -> List a -> Boolto check if all cards are are matched - The
setCardState : CardState -> Card -> Cardcan be changed to operate on aDeckinstead;setCardState : CardState -> Card -> Deck -> Deck. This will fit nicely with using Elm's "pipe operator" - The "pipe operator" (
|>) is great when you have multiple functions that all depend on the result of the previous function. For example:
myString =
String.toUpper (String.repeat 2 (String.reverse "olleh"))
-- myString == "HELLOHELLO"
can be written as
myString =
"olleh"
|> String.reverse
|> String.repeat 2
|> String.toUpper
-- myString == "HELLOHELLO"
In short: myFunction myArgument == myArgument |> myFunction.
This will prove useful with the updated setCardState function.
- "Let expressions" is a way of storing intermediate values (kind of like variables). The above example can also be written as:
myString =
let
reversed = String.reverse "olleh"
repeated = String.repeat 2 reversed
in
String.toUpper repeated
-- myString == "HELLOHELLO"
It's a way of saying "Let reversed and repeated be defined in the following expression but nowhere else".
Optional:
Refreshing the page every time you want to play another game is boring, so try to add a "restart game" button in the "Game over" view. Hint: it is common to have a top-level value called
initthat contains the initial state of themodel.