diff options
author | Santo Cariotti <santo@dcariotti.me> | 2025-04-13 13:24:08 +0200 |
---|---|---|
committer | Santo Cariotti <santo@dcariotti.me> | 2025-04-13 13:24:28 +0200 |
commit | 76f46e54175253d4b2ba61b9cb8f2525a48c15d8 (patch) | |
tree | 8664af77e9bd66ae578fa83870026c2d6bca6065 /pkg/ui | |
parent | 72382e2dd9e509e6467dab9bfd11b7c7ddcf918a (diff) |
Create adversary's server
Diffstat (limited to 'pkg/ui')
-rw-r--r-- | pkg/ui/multiplayer/multiplayer.go | 8 | ||||
-rw-r--r-- | pkg/ui/views/api.go | 44 | ||||
-rw-r--r-- | pkg/ui/views/game.go | 116 | ||||
-rw-r--r-- | pkg/ui/views/play.go | 107 | ||||
-rw-r--r-- | pkg/ui/views/views.go | 4 |
5 files changed, 176 insertions, 103 deletions
diff --git a/pkg/ui/multiplayer/multiplayer.go b/pkg/ui/multiplayer/multiplayer.go index 2f889c8..436388f 100644 --- a/pkg/ui/multiplayer/multiplayer.go +++ b/pkg/ui/multiplayer/multiplayer.go @@ -5,15 +5,15 @@ import ( ) type GameNetwork struct { - server *network.TCPNetwork - peer string + Server *network.TCPNetwork + Peer string } func NewGameNetwork(localID, localIP string, localPort int, callback func()) *GameNetwork { server := network.NewTCPNetwork(localID, localIP, localPort, callback) peer := "" return &GameNetwork{ - server: server, - peer: peer, + Server: server, + Peer: peer, } } diff --git a/pkg/ui/views/api.go b/pkg/ui/views/api.go new file mode 100644 index 0000000..3788f91 --- /dev/null +++ b/pkg/ui/views/api.go @@ -0,0 +1,44 @@ +package views + +import ( + "bufio" + "bytes" + "fmt" + "net/http" + "os" +) + +// getAuthorizationToken reads the authentication token from the .rahannarc file +func getAuthorizationToken() (string, error) { + f, err := os.Open(".rahannarc") + if err != nil { + return "", err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + var authorization string + for scanner.Scan() { + authorization = scanner.Text() + } + + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("error reading auth token: %v", err) + } + + return authorization, nil +} + +// sendAPIRequest sends an HTTP request to the API with the given parameters +func sendAPIRequest(method, url string, payload []byte, authorization string) (*http.Response, error) { + req, err := http.NewRequest(method, url, bytes.NewReader(payload)) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", authorization)) + + client := &http.Client{} + return client.Do(req) +} diff --git a/pkg/ui/views/game.go b/pkg/ui/views/game.go index e26e3c9..52d8459 100644 --- a/pkg/ui/views/game.go +++ b/pkg/ui/views/game.go @@ -1,7 +1,11 @@ package views import ( + "encoding/json" "fmt" + "os" + "strconv" + "strings" "github.com/boozec/rahanna/internal/api/database" "github.com/boozec/rahanna/pkg/ui/multiplayer" @@ -11,7 +15,7 @@ import ( "github.com/charmbracelet/lipgloss" ) -// Keyboard controls +// gameKeyMap defines the key bindings for the game view. type gameKeyMap struct { EnterNewGame key.Binding StartNewGame key.Binding @@ -19,18 +23,19 @@ type gameKeyMap struct { Quit key.Binding } -// Default key bindings for the game model +// defaultGameKeyMap provides the default key bindings for the game view. var defaultGameKeyMap = gameKeyMap{ GoLogout: key.NewBinding( - key.WithKeys("alt+Q", "alt+q"), + key.WithKeys("alt+q"), key.WithHelp("Alt+Q", "Logout"), ), Quit: key.NewBinding( - key.WithKeys("Q", "q"), - key.WithHelp(" Q", "Quit"), + key.WithKeys("q"), + key.WithHelp("Q", "Quit"), ), } +// GameModel represents the state of the game view. type GameModel struct { // UI dimensions width int @@ -40,25 +45,30 @@ type GameModel struct { keys playKeyMap // Game state - game *database.Game - network *multiplayer.GameNetwork + peer string + currentGameID int + game *database.Game + network *multiplayer.GameNetwork } -func NewGameModel(width, height int, game *database.Game, network *multiplayer.GameNetwork) GameModel { +// NewGameModel creates a new GameModel. +func NewGameModel(width, height int, peer string, currentGameID int, network *multiplayer.GameNetwork) GameModel { return GameModel{ - width: width, - height: height, - game: game, - network: network, + width: width, + height: height, + peer: peer, + currentGameID: currentGameID, + network: network, } } -// Init function for GameModel +// Init initializes the GameModel. func (m GameModel) Init() tea.Cmd { ClearScreen() - return textinput.Blink + return tea.Batch(textinput.Blink, m.getGame()) } +// Update handles incoming messages and updates the GameModel. func (m GameModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if exit := handleExit(msg); exit != nil { return m, exit @@ -69,22 +79,29 @@ func (m GameModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.handleWindowSize(msg) case tea.KeyMsg: return m.handleKeyPress(msg) + case database.Game: + return m.handleGetGameResponse(msg) } return m, nil } -// View function for GameModel +// View renders the GameModel. func (m GameModel) View() string { formWidth := getFormWidth(m.width) - // base := lipgloss.NewStyle().Align(lipgloss.Center).Width(m.width) - content := "abc" + var content string + if m.game != nil { + otherPlayer := "" + if m.peer == "peer-1" { + otherPlayer = m.game.Player2.Username + } else { + otherPlayer = m.game.Player1.Username + } + content = fmt.Sprintf("You're playing versus %s", otherPlayer) + } - // Build the main window with error handling windowContent := m.buildWindowContent(content, formWidth) - - // Create navigation buttons buttons := m.renderNavigationButtons() centeredContent := lipgloss.JoinVertical( @@ -113,11 +130,9 @@ func (m GameModel) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { switch { case key.Matches(msg, m.keys.GoLogout): return m, logout(m.width, m.height+1) - case key.Matches(msg, m.keys.Quit): return m, tea.Quit } - return m, nil } @@ -146,3 +161,60 @@ func (m GameModel) renderNavigationButtons() string { quitKey, ) } + +func (m *GameModel) handleGetGameResponse(msg database.Game) (tea.Model, tea.Cmd) { + m.game = &msg + if m.peer == "peer-1" { + m.network.Peer = msg.IP2 + } else { + m.network.Peer = msg.IP1 + } + return m, nil +} + +func (m *GameModel) getGame() tea.Cmd { + return func() tea.Msg { + var game database.Game + + // Get authorization token + authorization, err := getAuthorizationToken() + if err != nil { + return nil + } + + // Send API request + url := fmt.Sprintf("%s/play/%d", os.Getenv("API_BASE"), m.currentGameID) + resp, err := sendAPIRequest("GET", url, nil, authorization) + if err != nil { + return nil + } + defer resp.Body.Close() + + if err := json.NewDecoder(resp.Body).Decode(&game); err != nil { + return nil + } + + // Establish peer connection + if m.peer == "peer-1" { + if game.IP2 != "" { + ipParts := strings.Split(game.IP2, ":") + if len(ipParts) == 2 { + remoteIP := ipParts[0] + remotePortInt, _ := strconv.Atoi(ipParts[1]) + go m.network.Server.AddPeer("peer-2", remoteIP, remotePortInt) + } + } + } else { + if game.IP1 != "" { + ipParts := strings.Split(game.IP1, ":") + if len(ipParts) == 2 { + remoteIP := ipParts[0] + remotePortInt, _ := strconv.Atoi(ipParts[1]) + go m.network.Server.AddPeer("peer-1", remoteIP, remotePortInt) + } + } + } + + return game + } +} diff --git a/pkg/ui/views/play.go b/pkg/ui/views/play.go index 95621a3..4801556 100644 --- a/pkg/ui/views/play.go +++ b/pkg/ui/views/play.go @@ -1,12 +1,11 @@ package views import ( - "bufio" - "bytes" "encoding/json" "fmt" "net/http" "os" + "strconv" "strings" "github.com/boozec/rahanna/internal/api/database" @@ -21,7 +20,7 @@ import ( const ( chessBoard = ` - A B C D E F G H +A B C D E F G H +---------------+ 8 |♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜| 8 7 |♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟| 7 @@ -32,7 +31,7 @@ const ( 2 |♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙| 2 1 |♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖| 1 +---------------+ - A B C D E F G H +A B C D E F G H ` ) @@ -47,9 +46,10 @@ const ( ) type responseOk struct { - Name string `json:"name"` - IP string `json:"ip"` - Port int `json:"int"` + Name string `json:"name"` + GameID int `json:"id"` + IP string `json:"ip"` + Port int `json:"int"` } // API response types @@ -110,10 +110,11 @@ type PlayModel struct { paginator paginator.Model // Game state - playName string - game *database.Game - network *multiplayer.GameNetwork - games []database.Game // Store the list of games + playName string + currentGameId int + game *database.Game + network *multiplayer.GameNetwork + games []database.Game } // NewPlayModel creates a new play model instance @@ -159,7 +160,7 @@ func (m PlayModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { select { case <-start: - return m, SwitchModelCmd(NewGameModel(m.width, m.height+1, m.game, m.network)) + return m, SwitchModelCmd(NewGameModel(m.width, m.height+1, "peer-1", m.currentGameId, m.network)) default: } @@ -178,13 +179,6 @@ func (m PlayModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.handleError(msg) } - // Handle input updates when on the InsertCodePage - if m.page == InsertCodePage { - var cmd tea.Cmd - m.namePrompt, cmd = m.namePrompt.Update(msg) - return m, cmd - } - return m, nil } @@ -223,10 +217,13 @@ func (m PlayModel) handleWindowSize(msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) } func (m PlayModel) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch { case key.Matches(msg, m.keys.EnterNewGame): m.page = InsertCodePage - return m, nil + m.namePrompt, cmd = m.namePrompt.Update("suca") + return m, cmd case key.Matches(msg, m.keys.StartNewGame): m.page = StartGamePage @@ -250,6 +247,11 @@ func (m PlayModel) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.paginator, _ = m.paginator.Update(msg) + if m.page == InsertCodePage { + m.namePrompt, cmd = m.namePrompt.Update(msg) + return m, cmd + } + return m, nil } @@ -264,6 +266,7 @@ func (m *PlayModel) handlePlayResponse(msg playResponse) (tea.Model, tea.Cmd) { } } else { m.playName = msg.Ok.Name + m.currentGameId = msg.Ok.GameID m.network = multiplayer.NewGameNetwork("peer-1", msg.Ok.IP, msg.Ok.Port, func() { close(start) @@ -277,6 +280,14 @@ func (m *PlayModel) handleGameResponse(msg database.Game) (tea.Model, tea.Cmd) { m.isLoading = false m.game = &msg m.err = nil + ip := strings.Split(m.game.IP2, ":") + if len(ip) == 2 { + localIP := ip[0] + localPort, _ := strconv.ParseInt(ip[1], 10, 32) + network := multiplayer.NewGameNetwork("peer-2", localIP, int(localPort), func() {}) + + return m, SwitchModelCmd(NewGameModel(m.width, m.height+1, "peer-2", m.game.ID, network)) + } return m, nil } @@ -307,7 +318,6 @@ func (m PlayModel) renderPageContent(base lipgloss.Style) string { return base.Render(lipgloss.JoinVertical(lipgloss.Center, strings.Join(gamesStrings, "\n"), pageInfo)) } case InsertCodePage: - m.namePrompt.Focus() return m.renderInsertCodeContent(base) case StartGamePage: @@ -358,23 +368,6 @@ func (m PlayModel) renderInsertCodeContent(base lipgloss.Style) string { ) } - // When we have a play, show who we're playing against - if m.game != nil { - playerName := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#e67e22")). - Render(m.game.Player1.Username) - - statusMsg := fmt.Sprintf("You are playing versus %s", playerName) - - return base.Render( - lipgloss.NewStyle(). - Align(lipgloss.Center). - Width(m.width). - Bold(true). - Render(statusMsg), - ) - } - // Default: show input prompt return base.Render( lipgloss.JoinVertical(lipgloss.Left, @@ -518,13 +511,14 @@ func (m *PlayModel) newGameCallback() tea.Cmd { // Decode successful response var response struct { Name string `json:"name"` + ID int `json:"id"` Error string `json:"error"` } if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { return playResponse{Error: fmt.Sprintf("Error decoding JSON: %v", err)} } - return playResponse{Ok: responseOk{Name: response.Name, IP: ip, Port: port}} + return playResponse{Ok: responseOk{Name: response.Name, GameID: response.ID, IP: ip, Port: port}} } } @@ -582,41 +576,6 @@ func (m PlayModel) enterGame() tea.Cmd { } } -// getAuthorizationToken reads the authentication token from the .rahannarc file -func getAuthorizationToken() (string, error) { - f, err := os.Open(".rahannarc") - if err != nil { - return "", err - } - defer f.Close() - - scanner := bufio.NewScanner(f) - var authorization string - for scanner.Scan() { - authorization = scanner.Text() - } - - if err := scanner.Err(); err != nil { - return "", fmt.Errorf("error reading auth token: %v", err) - } - - return authorization, nil -} - -// sendAPIRequest sends an HTTP request to the API with the given parameters -func sendAPIRequest(method, url string, payload []byte, authorization string) (*http.Response, error) { - req, err := http.NewRequest(method, url, bytes.NewReader(payload)) - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", authorization)) - - client := &http.Client{} - return client.Do(req) -} - func (m *PlayModel) fetchGames() tea.Cmd { return func() tea.Msg { var games []database.Game diff --git a/pkg/ui/views/views.go b/pkg/ui/views/views.go index 0e463b7..4ea08b6 100644 --- a/pkg/ui/views/views.go +++ b/pkg/ui/views/views.go @@ -100,9 +100,7 @@ func SwitchModelCmd(model tea.Model) tea.Cmd { model: model, } - return func() tea.Msg { - return s - } + return tea.Batch(func() tea.Msg { return s }, s.model.Init()) } func (m RahannaModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { |