summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md3
-rw-r--r--assets/demo.pngbin0 -> 115008 bytes
-rw-r--r--go.mod1
-rw-r--r--go.sum3
-rw-r--r--internal/network/network.go2
-rw-r--r--pkg/ui/views/game.go88
-rw-r--r--pkg/ui/views/play.go23
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
+
+![demo](./assets/demo.png)
diff --git a/assets/demo.png b/assets/demo.png
new file mode 100644
index 0000000..ad55d0d
--- /dev/null
+++ b/assets/demo.png
Binary files differ
diff --git a/go.mod b/go.mod
index c3ec2c3..45692fb 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index 085824c..4e36162 100644
--- a/go.sum
+++ b/go.sum
@@ -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
}