Píšeme hru Wordle v Elmu
Publikováno: 21.2.2022
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.
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:
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:
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:
Model
a Msg
jsou data, init
, view
a update
jsou funkce.
Model
je stav aplikaceMsg
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
:
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
:
- funkci
CharKey
, která zChar
uděláKey
- konstantu
BackspaceKey
, která užKey
je - 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:
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).
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:
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:
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!
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 ] []
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)
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…
…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 :)
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:
- Elm pro JavaScript vývojáře
- Syntax reference
- Oficiální Guide, který obsahuje „kuchařku“ a vysvětlivky k běžným problémům a situacím
- 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
)