summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/api/main.go1
-rw-r--r--internal/api/database/models.go1
-rw-r--r--internal/api/handlers/handlers.go63
-rw-r--r--pkg/ui/views/game.go75
-rw-r--r--pkg/ui/views/game_api.go47
-rw-r--r--pkg/ui/views/game_moves.go17
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...)
}