最近流行りのWordleを真似してGoでCLIを作ってみました。
#Wordle
Wordleとは、お題となる伏せられた5文字の単語を6回の試行の間に当てるゲームです。回答ごとに、各文字の位置が一致しているか、含まれているかいないかといった情報が色で与えられるようになっています。
#仕様
今回は以下のような仕様で実装を行いました。
- 正解となる単語をランダムに単語帳から取得
- 単語帳に含まれる5文字の単語をユーザに入力させる
- 回答と正解を比較し、文字ごとに背景を色分けして表示する
- 正解に含まれ、位置も合っている文字は緑
- 正解に含まれる文字は黄色
- 正解に含まれない文字は赤
- アルファベット26文字それぞれの情報をリストで色分けして表示する
- 色分けは回答と同じ
- 正解したなら
correct!
と表示し回答履歴を文字抜きで色分けして表示する - 上記の正解となる単語の取得以外を6回まで繰り返す
単語帳については本家のページから拝借しました。
#実装
wordle.go
が主となるプログラムで、それ以外はこのプログラムから呼び出される関数などが記述されています。また、PlayWordle
関数をmain関数から呼び出して利用しています。
map[rune]int
で表現したアルファベットのリストや回答の各文字の状態はint
の定数を用いて表現していて、各ループで回答が入力されたら、rangeによるfor文でans
を一文字ずつ評価し、正解と位置まで一致している場合はEAT
、含まれている場合はBITE
、含まれていない場合はUNUSED
でマークしています。
各文字の状態に合わせて出力の背景色を変更する箇所については、\x1b[41m
などのエスケープ文字で背景色の書式を変更し、\x1b[0m
でリセットすることで実現しています。
最後に出力される履歴の部分は、背景色を変えた空白で表現しているので、コピーして本家のようにシェアすることができないですが、必要になったら変えるということで、、
package wordle_cli
import (
"fmt"
"math/rand"
"time"
)
const (
UNCHECKED = iota
UNUSED
BITE
EAT
)
var words []string
func PlayWordle() {
var history [][5]int
alphabet := map[rune]int{}
loadWords(&words)
c := 'a'
for i := 0; i < 26; i++ {
tmp := rune(int(c) + i)
alphabet[tmp] = UNCHECKED
}
rand.Seed(time.Now().UnixNano())
wordle := words[rand.Intn(len(words))]
for i := 0; i < 6; i++ {
ans := submitWord()
res := evaluateAnswer(wordle, ans)
history = append(history, res)
fmt.Printf("%d: ", i+1)
for j, v := range res {
if alphabet[rune(ans[j])] != EAT {
alphabet[rune(ans[j])] = v
}
printCharWithStatus(v, rune(ans[j]))
}
printAlphabet(alphabet)
fmt.Printf("\n\n")
if ans == wordle {
fmt.Println("correct!")
printHistory(history)
break
}
}
}
package wordle_cli
import (
"encoding/json"
"log"
"os"
)
type Words struct {
W []string `json:"words"`
}
func loadWords(wsp *[]string) {
b, err := os.ReadFile("words.json")
if err != nil {
log.Fatal(err)
}
var w Words
if err = json.Unmarshal(b, &w); err != nil {
log.Fatal(err)
}
*wsp = w.W
}
package wordle_cli
import (
"bufio"
"fmt"
"os"
)
var sc = bufio.NewScanner(os.Stdin)
func next() string {
sc.Scan()
return sc.Text()
}
func isInWords(word string) bool {
for _, v := range words {
if word == v {
return true
}
}
return false
}
func submitWord() string {
for {
fmt.Println("Submit a five-letter word:")
word := next()
if len(word) != 5 {
continue
} else if isInWords(word) {
return word
} else {
fmt.Println("Not in word list")
}
}
}
package wordle_cli
func contains(str string, c rune) bool {
for _, v := range str {
if c == v {
return true
}
}
return false
}
func evaluateAnswer(wordle string, ans string) [5]int {
res := [5]int{}
for i, v := range ans {
if v == rune(wordle[i]) {
res[i] = EAT
} else if contains(wordle, v) {
res[i] = BITE
} else {
res[i] = UNUSED
}
}
return res
}
package wordle_cli
import "fmt"
func printCharWithStatus(status int, c rune) {
switch status {
case UNUSED:
fmt.Printf("\x1b[41m%c\x1b[0m ", c)
case BITE:
fmt.Printf("\x1b[43m%c\x1b[0m ", c)
case EAT:
fmt.Printf("\x1b[42m%c\x1b[0m ", c)
}
}
func printAlphabet(alphabet map[rune]int) {
for i := 0; i < 26; i++ {
if i%10 == 0 {
fmt.Println()
}
switch alphabet[rune('a'+i)] {
case UNUSED:
fmt.Printf("\x1b[41m")
case BITE:
fmt.Printf("\x1b[43m")
case EAT:
fmt.Printf("\x1b[42m")
}
fmt.Printf("%c\u001B[0m ", rune('a'+i))
}
}
func printHistory(history [][5]int) {
for _, v := range history {
for _, vv := range v {
switch vv {
case UNUSED:
fmt.Printf("\x1b[41m \x1b[0m ")
case BITE:
fmt.Printf("\x1b[43m \x1b[0m ")
case EAT:
fmt.Printf("\x1b[42m \x1b[0m ")
}
}
fmt.Println()
}
}
package main
import "github.com/melq/wordle-cli"
func main() {
wordle_cli.PlayWordle()
}