diff options
author | Santo Cariotti <santo@dcariotti.me> | 2025-04-17 10:18:22 +0200 |
---|---|---|
committer | Santo Cariotti <santo@dcariotti.me> | 2025-04-17 10:18:22 +0200 |
commit | 169796db6fa9a31a3ec24b745754bf342acc9173 (patch) | |
tree | 7be46b856592ae2230a2df60185f80379e2d1dc5 /pkg/ui/views/game.go | |
parent | f60fadc54421c8e0aedb33e59270d4aa48e842d2 (diff) |
Choose a move
Diffstat (limited to 'pkg/ui/views/game.go')
-rw-r--r-- | pkg/ui/views/game.go | 233 |
1 files changed, 161 insertions, 72 deletions
diff --git a/pkg/ui/views/game.go b/pkg/ui/views/game.go index 4c5ed71..1087d5f 100644 --- a/pkg/ui/views/game.go +++ b/pkg/ui/views/game.go @@ -3,13 +3,13 @@ package views import ( "encoding/json" "fmt" - "math/rand" "os" "github.com/boozec/rahanna/internal/api/database" "github.com/boozec/rahanna/internal/network" "github.com/boozec/rahanna/pkg/ui/multiplayer" "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -18,30 +18,36 @@ import ( // gameKeyMap defines the key bindings for the game view. type gameKeyMap struct { - GoLogout key.Binding - RandomMove key.Binding - Quit key.Binding + GoLogout key.Binding + Quit key.Binding } // defaultGameKeyMap provides the default key bindings for the game view. var defaultGameKeyMap = gameKeyMap{ - RandomMove: key.NewBinding( - key.WithKeys("R", "r"), - key.WithHelp("R", "Random Move"), - ), GoLogout: key.NewBinding( key.WithKeys("alt+Q", "alt+q"), key.WithHelp("Alt+Q", "Logout"), ), Quit: key.NewBinding( key.WithKeys("Q", "q"), - key.WithHelp(" Q", "Quit"), + key.WithHelp(" Q", "Quit"), ), } // ChessMoveMsg is a message containing a received chess move. type ChessMoveMsg string +// UpdateMovesListMsg is a message to update the moves list +type UpdateMovesListMsg struct{} + +type item struct { + title string +} + +func (i item) Title() string { return i.title } +func (i item) Description() string { return "" } +func (i item) FilterValue() string { return i.title } + // GameModel represents the state of the game view. type GameModel struct { // UI dimensions @@ -59,10 +65,25 @@ type GameModel struct { chessGame *chess.Game incomingMoves chan string turn int + movesList list.Model } // NewGameModel creates a new GameModel. func NewGameModel(width, height int, peer string, currentGameID int, network *multiplayer.GameNetwork) GameModel { + listDelegate := list.NewDefaultDelegate() + listDelegate.ShowDescription = false + listDelegate.Styles.SelectedTitle = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder(), false, false, false, true). + BorderForeground(highlightColor). + Foreground(highlightColor). + Padding(0, 0, 0, 1) + + moveList := list.New([]list.Item{}, listDelegate, width/4, height/2) + moveList.Styles.Title = lipgloss.NewStyle(). + Background(highlightColor). + Foreground(lipgloss.Color("230")). + Padding(0, 1) + return GameModel{ width: width, height: height, @@ -73,13 +94,20 @@ func NewGameModel(width, height int, peer string, currentGameID int, network *mu chessGame: chess.NewGame(chess.UseNotation(chess.UCINotation{})), incomingMoves: make(chan string), turn: 0, + movesList: moveList, } } // Init initializes the GameModel. func (m GameModel) Init() tea.Cmd { ClearScreen() - return tea.Batch(textinput.Blink, m.getGame(), m.getMoves()) + return tea.Batch(textinput.Blink, m.getGame(), m.getMoves(), m.updateMovesListCmd()) +} + +func (m *GameModel) updateMovesListCmd() tea.Cmd { + return func() tea.Msg { + return UpdateMovesListMsg{} + } } // Update handles incoming messages and updates the GameModel. @@ -88,45 +116,142 @@ func (m GameModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, exit } + var cmds []tea.Cmd + var cmd tea.Cmd + switch msg := msg.(type) { case tea.WindowSizeMsg: - return m.handleWindowSize(msg) + m, cmd = m.handleWindowSizeMsg(msg) + cmds = append(cmds, cmd) + case UpdateMovesListMsg: + m = m.handleUpdateMovesListMsg() case tea.KeyMsg: - return m.handleKeyPress(msg) + m, cmd = m.handleKeyMsg(msg) + cmds = append(cmds, cmd) case ChessMoveMsg: - m.turn++ - err := m.chessGame.MoveStr(string(msg)) - if err != nil { - fmt.Println("Error applying move:", err) - } - return m, m.getMoves() + m, cmd = m.handleChessMoveMsg(msg) + cmds = append(cmds, cmd) case database.Game: - return m.handleGetGameResponse(msg) + m = m.handleDatabaseGameMsg(msg) + cmds = append(cmds, m.updateMovesListCmd()) + } + + if m.isMyTurn() { + m.movesList, cmd = m.movesList.Update(msg) + switch msg := msg.(type) { + case tea.KeyMsg: + if msg.Type == tea.KeyEnter { + selectedItem := m.movesList.SelectedItem() + if selectedItem != nil { + moveStr := selectedItem.(item).Title() + m.network.Server.Send(network.NetworkID(m.peer), []byte(moveStr)) + m.chessGame.MoveStr(moveStr) + m.turn++ + cmds = append(cmds, m.getMoves(), m.updateMovesListCmd()) + } + } + } + cmds = append(cmds, cmd) + } + + return m, tea.Batch(cmds...) +} + +func (m GameModel) handleWindowSizeMsg(msg tea.WindowSizeMsg) (GameModel, tea.Cmd) { + m.width = msg.Width + m.height = msg.Height + listWidth := m.width / 4 + m.movesList.SetSize(listWidth, m.height/2) + return m, m.updateMovesListCmd() +} + +func (m GameModel) handleUpdateMovesListMsg() GameModel { + if m.isMyTurn() && m.game != nil { + var items []list.Item + for _, move := range m.chessGame.ValidMoves() { + items = append(items, item{title: move.String()}) + } + m.movesList.SetItems(items) + m.movesList.Title = "Choose a move" + m.movesList.Select(0) + m.movesList.SetShowFilter(true) + m.movesList.SetFilteringEnabled(true) + } + return m +} + +func (m GameModel) handleKeyMsg(msg tea.KeyMsg) (GameModel, 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 } +func (m GameModel) handleChessMoveMsg(msg ChessMoveMsg) (GameModel, tea.Cmd) { + m.turn++ + err := m.chessGame.MoveStr(string(msg)) + if err != nil { + fmt.Println("Error applying move:", err) + } + return m, tea.Batch(m.getMoves(), m.updateMovesListCmd()) +} + +func (m GameModel) handleDatabaseGameMsg(msg database.Game) GameModel { + m.game = &msg + if m.peer == "peer-2" { + m.network.Peer = msg.IP2 + } else { + m.network.Peer = msg.IP1 + } + return m +} + // View renders the GameModel. func (m GameModel) View() string { formWidth := getFormWidth(m.width) - var content string - if m.game != nil { - yourTurn := "" - if m.turn%2 == 0 && m.peer == "peer-2" || m.turn%2 == 1 && m.peer == "peer-1" { - yourTurn = "[YOUR TURN]" - } + if m.game == nil { + return "Loading game..." + } - content = fmt.Sprintf("%s vs %s\n%s\n\n%s\n%s", - m.game.Player1.Username, - m.game.Player2.Username, - lipgloss.NewStyle().Foreground(highlightColor).Render(yourTurn), - m.chessGame.Position().Board().Draw(), - m.chessGame.String(), - ) + listWidth := formWidth / 4 + boardWidth := formWidth / 2 + notationWidth := formWidth - listWidth - boardWidth - 2 + + listHeight := m.height / 3 + boardHeight := m.height / 3 + notationHeight := m.height - listHeight - boardHeight - 2 + + listStyle := lipgloss.NewStyle().Width(listWidth).Height(listHeight).Padding(0, 1) + boardStyle := lipgloss.NewStyle().Width(boardWidth).Height(boardHeight).Align(lipgloss.Center).Padding(0, 1) + notationStyle := lipgloss.NewStyle().Width(notationWidth).Height(notationHeight).Padding(0, 1) + + var movesListView string + + if m.isMyTurn() { + m.movesList.SetSize(listWidth, listHeight-2) + movesListView = listStyle.Render(m.movesList.View()) + } else { + movesListView = listStyle.Render(lipgloss.Place(listWidth, listHeight, lipgloss.Center, lipgloss.Center, "Wait your turn")) } + 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.JoinHorizontal( + lipgloss.Top, + movesListView, + boardStyle.Render( + m.chessGame.Position().Board().Draw(), + ), + notationStyle.Render(fmt.Sprintf("Moves\n%s", m.chessGame.String())), + ), + ) + windowContent := m.buildWindowContent(content, formWidth) buttons := m.renderNavigationButtons() @@ -146,31 +271,6 @@ func (m GameModel) View() string { ) } -func (m GameModel) handleWindowSize(msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) { - m.width = msg.Width - m.height = msg.Height - return m, nil -} - -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.RandomMove): - if m.turn%2 == 0 && m.peer == "peer-2" || m.turn%2 == 1 && m.peer == "peer-1" { - moves := m.chessGame.ValidMoves() - move := moves[rand.Intn(len(moves))] - m.network.Server.Send(network.NetworkID(m.peer), []byte(move.String())) - m.chessGame.MoveStr(move.String()) - m.turn++ - } - return m, nil - case key.Matches(msg, m.keys.Quit): - return m, tea.Quit - } - return m, nil -} - func (m GameModel) buildWindowContent(content string, formWidth int) string { return lipgloss.JoinVertical( lipgloss.Center, @@ -182,10 +282,6 @@ func (m GameModel) buildWindowContent(content string, formWidth int) string { } func (m GameModel) renderNavigationButtons() string { - randomMoveKey := fmt.Sprintf("%s %s", - altCodeStyle.Render(m.keys.RandomMove.Help().Key), - m.keys.RandomMove.Help().Desc) - logoutKey := fmt.Sprintf("%s %s", altCodeStyle.Render(m.keys.GoLogout.Help().Key), m.keys.GoLogout.Help().Desc) @@ -196,22 +292,11 @@ func (m GameModel) renderNavigationButtons() string { return lipgloss.JoinVertical( lipgloss.Left, - randomMoveKey, logoutKey, quitKey, ) } -func (m *GameModel) handleGetGameResponse(msg database.Game) (tea.Model, tea.Cmd) { - m.game = &msg - if m.peer == "peer-2" { - 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 @@ -262,3 +347,7 @@ func (m *GameModel) getMoves() tea.Cmd { return ChessMoveMsg(move) } } + +func (m GameModel) isMyTurn() bool { + return m.turn%2 == 0 && m.peer == "peer-2" || m.turn%2 == 1 && m.peer == "peer-1" +} |