summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSanto Cariotti <santo@dcariotti.me>2025-04-27 11:15:11 +0200
committerSanto Cariotti <santo@dcariotti.me>2025-04-27 12:24:40 +0200
commit85a6b4c2915fbfb42978fd7d2e3f7bd67e650314 (patch)
treeeb705d5b97bb515d806049176df149890355e6e9
parent6a60cc1c133ccf42dae8498fc40cc3276fc91561 (diff)
Co-op mode with next player randomly
-rw-r--r--internal/api/database/models.go46
-rw-r--r--internal/api/handlers/handlers.go10
-rw-r--r--pkg/ui/multiplayer/multiplayer.go1
-rw-r--r--pkg/ui/views/game.go11
-rw-r--r--pkg/ui/views/game_api.go16
-rw-r--r--pkg/ui/views/game_moves.go67
-rw-r--r--pkg/ui/views/game_restore.go1
-rw-r--r--pkg/ui/views/game_util.go14
-rw-r--r--pkg/ui/views/play_api.go18
-rw-r--r--pkg/ui/views/play_keymap.go43
10 files changed, 170 insertions, 57 deletions
diff --git a/internal/api/database/models.go b/internal/api/database/models.go
index f065a8c..82a2649 100644
--- a/internal/api/database/models.go
+++ b/internal/api/database/models.go
@@ -17,24 +17,32 @@ const (
PairGameType GameType = "pair"
)
+type MoveChooseType string
+
+const (
+ SequentialChooseType MoveChooseType = "sequential"
+ RandomChooseType MoveChooseType = "random"
+)
+
type Game struct {
- ID int `json:"id"`
- Type GameType `json:"type"`
- Player1ID int `json:"-"`
- Player1 User `gorm:"foreignKey:Player1ID" json:"player1"`
- Player2ID *int `json:"-"`
- Player2 *User `gorm:"foreignKey:Player2ID;null" json:"player2"`
- Player3ID *int `json:"-"`
- Player3 *User `gorm:"foreignKey:Player3ID;null" json:"player3"`
- Player4ID *int `json:"-"`
- Player4 *User `gorm:"foreignKey:Player4ID;null" json:"player4"`
- Name string `json:"name"`
- IP1 string `json:"ip1"`
- IP2 string `json:"ip2"`
- IP3 string `json:"ip3"`
- IP4 string `json:"ip4"`
- Outcome string `json:"outcome"`
- LastPlayer int `json:"last_player"` // Last player entered in game
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt time.Time `json:"updated_at"`
+ ID int `json:"id"`
+ Type GameType `json:"type"`
+ MoveChoose MoveChooseType `json:"move_choose_type"`
+ Player1ID int `json:"-"`
+ Player1 User `gorm:"foreignKey:Player1ID" json:"player1"`
+ Player2ID *int `json:"-"`
+ Player2 *User `gorm:"foreignKey:Player2ID;null" json:"player2"`
+ Player3ID *int `json:"-"`
+ Player3 *User `gorm:"foreignKey:Player3ID;null" json:"player3"`
+ Player4ID *int `json:"-"`
+ Player4 *User `gorm:"foreignKey:Player4ID;null" json:"player4"`
+ Name string `json:"name"`
+ IP1 string `json:"ip1"`
+ IP2 string `json:"ip2"`
+ IP3 string `json:"ip3"`
+ IP4 string `json:"ip4"`
+ Outcome string `json:"outcome"`
+ LastPlayer int `json:"last_player"` // Last player entered in game
+ 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 29da3b3..c97dcbf 100644
--- a/internal/api/handlers/handlers.go
+++ b/internal/api/handlers/handlers.go
@@ -105,8 +105,9 @@ func NewPlay(w http.ResponseWriter, r *http.Request) {
}
var payload struct {
- IP string `json:"ip"`
- Type database.GameType `json:"type"`
+ IP string `json:"ip"`
+ Type database.GameType `json:"type"`
+ MoveChoose database.MoveChooseType `json:"move_choose_type"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
@@ -126,6 +127,7 @@ func NewPlay(w http.ResponseWriter, r *http.Request) {
play := database.Game{
Type: payload.Type,
+ MoveChoose: payload.MoveChoose,
Player1ID: claims.UserID,
Name: name,
IP1: payload.IP,
@@ -138,7 +140,9 @@ func NewPlay(w http.ResponseWriter, r *http.Request) {
return
}
- json.NewEncoder(w).Encode(map[string]interface{}{"id": play.ID, "type": play.Type, "name": name})
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "id": play.ID, "type": play.Type, "moove_choose_type": play.MoveChoose, "name": name,
+ })
}
func EnterGame(w http.ResponseWriter, r *http.Request) {
diff --git a/pkg/ui/multiplayer/multiplayer.go b/pkg/ui/multiplayer/multiplayer.go
index 9694859..7b67b5c 100644
--- a/pkg/ui/multiplayer/multiplayer.go
+++ b/pkg/ui/multiplayer/multiplayer.go
@@ -16,6 +16,7 @@ const (
MoveGameMessage MoveType = "new-move"
RestoreAckGameMessage MoveType = "restore-ack"
RestoreGameMessage MoveType = "restore"
+ DefineTurnMessage MoveType = "define-turn"
)
type GameMove struct {
diff --git a/pkg/ui/views/game.go b/pkg/ui/views/game.go
index 64e463b..12f4fcb 100644
--- a/pkg/ui/views/game.go
+++ b/pkg/ui/views/game.go
@@ -32,7 +32,7 @@ type GameModel struct {
network *multiplayer.GameNetwork
chessGame *chess.Game
incomingMoves chan multiplayer.GameMove
- turn int
+ turn p2p.NetworkID
availableMovesList list.Model
}
@@ -61,7 +61,6 @@ func NewGameModel(width, height int, currentGameID int, network *multiplayer.Gam
network: network,
chessGame: chess.NewGame(chess.UseNotation(chess.UCINotation{})),
incomingMoves: make(chan multiplayer.GameMove),
- turn: 0,
availableMovesList: moveList,
restore: restore,
}
@@ -104,6 +103,12 @@ func (m GameModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.userID, m.err = getUserID()
m, cmd = m.handleDatabaseGameMsg(msg)
cmds = append(cmds, cmd, m.updateMovesListCmd())
+ case SaveTurnMsg:
+ m, cmd = m.handleSaveTurnMsg(msg)
+ cmds = append(cmds, cmd)
+ case SendNewTurnMsg:
+ m, cmd = m.handleDefineTurnMsg()
+ cmds = append(cmds, cmd)
case EndGameMsg:
if msg.abandoned {
_ = m.getGame()()
@@ -136,7 +141,7 @@ func (m GameModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.network.SendAll([]byte("new-move"), []byte(moveStr))
m.err = nil
}
- cmds = append(cmds, m.getMoves(), m.updateMovesListCmd())
+ cmds = append(cmds, m.getMoves(), m.updateMovesListCmd(), m.sendNewTurnCmd())
if m.chessGame.Outcome() != chess.NoOutcome {
cmds = append(cmds, m.endGame(m.chessGame.Outcome().String(), false))
diff --git a/pkg/ui/views/game_api.go b/pkg/ui/views/game_api.go
index 66b63c1..32ea873 100644
--- a/pkg/ui/views/game_api.go
+++ b/pkg/ui/views/game_api.go
@@ -3,9 +3,12 @@ package views
import (
"encoding/json"
"fmt"
+ "math/rand"
"os"
+ "time"
"github.com/boozec/rahanna/internal/api/database"
+ "github.com/boozec/rahanna/pkg/p2p"
tea "github.com/charmbracelet/bubbletea"
"github.com/notnil/chess"
)
@@ -44,6 +47,19 @@ func (m GameModel) handleDatabaseGameMsg(msg database.Game) (GameModel, tea.Cmd)
}
}
+ if myPlayerNum == 1 && m.turn == p2p.EmptyNetworkID {
+ // FIXME: use another way instead of sleep
+ time.Sleep(2 * time.Second)
+ if m.game.MoveChoose == database.RandomChooseType {
+ players := []int{1, 3}
+ m.turn = m.playerPeer(players[rand.Intn(len(players))])
+ } else {
+ m.turn = m.playerPeer(1)
+ }
+ m.network.SendAll([]byte("define-turn"), []byte(string(m.turn)))
+
+ }
+
if m.restore {
cmd = func() tea.Msg {
return RestoreGameMsg{}
diff --git a/pkg/ui/views/game_moves.go b/pkg/ui/views/game_moves.go
index 70b2dc7..fefdf85 100644
--- a/pkg/ui/views/game_moves.go
+++ b/pkg/ui/views/game_moves.go
@@ -2,7 +2,9 @@ package views
import (
"fmt"
+ "math/rand"
+ "github.com/boozec/rahanna/internal/api/database"
"github.com/boozec/rahanna/pkg/p2p"
"github.com/boozec/rahanna/pkg/ui/multiplayer"
"github.com/charmbracelet/bubbles/list"
@@ -16,6 +18,9 @@ type UpdateMovesListMsg struct{}
// ChessMoveMsg is a message containing a received chess move.
type ChessMoveMsg string
+type SendNewTurnMsg struct{}
+type SaveTurnMsg string
+
type item struct {
title string
}
@@ -40,6 +45,8 @@ func (m *GameModel) getMoves() tea.Cmd {
switch multiplayer.MoveType(string(move.Type)) {
case multiplayer.AbandonGameMessage:
return EndGameMsg{abandoned: true}
+ case multiplayer.DefineTurnMessage:
+ return SaveTurnMsg(string(move.Payload))
case multiplayer.RestoreGameMessage:
return SendRestoreMsg(move.Source)
case multiplayer.RestoreAckGameMessage:
@@ -79,6 +86,60 @@ func (m GameModel) handleUpdateMovesListMsg() GameModel {
return m
}
+func (m GameModel) handleDefineTurnMsg() (GameModel, tea.Cmd) {
+ cmds := []tea.Cmd{m.getMoves(), m.updateMovesListCmd()}
+
+ switch m.game.Type {
+ case database.SingleGameType:
+ if m.network.Me() == m.playerPeer(1) {
+ m.turn = m.playerPeer(2)
+ } else {
+ m.turn = m.playerPeer(1)
+ }
+ case database.PairGameType:
+ switch m.game.MoveChoose {
+ case database.SequentialChooseType:
+ switch m.network.Me() {
+ case m.playerPeer(1):
+ m.turn = m.playerPeer(2)
+ case m.playerPeer(2):
+ m.turn = m.playerPeer(3)
+ case m.playerPeer(3):
+ m.turn = m.playerPeer(4)
+ case m.playerPeer(4):
+ m.turn = m.playerPeer(1)
+ }
+ case database.RandomChooseType:
+ var players []int
+ switch m.network.Me() {
+ case m.playerPeer(1):
+ players = []int{2, 4}
+ case m.playerPeer(3):
+ players = []int{2, 4}
+ case m.playerPeer(2):
+ players = []int{1, 3}
+ case m.playerPeer(4):
+ players = []int{1, 3}
+ }
+ m.turn = m.playerPeer(players[rand.Intn(len(players))])
+ default:
+ panic("should not be here")
+ }
+ }
+
+ m.network.SendAll([]byte("define-turn"), []byte(string(m.turn)))
+
+ return m, tea.Batch(cmds...)
+}
+
+func (m GameModel) handleSaveTurnMsg(msg SaveTurnMsg) (GameModel, tea.Cmd) {
+ cmds := []tea.Cmd{m.getMoves(), m.updateMovesListCmd()}
+
+ m.turn = p2p.NetworkID(msg)
+
+ return m, tea.Batch(cmds...)
+}
+
func (m GameModel) handleChessMoveMsg(msg ChessMoveMsg) (GameModel, tea.Cmd) {
m.err = m.chessGame.MoveStr(string(msg))
cmds := []tea.Cmd{m.getMoves(), m.updateMovesListCmd()}
@@ -89,3 +150,9 @@ func (m GameModel) handleChessMoveMsg(msg ChessMoveMsg) (GameModel, tea.Cmd) {
return m, tea.Batch(cmds...)
}
+
+func (m GameModel) sendNewTurnCmd() tea.Cmd {
+ return func() tea.Msg {
+ return SendNewTurnMsg{}
+ }
+}
diff --git a/pkg/ui/views/game_restore.go b/pkg/ui/views/game_restore.go
index 64d99ce..9879233 100644
--- a/pkg/ui/views/game_restore.go
+++ b/pkg/ui/views/game_restore.go
@@ -60,6 +60,7 @@ func (m GameModel) handleSendRestoreMsg(source p2p.NetworkID) tea.Cmd {
}
m.err = m.network.Send(source, []byte("restore-ack"), []byte(payload))
+ m.err = m.network.Send(source, []byte("define-turn"), []byte(string(m.turn)))
return nil
}
diff --git a/pkg/ui/views/game_util.go b/pkg/ui/views/game_util.go
index 8b8c658..a4b3b1a 100644
--- a/pkg/ui/views/game_util.go
+++ b/pkg/ui/views/game_util.go
@@ -3,7 +3,6 @@ package views
import (
"fmt"
- "github.com/boozec/rahanna/internal/api/database"
"github.com/boozec/rahanna/pkg/p2p"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
@@ -32,18 +31,7 @@ func (m GameModel) isMyTurn() bool {
return false
}
- var totalPlayers int
-
- switch m.game.Type {
- case database.SingleGameType:
- totalPlayers = 2
- case database.PairGameType:
- totalPlayers = 4
- }
-
- moves := len(m.chessGame.Moves())
- currentPlayer := (moves % totalPlayers) + 1
- return m.network.Me() == m.playerPeer(currentPlayer)
+ return m.network.Me() == m.turn
}
func (m GameModel) playerPeer(n int) p2p.NetworkID {
diff --git a/pkg/ui/views/play_api.go b/pkg/ui/views/play_api.go
index 3846abe..ff38843 100644
--- a/pkg/ui/views/play_api.go
+++ b/pkg/ui/views/play_api.go
@@ -18,11 +18,12 @@ import (
)
type responseOk struct {
- Name string `json:"name"`
- Type string `json:"type"`
- GameID int `json:"id"`
- IP string `json:"ip"`
- Port int `json:"int"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ MoveChoose string `json:"move_choose_type"`
+ GameID int `json:"id"`
+ IP string `json:"ip"`
+ Port int `json:"int"`
}
// API response types
@@ -159,7 +160,7 @@ func (m *PlayModel) handleGamesResponse(msg []database.Game) (tea.Model, tea.Cmd
return m, nil
}
-func (m *PlayModel) newGameCallback(gameType database.GameType) tea.Cmd {
+func (m *PlayModel) newGameCallback(gameType database.GameType, moveChooseType database.MoveChooseType) tea.Cmd {
return func() tea.Msg {
// Get authorization token
authorization, err := getAuthorizationToken()
@@ -177,8 +178,9 @@ func (m *PlayModel) newGameCallback(gameType database.GameType) tea.Cmd {
// Prepare request payload
payload, err := json.Marshal(map[string]string{
- "ip": fmt.Sprintf("%s:%d", ip, port),
- "type": string(gameType),
+ "ip": fmt.Sprintf("%s:%d", ip, port),
+ "type": string(gameType),
+ "move_choose_type": string(moveChooseType),
})
if err != nil {
return playResponse{Error: err.Error()}
diff --git a/pkg/ui/views/play_keymap.go b/pkg/ui/views/play_keymap.go
index 9d2f014..3cb9f71 100644
--- a/pkg/ui/views/play_keymap.go
+++ b/pkg/ui/views/play_keymap.go
@@ -22,14 +22,15 @@ const (
// Keyboard controls
type playKeyMap struct {
- EnterNewGame key.Binding
- StartNewSingleGame key.Binding
- StartNewPairGame key.Binding
- RestoreGame key.Binding
- GoLogout key.Binding
- NextPage key.Binding
- PrevPage key.Binding
- Exit key.Binding
+ EnterNewGame key.Binding
+ StartNewSingleGame key.Binding
+ StartNewPairGame key.Binding
+ StartNewPairRandomGame key.Binding
+ RestoreGame key.Binding
+ GoLogout key.Binding
+ NextPage key.Binding
+ PrevPage key.Binding
+ Exit key.Binding
}
// Default key bindings for the play model
@@ -44,7 +45,11 @@ var defaultPlayKeyMap = playKeyMap{
),
StartNewPairGame: key.NewBinding(
key.WithKeys("alt+p", "alt+P"),
- key.WithHelp("Alt+P", "Start a new pair play"),
+ key.WithHelp("Alt+P", "Start a new co-op play"),
+ ),
+ StartNewPairRandomGame: key.NewBinding(
+ key.WithKeys("alt+r", "alt+R"),
+ key.WithHelp("Alt+R", "Start a new co-op play (random choose)"),
),
RestoreGame: key.NewBinding(
key.WithKeys("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"),
@@ -83,7 +88,7 @@ func (m PlayModel) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.page = StartGamePage
if !m.isLoading {
m.isLoading = true
- return m, m.newGameCallback(database.SingleGameType)
+ return m, m.newGameCallback(database.SingleGameType, database.SequentialChooseType)
}
return m, cmd
@@ -93,7 +98,18 @@ func (m PlayModel) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.page = StartGamePage
if !m.isLoading {
m.isLoading = true
- return m, m.newGameCallback(database.PairGameType)
+ return m, m.newGameCallback(database.PairGameType, database.SequentialChooseType)
+ }
+
+ return m, cmd
+ }
+
+ case key.Matches(msg, m.keys.StartNewPairRandomGame):
+ if m.page == LandingPage {
+ m.page = StartGamePage
+ if !m.isLoading {
+ m.isLoading = true
+ return m, m.newGameCallback(database.PairGameType, database.RandomChooseType)
}
return m, cmd
@@ -162,6 +178,10 @@ func (m PlayModel) renderNavigationButtons() string {
altCodeStyle.Render(m.keys.StartNewPairGame.Help().Key),
m.keys.StartNewPairGame.Help().Desc)
+ startPairRandomKey := fmt.Sprintf("%s %s",
+ altCodeStyle.Render(m.keys.StartNewPairRandomGame.Help().Key),
+ m.keys.StartNewPairRandomGame.Help().Desc)
+
nextPageKey := fmt.Sprintf("%s %s",
altCodeStyle.Render(m.keys.NextPage.Help().Key),
m.keys.NextPage.Help().Desc)
@@ -175,6 +195,7 @@ func (m PlayModel) renderNavigationButtons() string {
enterKey,
startSingleKey,
startPairKey,
+ startPairRandomKey,
restoreKey,
lipgloss.JoinHorizontal(lipgloss.Left, prevPageKey, " | ", nextPageKey),
logoutKey,