summaryrefslogtreecommitdiff
path: root/pkg/ui/views/play.go
diff options
context:
space:
mode:
authorSanto Cariotti <santo@dcariotti.me>2025-04-08 14:37:33 +0200
committerSanto Cariotti <santo@dcariotti.me>2025-04-08 14:39:13 +0200
commit1f0d9ec8452f15c27cd33c4e3874454c35993743 (patch)
treec453a31ae5eb823aaf48868eea9fc4daf65f108b /pkg/ui/views/play.go
parentc5b10e28b358308d8349b940af09f64368172f2e (diff)
Use internal/pkg structure
Diffstat (limited to 'pkg/ui/views/play.go')
-rw-r--r--pkg/ui/views/play.go427
1 files changed, 427 insertions, 0 deletions
diff --git a/pkg/ui/views/play.go b/pkg/ui/views/play.go
new file mode 100644
index 0000000..991849e
--- /dev/null
+++ b/pkg/ui/views/play.go
@@ -0,0 +1,427 @@
+package views
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+
+ "github.com/boozec/rahanna/internal/api/database"
+ "github.com/boozec/rahanna/internal/network"
+ "github.com/charmbracelet/bubbles/key"
+ "github.com/charmbracelet/bubbles/textinput"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+)
+
+var chess string = `
+ A B C D E F G H
++---------------+
+8 |♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜| 8
+7 |♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟| 7
+6 |. . . . . . . .| 6
+5 |. . . . . . . .| 5
+4 |. . . . . . . .| 4
+3 |. . . . . . . .| 3
+2 |♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙| 2
+1 |♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖| 1
++---------------+
+ A B C D E F G H
+`
+
+type playKeyMap struct {
+ EnterNewGame key.Binding
+ StartNewGame key.Binding
+ GoLogout key.Binding
+ Quit key.Binding
+}
+
+type playResponse struct {
+ Name string `json:"name"`
+ Error string `json:"error"`
+}
+
+var defaultGameKeyMap = playKeyMap{
+ EnterNewGame: key.NewBinding(
+ key.WithKeys("alt+E", "alt+e"),
+ key.WithHelp("Alt+E", "Enter a play using code"),
+ ),
+ StartNewGame: key.NewBinding(
+ key.WithKeys("alt+s", "alt+s"),
+ key.WithHelp("Alt+S", "Start a new play"),
+ ),
+ GoLogout: key.NewBinding(
+ key.WithKeys("alt+Q", "alt+q"),
+ key.WithHelp("Alt+Q", "Logout"),
+ ),
+ Quit: key.NewBinding(
+ key.WithKeys("Q", "q"),
+ key.WithHelp(" Q", "Quit"),
+ ),
+}
+
+type PlayModelPage int
+
+const (
+ LandingPage PlayModelPage = iota
+ InsertCodePage
+ StartGamePage
+)
+
+type PlayModel struct {
+ width int
+ height int
+ err error
+ keys playKeyMap
+ namePrompt textinput.Model
+ page PlayModelPage
+ isLoading bool
+ playName string
+ play *database.Game
+}
+
+func NewPlayModel(width, height int) PlayModel {
+ namePrompt := textinput.New()
+ namePrompt.Prompt = " "
+ namePrompt.TextStyle = inputStyle
+ namePrompt.Placeholder = "rectangular-lake"
+ namePrompt.Focus()
+ namePrompt.CharLimit = 23
+ namePrompt.Width = 23
+
+ return PlayModel{
+ width: width,
+ height: height,
+ err: nil,
+ keys: defaultGameKeyMap,
+ namePrompt: namePrompt,
+ page: LandingPage,
+ isLoading: false,
+ playName: "",
+ play: nil,
+ }
+}
+
+func (m PlayModel) Init() tea.Cmd {
+ ClearScreen()
+ return textinput.Blink
+}
+
+func (m PlayModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ if exit := handleExit(msg); exit != nil {
+ return m, exit
+ }
+
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ m.width = msg.Width
+ m.height = msg.Height
+ return m, nil
+
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, m.keys.EnterNewGame):
+ m.page = InsertCodePage
+ return m, nil
+ case key.Matches(msg, m.keys.StartNewGame):
+ m.page = StartGamePage
+ if !m.isLoading {
+ m.isLoading = true
+ return m, m.newGameCallback()
+ }
+ case key.Matches(msg, m.keys.GoLogout):
+ return m, m.logout()
+ case key.Matches(msg, m.keys.Quit):
+ return m, tea.Quit
+ case msg.Type == tea.KeyEnter:
+ if m.page == InsertCodePage {
+ if !m.isLoading {
+ m.isLoading = true
+ return m, m.enterGame()
+ }
+ }
+ }
+ case playResponse:
+ m.isLoading = false
+ m.err = nil
+ if msg.Error != "" {
+ m.err = fmt.Errorf(msg.Error)
+ if msg.Error == "unauthorized" {
+ return m, m.logout()
+ }
+ } else {
+ m.playName = msg.Name
+ }
+ return m, nil
+ case database.Game:
+ m.isLoading = false
+ m.play = &msg
+ m.err = nil
+ return m, nil
+ case error:
+ m.isLoading = false
+ m.err = msg
+ }
+
+ var cmd tea.Cmd = nil
+
+ if m.page == InsertCodePage {
+ m.namePrompt, cmd = m.namePrompt.Update(msg)
+ }
+
+ return m, tea.Batch(cmd)
+}
+
+func (m PlayModel) View() string {
+ formWidth := getFormWidth(m.width)
+
+ var content string
+ base := lipgloss.NewStyle().Align(lipgloss.Center).Width(m.width)
+
+ switch m.page {
+ case LandingPage:
+ content = chess
+ m.namePrompt.Blur()
+ case InsertCodePage:
+ m.namePrompt.Focus()
+ var statusMsg string
+ if m.isLoading {
+ statusMsg = "Loading..."
+ content = base.
+ Render(
+ lipgloss.NewStyle().
+ Align(lipgloss.Center).
+ Bold(true).
+ Render(statusMsg),
+ )
+ } else if m.play != nil {
+ statusMsg = fmt.Sprintf("You are playing versus %s", lipgloss.NewStyle().Foreground(lipgloss.Color("#e67e22")).Render(m.play.Player1.Username))
+ content = base.
+ Render(
+ lipgloss.NewStyle().
+ Align(lipgloss.Center).
+ Width(m.width).
+ Bold(true).
+ Render(statusMsg),
+ )
+ } else {
+ statusMsg = fmt.Sprintf("Press %s to join", lipgloss.NewStyle().Italic(true).Render("Enter"))
+ content = base.
+ Render(
+ lipgloss.JoinVertical(lipgloss.Left,
+ lipgloss.NewStyle().Width(23).Render("Insert play code:"),
+ m.namePrompt.View(),
+ lipgloss.NewStyle().
+ Align(lipgloss.Center).
+ PaddingTop(2).
+ Width(23).
+ Bold(true).
+ Render(statusMsg),
+ ),
+ )
+ }
+
+ case StartGamePage:
+ var statusMsg string
+ if m.isLoading {
+ statusMsg = "Loading..."
+ } else if m.playName != "" {
+ statusMsg = fmt.Sprintf("Share `%s` to your friend", lipgloss.NewStyle().Italic(true).Foreground(lipgloss.Color("#F39C12")).Render(m.playName))
+ }
+
+ content = base.
+ Render(statusMsg)
+ }
+
+ var windowContent string
+ if m.err != nil {
+ formError := fmt.Sprintf("Error: %v", m.err.Error())
+ windowContent = lipgloss.JoinVertical(
+ lipgloss.Center,
+ windowStyle.Width(formWidth).Render(lipgloss.JoinVertical(
+ lipgloss.Center,
+ errorStyle.Align(lipgloss.Center).Width(formWidth-4).Render(formError),
+ content,
+ )),
+ )
+ } else {
+ windowContent = lipgloss.JoinVertical(
+ lipgloss.Center,
+ windowStyle.Width(formWidth).Render(lipgloss.JoinVertical(
+ lipgloss.Center,
+ content,
+ )),
+ )
+ }
+
+ enterKey := fmt.Sprintf("%s %s", altCodeStyle.Render(m.keys.EnterNewGame.Help().Key), m.keys.EnterNewGame.Help().Desc)
+ startKey := fmt.Sprintf("%s %s", altCodeStyle.Render(m.keys.StartNewGame.Help().Key), m.keys.StartNewGame.Help().Desc)
+ logoutKey := fmt.Sprintf("%s %s", altCodeStyle.Render(m.keys.GoLogout.Help().Key), m.keys.GoLogout.Help().Desc)
+ quitKey := fmt.Sprintf("%s %s", altCodeStyle.Render(m.keys.Quit.Help().Key), m.keys.Quit.Help().Desc)
+
+ // Vertically align the buttons
+ buttons := lipgloss.JoinVertical(
+ lipgloss.Left,
+ enterKey,
+ startKey,
+ logoutKey,
+ quitKey,
+ )
+
+ centeredContent := lipgloss.JoinVertical(
+ lipgloss.Center,
+ getLogo(m.width),
+ windowContent,
+ lipgloss.NewStyle().MarginTop(2).Render(buttons),
+ )
+
+ return lipgloss.Place(
+ m.width,
+ m.height,
+ lipgloss.Center,
+ lipgloss.Center,
+ centeredContent,
+ )
+}
+
+func (m PlayModel) newGameCallback() tea.Cmd {
+ return func() tea.Msg {
+ f, err := os.Open(".rahannarc")
+ if err != nil {
+ return playResponse{Error: err.Error()}
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ var authorization string
+ for scanner.Scan() {
+ authorization = scanner.Text()
+ }
+
+ if err := scanner.Err(); err != nil {
+ fmt.Println("Error during scanning:", err)
+ }
+
+ url := os.Getenv("API_BASE") + "/play"
+
+ port, err := network.GetRandomAvailablePort()
+ if err != nil {
+ return playResponse{Error: err.Error()}
+ }
+
+ payload, err := json.Marshal(map[string]string{
+ "ip": fmt.Sprintf("%s:%d", network.GetOutboundIP().String(), port),
+ })
+
+ if err != nil {
+ return playResponse{Error: err.Error()}
+ }
+
+ req, err := http.NewRequest("POST", url, bytes.NewReader(payload))
+ if err != nil {
+ return playResponse{Error: err.Error()}
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", authorization))
+
+ client := &http.Client{}
+
+ resp, err := client.Do(req)
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ var response playResponse
+ err = json.NewDecoder(resp.Body).Decode(&response)
+ if err != nil {
+ return playResponse{Error: fmt.Sprintf("HTTP error: %d, unable to decode body", resp.StatusCode)}
+ }
+ return playResponse{Error: response.Error}
+ }
+
+ var response playResponse
+ err = json.NewDecoder(resp.Body).Decode(&response)
+ if err != nil {
+ return playResponse{Error: fmt.Sprintf("Error decoding JSON: %v", err)}
+ }
+
+ return response
+ }
+}
+
+func (m PlayModel) enterGame() tea.Cmd {
+ return func() tea.Msg {
+ f, err := os.Open(".rahannarc")
+ if err != nil {
+ return playResponse{Error: err.Error()}
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ var authorization string
+ for scanner.Scan() {
+ authorization = scanner.Text()
+ }
+
+ if err := scanner.Err(); err != nil {
+ fmt.Println("Error during scanning:", err)
+ }
+
+ url := os.Getenv("API_BASE") + "/enter-game"
+
+ port, err := network.GetRandomAvailablePort()
+ if err != nil {
+ return playResponse{Error: err.Error()}
+ }
+
+ payload, err := json.Marshal(map[string]string{
+ "ip": fmt.Sprintf("%s:%d", network.GetOutboundIP().String(), port),
+ "name": m.namePrompt.Value(),
+ })
+
+ if err != nil {
+ return playResponse{Error: err.Error()}
+ }
+
+ req, err := http.NewRequest("POST", url, bytes.NewReader(payload))
+ if err != nil {
+ return playResponse{Error: err.Error()}
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", authorization))
+
+ client := &http.Client{}
+
+ resp, err := client.Do(req)
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ var response playResponse
+ err = json.NewDecoder(resp.Body).Decode(&response)
+ if err != nil {
+ return playResponse{Error: fmt.Sprintf("HTTP error: %d, unable to decode body", resp.StatusCode)}
+ }
+ return playResponse{Error: response.Error}
+ }
+
+ var response database.Game
+ err = json.NewDecoder(resp.Body).Decode(&response)
+ if err != nil {
+ return playResponse{Error: fmt.Sprintf("Error decoding JSON: %v", err)}
+ }
+
+ return response
+ }
+}
+
+func (m PlayModel) logout() tea.Cmd {
+ if err := os.Remove(".rahannarc"); err != nil {
+ return nil
+ }
+ return SwitchModelCmd(NewAuthModel(m.width, m.height+1))
+}