Initial commit
This commit is contained in:
commit
06d64bd3cb
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
|
@ -0,0 +1,212 @@
|
|||
### Go ###
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
### Go Patch ###
|
||||
/vendor/
|
||||
/Godeps/
|
||||
|
||||
### Intellij+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### Intellij+all Patch ###
|
||||
# Ignores the whole .idea folder and all .iml files
|
||||
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
|
||||
|
||||
.idea/*
|
||||
|
||||
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
|
||||
|
||||
*.iml
|
||||
modules.xml
|
||||
.idea/misc.xml
|
||||
*.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
.idea/sonarlint
|
||||
|
||||
### Linux ###
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
# Support for Project snippet scope
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
|
@ -0,0 +1,7 @@
|
|||
Copyright 2022 Jake Walker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,54 @@
|
|||
package game
|
||||
|
||||
import "tildegit.org/jakew/wordle/game/words"
|
||||
|
||||
type Guess struct {
|
||||
Letters []rune
|
||||
Correct []int
|
||||
Present []int
|
||||
}
|
||||
|
||||
type Board struct {
|
||||
Guesses [6]Guess
|
||||
CurrentGuess int
|
||||
Solution string
|
||||
Done bool
|
||||
Day int
|
||||
}
|
||||
|
||||
func NewBoard() *Board {
|
||||
solution, day := words.GetSolution()
|
||||
return &Board{
|
||||
Guesses: [6]Guess{},
|
||||
CurrentGuess: 0,
|
||||
Solution: solution,
|
||||
Day: day,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Board) AddWord(word string) string {
|
||||
if b.Done {
|
||||
return "You are done!"
|
||||
}
|
||||
|
||||
if len(word) != 5 {
|
||||
return "Word must be 5 long!"
|
||||
}
|
||||
if !words.IsValid(word) {
|
||||
return "Word is not in dictionary!"
|
||||
}
|
||||
|
||||
correct, present := words.Check(word, b.Solution)
|
||||
b.Guesses[b.CurrentGuess] = Guess{
|
||||
Letters: []rune(word),
|
||||
Correct: correct,
|
||||
Present: present,
|
||||
}
|
||||
b.CurrentGuess += 1
|
||||
|
||||
if len(correct) == 5 || b.CurrentGuess >= 6 {
|
||||
b.Done = true
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package score
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"tildegit.org/jakew/wordle/game"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Line struct {
|
||||
Word string `json:"word"`
|
||||
Correct []int `json:"correct"`
|
||||
Present []int `json:"present"`
|
||||
}
|
||||
|
||||
type Game struct {
|
||||
Day int `json:"day"`
|
||||
Date time.Time `json:"date"`
|
||||
Score int `json:"score"`
|
||||
Lines []Line `json:"lines"`
|
||||
}
|
||||
|
||||
type Scores struct {
|
||||
Games []Game `json:"games"`
|
||||
}
|
||||
|
||||
func ToGame(b *game.Board) *Game {
|
||||
g := &Game{
|
||||
Day: b.Day,
|
||||
Date: time.Now().In(time.UTC),
|
||||
Score: b.CurrentGuess,
|
||||
Lines: []Line{},
|
||||
}
|
||||
|
||||
for i := 0; i < b.CurrentGuess; i++ {
|
||||
g.Lines = append(g.Lines, Line{
|
||||
Word: string(b.Guesses[i].Letters),
|
||||
Correct: b.Guesses[i].Correct,
|
||||
Present: b.Guesses[i].Present,
|
||||
})
|
||||
}
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
func getSaveLocation() (string, error) {
|
||||
dirname, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dirname, "/.wordle.json"), nil
|
||||
}
|
||||
|
||||
func Load() (*Scores, error) {
|
||||
path, err := getSaveLocation()
|
||||
if err != nil {
|
||||
return &Scores{}, err
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return &Scores{Games: []Game{}}, nil
|
||||
} else if err != nil {
|
||||
return &Scores{}, err
|
||||
}
|
||||
|
||||
var scores Scores
|
||||
err = json.Unmarshal(b, &scores)
|
||||
return &scores, err
|
||||
}
|
||||
|
||||
func Save(scores *Scores) error {
|
||||
path, err := getSaveLocation()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err := json.Marshal(scores)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(path, b, 0644)
|
||||
return err
|
||||
}
|
||||
|
||||
func SaveBoard(b *game.Board) error {
|
||||
scores, err := Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scores.Games = append(scores.Games, *ToGame(b))
|
||||
err = Save(scores)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetLastGame() (*Game, error) {
|
||||
scores, err := Load()
|
||||
if err != nil {
|
||||
return &Game{}, err
|
||||
}
|
||||
|
||||
if len(scores.Games) <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &scores.Games[len(scores.Games)-1], nil
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package util
|
||||
|
||||
func ContainsInt(slice []int, n int) bool {
|
||||
for _, i := range slice {
|
||||
if i == n {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,66 @@
|
|||
package words
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"strings"
|
||||
"tildegit.org/jakew/wordle/game/util"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed words.txt
|
||||
var words string
|
||||
|
||||
//go:embed dictionary.txt
|
||||
var dictionary string
|
||||
|
||||
func GetSolution() (string, int) {
|
||||
wordSlice := strings.Split(words, "\n")
|
||||
today := time.Now().In(time.UTC).Truncate(24 * time.Hour)
|
||||
day := (today.UnixMilli() - 1624057200000) / 864e5
|
||||
index := day % int64(len(wordSlice))
|
||||
return strings.ToLower(strings.TrimSpace(wordSlice[index])), int(day)
|
||||
}
|
||||
|
||||
func contains(words string, word string) bool {
|
||||
slice := strings.Split(words, "\n")
|
||||
for _, item := range slice {
|
||||
if item != "" && item == word {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsValid(word string) bool {
|
||||
word = strings.TrimSpace(strings.ToLower(word))
|
||||
if len(word) != 5 {
|
||||
return false
|
||||
}
|
||||
return contains(words, word) || contains(dictionary, word)
|
||||
}
|
||||
|
||||
func Check(word string, solution string) (correct, present []int) {
|
||||
correct = []int{}
|
||||
present = []int{}
|
||||
exclude := []int{}
|
||||
word = strings.TrimSpace(strings.ToLower(word))
|
||||
|
||||
for i, letter := range word {
|
||||
// check if correct position
|
||||
if letter == int32(solution[i]) {
|
||||
correct = append(correct, i)
|
||||
continue
|
||||
}
|
||||
|
||||
// is the letter in the word
|
||||
for j, letter1 := range solution {
|
||||
if letter == letter1 && !util.ContainsInt(exclude, j) {
|
||||
present = append(present, i)
|
||||
exclude = append(exclude, j)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,108 @@
|
|||
package words
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCheck(t *testing.T) {
|
||||
type args struct {
|
||||
word string
|
||||
solution string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantCorrect []int
|
||||
wantPresent []int
|
||||
}{
|
||||
{
|
||||
name: "incorrect",
|
||||
args: args{word: "grass", solution: "hello"},
|
||||
wantCorrect: []int{},
|
||||
wantPresent: []int{},
|
||||
},
|
||||
{
|
||||
name: "incorrect position",
|
||||
args: args{word: "crash", solution: "hello"},
|
||||
wantCorrect: []int{},
|
||||
wantPresent: []int{4},
|
||||
},
|
||||
{
|
||||
name: "correct position",
|
||||
args: args{word: "hrass", solution: "hello"},
|
||||
wantCorrect: []int{0},
|
||||
wantPresent: []int{},
|
||||
},
|
||||
{
|
||||
name: "incorrect position double letter",
|
||||
// here there is a double letter in the word, only one being in the solution
|
||||
args: args{word: "green", solution: "hello"},
|
||||
wantCorrect: []int{},
|
||||
wantPresent: []int{2},
|
||||
},
|
||||
{
|
||||
name: "partial double letter",
|
||||
// here there is a double letter in the word, with one being in the correct place
|
||||
args: args{word: "hllxy", solution: "hello"},
|
||||
wantCorrect: []int{0, 2},
|
||||
wantPresent: []int{1},
|
||||
},
|
||||
{
|
||||
name: "correct",
|
||||
args: args{word: "hello", solution: "hello"},
|
||||
wantCorrect: []int{0, 1, 2, 3, 4},
|
||||
wantPresent: []int{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotCorrect, gotPresent := Check(tt.args.word, tt.args.solution)
|
||||
if !reflect.DeepEqual(gotCorrect, tt.wantCorrect) {
|
||||
t.Errorf("Check() gotCorrect = %v, want %v", gotCorrect, tt.wantCorrect)
|
||||
}
|
||||
if !reflect.DeepEqual(gotPresent, tt.wantPresent) {
|
||||
t.Errorf("Check() gotPresent = %v, want %v", gotPresent, tt.wantPresent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValid(t *testing.T) {
|
||||
type args struct {
|
||||
word string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{word: "hello"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "too short",
|
||||
args: args{word: "egg"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "too long",
|
||||
args: args{word: "jellyfish"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "not a word",
|
||||
args: args{word: "aaaaa"},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsValid(tt.args.word); got != tt.want {
|
||||
t.Errorf("IsValid() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
module tildegit.org/jakew/wordle
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbletea v0.19.3
|
||||
github.com/charmbracelet/lipgloss v0.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/containerd/console v1.0.2 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.9.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
||||
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed // indirect
|
||||
)
|
|
@ -0,0 +1,35 @@
|
|||
github.com/charmbracelet/bubbletea v0.19.3 h1:OKeO/Y13rQQqt4snX+lePB0QrnW80UdrMNolnCcmoAw=
|
||||
github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
|
||||
github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g=
|
||||
github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
|
||||
github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn9dE=
|
||||
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8=
|
||||
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0=
|
||||
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
@ -0,0 +1,170 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"os"
|
||||
"tildegit.org/jakew/wordle/game"
|
||||
"tildegit.org/jakew/wordle/game/score"
|
||||
"tildegit.org/jakew/wordle/game/util"
|
||||
"tildegit.org/jakew/wordle/game/words"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type model struct {
|
||||
board *game.Board
|
||||
buffer []rune
|
||||
error string
|
||||
}
|
||||
|
||||
func initialModel() model {
|
||||
m := model{
|
||||
board: game.NewBoard(),
|
||||
}
|
||||
|
||||
lastGame, err := score.GetLastGame()
|
||||
if err != nil {
|
||||
m.error = fmt.Sprintf("Could not load last game: %v", err.Error())
|
||||
return m
|
||||
}
|
||||
|
||||
if lastGame != nil {
|
||||
m.error = fmt.Sprintf("Come back tomorrow for a new word")
|
||||
m.board = &game.Board{
|
||||
Guesses: [6]game.Guess{},
|
||||
CurrentGuess: lastGame.Score,
|
||||
Solution: "",
|
||||
Done: true,
|
||||
Day: lastGame.Day,
|
||||
}
|
||||
|
||||
for i, line := range lastGame.Lines {
|
||||
m.board.Guesses[i] = game.Guess{
|
||||
Letters: []rune(line.Word),
|
||||
Correct: line.Correct,
|
||||
Present: line.Present,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
m.error = ""
|
||||
switch msg.Type {
|
||||
case tea.KeyCtrlC:
|
||||
return m, tea.Quit
|
||||
case tea.KeyBackspace:
|
||||
if !m.board.Done && len(m.buffer) > 0 {
|
||||
m.buffer = m.buffer[:len(m.buffer)-1]
|
||||
}
|
||||
case tea.KeyEnter:
|
||||
if m.board.Done {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if len(m.buffer) != 5 {
|
||||
m.error = "Word must be 5 long"
|
||||
} else if !words.IsValid(string(m.buffer)) {
|
||||
m.error = "Word not in dictionary"
|
||||
} else {
|
||||
msg := m.board.AddWord(string(m.buffer))
|
||||
if msg != "" {
|
||||
m.error = msg
|
||||
return m, nil
|
||||
}
|
||||
// clear input
|
||||
m.buffer = []rune{}
|
||||
|
||||
// save if win
|
||||
if m.board.Done {
|
||||
err := score.SaveBoard(m.board)
|
||||
if err != nil {
|
||||
m.error = fmt.Sprintf("Something went wrong saving the game: %v", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
if m.board.Done || len(msg.Runes) < 1 {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
r := unicode.ToLower(msg.Runes[0])
|
||||
if r >= 'a' && r <= 'z' && len(m.buffer) < 5 {
|
||||
m.buffer = append(m.buffer, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
// header
|
||||
s := headerStyle.Render(fmt.Sprintf("WORDLE #%d", m.board.Day)) + "\n"
|
||||
|
||||
for i, guess := range m.board.Guesses {
|
||||
line := " "
|
||||
for j := 0; j < 5; j++ {
|
||||
if i < m.board.CurrentGuess {
|
||||
style := charEmptyStyle
|
||||
if util.ContainsInt(guess.Correct, j) {
|
||||
style = charCorrectStyle
|
||||
} else if util.ContainsInt(guess.Present, j) {
|
||||
style = charWrongPositionStyle
|
||||
}
|
||||
|
||||
line += style.Render(string(unicode.ToUpper(guess.Letters[j])) + " ")
|
||||
} else {
|
||||
line += charEmptyStyle.Render("_ ")
|
||||
}
|
||||
}
|
||||
s += lineStyle.Render(line) + "\n"
|
||||
}
|
||||
|
||||
if !m.board.Done {
|
||||
footer := "> "
|
||||
for i := 0; i < 5; i++ {
|
||||
if i < len(m.buffer) {
|
||||
footer += string(unicode.ToUpper(m.buffer[i]))
|
||||
} else {
|
||||
footer += "_"
|
||||
}
|
||||
footer += " "
|
||||
}
|
||||
footer += "<"
|
||||
|
||||
s += promptStyle.Render(footer) + "\n"
|
||||
} else {
|
||||
msg := ""
|
||||
if m.board.CurrentGuess >= 6 {
|
||||
msg += fmt.Sprintf("Oof! The word was %s.", m.board.Solution)
|
||||
} else {
|
||||
msg += fmt.Sprintf("Congrats! You guessed in %d.", m.board.CurrentGuess)
|
||||
}
|
||||
msg += " Come back tomorrow for the next word. (Press CTRL+C to exit)"
|
||||
|
||||
s += promptStyle.Render(msg) + "\n"
|
||||
}
|
||||
|
||||
if m.error != "" {
|
||||
s += errorStyle.Render(m.error) + "\n"
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func main() {
|
||||
p := tea.NewProgram(initialModel())
|
||||
if err := p.Start(); err != nil {
|
||||
fmt.Printf("Something has gone wrong: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package main
|
||||
|
||||
import "github.com/charmbracelet/lipgloss"
|
||||
|
||||
var width = 30
|
||||
|
||||
// ---- colours ----
|
||||
|
||||
var colorGrey = lipgloss.AdaptiveColor{Light: "8", Dark: "7"}
|
||||
|
||||
// ---- main game ----
|
||||
|
||||
// the title of the game (i.e. wordle #123)
|
||||
var headerStyle = lipgloss.NewStyle().
|
||||
Foreground(colorGrey).
|
||||
Italic(true).
|
||||
Align(lipgloss.Center).
|
||||
Width(width)
|
||||
|
||||
// the bit where you type in the word
|
||||
var promptStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("12")).
|
||||
Align(lipgloss.Center).
|
||||
Width(width)
|
||||
|
||||
// the line of letters
|
||||
var lineStyle = lipgloss.NewStyle().Width(width).Align(lipgloss.Center)
|
||||
|
||||
// empty space or character not in word
|
||||
var charEmptyStyle = lipgloss.NewStyle().
|
||||
Foreground(colorGrey)
|
||||
|
||||
// character in wrong position
|
||||
var charWrongPositionStyle = charEmptyStyle.Copy().
|
||||
Foreground(lipgloss.Color("11"))
|
||||
|
||||
// character in correct position
|
||||
var charCorrectStyle = charEmptyStyle.Copy().
|
||||
Foreground(lipgloss.Color("10"))
|
||||
|
||||
var errorStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("9")).
|
||||
Italic(true).
|
||||
Align(lipgloss.Center).
|
||||
Width(width)
|
Loading…
Reference in New Issue