Files
inp2025/stages/wordle.go

180 lines
4.1 KiB
Go

package stages
import (
"fmt"
"net/http"
"net/url"
"strings"
"time"
"gitea.konchin.com/ytshih/inp2025/handlers/wordle"
"gitea.konchin.com/ytshih/inp2025/types"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/gorilla/websocket"
"github.com/vmihailenco/msgpack/v5"
)
const (
WEBSOCKET_RETRY int = 5
WEBSOCKET_BACKOFF time.Duration = 100 * time.Millisecond
)
type WordleClientModel struct {
*BaseModel
conn *websocket.Conn
state types.WordleState
shutdown types.ShutdownFunc
input textinput.Model
err error
}
func NewWordleClientModel(
base *BaseModel,
shutdown types.ShutdownFunc,
) *WordleClientModel {
input := textinput.New()
input.Focus()
input.CharLimit = types.GUESS_WORD_LENGTH
input.Width = types.GUESS_WORD_LENGTH
return &WordleClientModel{
BaseModel: base,
input: input,
shutdown: shutdown,
}
}
func (m *WordleClientModel) getState() tea.Cmd {
return func() tea.Msg {
if m.conn == nil {
u, err := url.Parse(m.client.BaseURL)
if err != nil {
m.err = fmt.Errorf("failed to parse BaseURL, %w", err)
}
u.Scheme = "ws"
u.Path = "/api/state"
for try := 0; try < WEBSOCKET_RETRY; try++ {
req, _ := http.NewRequest("GET",
/*placeholder*/ "http://localhost", nil)
req.SetBasicAuth(
m.client.UserInfo.Username,
m.client.UserInfo.Password)
m.conn, _, err = websocket.DefaultDialer.Dial(
u.String(), req.Header)
if err == nil {
break
}
time.Sleep(WEBSOCKET_BACKOFF)
}
if err != nil {
m.err = fmt.Errorf("failed to dial, %w", err)
}
}
_, b, err := m.conn.ReadMessage()
if err == nil {
var state types.WordleState
if err := msgpack.Unmarshal(b, &state); err != nil {
m.err = fmt.Errorf("failed to unmarshal state, %w", err)
}
return state
} else {
m.err = fmt.Errorf("failed to read message, %w", err)
return nil
}
}
}
func (m *WordleClientModel) postGuess(guess string) tea.Cmd {
return func() tea.Msg {
b, err := msgpack.Marshal(wordle.OperationGuess{
Username: m.client.UserInfo.Username,
Guess: guess,
})
if err != nil {
m.err = fmt.Errorf("failed to post guess, %w", err)
}
_, err = m.client.R().
SetBody(b).
Post("/api/guess")
if err != nil {
m.err = fmt.Errorf("failed to post guess, %w", err)
}
return nil
}
}
func (m *WordleClientModel) Init() tea.Cmd {
return tea.Sequence(tea.ClearScreen,
tea.Batch(m.getState(), textinput.Blink))
}
func (m *WordleClientModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
m.conn.Close()
m.shutdown()
return m, tea.Quit
case "enter":
if m.state.GameEnd {
m.conn.Close()
m.shutdown()
*m.queue = append(*m.queue,
tea.NewProgram(NewLobbyModel(m.BaseModel)))
return m, tea.Quit
}
if len(m.input.Value()) == types.GUESS_WORD_LENGTH {
input := strings.ToUpper(m.input.Value())
m.input.Reset()
cmds = append(cmds, m.postGuess(input))
}
}
case types.WordleState:
m.state = msg
if m.state.GameEnd {
m.input.Blur()
}
if _, ok := m.state.CurrentGuess[m.client.UserInfo.Username]; ok {
m.input.Blur()
} else {
cmds = append(cmds, m.input.Focus())
}
cmds = append(cmds, m.getState())
}
var cmd tea.Cmd
m.input, cmd = m.input.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (m *WordleClientModel) View() string {
var b strings.Builder
fmt.Fprintf(&b, "Login as '%s'\n", m.client.UserInfo.Username)
fmt.Fprintf(&b, "remote addr: %s\n", m.client.BaseURL)
b.WriteString(m.state.View() + "\n")
b.WriteString(types.NewWordBank(m.state).View() + "\n")
if m.state.GameEnd {
fmt.Fprintf(&b, "Game End. Press 'Enter' to lobby.\n")
} else {
if guess, ok := m.state.CurrentGuess[m.client.UserInfo.Username]; ok {
fmt.Fprintf(&b, "Current guess for the round: %s\n", guess)
} else {
fmt.Fprintf(&b, "guess: %s\n", m.input.View())
}
if m.err != nil {
fmt.Fprintf(&b, "error: %+v\n", m.err)
}
}
return b.String()
}