diff options
-rw-r--r-- | README.md | 3 | ||||
-rw-r--r-- | assets/demo.png | bin | 0 -> 115008 bytes | |||
-rw-r--r-- | go.mod | 1 | ||||
-rw-r--r-- | go.sum | 3 | ||||
-rw-r--r-- | internal/network/network.go | 2 | ||||
-rw-r--r-- | pkg/ui/views/game.go | 88 | ||||
-rw-r--r-- | pkg/ui/views/play.go | 23 |
7 files changed, 93 insertions, 27 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e574f4 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Rahanna + + diff --git a/assets/demo.png b/assets/demo.png Binary files differnew file mode 100644 index 0000000..ad55d0d --- /dev/null +++ b/assets/demo.png @@ -8,6 +8,7 @@ require ( github.com/charmbracelet/lipgloss v1.1.0 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/gorilla/mux v1.8.1 + github.com/notnil/chess v1.10.0 github.com/rs/cors v1.11.1 github.com/stretchr/testify v1.10.0 go.uber.org/zap v1.27.0 @@ -1,3 +1,4 @@ +github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -58,6 +59,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/notnil/chess v1.10.0 h1:RR3MgS9G6zZmJ+VPTJolyxdaIgxoUPyUUY+2iaw35G0= +github.com/notnil/chess v1.10.0/go.mod h1:cRuJUIBFq9Xki05TWHJxHYkC+fFpq45IWwk94DdlCrA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/internal/network/network.go b/internal/network/network.go index 0b4f4aa..cec024f 100644 --- a/internal/network/network.go +++ b/internal/network/network.go @@ -193,6 +193,8 @@ func (n *TCPNetwork) listenForMessages(conn net.Conn) { continue } + n.Logger.Sugar().Infof("Received message from '%s': %s", message.Source, string(message.Payload)) + n.OnReceiveFn(message) } } diff --git a/pkg/ui/views/game.go b/pkg/ui/views/game.go index 5a0972c..4c5ed71 100644 --- a/pkg/ui/views/game.go +++ b/pkg/ui/views/game.go @@ -3,36 +3,45 @@ 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/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/notnil/chess" ) // gameKeyMap defines the key bindings for the game view. type gameKeyMap struct { - EnterNewGame key.Binding - StartNewGame key.Binding - GoLogout key.Binding - Quit key.Binding + GoLogout key.Binding + RandomMove 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"), + key.WithKeys("alt+Q", "alt+q"), key.WithHelp("Alt+Q", "Logout"), ), Quit: key.NewBinding( - key.WithKeys("q"), - key.WithHelp("Q", "Quit"), + key.WithKeys("Q", "q"), + key.WithHelp(" Q", "Quit"), ), } +// ChessMoveMsg is a message containing a received chess move. +type ChessMoveMsg string + // GameModel represents the state of the game view. type GameModel struct { // UI dimensions @@ -40,13 +49,16 @@ type GameModel struct { height int // UI state - keys playKeyMap + keys gameKeyMap // Game state peer string currentGameID int game *database.Game network *multiplayer.GameNetwork + chessGame *chess.Game + incomingMoves chan string + turn int } // NewGameModel creates a new GameModel. @@ -54,16 +66,20 @@ func NewGameModel(width, height int, peer string, currentGameID int, network *mu return GameModel{ width: width, height: height, + keys: defaultGameKeyMap, peer: peer, currentGameID: currentGameID, network: network, + chessGame: chess.NewGame(chess.UseNotation(chess.UCINotation{})), + incomingMoves: make(chan string), + turn: 0, } } // Init initializes the GameModel. func (m GameModel) Init() tea.Cmd { ClearScreen() - return tea.Batch(textinput.Blink, m.getGame()) + return tea.Batch(textinput.Blink, m.getGame(), m.getMoves()) } // Update handles incoming messages and updates the GameModel. @@ -77,6 +93,13 @@ func (m GameModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.handleWindowSize(msg) case tea.KeyMsg: return m.handleKeyPress(msg) + case ChessMoveMsg: + m.turn++ + err := m.chessGame.MoveStr(string(msg)) + if err != nil { + fmt.Println("Error applying move:", err) + } + return m, m.getMoves() case database.Game: return m.handleGetGameResponse(msg) } @@ -90,13 +113,18 @@ func (m GameModel) View() string { var content string if m.game != nil { - otherPlayer := "" - if m.peer == "peer-1" { - otherPlayer = m.game.Player2.Username - } else { - otherPlayer = m.game.Player1.Username + yourTurn := "" + if m.turn%2 == 0 && m.peer == "peer-2" || m.turn%2 == 1 && m.peer == "peer-1" { + yourTurn = "[YOUR TURN]" } - content = fmt.Sprintf("You're playing versus %s", otherPlayer) + + 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(), + ) } windowContent := m.buildWindowContent(content, formWidth) @@ -128,6 +156,15 @@ 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 } @@ -145,6 +182,10 @@ 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) @@ -155,6 +196,7 @@ func (m GameModel) renderNavigationButtons() string { return lipgloss.JoinVertical( lipgloss.Left, + randomMoveKey, logoutKey, quitKey, ) @@ -162,7 +204,7 @@ func (m GameModel) renderNavigationButtons() string { func (m *GameModel) handleGetGameResponse(msg database.Game) (tea.Model, tea.Cmd) { m.game = &msg - if m.peer == "peer-1" { + if m.peer == "peer-2" { m.network.Peer = msg.IP2 } else { m.network.Peer = msg.IP1 @@ -193,7 +235,7 @@ func (m *GameModel) getGame() tea.Cmd { } // Establish peer connection - if m.peer == "peer-1" { + if m.peer == "peer-2" { if game.IP2 != "" { remote := game.IP2 go m.network.Server.AddPeer("peer-2", remote) @@ -208,3 +250,15 @@ func (m *GameModel) getGame() tea.Cmd { return game } } + +func (m *GameModel) getMoves() tea.Cmd { + m.network.Server.OnReceiveFn = func(msg network.Message) { + moveStr := string(msg.Payload) + m.incomingMoves <- moveStr + } + + return func() tea.Msg { + move := <-m.incomingMoves + return ChessMoveMsg(move) + } +} diff --git a/pkg/ui/views/play.go b/pkg/ui/views/play.go index cf1f6ca..3997a88 100644 --- a/pkg/ui/views/play.go +++ b/pkg/ui/views/play.go @@ -38,8 +38,6 @@ A B C D E F G H type PlayModelPage int -var start = make(chan int) - const ( LandingPage PlayModelPage = iota InsertCodePage @@ -97,6 +95,8 @@ var defaultPlayKeyMap = playKeyMap{ ), } +type StartGameMsg struct{} + type PlayModel struct { // UI dimensions width int @@ -159,12 +159,6 @@ func (m PlayModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, exit } - select { - case <-start: - return m, SwitchModelCmd(NewGameModel(m.width, m.height+1, "peer-1", m.currentGameId, m.network)) - default: - } - switch msg := msg.(type) { case tea.WindowSizeMsg: return m.handleWindowSize(msg) @@ -176,6 +170,8 @@ func (m PlayModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.handleGameResponse(msg) case []database.Game: return m.handleGamesResponse(msg) + case StartGameMsg: + return m, SwitchModelCmd(NewGameModel(m.width, m.height+1, "peer-2", m.currentGameId, m.network)) case error: return m.handleError(msg) } @@ -269,10 +265,17 @@ func (m *PlayModel) handlePlayResponse(msg playResponse) (tea.Model, tea.Cmd) { m.playName = msg.Ok.Name m.currentGameId = msg.Ok.GameID logger, _ := logger.GetLogger() + + callbackCompleted := make(chan bool) m.network = multiplayer.NewGameNetwork("peer-1", fmt.Sprintf("%s:%d", msg.Ok.IP, msg.Ok.Port), func() error { - start <- 1 + close(callbackCompleted) return nil }, logger) + + return m, func() tea.Msg { + <-callbackCompleted + return StartGameMsg{} + } } return m, nil @@ -292,7 +295,7 @@ func (m *PlayModel) handleGameResponse(msg database.Game) (tea.Model, tea.Cmd) { return nil }, logger) - return m, SwitchModelCmd(NewGameModel(m.width, m.height+1, "peer-2", m.game.ID, network)) + return m, SwitchModelCmd(NewGameModel(m.width, m.height+1, "peer-1", m.game.ID, network)) } return m, nil } |