Píšeme hru Wordle v Elmu

Publikováno: 21.2.2022

Elm logo
O Elm už vyšlo na Zdrojáku několik tutoriálů. Dnes si ukážeme vytvoření aplikace. Konkrétně do Elm přepíšeme populární hru Wordle.

Celý článek

Elm logo

Wordle je jednoduchá hra, která vzala internet útokem. Deník The New York Times ji od autora koupil za miliony dolarů. Počítač každý den vybere slovo o pěti písmenech, které máte najít. Máte na to šest pokusů. Počítač vám po každém pokusu poradí, zda se vámi zadané písmeno vyskytuje na správném místě, na jiném místě, nebo vůbec. V tomto článku si zjednodušenou verzi Wordle naimplementujeme v jazyce Elm.

Elm?

Začněme kratičkým úvodem k Elmu samotnému.

Elm je „čistě“ funkcionální jazyk, což zní honosně, ale znamená to pouze, že místo side efektů volaných a okamžitě vykonaných uvnitř jakékoli funkce má efekty explicitní. S HTTP dotazy, generováním náhodných dat, čtením aktuálního času a podobnými úkony se tedy pracuje jinak než např. v JavaScriptu, nicméně pro Wordle nic takového nebudeme potřebovat.

Elm se kompiluje do JavaScriptu a v naprosté většině se používá pro webové aplikace, a tak nepřekvapí, že se dá nainstalovat přes JS nástroje npm a yarn:

$ npm install elm  # nebo
$ yarn add elm

případně si můžete kompilátor nainstalovat skrze instalátor nebo váš oblíbený balíčkový manažer.

Bez instalace lze Elm vyzkoušet v editoru Ellie(pro potřeby tohoto článku bude naprosto stačit). Pokud znáte např. Codepen nebo JSFiddle, budete v něm jako doma.

???? Na Zdrojáku najdete seriál Elm – Hello world on the map a Velmi rychlý přehled Elm. ????

Hotová aplikace

Výsledek našeho snažení si můžete předem prohlédnout a zahrát na tomto Ellie odkaze. Kromě Elmu použijeme pouze CSS framework Tailwind.

Výsledek našeho snažení

Začínáme

Po prvním otevření Ellie(a souhlasu s podmínkami použití) uvidíte webový editor s předpřipraveným kódem pro jednoduchou Elm aplikaci. Kliknutím na tlačítko Compile se kód nalevo zkompiluje a aplikace spustí v pravé části editoru:

Editor Ellie

Ze všeho nejdříve do HTML kódu zapojíme slibovaný Tailwind, který nám umožní stylovat elementy zkráceným zápisem: např. <div class="border font-bold">Ahoj!</div> aplikuje styly border-width: 1px (docs) a font-weight: 700 (docs).

Upravte tedy v Ellie spodní levý panel HTML: mezi <head> a <style> přidejte řádek:

  <script src="https://cdn.tailwindcss.com"></script>

Pro vyzkoušení, že je Tailwind zapojený správně, upravíme Elm kód (levý horní panel) a Tailwind zkusíme použít. Mezi importy přidejte:

import Html.Attributes exposing (class)

což nám umožní ve view funkci přidat CSS třídy např. hlavnímu <div>u:

view : Model -> Html Msg
view model =
    div [ class "bg-sky-200 border border-sky-400 p-2 m-2" ]
      -- zbytek zůstává při starém

Po zkompilování by měl původní obsah aplikace být zabalen do modrého kabátku:

Tailwind třídy zapojeny!

Takhle připravený funkční kód najdete na této Ellie.

„The Elm Architecture“

Než začneme se hrou samotnou, pokusím se stručně popsat, jak Elm aplikace fungují. Protože je Elm čistě funkcionální, všechny funkce mají povoleno pouze vrátit data – žádná funkce nemůže dělat nic ve smyslu zápisu či čtení z databáze nebo vytváření DOM elementů na webové stránce.

Jak ale potom můžeme udělat cokoliv užitečného? S trochou nadsázky funkce místo toho, aby rovnou „odpálila rakety,“ může jen vrátit lísteček, na kterém je napsáno „prosím odpal rakety.“ Elm ke každému zkompilovanému programu přibalí i runtime, který jednak celou aplikaci odstartuje, ale zároveň potom sleduje návratové hodnoty určitých funkcí a podle těch vykonává efekty. Tím je programátor od side efektů odstíněn, jeho úkolem je jen vracet data.

Runtime

Runtime vykonává zhruba následující smyčku:

Diagram The Elm Architecture

Model a Msg jsou data, init, view a update jsou funkce.

  • Model je stav aplikace
  • Msg je událost, typicky vyvolaná uživatelem (například zmáčknutí tlačítka, psaní do formuláře a podobně)
  • funkce init vytvoří počáteční stav
  • funkce update z aktuálního stavu a události vytvoří stav nový
  • funkce view je volána po každé změně stavu a diktuje, jak má aktuální stav být zobrazen na samotné webové stránce

Za zmínku stojí, že vše funguje na deklarativním principu: „Chci, aby DOM vypadal takto, a je mi jedno, jak vypadá momentálně. Runtime, snaž se.“ Tedy naprostý protiklad například jQuery (budiž mu země lehká), kde programátor upravoval DOM ručně.

Elm runtime by teoreticky mohl celý DOM pokaždé smazat a vytvořit znovu, ale je přece jen trochu šetrnější. Srovná si to, co funkce view vrátila předtím a co vrátila nyní, udělá si diff a na reálném DOMu vykoná jen to nejnutnější, aby byl úkol splněn.

Tato smyčka má ekvivalent v React+Redux světě (a byla koneckonců těmito knihovnami zpopularizována): React by se dal připodobnit k funkci view a Redux k init, Msg, Model a update.

Programátor tak pracuje celou dobu s funkcemi bez side-efektů. Ty jsou vytěsněny mimo jazyk Elm do onoho runtime.

Wordle, virtuální klávesnice

Dost teorie, pojďme si něco konečně naprogramovat.

Tip: Doporučuji pro „hlubší prožitek“ kód nekopírovat, ale přepisovat ručně. Minimálně mně samotnému toto pomáhá lépe nové informace udržet a více nad kódem uvažovat.

Ve Wordle hráč vidí jen dvě věci: klávesnici a mřížku 5×6 pro své pokusy. Vevnitř view to tedy bude vypadat zhruba takto:

-- místo původní funkce view

view : Model -> Html Msg
view model =
    div []
        [ viewGrid
        , viewKeyboard
        ]


viewGrid : Html Msg
viewGrid =
    text "TODO grid"


viewKeyboard : Html Msg
viewKeyboard =
    text "TODO keyboard"

Načrtněme klávesnici: jsou to tři řádky s QWERTY rozložením, kde spodní řádek navíc bude obsahovat Enter a Backspace. Máme tedy tři různé druhy tlačítek, které budou dohromady vysílat tři různé druhy akcí: přidej znak, zruš posledně přidaný znak, potvrď pokus:

-- místo původního typu Msg

type Key
    = CharKey Char
    | BackspaceKey
    | EnterKey


type Msg
    = AddChar Char
    | RemoveLastChar
    | EnterGuess

Tímto jsme vytvořili úplně nový typ Key, pomocí kterého za pár chvil přesně a stručně vyjádříme klávesnici, a typ Msg nyní obsahuje tři akce, které pokrývají veškerou interakci, kterou ke hře Wordle budeme potřebovat.

Pokud po přidání těchto dvou typů nyní zkusíte aplikaci zkompilovat (dejte pozor na to, že typ Msg se v aplikaci už jednou vyskytuje – duplicity nejsou povoleny), zjistíte, že se kompilátoru něco nelíbí. Akce Increment a Decrement, které jsme smazali, jsou stále ještě použity v původním kódu, konkrétně ve funkci update:

Kompilátor upozorňuje, že jsme zapomněli smazat staré Msg

Abychom kompilátor uklidnili, funkci update zatím pro jednoduchost nahradíme jedním velkým TODO: reakce na uživatelovu interakci a změny stavů nás budou zajímat až potom, co vyřešíme náš view.

-- místo původní funkce update

update : Msg -> Model -> Model
update msg model =
    Debug.todo "update"

Funkce Debug.todo nám umožní vložit do kódu zástupnou hodnotu za něco, co budeme chtít řešit později. Zkompiluje se do JavaScriptového throw ..., a tedy zastaví chod aplikace. To nás zatím trápit nemusí, protože naše view funkce momentálně negeneruje žádné Msg, a tedy runtime nemá důvod volat funkci update.

Vraťme se zpátky k viewKeyboard a našemu novému typu Key, a zadefinujme, jak jdou klávesy za sebou:

keys : List (List Key)
keys =
    [ charKeyRow "QWERTYUIOP"
    , charKeyRow "ASDFGHJKL"
    , [ EnterKey ] ++ charKeyRow "ZXCVBNM" ++ [ BackspaceKey ]
    ]


charKeyRow : String -> List Key
charKeyRow string =
    string
        |> String.toList
        |> List.map CharKey

Funkce String.toList, kterou jsme použili ve funkci charKeyRow, převede String na List Char, tedy seznam znaků, ze kterých je daný řetězec složen.

Následně každý znak v tomto seznamu pomocí List.map proženeme funkcí CharKey, a získáme nový seznam (v Elmu je vše imutabilní) s těmito výsledky. Tento „pipeline“ (|>) zápis je ekvivalentní tomuto:

charKeyRow string =
    List.map (\char -> CharKey char) (String.toList string)

Za vysvětlení stojí ještě to, proč je CharKey funkce. Když jsme nadefinovali typ Key:

type Key
    = CharKey Char
    | BackspaceKey
    | EnterKey

stvořili jsme tím tři „konstruktory“ – tři způsoby, jak vytvořit hodnotu typu Key:

  1. funkci CharKey, která z Char udělá Key
  2. konstantu BackspaceKey, která už Key je
  3. konstantu EnterKey, totéž.

Tímpádem například CharKey 'A' je jedna z možných Key hodnot.

Kdybychom si nyní hodnotu keys zobrazili například skrze elm repl, viděli bychom cca něco takového:

Zkrácená klávesnice v REPL

Nyní nám už nic nebrání tyto tři řádky zobrazit ve viewKeyboard. Postupně rozbalíme keys : List (List Key) a každou z těchto tří vrstev (klávesnice, řádek, klávesa) převedeme na HTML. Znovu nám k tomu pomůže funkce List.map:

-- místo původní funkce viewKeyboard

viewKeyboard : Html Msg
viewKeyboard =
    div [ class "flex flex-col gap-2" ]
        (List.map viewKeyboardRow keys)


viewKeyboardRow : List Key -> Html Msg
viewKeyboardRow row =
    div [ class "flex flex-row justify-center gap-2" ]
        (List.map viewKeyboardKey row)


viewKeyboardKey : Key -> Html Msg
viewKeyboardKey key =
    div [ class "p-2 border border-gray-300 bg-gray-100 hover:bg-gray-50 cursor-pointer" ]
        [ text (keyLabel key) ]


keyLabel : Key -> String
keyLabel key =
    case key of
        CharKey char -> String.fromChar char
        EnterKey -> "ENTER"
        BackspaceKey -> "⬅"

V tuto chvíli byste měli po zkompilování vidět virtuální klávesnici (pokud ne, můžete svůj kód porovnat s Ellie pro tuto sekci).

Vykreslená klávesnice

Na chvíli se zastavím u naší nové funkce keyLabel. Ta ilustruje, jakým způsobem se v Elmu pracuje s těmito „custom typy“ jako Key nebo Msg. Syntaxe case .. of může připomínat switch, ale oproti switchi má výhodu v tom, že programátora hlídá, jestli se vyjádřil ke všem možným stavům (exhaustive pattern matching). To oceníte ve chvíli, kdy přidáváte do typu novou variantu. Například kdybychom přidali do typu Key variantu BossKey a chtěli aplikaci zkompilovat, kompilátor nám připomene všechna místa, kde se musíme rozhodnout, co s touto hodnotou dělat:

Ukázka case..of

Záchytný bod pro tuto kapitolu najdete na této Ellie.

Hrací mřížka

Klávesnici bychom měli, ale nad ní se nám připomíná "TODO grid". Pojďme tedy zobrazit samotnou hrací mřížku. Nejprve se zamysleme, co by měla funkce viewGrid očekávat na vstupu. Určitě jí musíme dodat předchozí hráčovy pokusy, např. ["HELLO", "THERE"]. Budeme také potřebovat držet aktuální nedokončený pokus (0-5 napsaných písmen): např. "WOR". A abychom tyto hráčovy pokusy mohli „oznámkovat“ (vybarvit písmena šedě / žlutě / zeleně), musíme znát i hledané slovo ("WORLD"). Dohromady nám to tedy dává pro stav aplikace typ:

-- místo původního typu Model

type alias Model =
    { guesses : List String
    , nextGuess : String
    , word : String
    }

Náš počáteční stav bude kromě hádaného slova zet prázdnotou:

-- místo původní hodnoty initialModel

initialModel : Model
initialModel =
    { guesses = []
    , nextGuess = ""
    , word = "WORLD"
    }

Nicméně abychom mohli iterovat nad hrací mřížkou, hodilo by se nám mít nějaký pokročilejší příklad. Zadefinujme si tedy příklad z předchozího odstavce:

exampleModel : Model
exampleModel =
    { guesses =
        [ "HELLO"
        , "THERE"
        ]
    , nextGuess = "WOR"
    , word = "WORLD"
    }

A nyní ho můžeme zapojit do funkce viewGrid. Ta momentálně nevyžaduje žádný parametr, takže jí jeden budeme muset přidat. Pozor, funkci zavolejte s hodnotou exampleModel, ne model. Až později v rámci kapitoly o interaktivitě tento model vyměníme za ten správný.

-- místo původních funkcí view a viewGrid

view : Model -> Html Msg
view model =
    div []
        [ viewGrid exampleModel
        , viewKeyboard
        ]


viewGrid : Model -> Html Msg
viewGrid model =
    text "TODO grid"

Na aplikaci, kdybyste ji zkompilovali a spustili, nepůjdou tyto změny zatím nijak poznat: model nijak nepoužíváme. Pojďme si znovu připomenout, jak má mřížka vypadat: šest řádků o pěti písmenech. Nejdříve tedy zobrazíme předchozí hráčovy pokusy, každý na jednom řádku, potom jeden řádek s aktuálně tvořeným pokusem, a poté zbytek prázdných řádků tak, abychom ve finále měli řádků šest.

-- místo původní funkce viewGrid

maxGuesses : Int
maxGuesses =
    6

viewGrid : Model -> Html Msg
viewGrid model =
    let
        isNextGuessEmpty : Bool
        isNextGuessEmpty =
             String.isEmpty model.nextGuess

        currentGuessRows : Int
        currentGuessRows = 
            if isNextGuessEmpty then
                0

            else
                1

        unusedGuesses : Int
        unusedGuesses =
            maxGuesses
                - List.length model.guesses
                - currentGuessRows

        nextGuessRow : List (Html Msg)
        nextGuessRow =
            if isNextGuessEmpty then
                []

            else
                [ viewNextGuess model.nextGuess ]

        scoredGuesses : List (List ( Char, Score ))
        scoredGuesses =
            List.map (scoreGuess model.word) model.guesses
    in
    div [ class "flex flex-col gap-2" ]
        (List.map viewScoredGuess scoredGuesses
            ++ nextGuessRow
            ++ List.repeat unusedGuesses emptyRow
        )

viewNextGuess : String -> Html Msg
viewNextGuess nextGuess =
    text "TODO viewNextGuess"

viewScoredGuess : List ( Char, Score ) -> Html Msg
viewScoredGuess scoredGuess =
    text "TODO viewScoredGuess"

emptyRow : Html Msg
emptyRow =
    text "TODO emptyRow"

scoreGuess : String -> String -> List ( Char, Score )
scoreGuess word guess =
    Debug.todo "scoreGuess"

type Score
    = Missed -- grey
    | PresentElsewhere -- yellow
    | Correct -- green

Vevnitř let..in bloku můžeme vidět onu kalkulaci, kolik prázdných řádků bude třeba zobrazit (unusedGuesses). Kromě toho zde i top-down způsobem pomalu rozkrýváme, co za funkce budeme muset naimplementovat dále, a každé z nich jsme dali TODO implementaci.

Kromě toho máme i nový typ Score pro oznámkování a vybarvení každého písmena v předchozích pokusech. Pokud písmeno bude zelené, znamená to, že hráč se trefil i písmenem i místem. Pokud bude žluté, trefil se jen písmenem, nicméně toto písmeno bude na jiné pozici. Pokud bude šedé, nevyskytuje se v hledaném slově nikde.

Pojďme tuto logiku z předchozího odstavce naimplementovat. Použijeme k tomu datovou strukturu Set, tedy množinu, a užitečnou funkci List.map2, která ze dvou seznamů udělá seznam nový: zavolá danou funkci nad prvními prvky seznamu, poté nad druhými prvky, a tak dále:

List.map2 fn [10,20,30] [100,200,300]
-->
[ fn 10 100
, fn 20 200
, fn 30 300
]

To se nám bude hodit: místo toho, abychom například zjišťovali, na jakých indexech jsou která písmena, prostě jen „nalajnujeme“ tyto dva seznamy vedle sebe, a písmeno bude zelené pouze, když budou tato písmena na stejných indexech souhlasit. Viz níže:

-- nahoře mezi importy

import Set exposing (Set)

-- místo původní funkce scoreGuess

scoreGuess : String -> String -> List ( Char, Score )
scoreGuess word guess =
    let
        guessChars : List Char
        guessChars =
            String.toList guess
      
        wordChars : List Char
        wordChars =
            String.toList word

        wordSet : Set Char
        wordSet =
            Set.fromList wordChars
    in
    List.map2
        (\correctChar guessChar ->
            ( guessChar
            , if correctChar == guessChar then
                Correct

              else if Set.member guessChar wordSet then
                PresentElsewhere

              else
                Missed
            )
        )
        wordChars
        guessChars

Pro lepší představu znovu ukážu v elm repl, co z této funkce vypadne na několika příkladech:

Skórování pokusu

Pokud nyní zkusíte kód zkompilovat a spustit, nad virtuální klávesnicí uvidíte jen nevzhlednou změť TODOs. Pojďme na ně:

-- místo původní funkce view (vylepšíme styly)

view : Model -> Html Msg
view model =
    div [ class "p-8 flex flex-col gap-8 items-center" ]
        [ viewGrid exampleModel
        , viewKeyboard
        ]

-- místo původní funkce viewScoredGuess

rowClasses : String
rowClasses =
    "gap-2 flex flex-row"

cellClasses : String
cellClasses =
    "w-[58px] h-[58px] border border-gray-400 text-gray-600 text-xl flex items-center justify-center"

viewScoredGuess : List ( Char, Score ) -> Html msg
viewScoredGuess chars =
    div [ class rowClasses ]
        (List.map viewScoredChar chars)

viewScoredChar : ( Char, Score ) -> Html msg
viewScoredChar ( char, score ) =
    div
        [ class cellClasses
        , class "text-white"
        , class (scoreClass score)
        ]
        [ text (String.fromChar char) ]

scoreClass : Score -> String
scoreClass score =
    case score of
        Correct ->
            "bg-lime-500"

        PresentElsewhere ->
            "bg-yellow-500"

        Missed ->
            "bg-gray-400"

Po kompilaci a spuštění byste měli nyní vidět hráčovy pokusy, hezky oznámkované. Naše hrací plocha se pomalu začíná rýsovat!

Hrací plocha se začíná rýsovat

Pokračujeme hráčovým aktuálním pokusem. Kromě písmen z tohoto pokusu budeme muset zobrazit ještě prázdná políčka – tak, aby jich na řádku bylo dohromady pět.

-- místo původní funkce viewNextGuess

wordSize : Int
wordSize =
    5

viewNextGuess : String -> Html Msg
viewNextGuess nextGuess =
    let
        padding : Int
        padding =
            wordSize - String.length nextGuess

        guessCells : List (Html Msg)
        guessCells =
            nextGuess
                |> String.toList
                |> List.map viewNextGuessChar

        emptyCells : List (Html Msg)
        emptyCells =
            List.repeat padding emptyCell
    in
    div [ class rowClasses ]
        (guessCells ++ emptyCells)

viewNextGuessChar : Char -> Html Msg
viewNextGuessChar char =
    div [ class cellClasses ]
        [ Html.text (String.fromChar char) ]

emptyCell : Html Msg
emptyCell =
    div [ class cellClasses ] []
Zobrazujeme další hráčův pokus

Nyní už jako poslední TODO zbývá jen funkce emptyRow. Nebude nijak složitá, jen zobrazíme pět prázdných políček (funkci pro zobrazení prázdného políčka emptyCell jsme si nadefinovali o krok výše).

-- místo původní funkce emptyRow

emptyRow : Html Msg
emptyRow =
    div [ class rowClasses ]
        (List.repeat wordSize emptyCell)
Statický pohled hotov

Jako v předchozích kapitolách i zde nabízím záchytný bod (Ellie).

Interaktivita

Zdálo by se, že máme hotovo. Ale momentálně staticky zobrazujeme ukázkový stav a ignorujeme ten skutečný, a i kdyby tomu tak nebylo, tak se momentálně hráč může snažit jak chce, ale hra všechny jeho kliky ignoruje. Pojďme to změnit. Nejdříve tedy začneme používat reálný model ve funkci view:

-- místo původní funkce view

view : Model -> Html Msg
view model =
    div [ class "p-8 flex flex-col gap-8 items-center" ]
        [ viewGrid model
        , viewKeyboard
        ]

Nyní potřebujeme začít generovat Msg události při kliku na naši virtuální klávesnici. K tomu využijeme funkci Html.Events.onClick:, kterou už máme naimportovanou z dřívějška.

-- místo původní funkce viewKeyboardKey

viewKeyboardKey : Key -> Html Msg
viewKeyboardKey key =
    div
        [ class "p-2 border border-gray-300 bg-gray-100 hover:bg-gray-50 cursor-pointer"
        , onClick (keyMsg key)
        ]
        [ text (keyLabel key) ]

keyMsg : Key -> Msg
keyMsg key =
    case key of
        CharKey char ->
            AddChar char

        EnterKey ->
            EnterGuess

        BackspaceKey ->
            RemoveLastChar

V tuto chvíli naše klávesy konečně něco dělají! Zmáčkněte libovolnou klávesu a…

Chybová hláška z Debug.todo ve funkci update

…a kruci. Zapomněli jsme tyto Msg zpracovat ve funkci update. Chybová hláška říká, že se máme podívat na řádek 50 (aspoň v mém případě. Podle toho, jak šetříte řádky, možná bude vaše číslo jiné. Mimochodem, zkuste zmáčknout „Format Elm Code“ tlačítko vlevo od tlačítka „Compile“!).

Na řádku 50 nacházíme Debug.todo "update" vevnitř funkce update. Pojďme tedy něco naimplementovat. Nejprve uděláme case..of, s jedním Debug.todo pro každou variantu Msg.

-- místo původní funkce update

update : Msg -> Model -> Model
update msg model =
    case msg of
        AddChar char ->
            Debug.todo "AddChar"

        RemoveLastChar ->
            Debug.todo "RemoveLastChar"

        EnterGuess ->
            Debug.todo "EnterGuess"

Jako první se podíváme na AddChar. Intuitivně bychom si měli zapamatovat dané písmeno, pokud na něj ještě máme místo. Pokud je pokus už celý vyplněn, budeme tuto Msg ignorovat:

"WO"
--> AddChar "R" -->
"WOR"


"WORLD"
--> AddChar "X" -->
"WORLD"

Další situace, kdy bychom ji měli ignorovat, je, když hráč už vyčerpal všechny své pokusy. Implementace může vypadat například takto:

-- vevnitř update a AddChar (místo Debug.todo)

if String.length model.nextGuess >= wordSize then
    model

else if List.length model.guesses >= maxGuesses then
    model

else
    { model | nextGuess = model.nextGuess ++ String.fromChar char }

Pro vysvětlení připomínám, že v Elmu je vše imutabilní. To znamená, že nemůžeme udělat něco ve stylu model.nextGuess += String.fromChar char. Vždy pouze dostaneme hodnotu na vstupu a vracíme hodnotu na výstupu. V tomto případě syntaxe { record | field = newValue } nemutuje původní rekord, ale vrací nový rekord, totožný s tím původním až na požadované změny.

Dále se podívejme na RemoveLastChar. Zde bychom mohli udělat podmínku jako v AddChar, akorát pro prázdný řetězec, ale místo toho využijeme toho, jak se chová funkce String.left: pokud řekneme, že chceme z řetězce vytáhnout prvních 0 nebo -1 znaků, vrátí prázdný řetězec. To se nám hodí:

String.left 1 "HELLO"
-->
"H"

String.left 0 "HELLO"
-->
""

String.left -1 "HELLO"
-->
""

Implementace RemoveLastChar tedy bude jednodušší a bude se správně chovat i v případě prázdného řetězce:

-- vevnitř update a RemoveLastChar (místo Debug.todo)

{ model | nextGuess =
    String.left
        (String.length model.nextGuess - 1)
        model.nextGuess
}

A jako poslední část interaktivity nás čeká EnterGuess. Pokud má hráčův pokus správný počet znaků a pokud je pro něj mezi předchozími pokusy ještě místo, můžeme ho přesunout do seznamu předchozích pokusů a aktuální pokus vyresetovat. Pokud ne, budeme dělat, že se nic nestalo:

-- vevnitř update a EnterGuess (místo Debug.todo)

if List.length model.guesses >= maxGuesses then
    model

else if String.length model.nextGuess /= wordSize then
    model

else
    { model
        | guesses = model.guesses ++ [ model.nextGuess ]
        , nextGuess = ""
    }

Vaše Ellie by měla vypadat zhruba takto. Nyní můžeme směle prohlásit, že je hra hratelná. Uložte Ellie skrze tlačítko „Save“ a běžte se pochlubit! Já počkám :)

Konečný výsledek

Outro

Tutoriál skončil – gratuluji! Máte za sebou první kontakt s jazykem Elm. Z mé zkušenosti (autor píše v Elmu komplexní dataviz aplikace pro britskou firmu GWI) rozhodně dostává mottu na své domovské stránce:

A delightful language for reliable web applications. (Příjemný jazyk pro spolehlivé webové aplikace.)

https://elm-lang.org

Pokud máte chuť s vaší implementací hry Wordle pokračovat, hru by šlo mnoha způsoby vylepšit. Níže nabízím pár nápadů, které můžete zkusit naimplementovat. Doporučuji také projít si nebo mít poblíž tyto zdroje:

  1. Elm pro JavaScript vývojáře
  2. Syntax reference
  3. Oficiální Guide, který obsahuje „kuchařku“ a vysvětlivky k běžným problémům a situacím
  4. Dokumentace všech dostupných balíčků, hlavně elm/core

V případě záseku je nejlepší místo, kde se zeptat na radu, Elm Slack a jeho kanály #beginners a #help, případně můžete zavítat i do českého kanálu #cz-sk.

TODOs

  • Poté, co hráč uhodl celé slovo správně (jeden celý řádek je zelený), zobrazte pod klávesnicí zelený nápis „YOU WON“.
  • Naopak poté, co hráč vyčerpal všechny pokusy a slovo neuhodl, zobrazte pod klávesnicí červený nápis „YOU LOST“.
  • Místo toho, aby hledané slovo bylo vždy "WORLD", jej při inicializaci vyberte náhodně ze seznamu. (Guide: Random)
  • Kombinace předchozích bodů: Po vyhrané nebo prohrané hře dovolte hráči hrát znovu pomocí nového tlačítka „PLAY AGAIN“.
  • Kromě psaní klikáním myší na virtuální klávesnici dovolte hráči psát i pomocí reálné klávesnice. (Browser.Events.onKeyDown)
  • Vybarvěte písmena na virtuální klávesnici: zeleně, pokud bylo ve kterémkoliv předchozím pokusu správně uhádnuto, případně žluté, pokud ho hráč uhodl pouze „žlutě“.
  • Při zadání písmene do tabulky nebo smazání písmene z aktuálního pokusu vizuálně otočte políčko pomocí CSS animací.
  • Při výhře zobrazte hráčovu sekvenci pokusů pomocí znaků ????⬛????
  • Dovolte hráči tuto sekvenci pokusů zkopírovat do schránky (Guide: Ports, MDN: Clipboard.writeText)
Wordle na Twitteru

Nahoru
Tento web používá k poskytování služeb a analýze návštěvnosti soubory cookie. Používáním tohoto webu s tímto souhlasíte. Další informace