From 0eebeb579646dc194f8153e16c3fa778d67d520d Mon Sep 17 00:00:00 2001 From: Alessandro Re Date: Sat, 10 Feb 2024 16:12:32 +0100 Subject: [PATCH 01/14] feat: match logic implemented --- notes.md | 4 +- src/Word.elm | 156 +++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 134 insertions(+), 26 deletions(-) diff --git a/notes.md b/notes.md index 1e961c8..5dac4e1 100644 --- a/notes.md +++ b/notes.md @@ -17,5 +17,5 @@ implement logic - [x] new module word - [x] refactor word using suggestion from redInk style guide: https://github.com/NoRedInk/elm-style-guide?tab=readme-ov-file#identifiers - [x] test suite -- [ ] implement logic -- [ ] link with main \ No newline at end of file +- [x] implement logic +- [ ] link with main diff --git a/src/Word.elm b/src/Word.elm index 57e76aa..bb492a4 100644 --- a/src/Word.elm +++ b/src/Word.elm @@ -1,12 +1,13 @@ module Word exposing (..) -import Test exposing (Test, describe, test) + import Expect +import Test exposing (Test, describe, test) + type Match - = No --grey + = Missing --grey | Exact --green - | Almost --yellow - | Unmatched --white + | Present --yellow type alias Letter = @@ -23,38 +24,145 @@ type Tile type Word = Word (List Tile) -fromCharList : List Char -> Word -fromCharList chars = - Word (List.map (\char -> FilledTile { char = char, match = Unmatched }) chars) + +toMatched : List Char -> List Char -> List Letter +toMatched matchStatus chars = + List.map2 + (\m c -> + { char = c + , match = + case m of + 'e' -> + Exact + + 'p' -> + Present + + _ -> + Missing + } + ) + matchStatus + chars zip a b = List.map2 Tuple.pair a b -matchExactTile : (Letter, Letter) -> (Letter, Letter) -matchExactTile ( a, b ) = - if a.char == b.char then - ( { a | match = Exact }, { b | match = Exact } ) - -- default format with a blank line -> error when pasting in elm repl +at : List t -> Int -> Maybe t +at list i = + List.head (List.drop i (List.take (i + 1) list)) + + +set : List t -> Int -> t -> List t +set list i x = + if i < List.length list then + List.take i list ++ [ x ] ++ List.drop (i + 1) list + else - ( a, b ) + list + + +matchExact : List Char -> List Char -> List Char +matchExact a b = + List.take (List.length a) + (List.map2 + (\x y -> + if x == y then + 'e' + + else + '.' + ) + a + b + ++ List.repeat (List.length a - List.length b) '.' + ) + + +match : List Char -> List Char -> List Char +match guess solution = + let + guessExact : List Char + guessExact = + matchExact guess solution + + solutionExact : List Char + solutionExact = + matchExact solution guess + + innerLoop i j sol result = + if j < List.length sol then + if at sol j /= Just '.' then + innerLoop i (j + 1) sol result + + else if at solution j == at guess i then + ( set sol j 'p' + , set result i 'p' + ) + + else + innerLoop i (j + 1) sol result + + else + ( sol, result ) + + outerLoop i sol result = + if i < List.length guess then + if at result i == Just 'e' then + outerLoop (i + 1) sol result + + else + let + ( solInner, resultInner ) = + innerLoop i 0 sol result + in + outerLoop (i + 1) solInner resultInner + else + ( sol, result ) -matchExact ( sol, guess ) = - List.unzip (List.map matchExactTile (zip sol guess)) + ( solUpdate, resultUpdate ) = + outerLoop 0 solutionExact guessExact + in + resultUpdate suite : Test suite = + let + testMatch g s e = + let + guess = + String.toList g + + solution = + String.toList s + + expect = + String.toList e + in + test (g ++ "-" ++ s ++ "=" ++ e) <| \_ -> Expect.equal (match guess solution) expect + in describe "Wordle Match Logic" - [ describe "MatchExact" [ - test "FURBA -> BUFFA" <| - \_ -> - let - guess = "FURBA" - solution = "BUFFA" - in - Expect.equal guess solution - ] + [ describe "MatchExact" + [ testMatch "FURBA" "BUFFA" "pe.pe" + , testMatch "BABBA" "CACCA" ".e..e" + , testMatch "BAA" "CCB" "p.." + , testMatch "longer" "smal" "p....." + , testMatch "SMAL" "Soooosoos" "e..." + , testMatch "LLxxx" "yyLLL" "pp..." + , testMatch "yyLLL" "LLxxx" "..pp." + , testMatch "ABB" "AA" "e.." + , testMatch "AxyA" "zAAw" "p..p" + ] + , describe "Lista" + [ test "3o" <| + \_ -> Expect.equal (at [ 0, 1, 2, 3, 4 ] 2) (Just 2) + , test "set 0" <| \_ -> Expect.equal (set [ 0, 1, 2 ] 0 9) [ 9, 1, 2 ] + , test "set 1" <| \_ -> Expect.equal (set [ 0, 1, 2 ] 1 9) [ 0, 9, 2 ] + , test "set 2" <| \_ -> Expect.equal (set [ 0, 1, 2 ] 2 9) [ 0, 1, 9 ] + , test "set 3" <| \_ -> Expect.equal (set [ 0, 1, 2 ] 3 9) [ 0, 1, 2 ] + ] ] From d0b4ca7eb624d49c6ec156c8156ddf122698b602 Mon Sep 17 00:00:00 2001 From: Alessandro Re Date: Sat, 10 Feb 2024 17:24:48 +0100 Subject: [PATCH 02/14] chore: minor type refactoring --- notes.md | 2 ++ src/Word.elm | 53 ++++++++++++++++++++++------------------------------ 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/notes.md b/notes.md index 5dac4e1..59c5e6b 100644 --- a/notes.md +++ b/notes.md @@ -19,3 +19,5 @@ implement logic - [x] test suite - [x] implement logic - [ ] link with main + - [x] refactor word logic using algebric types "MatchLetter" + - [ ] refactor model diff --git a/src/Word.elm b/src/Word.elm index bb492a4..bf5f9f0 100644 --- a/src/Word.elm +++ b/src/Word.elm @@ -1,46 +1,37 @@ -module Word exposing (..) +module Word exposing (MatchedChar(..), matchGuess) import Expect import Test exposing (Test, describe, test) -type Match - = Missing --grey - | Exact --green - | Present --yellow +type MatchedChar + = Missing Char --grey + | Exact Char --green + | Present Char --yellow -type alias Letter = - { char : Char - , match : Match - } - - -type Tile - = EmptyTile - | FilledTile Letter - - -type Word - = Word (List Tile) +matchGuess : List Char -> List Char -> List MatchedChar +matchGuess guess solution = + let + matchStatus = + match guess solution + in + toMatched matchStatus guess -toMatched : List Char -> List Char -> List Letter +toMatched : List Char -> List Char -> List MatchedChar toMatched matchStatus chars = List.map2 (\m c -> - { char = c - , match = - case m of - 'e' -> - Exact - - 'p' -> - Present - - _ -> - Missing - } + case m of + 'e' -> + Exact c + + 'p' -> + Present c + + _ -> + Missing c ) matchStatus chars From d87d0ce05f166c64d91c3f802e21d09aa2d59e8d Mon Sep 17 00:00:00 2001 From: Alessandro Re Date: Sat, 10 Feb 2024 17:37:41 +0100 Subject: [PATCH 03/14] WIP: converting matched char to tiles --- src/Main.elm | 153 +++++++++++++++++++++++++++++---------------------- 1 file changed, 86 insertions(+), 67 deletions(-) diff --git a/src/Main.elm b/src/Main.elm index 7dbd247..1d58cf9 100644 --- a/src/Main.elm +++ b/src/Main.elm @@ -8,6 +8,7 @@ import Element.Border as Border import Element.Font as Font import Html exposing (Html) import Html.Events exposing (onClick) +import Word exposing (..) main = @@ -15,9 +16,9 @@ main = type alias Model = - { guesses : List Word - , current : Word - , solution : List Char + { guesses : List (List MatchedChar) -- diventa quella della logica + , current : List Char -- diventa List Char + , solution : List Char -- rimane } @@ -84,30 +85,28 @@ viewHeaderButton = --- GRID - - -type Match - = No --grey - | Exact --green - | Almost --yellow - | Unmatched --white - - -type alias Letter = - { char : Char - , match : Match - } +-- type Match +-- = Noxx --grey +-- | Exactxx --green +-- | Almostxx --yellow +-- | Unmatchedxx --white +-- type alias Letter = +-- { char : Char +-- , match : Match +-- } type Tile = EmptyTile - | FilledTile Letter + | FilledTile MatchedChar -type alias Word = - List Tile +-- type Word +-- = List Letter + +viewGridArea : Model -> Element Msg viewGridArea model = el [ bgYell, width fill, height (fillPortion 2) ] (viewGrid model) @@ -116,39 +115,37 @@ emptyTile = EmptyTile -testWord : Word -testWord = - [ FilledTile { char = 'A', match = No } - , FilledTile { char = 'B', match = Exact } - , FilledTile { char = 'C', match = Almost } - , FilledTile { char = 'D', match = Unmatched } - , EmptyTile - ] + +-- testWord : Word +-- testWord = +-- [ FilledTile { char = 'A', match = No } +-- , FilledTile { char = 'B', match = Exact } +-- , FilledTile { char = 'C', match = Almost } +-- , FilledTile { char = 'D', match = Unmatched } +-- , EmptyTile +-- ] testGuess1 = - [ FilledTile { char = 'P', match = No } - , FilledTile { char = 'O', match = No } - , FilledTile { char = 'S', match = No } - , FilledTile { char = 'T', match = No } - , FilledTile { char = 'A', match = Exact } + [ Missing 'P' + , Missing 'O' + , Missing 'S' + , Missing 'T' + , Exact 'A' ] testGuess2 = - [ FilledTile { char = 'F', match = Almost } - , FilledTile { char = 'U', match = Exact } - , FilledTile { char = 'R', match = No } - , FilledTile { char = 'B', match = Almost } - , FilledTile { char = 'A', match = Exact } + [ Present 'F' + , Exact 'U' + , Missing 'R' + , Present 'B' + , Exact 'A' ] testCurrent = - [ FilledTile { char = 'B', match = Unmatched } - , FilledTile { char = 'U', match = Unmatched } - , FilledTile { char = 'F', match = Unmatched } - ] + [ 'B', 'U', 'F' ] testSolution = @@ -159,26 +156,44 @@ emptyWord = List.repeat 5 emptyTile +padRightTake : Int -> f -> List f -> List f padRightTake n padFill aList = List.take n (aList ++ List.repeat n padFill) +guessToTile : List MatchedChar -> List Tile +guessToTile x = + List.map FilledTile x + + +getWords : Model -> List (List Tile) getWords model = padRightTake 6 emptyWord - (model.guesses ++ [ padRightTake 5 EmptyTile model.current ]) + (List.map guessToTile model.guesses ++ [ padRightTake 5 EmptyTile (List.map (\c -> EmptyTile) model.current) ]) +viewGrid : Model -> Element Msg viewGrid model = + -- column [ centerX, centerY, spacing 5 ] (List.map viewTileRow (getWords model)) column [ centerX, centerY, spacing 5 ] (List.map viewTileRow (getWords model)) + +-- [ row [ spacing 5 ] +-- (List.map viewTile +-- [ EmptyTile, EmptyTile, EmptyTile ] +-- ) +-- ] + + +viewTileRow : List Tile -> Element Msg viewTileRow word = row [ spacing 5 ] (List.map viewTile - (padRightTake 5 emptyTile word) + (padRightTake 5 EmptyTile word) ) @@ -187,57 +202,49 @@ tileBgColor tile = EmptyTile -> bgWhite - FilledTile { match } -> + FilledTile match -> case match of - No -> + Missing _ -> bgDarkGray - Exact -> + Exact _ -> bgGreen - Almost -> + Present _ -> bgYellow - Unmatched -> - bgWhite - tileBorderColor tile = case tile of EmptyTile -> colorGray - FilledTile { match } -> + FilledTile match -> case match of - No -> + Missing _ -> colorDarkGray - Exact -> + Exact _ -> colorGreen - Almost -> + Present _ -> colorYellow - Unmatched -> - colorBlack - -tileFontColor : Match -> Color +tileFontColor : MatchedChar -> Color tileFontColor match = case match of - No -> + Missing _ -> colorWhite - Exact -> + Exact _ -> colorWhite - Almost -> + Present _ -> colorWhite - Unmatched -> - colorBlack - +viewTile : Tile -> Element Msg viewTile tile = el [ width (px 62) @@ -257,15 +264,27 @@ viewTileChar tile = EmptyTile -> el [ centerX, centerY ] (text (String.fromChar ' ')) - FilledTile ftile -> + FilledTile match -> + let + ( color, char ) = + case match of + Missing c -> + ( colorWhite, c ) + + Exact c -> + ( colorWhite, c ) + + Present c -> + ( colorWhite, c ) + in el [ centerX , centerY - , Font.color (tileFontColor ftile.match) + , Font.color color , Font.size 32 , Font.bold ] - (text (String.fromChar ftile.char)) + (text (String.fromChar char)) From b952a76624d9d7180d642463604da3baf46a7623 Mon Sep 17 00:00:00 2001 From: Alessandro Re Date: Mon, 12 Feb 2024 18:33:59 +0100 Subject: [PATCH 04/14] feat: better matching algorithm --- src/Word.elm | 120 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 4 deletions(-) diff --git a/src/Word.elm b/src/Word.elm index bf5f9f0..d2b0501 100644 --- a/src/Word.elm +++ b/src/Word.elm @@ -1,4 +1,4 @@ -module Word exposing (MatchedChar(..), matchGuess) +module Word exposing (MatchedChar(..), matchGuess, suite) import Expect import Test exposing (Test, describe, test) @@ -120,6 +120,93 @@ match guess solution = resultUpdate + +-- Prepend element to second element of a tuple +-- (1, [9, 8, 7]) 10 -> (1, [10, 9, 8, 7]) + + +t2prepend : ( t, List t ) -> t -> ( t, List t ) +t2prepend ( a, y ) x = + ( a, x :: y ) + + + +-- finder looks for a character inside a list of previously matched chars; +-- returns the state of the match and a new list of matched chars. +-- E.g. 'F' in 'bUffA' -> 'bUFfA' (upper case is a match) + + +finder : Char -> List MatchedChar -> List Char -> ( MatchedChar, List MatchedChar ) +finder soggetto listaStato listaLettereTarget = + case ( listaStato, listaLettereTarget ) of + ( [], _ ) -> + ( Missing soggetto, [] ) + + ( _, [] ) -> + ( Missing soggetto, [] ) + + ( (Missing c) :: ls, l :: ll ) -> + if soggetto == l then + -- L'abbiamo trovato + ( Present soggetto, Present soggetto :: ls ) + + else + t2prepend (finder soggetto ls ll) (Missing c) + + -- ( (Exact c) :: ls, l :: ll ) -> + -- -- Devo saltare questo caso, perché la lettera è già presa + -- t2prepend (finder soggetto ls ll) (Exact c) + -- ( (Present c) :: ls, l :: ll ) -> + -- t2prepend (finder soggetto ls ll) (Present c) + ( m :: ls, l :: ll ) -> + t2prepend (finder soggetto ls ll) m + + +rematch : List Char -> List Char -> List MatchedChar +rematch guess solution = + let + equalChars : List MatchedChar + equalChars = + List.map2 + (\g s -> + if g == s then + Exact g + + else + Missing g + ) + guess + solution + + secondStep : Int -> List Char -> List MatchedChar -> List Char -> List MatchedChar + secondStep i gue stato target = + case ( gue, List.drop i stato ) of + ( [], _ ) -> + [] + + ( _, [] ) -> + [] + + ( g :: gs, (Exact _) :: ss ) -> + -- Se una lettera è sicura significa che l'abbiamo già trovata + -- e non ha senso cercare ricorsivamente. + Exact g :: secondStep (i + 1) gs stato target + + ( g :: gs, _ :: ss ) -> + -- Non sono sicuro, devo cercare + case finder g stato target of + ( Exact _, nuovoStato ) -> + Exact g :: secondStep (i + 1) gs nuovoStato target + + ( Missing _, nuovoStato ) -> + Missing g :: secondStep (i + 1) gs nuovoStato target + + ( Present _, nuovoStato ) -> + Present g :: secondStep (i + 1) gs nuovoStato target + in + secondStep 0 guess equalChars solution + + suite : Test suite = let @@ -134,19 +221,27 @@ suite = expect = String.toList e in - test (g ++ "-" ++ s ++ "=" ++ e) <| \_ -> Expect.equal (match guess solution) expect + test (g ++ "-" ++ s ++ "=" ++ e) <| + \_ -> + Expect.all + [ Expect.equal (match guess solution) + , \hint -> Expect.equal (rematch guess solution) (toMatched hint guess) + ] + expect in describe "Wordle Match Logic" [ describe "MatchExact" [ testMatch "FURBA" "BUFFA" "pe.pe" , testMatch "BABBA" "CACCA" ".e..e" , testMatch "BAA" "CCB" "p.." - , testMatch "longer" "smal" "p....." , testMatch "SMAL" "Soooosoos" "e..." , testMatch "LLxxx" "yyLLL" "pp..." , testMatch "yyLLL" "LLxxx" "..pp." - , testMatch "ABB" "AA" "e.." , testMatch "AxyA" "zAAw" "p..p" + + -- Pattern longer than solution is not supported by rematch + -- , testMatch "longer" "smal" "p....." + -- , testMatch "ABB" "AA" "e.." ] , describe "Lista" [ test "3o" <| @@ -156,4 +251,21 @@ suite = , test "set 2" <| \_ -> Expect.equal (set [ 0, 1, 2 ] 2 9) [ 0, 1, 9 ] , test "set 3" <| \_ -> Expect.equal (set [ 0, 1, 2 ] 3 9) [ 0, 1, 2 ] ] + , describe "finding" + [ test "B in FuRbBo" <| + \_ -> + Expect.equal + (finder 'B' [ Missing 'F', Exact 'U', Missing 'R', Exact 'B', Missing 'B', Missing 'O' ] [ 'F', 'U', 'R', 'B', 'B', 'O' ]) + ( Present 'B', [ Missing 'F', Exact 'U', Missing 'R', Exact 'B', Present 'B', Missing 'O' ] ) + , test "B in Rbbo" <| + \_ -> + Expect.equal + (finder 'B' [ Missing 'R', Exact 'B', Present 'B', Missing 'O' ] [ 'R', 'B', 'B', 'O' ]) + ( Missing 'B', [ Missing 'R', Exact 'B', Present 'B', Missing 'O' ] ) + , test "finding F(URBA) in (B)UFFA" <| + \_ -> + Expect.equal + (finder 'F' [ Exact 'U', Missing 'F', Missing 'F', Exact 'A' ] [ 'U', 'F', 'F', 'A' ]) + ( Present 'F', [ Exact 'U', Present 'F', Missing 'F', Exact 'A' ] ) + ] ] From d62b92d5f7f552cb8f69a275dfee9115dabad8e9 Mon Sep 17 00:00:00 2001 From: Alessandro Re Date: Mon, 12 Feb 2024 18:42:20 +0100 Subject: [PATCH 05/14] chore: some cleaning --- src/Word.elm | 59 ++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/src/Word.elm b/src/Word.elm index d2b0501..715623e 100644 --- a/src/Word.elm +++ b/src/Word.elm @@ -131,35 +131,31 @@ t2prepend ( a, y ) x = --- finder looks for a character inside a list of previously matched chars; +-- finder looks for a (guessed) character inside a list of previously matched chars; -- returns the state of the match and a new list of matched chars. -- E.g. 'F' in 'bUffA' -> 'bUFfA' (upper case is a match) finder : Char -> List MatchedChar -> List Char -> ( MatchedChar, List MatchedChar ) -finder soggetto listaStato listaLettereTarget = - case ( listaStato, listaLettereTarget ) of +finder g matchState solution = + case ( matchState, solution ) of ( [], _ ) -> - ( Missing soggetto, [] ) + ( Missing g, [] ) ( _, [] ) -> - ( Missing soggetto, [] ) + ( Missing g, [] ) ( (Missing c) :: ls, l :: ll ) -> - if soggetto == l then - -- L'abbiamo trovato - ( Present soggetto, Present soggetto :: ls ) + if g == l then + -- We found the char matching with a unassigned character + ( Present g, Present g :: ls ) else - t2prepend (finder soggetto ls ll) (Missing c) + t2prepend (finder g ls ll) (Missing c) - -- ( (Exact c) :: ls, l :: ll ) -> - -- -- Devo saltare questo caso, perché la lettera è già presa - -- t2prepend (finder soggetto ls ll) (Exact c) - -- ( (Present c) :: ls, l :: ll ) -> - -- t2prepend (finder soggetto ls ll) (Present c) + -- If it's Exact or Present, then we skip the letter and try the next one. ( m :: ls, l :: ll ) -> - t2prepend (finder soggetto ls ll) m + t2prepend (finder g ls ll) m rematch : List Char -> List Char -> List MatchedChar @@ -178,33 +174,36 @@ rematch guess solution = guess solution - secondStep : Int -> List Char -> List MatchedChar -> List Char -> List MatchedChar - secondStep i gue stato target = - case ( gue, List.drop i stato ) of + matchGuessChars : Int -> List Char -> List MatchedChar -> List Char -> List MatchedChar + matchGuessChars i gue state target = + case ( gue, List.drop i state ) of + -- It's over when there are no more letters in the guess to match. ( [], _ ) -> [] + -- If the state is empty, we cannot match anything ( _, [] ) -> [] ( g :: gs, (Exact _) :: ss ) -> - -- Se una lettera è sicura significa che l'abbiamo già trovata - -- e non ha senso cercare ricorsivamente. - Exact g :: secondStep (i + 1) gs stato target + -- If a letter is an exact match, it means we found it already + -- so no need to find it. Let's proceed to the next letter. + Exact g :: matchGuessChars (i + 1) gs state target ( g :: gs, _ :: ss ) -> - -- Non sono sicuro, devo cercare - case finder g stato target of - ( Exact _, nuovoStato ) -> - Exact g :: secondStep (i + 1) gs nuovoStato target + -- Not sure if this letter is present: let's search for it + -- in the whole solution. This might update the state. + case finder g state target of + ( Exact _, newState ) -> + Exact g :: matchGuessChars (i + 1) gs newState target - ( Missing _, nuovoStato ) -> - Missing g :: secondStep (i + 1) gs nuovoStato target + ( Missing _, newState ) -> + Missing g :: matchGuessChars (i + 1) gs newState target - ( Present _, nuovoStato ) -> - Present g :: secondStep (i + 1) gs nuovoStato target + ( Present _, newState ) -> + Present g :: matchGuessChars (i + 1) gs newState target in - secondStep 0 guess equalChars solution + matchGuessChars 0 guess equalChars solution suite : Test From 159e907cb44000ef36f01116d8d80aea2d6d7999 Mon Sep 17 00:00:00 2001 From: Alessandro Re Date: Mon, 12 Feb 2024 19:06:28 +0100 Subject: [PATCH 06/14] chore: some cleaning --- src/Main.elm | 68 +++++++++++++--------------------------------------- 1 file changed, 16 insertions(+), 52 deletions(-) diff --git a/src/Main.elm b/src/Main.elm index 1d58cf9..2e7d205 100644 --- a/src/Main.elm +++ b/src/Main.elm @@ -1,4 +1,4 @@ -module Main exposing (main, tileFontColor) +module Main exposing (main) import Array exposing (Array) import Browser @@ -83,29 +83,11 @@ viewHeaderButton = el [ alignRight, bgPink ] (text "Button") - ---- GRID --- type Match --- = Noxx --grey --- | Exactxx --green --- | Almostxx --yellow --- | Unmatchedxx --white --- type alias Letter = --- { char : Char --- , match : Match --- } - - type Tile = EmptyTile | FilledTile MatchedChar - --- type Word --- = List Letter - - viewGridArea : Model -> Element Msg viewGridArea model = el [ bgYell, width fill, height (fillPortion 2) ] (viewGrid model) @@ -115,17 +97,6 @@ emptyTile = EmptyTile - --- testWord : Word --- testWord = --- [ FilledTile { char = 'A', match = No } --- , FilledTile { char = 'B', match = Exact } --- , FilledTile { char = 'C', match = Almost } --- , FilledTile { char = 'D', match = Unmatched } --- , EmptyTile --- ] - - testGuess1 = [ Missing 'P' , Missing 'O' @@ -181,14 +152,6 @@ viewGrid model = (List.map viewTileRow (getWords model)) - --- [ row [ spacing 5 ] --- (List.map viewTile --- [ EmptyTile, EmptyTile, EmptyTile ] --- ) --- ] - - viewTileRow : List Tile -> Element Msg viewTileRow word = row [ spacing 5 ] @@ -244,6 +207,19 @@ tileFontColor match = colorWhite +tileChar : MatchedChar -> Char +tileChar match = + case match of + Missing c -> + c + + Exact c -> + c + + Present c -> + c + + viewTile : Tile -> Element Msg viewTile tile = el @@ -265,26 +241,14 @@ viewTileChar tile = el [ centerX, centerY ] (text (String.fromChar ' ')) FilledTile match -> - let - ( color, char ) = - case match of - Missing c -> - ( colorWhite, c ) - - Exact c -> - ( colorWhite, c ) - - Present c -> - ( colorWhite, c ) - in el [ centerX , centerY - , Font.color color + , Font.color (tileFontColor match) , Font.size 32 , Font.bold ] - (text (String.fromChar char)) + (text (String.fromChar (tileChar match))) From d519dcf605012ca7e400a9bb6581b3200b96be78 Mon Sep 17 00:00:00 2001 From: Alessandro Re Date: Mon, 12 Feb 2024 19:07:56 +0100 Subject: [PATCH 07/14] doc: notes updated --- notes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notes.md b/notes.md index 59c5e6b..4e331cf 100644 --- a/notes.md +++ b/notes.md @@ -18,6 +18,6 @@ implement logic - [x] refactor word using suggestion from redInk style guide: https://github.com/NoRedInk/elm-style-guide?tab=readme-ov-file#identifiers - [x] test suite - [x] implement logic -- [ ] link with main +- [x] link with main - [x] refactor word logic using algebric types "MatchLetter" - - [ ] refactor model + - [x] refactor model From d34dd37e5df935ea9b5e6ecad8aa0060b8714cf2 Mon Sep 17 00:00:00 2001 From: Alessandro Re Date: Mon, 12 Feb 2024 19:16:50 +0100 Subject: [PATCH 08/14] fix: exported rematch --- src/Word.elm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Word.elm b/src/Word.elm index 715623e..8253d49 100644 --- a/src/Word.elm +++ b/src/Word.elm @@ -1,4 +1,4 @@ -module Word exposing (MatchedChar(..), matchGuess, suite) +module Word exposing (MatchedChar(..), matchGuess, rematch, suite) import Expect import Test exposing (Test, describe, test) From 1e16248be6bf58f9cbbc2b4c0dff840e0c502888 Mon Sep 17 00:00:00 2001 From: Alessandro Re Date: Sat, 16 Mar 2024 12:46:28 +0100 Subject: [PATCH 09/14] feat: keyboard responds to events --- elm.json | 2 +- src/Main.elm | 112 +++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 97 insertions(+), 17 deletions(-) diff --git a/elm.json b/elm.json index d2ca61a..6c47605 100644 --- a/elm.json +++ b/elm.json @@ -9,12 +9,12 @@ "elm/browser": "1.0.2", "elm/core": "1.0.5", "elm/html": "1.0.0", + "elm/json": "1.1.3", "elm-explorations/test": "2.2.0", "mdgriffith/elm-ui": "1.1.8" }, "indirect": { "elm/bytes": "1.0.8", - "elm/json": "1.1.3", "elm/random": "1.0.0", "elm/time": "1.0.0", "elm/url": "1.0.0", diff --git a/src/Main.elm b/src/Main.elm index 2e7d205..04ee5cf 100644 --- a/src/Main.elm +++ b/src/Main.elm @@ -2,17 +2,20 @@ module Main exposing (main) import Array exposing (Array) import Browser +import Browser.Events import Element exposing (..) import Element.Background as Background import Element.Border as Border +import Element.Events as Ev import Element.Font as Font import Html exposing (Html) -import Html.Events exposing (onClick) +import Html.Events +import Json.Decode as Decode import Word exposing (..) main = - Browser.sandbox { init = init, update = update, view = view } + Browser.element { init = init, update = update, view = view, subscriptions = subscriptions } type alias Model = @@ -22,15 +25,17 @@ type alias Model = } -init : Model -init = - { guesses = - [ testGuess1 - , testGuess2 - ] - , current = testCurrent - , solution = testSolution - } +init : () -> ( Model, Cmd Msg ) +init _ = + ( { guesses = + [ testGuess1 + , testGuess2 + ] + , current = testCurrent + , solution = testSolution + } + , Cmd.none + ) type Msg @@ -39,9 +44,67 @@ type Msg | Confirm -update : Msg -> Model -> Model +update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = - model + ( case msg of + KeyPressed c -> + -- Add to the current solution as long as it's shorter than 5, + -- then just ignore letters + if List.length model.current < 5 then + Debug.log "Pressed" { model | current = model.current ++ [ c ] } + + else + model + + Confirm -> + Debug.todo "Confirm" + + Backspace -> + -- Remove last character from current, as long as it's not empty + Debug.log "Chomped" { model | current = List.take (List.length model.current - 1) model.current } + , Cmd.none + ) + + + +--- SUBSCRIPTIONS + + +subscriptions : Model -> Sub Msg +subscriptions m = + -- Let's decode the `key` field as a string, then try to decode + -- the string and see if it is a valid character: if the character is invalid, + -- the decoding should fail and no message shall be send: only Enter, + -- Backspace and letters are valid. + Browser.Events.onKeyDown + (Decode.andThen decodeKey (Decode.field "key" Decode.string)) + + + +-- Take a string and returns a decoder: it will succeed for Enter, Backpace and +-- ASCII alphas, but fail for anything else + + +decodeKey : String -> Decode.Decoder Msg +decodeKey str = + case str of + "Enter" -> + Decode.succeed Confirm + + "Backspace" -> + Decode.succeed Backspace + + _ -> + case String.uncons str of + Just ( ch, "" ) -> + if Char.isAlpha ch then + Decode.succeed (KeyPressed ch) + + else + Decode.fail "Not alpha" + + _ -> + Decode.fail "Another control" @@ -294,13 +357,30 @@ viewKeyString k = "<-" -viewMakeButton : Keyboard -> Element msg +viewKeyEvent k = + case k of + Key c -> + Ev.onClick (KeyPressed c) + + KeyBackspace -> + Ev.onClick Backspace + + KeyEnter -> + Ev.onClick Confirm + + +viewMakeButton : Keyboard -> Element Msg viewMakeButton k = - el [ bgCyan, height (px 58), width (px (viewKeyWidth k)) ] + el + [ bgCyan + , height (px 58) + , width (px (viewKeyWidth k)) + , viewKeyEvent k + ] (el [ centerX, centerY ] (text (viewKeyString k))) -viewKeyboardRow : List Keyboard -> Element msg +viewKeyboardRow : List Keyboard -> Element Msg viewKeyboardRow keys = row [ spacing 5, centerX ] (List.map viewMakeButton keys) From e4d3dbb64aa93272639e120c9d59ffe4ededca72 Mon Sep 17 00:00:00 2001 From: Alessandro Re Date: Sat, 16 Mar 2024 16:45:38 +0100 Subject: [PATCH 10/14] feat: connected keyboard and matching logic --- src/Main.elm | 137 ++++++++++++++++++++++++++++++++++++++++++--------- src/Word.elm | 113 +++--------------------------------------- 2 files changed, 121 insertions(+), 129 deletions(-) diff --git a/src/Main.elm b/src/Main.elm index 04ee5cf..7da017a 100644 --- a/src/Main.elm +++ b/src/Main.elm @@ -19,9 +19,9 @@ main = type alias Model = - { guesses : List (List MatchedChar) -- diventa quella della logica - , current : List Char -- diventa List Char - , solution : List Char -- rimane + { guesses : List (List MatchedChar) + , current : List Char + , solution : List Char } @@ -46,26 +46,44 @@ type Msg update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = + let + currentGuessLen = + List.length model.current + in ( case msg of KeyPressed c -> -- Add to the current solution as long as it's shorter than 5, -- then just ignore letters - if List.length model.current < 5 then - Debug.log "Pressed" { model | current = model.current ++ [ c ] } + if currentGuessLen < 5 then + Debug.log "Pressed" { model | current = model.current ++ [ Char.toUpper c ] } else model Confirm -> - Debug.todo "Confirm" + -- Confirmation can happen only if guess has length 5 + if currentGuessLen < 5 then + model + + else + confirmGuess model Backspace -> -- Remove last character from current, as long as it's not empty - Debug.log "Chomped" { model | current = List.take (List.length model.current - 1) model.current } + Debug.log "Chomped" { model | current = List.take (currentGuessLen - 1) model.current } , Cmd.none ) +confirmGuess : Model -> Model +confirmGuess model = + let + matched = + rematch model.current model.solution + in + { model | current = [], guesses = model.guesses ++ [ matched ] } + + --- SUBSCRIPTIONS @@ -117,7 +135,7 @@ view model = (column [ width (fill |> maximum 500), height fill, centerX, bgCyan ] [ viewHeader , viewGridArea model - , viewKeyboardArea + , viewKeyboardArea model ] ) @@ -146,9 +164,14 @@ viewHeaderButton = el [ alignRight, bgPink ] (text "Button") + +-- A tile in the game, it can be empty or contains a matched char + + type Tile = EmptyTile | FilledTile MatchedChar + | UncheckedTile Char viewGridArea : Model -> Element Msg @@ -190,22 +213,44 @@ emptyWord = List.repeat 5 emptyTile + +-- Given a list, it will take the first n element from it; if there are less, +-- they are filled with padFill. + + padRightTake : Int -> f -> List f -> List f padRightTake n padFill aList = List.take n (aList ++ List.repeat n padFill) -guessToTile : List MatchedChar -> List Tile -guessToTile x = + +-- Convert a list matched chars into a list of (filled) tiles + + +tiledGuess : List MatchedChar -> List Tile +tiledGuess x = List.map FilledTile x getWords : Model -> List (List Tile) getWords model = + let + -- Convert previous guesses into tiles + tiledGuesses = + List.map tiledGuess model.guesses + + -- Convert current guess into tiles + tiledCurrent = + List.map (\c -> UncheckedTile c) model.current + + -- Pad current guess + paddedCurrent = + padRightTake 5 EmptyTile tiledCurrent + in padRightTake 6 emptyWord - (List.map guessToTile model.guesses ++ [ padRightTake 5 EmptyTile (List.map (\c -> EmptyTile) model.current) ]) + (tiledGuesses ++ [ paddedCurrent ]) viewGrid : Model -> Element Msg @@ -239,6 +284,9 @@ tileBgColor tile = Present _ -> bgYellow + UncheckedTile _ -> + bgWhite + tileBorderColor tile = case tile of @@ -256,6 +304,9 @@ tileBorderColor tile = Present _ -> colorYellow + UncheckedTile _ -> + colorDarkGray + tileFontColor : MatchedChar -> Color tileFontColor match = @@ -313,6 +364,17 @@ viewTileChar tile = ] (text (String.fromChar (tileChar match))) + UncheckedTile ch -> + el + [ centerX + , centerY + + -- , Font.color (tileFontColor match) + , Font.size 32 + , Font.bold + ] + (text (String.fromChar ch)) + -- KEYBOARD @@ -324,15 +386,17 @@ type Keyboard | KeyEnter -viewKeyboardArea = - el [ bgPink, width fill, height (fillPortion 1) ] viewKeyboard +viewKeyboardArea : Model -> Element Msg +viewKeyboardArea model = + el [ bgPink, width fill, height (fillPortion 1) ] (viewKeyboard model) -viewKeyboard = +viewKeyboard : Model -> Element Msg +viewKeyboard model = column [ centerX, centerY, spacing 5 ] - [ viewKeyboardRow (List.map Key (String.toList "QWERTYUIOP")) - , viewKeyboardRow (List.map Key (String.toList "ASDFGHJKL")) - , viewKeyboardRow ((KeyEnter :: List.map Key (String.toList "ZXCVBNM")) ++ [ KeyBackspace ]) + [ viewKeyboardRow model (List.map Key (String.toList "QWERTYUIOP")) + , viewKeyboardRow model (List.map Key (String.toList "ASDFGHJKL")) + , viewKeyboardRow model ((KeyEnter :: List.map Key (String.toList "ZXCVBNM")) ++ [ KeyBackspace ]) ] @@ -369,10 +433,37 @@ viewKeyEvent k = Ev.onClick Confirm -viewMakeButton : Keyboard -> Element Msg -viewMakeButton k = + +-- Returns the color of a keyboard button depending on matching of past guesses + + +buttonColor model k = + -- TODO the color is the "best" match found so far for a letter: if C is + -- Present in the first guess (yellow) and Exact in the second guess (green) + -- the C key should be colored green. + case k of + Key 'X' -> + bgYell + + Key ch -> + bgCyan + + KeyBackspace -> + bgCyan + + KeyEnter -> + bgCyan + + + +-- Creates the element for a keyboard button. It will highlight the element +-- depending on the current guesses. + + +viewMakeButton : Model -> Keyboard -> Element Msg +viewMakeButton model k = el - [ bgCyan + [ buttonColor model k , height (px 58) , width (px (viewKeyWidth k)) , viewKeyEvent k @@ -380,10 +471,10 @@ viewMakeButton k = (el [ centerX, centerY ] (text (viewKeyString k))) -viewKeyboardRow : List Keyboard -> Element Msg -viewKeyboardRow keys = +viewKeyboardRow : Model -> List Keyboard -> Element Msg +viewKeyboardRow model keys = row [ spacing 5, centerX ] - (List.map viewMakeButton keys) + (List.map (viewMakeButton model) keys) diff --git a/src/Word.elm b/src/Word.elm index 8253d49..7843a4e 100644 --- a/src/Word.elm +++ b/src/Word.elm @@ -1,4 +1,4 @@ -module Word exposing (MatchedChar(..), matchGuess, rematch, suite) +module Word exposing (MatchedChar(..), rematch, suite) import Expect import Test exposing (Test, describe, test) @@ -10,15 +10,6 @@ type MatchedChar | Present Char --yellow -matchGuess : List Char -> List Char -> List MatchedChar -matchGuess guess solution = - let - matchStatus = - match guess solution - in - toMatched matchStatus guess - - toMatched : List Char -> List Char -> List MatchedChar toMatched matchStatus chars = List.map2 @@ -37,89 +28,6 @@ toMatched matchStatus chars = chars -zip a b = - List.map2 Tuple.pair a b - - -at : List t -> Int -> Maybe t -at list i = - List.head (List.drop i (List.take (i + 1) list)) - - -set : List t -> Int -> t -> List t -set list i x = - if i < List.length list then - List.take i list ++ [ x ] ++ List.drop (i + 1) list - - else - list - - -matchExact : List Char -> List Char -> List Char -matchExact a b = - List.take (List.length a) - (List.map2 - (\x y -> - if x == y then - 'e' - - else - '.' - ) - a - b - ++ List.repeat (List.length a - List.length b) '.' - ) - - -match : List Char -> List Char -> List Char -match guess solution = - let - guessExact : List Char - guessExact = - matchExact guess solution - - solutionExact : List Char - solutionExact = - matchExact solution guess - - innerLoop i j sol result = - if j < List.length sol then - if at sol j /= Just '.' then - innerLoop i (j + 1) sol result - - else if at solution j == at guess i then - ( set sol j 'p' - , set result i 'p' - ) - - else - innerLoop i (j + 1) sol result - - else - ( sol, result ) - - outerLoop i sol result = - if i < List.length guess then - if at result i == Just 'e' then - outerLoop (i + 1) sol result - - else - let - ( solInner, resultInner ) = - innerLoop i 0 sol result - in - outerLoop (i + 1) solInner resultInner - - else - ( sol, result ) - - ( solUpdate, resultUpdate ) = - outerLoop 0 solutionExact guessExact - in - resultUpdate - - -- Prepend element to second element of a tuple -- (1, [9, 8, 7]) 10 -> (1, [10, 9, 8, 7]) @@ -158,6 +66,11 @@ finder g matchState solution = t2prepend (finder g ls ll) m + +-- Given a guess and a solution, returns a list of matched characters +-- Does not handle the case of guess longer than soludion + + rematch : List Char -> List Char -> List MatchedChar rematch guess solution = let @@ -222,11 +135,7 @@ suite = in test (g ++ "-" ++ s ++ "=" ++ e) <| \_ -> - Expect.all - [ Expect.equal (match guess solution) - , \hint -> Expect.equal (rematch guess solution) (toMatched hint guess) - ] - expect + Expect.equal (rematch guess solution) (toMatched expect guess) in describe "Wordle Match Logic" [ describe "MatchExact" @@ -242,14 +151,6 @@ suite = -- , testMatch "longer" "smal" "p....." -- , testMatch "ABB" "AA" "e.." ] - , describe "Lista" - [ test "3o" <| - \_ -> Expect.equal (at [ 0, 1, 2, 3, 4 ] 2) (Just 2) - , test "set 0" <| \_ -> Expect.equal (set [ 0, 1, 2 ] 0 9) [ 9, 1, 2 ] - , test "set 1" <| \_ -> Expect.equal (set [ 0, 1, 2 ] 1 9) [ 0, 9, 2 ] - , test "set 2" <| \_ -> Expect.equal (set [ 0, 1, 2 ] 2 9) [ 0, 1, 9 ] - , test "set 3" <| \_ -> Expect.equal (set [ 0, 1, 2 ] 3 9) [ 0, 1, 2 ] - ] , describe "finding" [ test "B in FuRbBo" <| \_ -> From b7f571cd4502cde06b6bb942c83c81ea1e7cab74 Mon Sep 17 00:00:00 2001 From: Alessandro Re Date: Sat, 16 Mar 2024 17:17:46 +0100 Subject: [PATCH 11/14] feat: keyboard now changes color --- src/Main.elm | 74 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/src/Main.elm b/src/Main.elm index 7da017a..72541ad 100644 --- a/src/Main.elm +++ b/src/Main.elm @@ -27,10 +27,7 @@ type alias Model = init : () -> ( Model, Cmd Msg ) init _ = - ( { guesses = - [ testGuess1 - , testGuess2 - ] + ( { guesses = [ testGuess1, testGuess2 ] , current = testCurrent , solution = testSolution } @@ -183,32 +180,22 @@ emptyTile = EmptyTile +testSolution = + [ 'B', 'U', 'F', 'F', 'A' ] + + testGuess1 = - [ Missing 'P' - , Missing 'O' - , Missing 'S' - , Missing 'T' - , Exact 'A' - ] + rematch [ 'P', 'O', 'S', 'T', 'A' ] testSolution testGuess2 = - [ Present 'F' - , Exact 'U' - , Missing 'R' - , Present 'B' - , Exact 'A' - ] + rematch [ 'F', 'U', 'R', 'B', 'A' ] testSolution testCurrent = [ 'B', 'U', 'F' ] -testSolution = - [ 'B', 'U', 'F', 'F', 'A' ] - - emptyWord = List.repeat 5 emptyTile @@ -434,6 +421,48 @@ viewKeyEvent k = +-- bestLetterColor : Model -> Char -> Element.Attr d m + + +bestLetterColor model char = + let + improve : MatchedChar -> MatchedChar -> MatchedChar + improve ch st = + case ch of + Missing c -> + -- We cannot improve with another missing char, leave state as is + st + + Exact c -> + if c == char then + -- If we find an exact match, that's the best possible + Exact char + + else + -- The character is different, leave state as is + st + + Present c -> + if c == char then + case st of + Missing _ -> + Present char + + _ -> + st + + else + st + + -- Initially, we say that char is missing + initialState = + Missing char + in + -- bgYell + List.foldl improve initialState (List.concat model.guesses) + + + -- Returns the color of a keyboard button depending on matching of past guesses @@ -442,11 +471,8 @@ buttonColor model k = -- Present in the first guess (yellow) and Exact in the second guess (green) -- the C key should be colored green. case k of - Key 'X' -> - bgYell - Key ch -> - bgCyan + tileBgColor (FilledTile (bestLetterColor model ch)) KeyBackspace -> bgCyan From c43f9360e6e56c24a0317fd8a455140a8f96ce89 Mon Sep 17 00:00:00 2001 From: Alessandro Re Date: Sat, 16 Mar 2024 17:28:14 +0100 Subject: [PATCH 12/14] fix: removed initial guesses --- src/Main.elm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Main.elm b/src/Main.elm index 72541ad..6cc3c00 100644 --- a/src/Main.elm +++ b/src/Main.elm @@ -27,7 +27,7 @@ type alias Model = init : () -> ( Model, Cmd Msg ) init _ = - ( { guesses = [ testGuess1, testGuess2 ] + ( { guesses = [] , current = testCurrent , solution = testSolution } From 901f93de49f886a68137c4138b84decb6f94b2d6 Mon Sep 17 00:00:00 2001 From: Alessandro Re Date: Sat, 16 Mar 2024 17:29:26 +0100 Subject: [PATCH 13/14] fix: minor cleaning --- src/Main.elm | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/Main.elm b/src/Main.elm index 6cc3c00..0c6e425 100644 --- a/src/Main.elm +++ b/src/Main.elm @@ -28,7 +28,7 @@ type alias Model = init : () -> ( Model, Cmd Msg ) init _ = ( { guesses = [] - , current = testCurrent + , current = [] , solution = testSolution } , Cmd.none @@ -184,18 +184,6 @@ testSolution = [ 'B', 'U', 'F', 'F', 'A' ] -testGuess1 = - rematch [ 'P', 'O', 'S', 'T', 'A' ] testSolution - - -testGuess2 = - rematch [ 'F', 'U', 'R', 'B', 'A' ] testSolution - - -testCurrent = - [ 'B', 'U', 'F' ] - - emptyWord = List.repeat 5 emptyTile From f6afd76b250c127e19e60dca2a722c88efdc4f31 Mon Sep 17 00:00:00 2001 From: Alessandro Re Date: Sat, 16 Mar 2024 17:33:46 +0100 Subject: [PATCH 14/14] fix: gh pages deploy --- .github/workflows/elm-to-gh-pages.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/elm-to-gh-pages.yml b/.github/workflows/elm-to-gh-pages.yml index 08b7e9c..fd9fefc 100644 --- a/.github/workflows/elm-to-gh-pages.yml +++ b/.github/workflows/elm-to-gh-pages.yml @@ -4,7 +4,7 @@ name: Deploy an Elm app to GitHub Pages on: # Runs on pushes targeting the default branch push: - branches: ["main"] + # branches: ["main", "] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -44,7 +44,7 @@ jobs: uses: actions/upload-pages-artifact@v1 with: path: '.' - + - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v2