diff options
-rw-r--r-- | cmd/api/main.go | 1 | ||||
-rw-r--r-- | internal/api/database/models.go | 1 | ||||
-rw-r--r-- | internal/api/handlers/handlers.go | 63 | ||||
-rw-r--r-- | pkg/ui/views/game.go | 75 | ||||
-rw-r--r-- | pkg/ui/views/game_api.go | 47 | ||||
-rw-r--r-- | pkg/ui/views/game_moves.go | 17 |
6 files changed, 187 insertions, 17 deletions
diff --git a/cmd/api/main.go b/cmd/api/main.go index 1ce83c9..4bd538a 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -22,6 +22,7 @@ func main() { r.Handle("/play", middleware.AuthMiddleware(http.HandlerFunc(handlers.NewPlay))).Methods(http.MethodPost) r.Handle("/play", middleware.AuthMiddleware(http.HandlerFunc(handlers.AllPlay))).Methods(http.MethodGet) r.Handle("/play/{id}", middleware.AuthMiddleware(http.HandlerFunc(handlers.GetGameId))).Methods(http.MethodGet) + r.Handle("/play/{id}/end", middleware.AuthMiddleware(http.HandlerFunc(handlers.EndGame))).Methods(http.MethodPost) r.Handle("/enter-game", middleware.AuthMiddleware(http.HandlerFunc(handlers.EnterGame))).Methods(http.MethodPost) log.Info("Serving on :8080") diff --git a/internal/api/database/models.go b/internal/api/database/models.go index a6e76c5..cd0d12d 100644 --- a/internal/api/database/models.go +++ b/internal/api/database/models.go @@ -19,6 +19,7 @@ type Game struct { Name string `json:"name"` IP1 string `json:"ip1"` IP2 string `json:"ip2"` + Outcome string `json:"outcome"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } diff --git a/internal/api/handlers/handlers.go b/internal/api/handlers/handlers.go index 55fa121..41779c7 100644 --- a/internal/api/handlers/handlers.go +++ b/internal/api/handlers/handlers.go @@ -130,6 +130,7 @@ func NewPlay(w http.ResponseWriter, r *http.Request) { Name: name, IP1: payload.IP, IP2: "", + Outcome: "*", } result := db.Create(&play) @@ -266,3 +267,65 @@ func GetGameId(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(game) } + +func EndGame(w http.ResponseWriter, r *http.Request) { + log, _ := logger.GetLogger() + vars := mux.Vars(r) + id := vars["id"] + log.Info(fmt.Sprintf("POST /play/%s/end", id)) + + claims, err := auth.ValidateJWT(r.Header.Get("Authorization")) + + if err != nil { + JsonError(&w, err.Error()) + return + } + + var payload struct { + Outcome string `json:"outcome"` + } + + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + JsonError(&w, err.Error()) + return + } + + if err != nil { + JsonError(&w, err.Error()) + return + } + + db, _ := database.GetDb() + + var game database.Game + + // FIXME: this is not secure + result := db.Where("id = ? AND (player1_id = ? OR player2_id = ?)", id, claims.UserID, claims.UserID).First(&game) + if result.Error != nil { + JsonError(&w, result.Error.Error()) + return + } + + game.Outcome = payload.Outcome + + if err := db.Save(&game).Error; err != nil { + JsonError(&w, err.Error()) + return + } + + result = db.Where("id = ?", game.ID). + Preload("Player1", func(db *gorm.DB) *gorm.DB { + return db.Omit("Password") + }). + Preload("Player2", func(db *gorm.DB) *gorm.DB { + return db.Omit("Password") + }). + First(&game) + + if result.Error != nil { + JsonError(&w, result.Error.Error()) + return + } + + json.NewEncoder(w).Encode(game) +} diff --git a/pkg/ui/views/game.go b/pkg/ui/views/game.go index eb65c96..d545fcb 100644 --- a/pkg/ui/views/game.go +++ b/pkg/ui/views/game.go @@ -93,8 +93,12 @@ func (m GameModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m, cmd = m.handleChessMoveMsg(msg) cmds = append(cmds, cmd) case database.Game: - m = m.handleDatabaseGameMsg(msg) - cmds = append(cmds, m.updateMovesListCmd()) + m, cmd = m.handleDatabaseGameMsg(msg) + cmds = append(cmds, cmd, m.updateMovesListCmd()) + case EndGameMsg: + return m, nil + case error: + m.err = msg } if m.isMyTurn() { @@ -105,10 +109,20 @@ func (m GameModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { selectedItem := m.availableMovesList.SelectedItem() if selectedItem != nil { moveStr := strings.Replace(selectedItem.(item).Title(), " → ", "", 1) - m.network.Server.Send(network.NetworkID(m.peer), []byte(moveStr)) - m.chessGame.MoveStr(moveStr) - m.turn++ + moveStr = strings.Replace(moveStr, " ", "", 1) + err := m.chessGame.MoveStr(moveStr) + if err != nil { + m.err = err + } else { + m.turn++ + m.network.Server.Send(network.NetworkID(m.peer), []byte(moveStr)) + m.err = nil + } cmds = append(cmds, m.getMoves(), m.updateMovesListCmd()) + + if m.chessGame.Outcome() != chess.NoOutcome { + cmds = append(cmds, m.endGame()) + } } } } @@ -140,20 +154,57 @@ func (m GameModel) View() string { var availableMovesListView string - if m.isMyTurn() { - m.availableMovesList.SetSize(listWidth, listHeight-2) - availableMovesListView = listStyle.Render(m.availableMovesList.View()) + if m.game.Outcome == chess.NoOutcome.String() { + if m.isMyTurn() { + m.availableMovesList.SetSize(listWidth, listHeight-2) + availableMovesListView = listStyle.Render(m.availableMovesList.View()) + } else { + availableMovesListView = listStyle.Render(lipgloss.Place(listWidth, listHeight, lipgloss.Center, lipgloss.Center, "Wait your turn")) + } } else { - availableMovesListView = listStyle.Render(lipgloss.Place(listWidth, listHeight, lipgloss.Center, lipgloss.Center, "Wait your turn")) + var outcome string + switch m.game.Outcome { + case "1-0": + outcome = "White won" + if m.peer == "peer-2" { + outcome += " (YOU)" + } + case "0-1": + outcome = "Black won" + if m.peer == "peer-1" { + outcome += " (YOU)" + } + case "1/2-1/2": + outcome = "Draw" + default: + outcome = "NoOutcome" + } + + availableMovesListView = listStyle.Render( + lipgloss.JoinVertical( + lipgloss.Left, + lipgloss.NewStyle().Background(highlightColor).Foreground(lipgloss.Color("230")).Padding(0, 1).MarginBottom(1).Render("Result"), + outcome, + m.game.Outcome, + ), + ) } var movesListStr string for i, move := range m.chessGame.Moves() { + s1 := move.S1().String() + s2 := move.S2().String() + var promo string + + if move.Promo().String() != "" { + promo = " " + move.Promo().String() + } + if i%2 == 0 { - movesListStr += altCodeStyle.Render(fmt.Sprintf("[%d]", i/2)) + fmt.Sprintf(" %s → %s", move.S1().String(), move.S2().String()) + movesListStr += altCodeStyle.Render(fmt.Sprintf("[%d]", i/2)) + fmt.Sprintf(" %s → %s%s", s1, s2, promo) } else { - movesListStr += fmt.Sprintf(", %s → %s\n", move.S1().String(), move.S2().String()) + movesListStr += fmt.Sprintf(", %s → %s%s\n", s1, s2, promo) } } @@ -173,7 +224,7 @@ func (m GameModel) View() string { content := lipgloss.JoinVertical( lipgloss.Center, - lipgloss.NewStyle().Foreground(lipgloss.Color("#f1c40f")).Render(fmt.Sprintf("%s vs %s", m.game.Player1.Username, m.game.Player2.Username)), + lipgloss.NewStyle().Foreground(lipgloss.Color("#f1c40f")).Render(fmt.Sprintf("♔ %s vs ♚ %s", m.game.Player1.Username, m.game.Player2.Username)), lipgloss.JoinHorizontal( lipgloss.Top, availableMovesListView, diff --git a/pkg/ui/views/game_api.go b/pkg/ui/views/game_api.go index 5d4edf9..75aa0a1 100644 --- a/pkg/ui/views/game_api.go +++ b/pkg/ui/views/game_api.go @@ -7,16 +7,26 @@ import ( "github.com/boozec/rahanna/internal/api/database" tea "github.com/charmbracelet/bubbletea" + "github.com/notnil/chess" ) -func (m GameModel) handleDatabaseGameMsg(msg database.Game) GameModel { +func (m GameModel) handleDatabaseGameMsg(msg database.Game) (GameModel, tea.Cmd) { m.game = &msg if m.peer == "peer-2" { m.network.Peer = msg.IP2 } else { m.network.Peer = msg.IP1 } - return m + + var cmd tea.Cmd + + if m.game.Outcome != chess.NoOutcome.String() { + cmd = func() tea.Msg { + return EndGameMsg{} + } + } + + return m, cmd } func (m *GameModel) getGame() tea.Cmd { @@ -57,3 +67,36 @@ func (m *GameModel) getGame() tea.Cmd { return game } } + +type EndGameMsg struct{} + +func (m *GameModel) endGame() tea.Cmd { + return func() tea.Msg { + var game database.Game + + // Get authorization token + authorization, err := getAuthorizationToken() + if err != nil { + return err + } + + // Prepare request payload + payload, err := json.Marshal(map[string]string{ + "outcome": m.chessGame.Outcome().String(), + }) + + // Send API request + url := fmt.Sprintf("%s/play/%d/end", os.Getenv("API_BASE"), m.currentGameID) + resp, err := sendAPIRequest("POST", url, payload, authorization) + if err != nil { + return err + } + defer resp.Body.Close() + + if err := json.NewDecoder(resp.Body).Decode(&game); err != nil { + return err + } + + return game + } +} diff --git a/pkg/ui/views/game_moves.go b/pkg/ui/views/game_moves.go index de2ef94..2fa7c9e 100644 --- a/pkg/ui/views/game_moves.go +++ b/pkg/ui/views/game_moves.go @@ -6,6 +6,7 @@ import ( "github.com/boozec/rahanna/internal/network" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" + "github.com/notnil/chess" ) // UpdateMovesListMsg is a message to update the moves list @@ -44,9 +45,13 @@ func (m GameModel) handleUpdateMovesListMsg() GameModel { if m.isMyTurn() && m.game != nil { var items []list.Item for _, move := range m.chessGame.ValidMoves() { + var promo string + if move.Promo().String() != "" { + promo = " " + move.Promo().String() + } items = append( items, - item{title: fmt.Sprintf("%s → %s", move.S1().String(), move.S2().String())}, + item{title: fmt.Sprintf("%s → %s%s", move.S1().String(), move.S2().String(), promo)}, ) } m.availableMovesList.SetItems(items) @@ -60,12 +65,18 @@ func (m GameModel) handleUpdateMovesListMsg() GameModel { } func (m GameModel) handleChessMoveMsg(msg ChessMoveMsg) (GameModel, tea.Cmd) { - m.turn++ err := m.chessGame.MoveStr(string(msg)) + cmds := []tea.Cmd{m.getMoves(), m.updateMovesListCmd()} if err != nil { m.err = err } else { + m.turn++ m.err = nil } - return m, tea.Batch(m.getMoves(), m.updateMovesListCmd()) + + if m.chessGame.Outcome() != chess.NoOutcome { + cmds = append(cmds, m.endGame()) + } + + return m, tea.Batch(cmds...) } |