diff options
author | Santo Cariotti <santo@dcariotti.me> | 2025-04-27 11:15:11 +0200 |
---|---|---|
committer | Santo Cariotti <santo@dcariotti.me> | 2025-04-27 12:24:40 +0200 |
commit | 85a6b4c2915fbfb42978fd7d2e3f7bd67e650314 (patch) | |
tree | eb705d5b97bb515d806049176df149890355e6e9 | |
parent | 6a60cc1c133ccf42dae8498fc40cc3276fc91561 (diff) |
Co-op mode with next player randomly
-rw-r--r-- | internal/api/database/models.go | 46 | ||||
-rw-r--r-- | internal/api/handlers/handlers.go | 10 | ||||
-rw-r--r-- | pkg/ui/multiplayer/multiplayer.go | 1 | ||||
-rw-r--r-- | pkg/ui/views/game.go | 11 | ||||
-rw-r--r-- | pkg/ui/views/game_api.go | 16 | ||||
-rw-r--r-- | pkg/ui/views/game_moves.go | 67 | ||||
-rw-r--r-- | pkg/ui/views/game_restore.go | 1 | ||||
-rw-r--r-- | pkg/ui/views/game_util.go | 14 | ||||
-rw-r--r-- | pkg/ui/views/play_api.go | 18 | ||||
-rw-r--r-- | pkg/ui/views/play_keymap.go | 43 |
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, |