From f2e31b19c5e4653f009d869b54f85e912f345ccd Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Fri, 11 Feb 2022 07:04:38 -0500 Subject: [PATCH 01/14] WIP: scaffold gameState and statistics types This is a WIP and experiemental effort to more directly mirror the structure of the real Wordle. --- main.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/main.go b/main.go index 8cc2a2a..6db2eb5 100644 --- a/main.go +++ b/main.go @@ -36,6 +36,46 @@ var ( words embed.FS ) +// example: {"currentStreak":1,"maxStreak":1,"guesses":{"1":1,"2":0,"3":0,"4":0,"5":0,"6":0,"fail":0},"winPercentage":100,"gamesPlayed":1,"gamesWon":1,"averageGuesses":1} +type statistics struct { + currentStreak int + maxStreak int + guesses map[string]int + winsPercentage int + gamesPlayed int + gamesWon int + averageGuesses int +} + +// example: {"boardState":["beach","under","","","",""],"evaluations":[["absent","present","absent","present","absent"],["correct","absent","absent","correct","correct"],null,null,null,null],"rowIndex":2,"solution":"ulcer","gameStatus":"IN_PROGRESS","lastPlayedTs":1644580347374,"lastCompletedTs":null,"restoringFromLocalStorage":null,"hardMode":false} +type gameState struct { + // a slice of guesses + // example: []string{"beach", "", "", "", "", ""} + boardState []string + + // a slice of slices, representing each guess's evaluated chars + // example: []string{[]string{"correct", "present", "absent", "absent", "absent"}, []string{}, []string{}, []string{}, []string{}, []string{}} + evaluations [][]string + + // the current row + rowIndex int + + // the solution word + solution string + + // example: IN_PROGRESS + // TODO: what are the other possible values? + gameStatus string + + // example: 1644580347374 + lastPlayedTS time.Time + + // example: 1644580347374 + lastCompletedTS time.Time + + hardMode bool +} + type wordle struct { word string guesses []map[string][wordLength]tileColor From 3c30b203e61b2e5a624117eead20556d88490bcf Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Fri, 11 Feb 2022 07:16:30 -0500 Subject: [PATCH 02/14] use wordle.state.solution rather than wordle.word --- main.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/main.go b/main.go index 6db2eb5..ac6c334 100644 --- a/main.go +++ b/main.go @@ -77,7 +77,7 @@ type gameState struct { } type wordle struct { - word string + state *gameState guesses []map[string][wordLength]tileColor in io.Reader out io.Writer @@ -122,7 +122,7 @@ func (w *wordle) getLetterTileColors(guess string) [wordLength]tileColor { } for j, guessLetter := range guess { - for k, letter := range w.word { + for k, letter := range w.state.solution { if guessLetter == letter { if j == k { colors[j] = green @@ -158,6 +158,7 @@ func (w *wordle) write(str string) { func (w *wordle) run() { reader := bufio.NewScanner(w.in) + solution := w.state.solution w.write(fmt.Sprintf("Version: \t%s\n", version)) w.write("Info: \t\thttps://github.com/mdb/wordle\n") @@ -174,22 +175,22 @@ func (w *wordle) run() { break } - if len(guess) != len(w.word) { + if len(guess) != len(solution) { w.write(fmt.Sprintf("%s is not a %v-letter word. Try again...\n", guess, wordLength)) guessCount-- } - if len(guess) == len(w.word) { + if len(guess) == len(solution) { w.displayGrid(guess, guessCount) } - if guess == w.word { + if guess == solution { break } if guessCount == maxGuesses-1 { fmt.Println() - w.displayRow(w.word, w.getLetterTileColors(w.word)) + w.displayRow(solution, w.getLetterTileColors(solution)) os.Exit(1) } } @@ -197,9 +198,11 @@ func (w *wordle) run() { func newWordle(word string, in io.Reader, out io.Writer) *wordle { return &wordle{ - word: word, - in: in, - out: out, + in: in, + out: out, + state: &gameState{ + solution: word, + }, } } From 17c88404e6f0de1f1f56ac44402c087034f7878f Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Fri, 11 Feb 2022 07:24:49 -0500 Subject: [PATCH 03/14] use w.state.rowIndex rather than guessCount --- main.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index ac6c334..6de69ee 100644 --- a/main.go +++ b/main.go @@ -47,10 +47,10 @@ type statistics struct { averageGuesses int } -// example: {"boardState":["beach","under","","","",""],"evaluations":[["absent","present","absent","present","absent"],["correct","absent","absent","correct","correct"],null,null,null,null],"rowIndex":2,"solution":"ulcer","gameStatus":"IN_PROGRESS","lastPlayedTs":1644580347374,"lastCompletedTs":null,"restoringFromLocalStorage":null,"hardMode":false} +// example: {"boardState":["BEACH","UNDER","","","",""],"evaluations":[["absent","present","absent","present","absent"],["correct","absent","absent","correct","correct"],null,null,null,null],"rowIndex":2,"solution":"ulcer","gameStatus":"IN_PROGRESS","lastPlayedTs":1644580347374,"lastCompletedTs":null,"restoringFromLocalStorage":null,"hardMode":false} type gameState struct { // a slice of guesses - // example: []string{"beach", "", "", "", "", ""} + // example: []string{"BEACH", "", "", "", "", ""} boardState []string // a slice of slices, representing each guess's evaluated chars @@ -165,7 +165,7 @@ func (w *wordle) run() { w.write("About: \t\tA CLI adaptation of Josh Wardle's Wordle (https://powerlanguage.co.uk/wordle/)\n\n") w.write(fmt.Sprintf("Guess a %v-letter word within %v guesses...\n", wordLength, maxGuesses)) - for guessCount := 0; guessCount < maxGuesses; guessCount++ { + for w.state.rowIndex = 0; w.state.rowIndex < maxGuesses; w.state.rowIndex++ { w.write(fmt.Sprintf("\nGuess (%v/%v): ", len(w.guesses)+1, maxGuesses)) reader.Scan() @@ -175,20 +175,22 @@ func (w *wordle) run() { break } + w.state.boardState = append(w.state.boardState, guess) + if len(guess) != len(solution) { w.write(fmt.Sprintf("%s is not a %v-letter word. Try again...\n", guess, wordLength)) - guessCount-- + w.state.rowIndex-- } if len(guess) == len(solution) { - w.displayGrid(guess, guessCount) + w.displayGrid(guess, w.state.rowIndex) } if guess == solution { break } - if guessCount == maxGuesses-1 { + if w.state.rowIndex == maxGuesses-1 { fmt.Println() w.displayRow(solution, w.getLetterTileColors(solution)) os.Exit(1) From f447f67aca624eb6538ffd94250df676088e74cf Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Fri, 11 Feb 2022 07:34:45 -0500 Subject: [PATCH 04/14] only append to boardState if guess length is correct --- main.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/main.go b/main.go index 6de69ee..d1f5e58 100644 --- a/main.go +++ b/main.go @@ -175,14 +175,13 @@ func (w *wordle) run() { break } - w.state.boardState = append(w.state.boardState, guess) - if len(guess) != len(solution) { w.write(fmt.Sprintf("%s is not a %v-letter word. Try again...\n", guess, wordLength)) w.state.rowIndex-- } if len(guess) == len(solution) { + w.state.boardState = append(w.state.boardState, guess) w.displayGrid(guess, w.state.rowIndex) } From 95ca8ab2a6041e98c5a0170d7d7507c4373814c0 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Sun, 13 Feb 2022 10:26:10 -0500 Subject: [PATCH 05/14] store evaluations in game state --- main.go | 26 +++++++++++++++++++++++++- main_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index d1f5e58..405b3e7 100644 --- a/main.go +++ b/main.go @@ -55,7 +55,7 @@ type gameState struct { // a slice of slices, representing each guess's evaluated chars // example: []string{[]string{"correct", "present", "absent", "absent", "absent"}, []string{}, []string{}, []string{}, []string{}, []string{}} - evaluations [][]string + evaluations [maxGuesses][wordLength]string // the current row rowIndex int @@ -152,6 +152,29 @@ func (w *wordle) displayEmptyRows(guessCount int) { } } +func (w *wordle) evaluateGuess(guess string) [wordLength]string { + evaluation := [wordLength]string{} + + for i := 0; i < wordLength; i++ { + evaluation[i] = "absent" + } + + for j, guessLetter := range guess { + for k, letter := range w.state.solution { + if guessLetter == letter { + if j == k { + evaluation[j] = "correct" + break + } + + evaluation[j] = "present" + } + } + } + + return evaluation +} + func (w *wordle) write(str string) { w.out.Write([]byte(str)) } @@ -182,6 +205,7 @@ func (w *wordle) run() { if len(guess) == len(solution) { w.state.boardState = append(w.state.boardState, guess) + w.state.evaluations[w.state.rowIndex] = w.evaluateGuess(guess) w.displayGrid(guess, w.state.rowIndex) } diff --git a/main_test.go b/main_test.go index 2892692..09424e5 100644 --- a/main_test.go +++ b/main_test.go @@ -88,3 +88,40 @@ func TestRun(t *testing.T) { }) } } + +func Test_evaluateGuess(t *testing.T) { + tests := []struct { + word string + guess string + expected [wordLength]string + }{{ + word: "seaks", + guess: "seaks", + expected: [wordLength]string{"correct", "correct", "correct", "correct", "correct"}, + }, { + word: "seaks", + guess: "beach", + expected: [wordLength]string{"absent", "correct", "correct", "absent", "absent"}, + }, { + word: "later", + guess: "beach", + expected: [wordLength]string{"absent", "present", "present", "absent", "absent"}, + }} + + for _, test := range tests { + t.Run(fmt.Sprintf("the word is '%s' and the guess ie '%s'", test.word, test.guess), func(t *testing.T) { + w := &wordle{ + state: &gameState{ + solution: test.word, + }, + } + + evaluations := w.evaluateGuess(test.guess) + for i, eval := range evaluations { + if eval != test.expected[i] { + t.Errorf("expected '%s' to equal '%s'; got '%s'", test.guess, test.expected[i], eval) + } + } + }) + } +} From a0814a2e46ab0c51b09b4b593ee995d20732889a Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Sun, 13 Feb 2022 11:27:48 -0500 Subject: [PATCH 06/14] displayGrid uses game state --- main.go | 47 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/main.go b/main.go index 405b3e7..87b37ad 100644 --- a/main.go +++ b/main.go @@ -51,7 +51,7 @@ type statistics struct { type gameState struct { // a slice of guesses // example: []string{"BEACH", "", "", "", "", ""} - boardState []string + boardState [maxGuesses]string // a slice of slices, representing each guess's evaluated chars // example: []string{[]string{"correct", "present", "absent", "absent", "absent"}, []string{}, []string{}, []string{}, []string{}, []string{}} @@ -101,17 +101,24 @@ func (w *wordle) displayRow(word string, colors [wordLength]tileColor) { w.write("\n") } -func (w *wordle) displayGrid(guess string, guessCount int) { - tileColors := w.getLetterTileColors(guess) - w.guesses = append(w.guesses, map[string][wordLength]tileColor{guess: tileColors}) +func (w *wordle) displayGrid() { + for i, guess := range w.state.boardState { + for j, guessLetter := range guess { + switch w.state.evaluations[i][j] { + case "correct": + w.write("\033[42m\033[1;30m") + case "present": + w.write("\033[43m\033[1;30m") + case "absent": + w.write("\033[40m\033[1;37m") + } - for _, guess := range w.guesses { - for g, colors := range guess { - w.displayRow(g, colors) + w.write(fmt.Sprintf(" %c ", guessLetter)) + w.write("\033[m\033[m") } - } - w.displayEmptyRows(guessCount) + w.write("\n") + } } func (w *wordle) getLetterTileColors(guess string) [wordLength]tileColor { @@ -204,9 +211,9 @@ func (w *wordle) run() { } if len(guess) == len(solution) { - w.state.boardState = append(w.state.boardState, guess) + w.state.boardState[w.state.rowIndex] = guess w.state.evaluations[w.state.rowIndex] = w.evaluateGuess(guess) - w.displayGrid(guess, w.state.rowIndex) + w.displayGrid() } if guess == solution { @@ -222,13 +229,29 @@ func (w *wordle) run() { } func newWordle(word string, in io.Reader, out io.Writer) *wordle { - return &wordle{ + w := &wordle{ in: in, out: out, state: &gameState{ solution: word, }, } + emptyGuessChar := "*" + emptyGuess := "" + emptyGuessEvaluation := [wordLength]string{} + + for i := 0; i < wordLength; i++ { + emptyGuess = emptyGuess + emptyGuessChar + fmt.Println(emptyGuess) + emptyGuessEvaluation[i] = "absent" + } + + for i := 0; i < maxGuesses; i++ { + w.state.evaluations[i] = emptyGuessEvaluation + w.state.boardState[i] = emptyGuess + } + + return w } func getWordFromFile() string { From 00c5b10c71b4970fa6d2c5939d8035ceb433d0c7 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Sun, 13 Feb 2022 11:49:16 -0500 Subject: [PATCH 07/14] clean up grid display --- main.go | 95 +++++++++++++++++++++++++--------------------------- main_test.go | 10 +++--- 2 files changed, 51 insertions(+), 54 deletions(-) diff --git a/main.go b/main.go index 87b37ad..bd765a9 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,14 @@ const ( green ) +type evaluation int + +const ( + absent evaluation = iota + present + correct +) + var ( // passed in at build time version string @@ -54,8 +62,7 @@ type gameState struct { boardState [maxGuesses]string // a slice of slices, representing each guess's evaluated chars - // example: []string{[]string{"correct", "present", "absent", "absent", "absent"}, []string{}, []string{}, []string{}, []string{}, []string{}} - evaluations [maxGuesses][wordLength]string + evaluations [maxGuesses][wordLength]evaluation // the current row rowIndex int @@ -63,7 +70,7 @@ type gameState struct { // the solution word solution string - // example: IN_PROGRESS + // example: IN_PROGRESS, FAIL // TODO: what are the other possible values? gameStatus string @@ -83,19 +90,9 @@ type wordle struct { out io.Writer } -func (w *wordle) displayRow(word string, colors [wordLength]tileColor) { - for i, c := range word { - switch colors[i] { - case green: - w.write("\033[42m\033[1;30m") - case yellow: - w.write("\033[43m\033[1;30m") - case gray: - w.write("\033[40m\033[1;37m") - } - - w.write(fmt.Sprintf(" %c ", c)) - w.write("\033[m\033[m") +func (w *wordle) displaySolution() { + for _, char := range w.state.solution { + w.displayGreenTile(char) } w.write("\n") @@ -105,22 +102,37 @@ func (w *wordle) displayGrid() { for i, guess := range w.state.boardState { for j, guessLetter := range guess { switch w.state.evaluations[i][j] { - case "correct": - w.write("\033[42m\033[1;30m") - case "present": - w.write("\033[43m\033[1;30m") - case "absent": - w.write("\033[40m\033[1;37m") + case correct: + w.displayGreenTile(guessLetter) + case present: + w.displayYellowTile(guessLetter) + case absent: + w.displayGrayTile(guessLetter) } - - w.write(fmt.Sprintf(" %c ", guessLetter)) - w.write("\033[m\033[m") } w.write("\n") } } +func (w *wordle) displayGreenTile(char rune) { + w.write("\033[42m\033[1;30m") + w.write(fmt.Sprintf(" %c ", char)) + w.write("\033[m\033[m") +} + +func (w *wordle) displayYellowTile(char rune) { + w.write("\033[43m\033[1;30m") + w.write(fmt.Sprintf(" %c ", char)) + w.write("\033[m\033[m") +} + +func (w *wordle) displayGrayTile(char rune) { + w.write("\033[40m\033[1;37m") + w.write(fmt.Sprintf(" %c ", char)) + w.write("\033[m\033[m") +} + func (w *wordle) getLetterTileColors(guess string) [wordLength]tileColor { colors := [wordLength]tileColor{} @@ -144,37 +156,22 @@ func (w *wordle) getLetterTileColors(guess string) [wordLength]tileColor { return colors } -func (w *wordle) displayEmptyRows(guessCount int) { - emptyGuessChars := []string{} - for i := 0; i < wordLength; i++ { - emptyGuessChars = append(emptyGuessChars, "*") - } - - emptyGuess := strings.Join(emptyGuessChars, "") - emptyTileColors := w.getLetterTileColors(emptyGuess) - emptyRowCount := maxGuesses - guessCount - 1 - - for i := 0; i < emptyRowCount; i++ { - w.displayRow(emptyGuess, emptyTileColors) - } -} - -func (w *wordle) evaluateGuess(guess string) [wordLength]string { - evaluation := [wordLength]string{} +func (w *wordle) evaluateGuess(guess string) [wordLength]evaluation { + evaluation := [wordLength]evaluation{} for i := 0; i < wordLength; i++ { - evaluation[i] = "absent" + evaluation[i] = absent } for j, guessLetter := range guess { for k, letter := range w.state.solution { if guessLetter == letter { if j == k { - evaluation[j] = "correct" + evaluation[j] = correct break } - evaluation[j] = "present" + evaluation[j] = present } } } @@ -196,7 +193,7 @@ func (w *wordle) run() { w.write(fmt.Sprintf("Guess a %v-letter word within %v guesses...\n", wordLength, maxGuesses)) for w.state.rowIndex = 0; w.state.rowIndex < maxGuesses; w.state.rowIndex++ { - w.write(fmt.Sprintf("\nGuess (%v/%v): ", len(w.guesses)+1, maxGuesses)) + w.write(fmt.Sprintf("\nGuess (%v/%v): ", w.state.rowIndex+1, maxGuesses)) reader.Scan() guess := strings.ToUpper(reader.Text()) @@ -222,7 +219,7 @@ func (w *wordle) run() { if w.state.rowIndex == maxGuesses-1 { fmt.Println() - w.displayRow(solution, w.getLetterTileColors(solution)) + w.displaySolution() os.Exit(1) } } @@ -238,12 +235,12 @@ func newWordle(word string, in io.Reader, out io.Writer) *wordle { } emptyGuessChar := "*" emptyGuess := "" - emptyGuessEvaluation := [wordLength]string{} + emptyGuessEvaluation := [wordLength]evaluation{} for i := 0; i < wordLength; i++ { emptyGuess = emptyGuess + emptyGuessChar fmt.Println(emptyGuess) - emptyGuessEvaluation[i] = "absent" + emptyGuessEvaluation[i] = absent } for i := 0; i < maxGuesses; i++ { diff --git a/main_test.go b/main_test.go index 09424e5..9c8c2af 100644 --- a/main_test.go +++ b/main_test.go @@ -93,19 +93,19 @@ func Test_evaluateGuess(t *testing.T) { tests := []struct { word string guess string - expected [wordLength]string + expected [wordLength]evaluation }{{ word: "seaks", guess: "seaks", - expected: [wordLength]string{"correct", "correct", "correct", "correct", "correct"}, + expected: [wordLength]evaluation{correct, correct, correct, correct, correct}, }, { word: "seaks", guess: "beach", - expected: [wordLength]string{"absent", "correct", "correct", "absent", "absent"}, + expected: [wordLength]evaluation{absent, correct, correct, absent, absent}, }, { word: "later", guess: "beach", - expected: [wordLength]string{"absent", "present", "present", "absent", "absent"}, + expected: [wordLength]evaluation{absent, present, present, absent, absent}, }} for _, test := range tests { @@ -119,7 +119,7 @@ func Test_evaluateGuess(t *testing.T) { evaluations := w.evaluateGuess(test.guess) for i, eval := range evaluations { if eval != test.expected[i] { - t.Errorf("expected '%s' to equal '%s'; got '%s'", test.guess, test.expected[i], eval) + t.Errorf("expected '%s' to equal '%v'; got '%v'", test.guess, test.expected[i], eval) } } }) From 348d5f0c3294c2c6230d66c56d14d8d3da7400b5 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Sun, 13 Feb 2022 11:50:50 -0500 Subject: [PATCH 08/14] remove un-used funcs --- main.go | 38 +++----------------------------------- 1 file changed, 3 insertions(+), 35 deletions(-) diff --git a/main.go b/main.go index bd765a9..6cab7a2 100644 --- a/main.go +++ b/main.go @@ -19,14 +19,6 @@ const ( wordLength int = 5 ) -type tileColor int - -const ( - gray tileColor = iota - yellow - green -) - type evaluation int const ( @@ -84,10 +76,9 @@ type gameState struct { } type wordle struct { - state *gameState - guesses []map[string][wordLength]tileColor - in io.Reader - out io.Writer + state *gameState + in io.Reader + out io.Writer } func (w *wordle) displaySolution() { @@ -133,29 +124,6 @@ func (w *wordle) displayGrayTile(char rune) { w.write("\033[m\033[m") } -func (w *wordle) getLetterTileColors(guess string) [wordLength]tileColor { - colors := [wordLength]tileColor{} - - for i := range colors { - colors[i] = gray - } - - for j, guessLetter := range guess { - for k, letter := range w.state.solution { - if guessLetter == letter { - if j == k { - colors[j] = green - break - } - - colors[j] = yellow - } - } - } - - return colors -} - func (w *wordle) evaluateGuess(guess string) [wordLength]evaluation { evaluation := [wordLength]evaluation{} From a15e79d04c75b8997101747c2c81a714fdd5c438 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Sun, 13 Feb 2022 12:05:10 -0500 Subject: [PATCH 09/14] state is part of wordle struct there is arguably no need for separate types --- main.go | 67 +++++++++++++++++++++++++--------------------------- main_test.go | 4 +--- 2 files changed, 33 insertions(+), 38 deletions(-) diff --git a/main.go b/main.go index 6cab7a2..62303a9 100644 --- a/main.go +++ b/main.go @@ -15,8 +15,9 @@ import ( ) const ( - maxGuesses int = 6 - wordLength int = 5 + maxGuesses int = 6 + wordLength int = 5 + emptyGuessChar string = "*" ) type evaluation int @@ -47,11 +48,13 @@ type statistics struct { averageGuesses int } -// example: {"boardState":["BEACH","UNDER","","","",""],"evaluations":[["absent","present","absent","present","absent"],["correct","absent","absent","correct","correct"],null,null,null,null],"rowIndex":2,"solution":"ulcer","gameStatus":"IN_PROGRESS","lastPlayedTs":1644580347374,"lastCompletedTs":null,"restoringFromLocalStorage":null,"hardMode":false} -type gameState struct { +type wordle struct { + in io.Reader + out io.Writer + // a slice of guesses // example: []string{"BEACH", "", "", "", "", ""} - boardState [maxGuesses]string + guesses [maxGuesses]string // a slice of slices, representing each guess's evaluated chars evaluations [maxGuesses][wordLength]evaluation @@ -75,14 +78,8 @@ type gameState struct { hardMode bool } -type wordle struct { - state *gameState - in io.Reader - out io.Writer -} - func (w *wordle) displaySolution() { - for _, char := range w.state.solution { + for _, char := range w.solution { w.displayGreenTile(char) } @@ -90,9 +87,9 @@ func (w *wordle) displaySolution() { } func (w *wordle) displayGrid() { - for i, guess := range w.state.boardState { + for i, guess := range w.guesses { for j, guessLetter := range guess { - switch w.state.evaluations[i][j] { + switch w.evaluations[i][j] { case correct: w.displayGreenTile(guessLetter) case present: @@ -108,18 +105,20 @@ func (w *wordle) displayGrid() { func (w *wordle) displayGreenTile(char rune) { w.write("\033[42m\033[1;30m") - w.write(fmt.Sprintf(" %c ", char)) - w.write("\033[m\033[m") + w.displayOnTile(char) } func (w *wordle) displayYellowTile(char rune) { w.write("\033[43m\033[1;30m") - w.write(fmt.Sprintf(" %c ", char)) - w.write("\033[m\033[m") + w.displayOnTile(char) } func (w *wordle) displayGrayTile(char rune) { w.write("\033[40m\033[1;37m") + w.displayOnTile(char) +} + +func (w *wordle) displayOnTile(char rune) { w.write(fmt.Sprintf(" %c ", char)) w.write("\033[m\033[m") } @@ -132,7 +131,7 @@ func (w *wordle) evaluateGuess(guess string) [wordLength]evaluation { } for j, guessLetter := range guess { - for k, letter := range w.state.solution { + for k, letter := range w.solution { if guessLetter == letter { if j == k { evaluation[j] = correct @@ -153,15 +152,15 @@ func (w *wordle) write(str string) { func (w *wordle) run() { reader := bufio.NewScanner(w.in) - solution := w.state.solution + solution := w.solution w.write(fmt.Sprintf("Version: \t%s\n", version)) w.write("Info: \t\thttps://github.com/mdb/wordle\n") w.write("About: \t\tA CLI adaptation of Josh Wardle's Wordle (https://powerlanguage.co.uk/wordle/)\n\n") w.write(fmt.Sprintf("Guess a %v-letter word within %v guesses...\n", wordLength, maxGuesses)) - for w.state.rowIndex = 0; w.state.rowIndex < maxGuesses; w.state.rowIndex++ { - w.write(fmt.Sprintf("\nGuess (%v/%v): ", w.state.rowIndex+1, maxGuesses)) + for w.rowIndex = 0; w.rowIndex < maxGuesses; w.rowIndex++ { + w.write(fmt.Sprintf("\nGuess (%v/%v): ", w.rowIndex+1, maxGuesses)) reader.Scan() guess := strings.ToUpper(reader.Text()) @@ -172,12 +171,12 @@ func (w *wordle) run() { if len(guess) != len(solution) { w.write(fmt.Sprintf("%s is not a %v-letter word. Try again...\n", guess, wordLength)) - w.state.rowIndex-- + w.rowIndex-- } if len(guess) == len(solution) { - w.state.boardState[w.state.rowIndex] = guess - w.state.evaluations[w.state.rowIndex] = w.evaluateGuess(guess) + w.guesses[w.rowIndex] = guess + w.evaluations[w.rowIndex] = w.evaluateGuess(guess) w.displayGrid() } @@ -185,7 +184,7 @@ func (w *wordle) run() { break } - if w.state.rowIndex == maxGuesses-1 { + if w.rowIndex == maxGuesses-1 { fmt.Println() w.displaySolution() os.Exit(1) @@ -195,25 +194,23 @@ func (w *wordle) run() { func newWordle(word string, in io.Reader, out io.Writer) *wordle { w := &wordle{ - in: in, - out: out, - state: &gameState{ - solution: word, - }, + in: in, + out: out, + solution: word, } - emptyGuessChar := "*" emptyGuess := "" emptyGuessEvaluation := [wordLength]evaluation{} for i := 0; i < wordLength; i++ { emptyGuess = emptyGuess + emptyGuessChar - fmt.Println(emptyGuess) emptyGuessEvaluation[i] = absent } + // By seeding w with dummy guesses and dummy evaluations, + // displayGrid displays remaining rows with each grid rendering. for i := 0; i < maxGuesses; i++ { - w.state.evaluations[i] = emptyGuessEvaluation - w.state.boardState[i] = emptyGuess + w.evaluations[i] = emptyGuessEvaluation + w.guesses[i] = emptyGuess } return w diff --git a/main_test.go b/main_test.go index 9c8c2af..bf1f182 100644 --- a/main_test.go +++ b/main_test.go @@ -111,9 +111,7 @@ func Test_evaluateGuess(t *testing.T) { for _, test := range tests { t.Run(fmt.Sprintf("the word is '%s' and the guess ie '%s'", test.word, test.guess), func(t *testing.T) { w := &wordle{ - state: &gameState{ - solution: test.word, - }, + solution: test.word, } evaluations := w.evaluateGuess(test.guess) From 44ed0ea737f0164e3f0dfd8de264c54831660a9e Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Sun, 13 Feb 2022 12:07:20 -0500 Subject: [PATCH 10/14] use 'guessIndex' rather than 'rowIndex' 'guessIndex' is a bit more descriptive --- main.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/main.go b/main.go index 62303a9..992c529 100644 --- a/main.go +++ b/main.go @@ -59,8 +59,8 @@ type wordle struct { // a slice of slices, representing each guess's evaluated chars evaluations [maxGuesses][wordLength]evaluation - // the current row - rowIndex int + // the current guess index + guessIndex int // the solution word solution string @@ -159,8 +159,8 @@ func (w *wordle) run() { w.write("About: \t\tA CLI adaptation of Josh Wardle's Wordle (https://powerlanguage.co.uk/wordle/)\n\n") w.write(fmt.Sprintf("Guess a %v-letter word within %v guesses...\n", wordLength, maxGuesses)) - for w.rowIndex = 0; w.rowIndex < maxGuesses; w.rowIndex++ { - w.write(fmt.Sprintf("\nGuess (%v/%v): ", w.rowIndex+1, maxGuesses)) + for w.guessIndex = 0; w.guessIndex < maxGuesses; w.guessIndex++ { + w.write(fmt.Sprintf("\nGuess (%v/%v): ", w.guessIndex+1, maxGuesses)) reader.Scan() guess := strings.ToUpper(reader.Text()) @@ -171,12 +171,12 @@ func (w *wordle) run() { if len(guess) != len(solution) { w.write(fmt.Sprintf("%s is not a %v-letter word. Try again...\n", guess, wordLength)) - w.rowIndex-- + w.guessIndex-- } if len(guess) == len(solution) { - w.guesses[w.rowIndex] = guess - w.evaluations[w.rowIndex] = w.evaluateGuess(guess) + w.guesses[w.guessIndex] = guess + w.evaluations[w.guessIndex] = w.evaluateGuess(guess) w.displayGrid() } @@ -184,7 +184,7 @@ func (w *wordle) run() { break } - if w.rowIndex == maxGuesses-1 { + if w.guessIndex == maxGuesses-1 { fmt.Println() w.displaySolution() os.Exit(1) From acb285d2fcaa3cb3eee9ec4f8083330a26cd05be Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Mon, 14 Feb 2022 07:42:28 -0500 Subject: [PATCH 11/14] bump version --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f3b5180..455ca85 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION = 0.0.7 +VERSION = 0.0.8 SOURCE = ./... .DEFAULT_GOAL := build From 2615e0597f4426841b9809db27d377d07f9f71a2 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Fri, 11 Feb 2022 07:06:01 -0500 Subject: [PATCH 12/14] add README improvement candidate --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8c49c5a..753334f 100644 --- a/README.md +++ b/README.md @@ -62,3 +62,4 @@ make * Should there be a `-practice` flag to support `-practice` mode? If so, should it use old solutions or perhaps https://raw.githubusercontent.com/dwyl/english-words/master/words_alpha.txt ? * Should there be a `-hard` flag to support a hard mode? * Could `wordle` support configurable backends -- like `git` -- for persisting stats and current state? +* Should `wordle` display a "Not in word list" message, similar to real Wordle? From 38f34927b3eb508bce06d796480a9060ae2486b4 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Fri, 22 Jul 2022 08:53:07 -0400 Subject: [PATCH 13/14] fine-tune types and add code comments --- main.go | 77 ++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/main.go b/main.go index 992c529..da4dc76 100644 --- a/main.go +++ b/main.go @@ -37,45 +37,61 @@ var ( words embed.FS ) -// example: {"currentStreak":1,"maxStreak":1,"guesses":{"1":1,"2":0,"3":0,"4":0,"5":0,"6":0,"fail":0},"winPercentage":100,"gamesPlayed":1,"gamesWon":1,"averageGuesses":1} -type statistics struct { - currentStreak int - maxStreak int - guesses map[string]int - winsPercentage int - gamesPlayed int - gamesWon int - averageGuesses int +// history homes historical data. +// TODO: While not currently used, in the future it could be saved as a +// JSON file (or perhaps pluggible backend?) to support a `wordle -statistics` +// feature that displays a visualization of historical data, or something similar-ish. +// For comparison, an example of the original statics JSON: +// {"currentStreak":1,"maxStreak":1,"guesses":{"1":1,"2":0,"3":0,"4":0,"5":0,"6":0,"fail":0},"winPercentage":100,"gamesPlayed":1,"gamesWon":1,"averageGuesses":1} +// However, history diverges from the original wordle and represents all this a +// bit differently... +type history struct { + // currentStreak is the current streak. + currentStreak int + + // maxStreak is the maximum streak, historically. + maxStreak int + + // games is a slice of past games played. + games []game } +// game is the historical representation of a particular game played. +type game struct { + // id is the index identifying of the game (i.e. the first game played, the second game played, etc.). + id int + + // guessCount is the total number of guesses used before game completion. + guessCount int + + // success represents whether the player successfully guessed the word. + success bool + + // complete represents whether a game was played until completion. + complete bool + + // time represents when the game was last played. + time time.Time +} + +// wordle is a word guessing game based on Josh Wardle's Wordle (https://powerlanguage.co.uk/wordle/). +// It represents an instance of the wordle game. type wordle struct { in io.Reader out io.Writer - // a slice of guesses + // guesses is a slice of word guesses. // example: []string{"BEACH", "", "", "", "", ""} guesses [maxGuesses]string - // a slice of slices, representing each guess's evaluated chars + // evaluations is a slice of slices, representing an evaluation of each character of each guess. evaluations [maxGuesses][wordLength]evaluation - // the current guess index + // guessIndex is the current guess index. guessIndex int - // the solution word + // solution is the solution word. solution string - - // example: IN_PROGRESS, FAIL - // TODO: what are the other possible values? - gameStatus string - - // example: 1644580347374 - lastPlayedTS time.Time - - // example: 1644580347374 - lastCompletedTS time.Time - - hardMode bool } func (w *wordle) displaySolution() { @@ -193,6 +209,9 @@ func (w *wordle) run() { } func newWordle(word string, in io.Reader, out io.Writer) *wordle { + // TODO: Consider configuring wordle with a 'history' that includes 'games'. + // This could allow the wordle to render with a pre-populated grid showing + // the current day's game state if the game is still in-progress and incomplete. w := &wordle{ in: in, out: out, @@ -206,7 +225,7 @@ func newWordle(word string, in io.Reader, out io.Writer) *wordle { emptyGuessEvaluation[i] = absent } - // By seeding w with dummy guesses and dummy evaluations, + // By seeding with dummy guesses and dummy evaluations, // displayGrid displays remaining rows with each grid rendering. for i := 0; i < maxGuesses; i++ { w.evaluations[i] = emptyGuessEvaluation @@ -229,6 +248,12 @@ func getWordFromFile() string { return strings.ToUpper(strings.Split(string(data), ",")[daysSinceStart]) } +// getWordFromURL populates the wordle word via a random word chosen +// from those listed at a remote URL, rather than via the in-baked +// per-day list of wordle words. +// While it's not currently used, it could be used in the future to +// enable something like a `wordle -for-sport` feature that allows +// users to play multiple games/day "for sport." func getWordFromURL() string { // NOTE: this list inludes many uncommon and seemingly not-English words. Is there a better data source? res, err := http.Get("https://raw.githubusercontent.com/dwyl/english-words/master/words_alpha.txt") From b0b44416776c2cb1eb76c29f2eff3934d80bb0ba Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Sat, 23 Jul 2022 06:36:57 -0400 Subject: [PATCH 14/14] use go 1.18 --- .github/workflows/main.yml | 4 ++-- go.mod | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6cf1e8b..9819e9b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.17 + go-version: 1.18 - name: Build run: make - name: Ensure unique version @@ -35,7 +35,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.17 + go-version: 1.18 - name: Create release tag run: make tag env: diff --git a/go.mod b/go.mod index ffc5f22..2e01b44 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/mdb/wordle -go 1.17 +go 1.18