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 input textinput.Model err error } func NewWordleClientModel( base *BaseModel, ) *WordleClientModel { input := textinput.New() input.Focus() input.CharLimit = types.GUESS_WORD_LENGTH input.Width = types.GUESS_WORD_LENGTH return &WordleClientModel{ BaseModel: base, input: input, } } 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.PostGuessInput{ 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() return m, tea.Quit case "enter": if m.state.GameEnd { m.conn.Close() p := tea.NewProgram(NewLobbyModel(m.BaseModel)) m.Push(types.Program{ Run: func() error { _, err := p.Run(); return err }, Stage: types.StageLobby, }) 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() }