Introduktion til Elm

Funktionel programmering

Først til 100!

Så er vi i gang, og vores første dynamiske hjemmeside virker! I dette kapitel skal vi udvikle vores hjemmeside til et egentlig spil. Spillets regler er som følger:

Talspillet
Vores hjemmeside, som vi byggede i kapitel 5, er et rigtig godt udgangspunkt for koden, så lad os gå i gang med det samme!
Øvelse

Vi begynder med at bygge funktionaliteten, så spillerne kan lægge tallene fra 1 til 9 til. Det kan gøres ved at løse følgende delopgaver:

  1. Begynd med at lave de relevante beskeder. De kunne for eksempel hedde AddOne, AddTwo, osv.
  2. Modificer update-funktionen, så den kan håntere alle de forskellige beskeder.
  3. Lav nu tilsvarende knapper i view-funktionen.
  4. Spil spillet med en makker et par gange. Bare for sjov :)

Spillet er måske lidt for let, og du har måske allerede opdaget, at spiller 2 let kan vinde ved at sørge for altid at ramme 10, 20, 30, osv. Lad os prøve at gøre det lidt mere udfordrende for spillerne.

Øvelse

Tilføj en knap, der ganger tællerens nuværende tal med sig selv! Du skal naturligvis også lave den tilhørende funktionalitet.

Spillet fungerer jo i princippet fint, men det kræver at spillerne selv sørger for at stoppe, når den ene rammer 100. Det kigger vi på nu: Lad os beslutte, at hvis en spiller gør noget, så tælleren kommer over 100, så taber den pågældende spiller, og tælleren skal sættes til 0 igen.

Øvelse:

Modificer update-funktionen, så den tjekker om tælleren er røget over 100. Måske kan du lade dig inspirere af nedenstående kode:

case ((model + 7) > 100) of
	True ->
		0
	False ->
		model + 7

Hint!

Man kan godt lave pattern matching inden i pattern matching:

case message of 
	AddOne ->
		case ((model + 1) > 100) of
			True ->
				0
			False ->
				model +1
	AddTwo ->
		case ((model + 2) > 100) of 
			True ->
				0
			False ->
			 model + 2
	AddThree ->
		...
	

Så langt så godt! Vi mangler stadig at udråbe en vinder, når den ene spiller rammer 100. For at gøre det må vi udvidde vores Model, så den ikke bare er et tal, for nu skal den også indeholde en besked til vinderen! En besked må være en String, så vi skal udvidde vores model til at være af typen:

type alias Model =
	{ count : Int
	, message : String
	}

Vi bruger altså en Record som Model, så modellen kan huske flere værdier samtidig. Når vi ændrer modellen skal vores nuværende kode tilpasses. Resten af koden skal jo vide, hvor den skal slå henholdsvis count og message op. Lad os se på et eksempel.

Eksempel
Lad os sige, at vi har defineret typen Model som følger, og at model er af denne type:
type alias Model =
	{ count : Int
	, message : String
	}

model : Model
model =
	{count = 12
	, message = "Tillykke! Du har vundet"
	} 
Nu kan vi tilgå værdierne i model på følgende måde:
model.count
returnerer værdien 12, mens
model.message

returernerer værdien "Tillykke! Du har vundet".

Dette kan vi bruge i vores view-funktion. For eksempel således:
view: Model -> Html Msg
view model =
	Html.div [] 
		[ Html.text (stringFromInt model.count)
		, Html.text model.message
		]
Dette vil først vise tallet i vores model.count, og dernæst beskeden i model.message.

Nu har vi styr på, hvordan man læser information fra en Record, men vi skal også se, hvordan man laver en Record med den information, som vi gerne vil have! Det foregår i update-funktionen, og her er et eksempel.

Eksempel
Følgende ligner din nuværende update-funktion
update: Msg -> Model -> Model
update message model = 
	case message of
		AddOne ->
			model + 1
		AddTwo ->
			model +2
		...

Din update-funktion er lige nu bygget til at returnere en Model som faktisk bare er et Int. Når vi nu har ændret vores Model type til at være en Record, så skal output fra vores update-funktion også være en tilsvarende Record - ellers ændrer modellen jo type undervejs i kredsløbet, og det går ikke!

Vi kan rette ovenstående til på følgende måde:

update: Msg -> Model -> Model
update message model = 
	case message of
		AddOne ->
			{ model | count = model.count + 1 }
		AddTwo ->
			{ model | count = model.count + 2 }
		...

Lad os lige gennemgår hvad følgende betyder:

{ model | count = model.count + 1 }
De to {} betyder, at vi nu laver en Record. Det der står til venstre for | angiver, den Record vi er i gang med at lave skal være en kopi af model, som jo netop er en Record. Det der står til højre for | angiver de ændringer, vi gerne vil lave i vores nye Record i forhold til model. I eksempelet vil vi altså gerne øge count med én i forhold til den gamle model.

Hel konkret, så lad os forestille os at:

model == {count = 13, message = "Hej"}
så vil vi have:
 { model | count = model.count + 1 } == {count = 14, message = "Hej"}

Det er faktisk næsten som at læse matematik. Du kan tænke på ovenstående som: Vi vil gerne lave en ny Record, som tager udgangspunkt i model men hvorom det gælder, at count = model.count + 1.

Øvelse
Nu skal vi vise en besked, når en spiller rammer 100! Vi deler øvelsen op i følgende delopgaver.
  1. Begynd med at ændre din model, så den har typen:
    type alias Model =
    	{ count : Int
    	, message : String
    	}
  2. Modificer din init-funktion, så vores hjemmeside starter med følgende model:
    init =
    	{ count = 0
    	, message = ""
    	}
  3. Modificer din view-funktion, så den kan tage imod værdier fra den nye Model type.
  4. Modificer update-funktionen, så den returnerer objekter af den nye Model type.
  5. Tjek at det hele virker som før!
  6. Modificer din view-funktion, så den viser modellens message. Du kan for eksempel gøre det ved at tilføje følgende:
    Html.div [] [ Html.text model.message]
  7. Modificer din update-funktion, så model.message ændres til "Tillykke! Du har vundet!", hvis en spiller rammer præcis 100.
  8. Modificer din update-funktion, så model.message igen sættes til "", når man trykker på reset-knappen.
  9. Tjek at det hele virker i browseren.

Sejt! Du har lavet dit allerførste spil i browseren! Det er måske ikke det mest avancerede spil, men herfra er det faktisk kun fantasien, der sætter grænser. Hvis du har overskud så følger her et par forslag til ekstraopgaver, som du kan arbejde videre med.

Ekstra øvelse: Husk et tal
Udvid dit spil, så spillerne kan vælge at "huske" et tal, som tælleren viser. Senere i spillet skal spillerne kunne lægge dette tal til tælleren.
Ekstra øvelse: Styling
Det er sjovere at spille et spil, der er flot! Nu skal du få dit spil til at ligne en lommeregner. I mappen assets ligger der en fil der hedder numbergame.css. CSS-filen indeholder tre klasser: calculator, display og buttons. CSS-filen er allerede linket til din template, så du kan bruge de tre klasser. Du kan for eksempel bruge de tre klasser sådan her:
view : Model -> Html Msg
view model =
    Html.div []
        [ Html.header []
            [ Html.h1 [] [ Html.text "Lommeregner-spillet!" ]
            ]
        , Html.div [ Html.Attributes.class "calculator" ]
            [ Html.div [ Html.Attributes.class "display"] 
                [ Html.text (String.fromInt model.count) ]
            , Html.div [ Html.Attributes.class "buttons"]
                [ Html.button [ Html.Events.onClick (AddOne) ]
                    [ Html.text "+1" ]
                , Html.text " "
                , Html.button [ Html.Events.onClick (AddTwo) ]
                    [ Html.text "+2" ]
                , Html.text " "
                , Html.button [ Html.Events.onClick (AddThree) ]
                    [ Html.text "+3" ]
                , Html.div []
                    [ Html.button [ Html.Events.onClick Reset ]
                        [ Html.text "Nulstil" ]
                    ]
                ]
            ] 
        ]
Prøv at ændre i CSS-filen så dit spil, kommer til at så ud som du gerne vil have det.
Ekstra øvelse: 2 spillere (svær!)
Udvid dit spil, så det er let at se, hvis tur det er. Her er et par forslag til, hvordan man kunne gøre. vælg én af dem.