summaryrefslogtreecommitdiff
path: root/pkg/ui/views/game.go
diff options
context:
space:
mode:
authorSanto Cariotti <santo@dcariotti.me>2025-04-17 10:18:22 +0200
committerSanto Cariotti <santo@dcariotti.me>2025-04-17 10:18:22 +0200
commit169796db6fa9a31a3ec24b745754bf342acc9173 (patch)
tree7be46b856592ae2230a2df60185f80379e2d1dc5 /pkg/ui/views/game.go
parentf60fadc54421c8e0aedb33e59270d4aa48e842d2 (diff)
Choose a move
Diffstat (limited to 'pkg/ui/views/game.go')
-rw-r--r--pkg/ui/views/game.go233
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"
+}