Skip to content

Commit

Permalink
feat(Multiplayer): ✨Add Multiplayer functionality and End Game model
Browse files Browse the repository at this point in the history
  • Loading branch information
DziedzicGrzegorz committed May 30, 2024
1 parent c7ff43a commit 4109ee4
Show file tree
Hide file tree
Showing 5 changed files with 313 additions and 13 deletions.
234 changes: 234 additions & 0 deletions Multiplayer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package main

import (
"fmt"
"time"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)

var (
cellStyle = lipgloss.NewStyle().Align(lipgloss.Center, lipgloss.Center).Width(5).Height(3).Border(lipgloss.NormalBorder())
boardStyle = lipgloss.NewStyle().Border(lipgloss.RoundedBorder())
headerStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#FFD700")).Align(lipgloss.Center).Margin(0, 0, 2, 0)
errorStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#FF0000")).Align(lipgloss.Center)
blinkingStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")).Bold(true).Blink(true)
)

type GameModel struct {
width int
height int
cursor int
board [3][3]int
current int
winner int
blink bool
turnCount int
errorMessage string
}

func NewGameModel(width, height int) *GameModel {
return &GameModel{
width: width,
height: height,
cursor: 0,
current: 1, // X starts
board: [3][3]int{},
turnCount: 9,
}
}

func (m *GameModel) Init() tea.Cmd {
return tea.Batch(tea.EnterAltScreen)
}

func (m *GameModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "up", "k":
m.moveCursor(-3)
case "down", "j":
m.moveCursor(3)
case "left", "h":
m.moveCursor(-1)
case "right", "l":
m.moveCursor(1)
case "enter":
if !m.placeMarker() {
m.errorMessage = "Cannot overwrite existing marker!"
} else {
m.errorMessage = ""
m.winner = m.checkWinner()
if m.winner != 0 {
endMessage := fmt.Sprintf("Player %s wins!", m.currentMarker())
endGameModel := NewEndGameModel(m.width, m.height, endMessage, false)
//sleep for 500 ms for better UX
time.Sleep(500 * time.Millisecond)
return endGameModel, endGameModel.Init()
}
m.turnCount--
if m.turnCount == 0 {
endMessage := "It's a draw!"
endGameModel := NewEndGameModel(m.width, m.height, endMessage, true)
//sleep for 500 ms for better UX
time.Sleep(500 * time.Millisecond)
return endGameModel, endGameModel.Init()
}
m.switchPlayer()
}
case "ctrl+c", "q", "esc":
return m, tea.Quit
}

case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
}
return m, nil
}

func (m *GameModel) View() string {
return m.renderBoard()
}

func (m *GameModel) moveCursor(delta int) {
// Clear the error message when move is made
m.errorMessage = ""

newCursor := m.cursor + delta
// Get the column
col := m.cursor % 3

// Calculate the new row and column
newRow, newCol := newCursor/3, newCursor%3

// Check for boundary conditions
if newRow < 0 || newRow >= 3 || newCol < 0 || newCol >= 3 {
newCursor = m.cursor
}

// Handle wrapping around the edges
if (col == 2 && delta == 1) || (col == 0 && delta == -1) {
newCursor = m.cursor
}

m.cursor = newCursor
}

func (m *GameModel) placeMarker() bool {
row, col := m.cursor/3, m.cursor%3
// Check if the cell is empty
if m.board[row][col] != 0 {
return false
}
// Place the marker
m.board[row][col] = m.current
return true
}

func (m *GameModel) switchPlayer() {
// Switch player if 1 (X) or -1 (O)
m.current = -m.current
}

func (m *GameModel) currentMarker() string {
if m.current == 1 {
return "X"
}
return "O"
}

func (m *GameModel) checkWinner() int {
winningLines := [][3][2]int{
{{0, 0}, {0, 1}, {0, 2}}, // first row
{{1, 0}, {1, 1}, {1, 2}}, // second row
{{2, 0}, {2, 1}, {2, 2}}, // third row
{{0, 0}, {1, 0}, {2, 0}}, // first column
{{0, 1}, {1, 1}, {2, 1}}, // second column
{{0, 2}, {1, 2}, {2, 2}}, // third column
{{0, 0}, {1, 1}, {2, 2}}, // diagonal top-left to bottom-right
{{0, 2}, {1, 1}, {2, 0}}, // diagonal top-right to bottom-left
}

for _, line := range winningLines {
a, b, c := line[0], line[1], line[2]
// Check if all cells in the line are the same and not empty
if m.board[a[0]][a[1]] != 0 &&
m.board[a[0]][a[1]] == m.board[b[0]][b[1]] &&
m.board[a[0]][a[1]] == m.board[c[0]][c[1]] {
// Return the winning marker
return m.board[a[0]][a[1]]
}
}

return 0
}

func (m *GameModel) renderBoard() string {
var cells []string
for i := 0; i < 9; i++ {
row, col := i/3, i%3
cell := m.board[row][col]
cellStr := " "

style := cellStyle

if i == m.cursor {
cellStr = blinkingStyle.Render(m.currentMarker())
} else if cell == 1 {
cellStr = "X"
} else if cell == -1 {
cellStr = "O"
}

// Ustawianie granic
if col == 0 || col == 2 {
style = style.UnsetBorderLeft().UnsetBorderRight().UnsetBorderBottom()
if row == 0 {
style = style.UnsetBorderStyle()
}
}
if col == 1 {
style = style.UnsetBorderBottom()
if row == 0 {
style = style.UnsetBorderTop()
}
}

cells = append(cells, style.Render(cellStr))
}

// Informacja o aktualnym graczu
currentPlayer := fmt.Sprintf("Current player: %s\n", m.currentMarker())

header := headerStyle.Render(currentPlayer)

// Informacja o grze
footer := subtleStyle.Render("j/k, up/down: move | h/l, left/right: move | enter: select | ctrl+c: quit")

// Joining all cells horizontally
board := lipgloss.JoinVertical(lipgloss.Left,
lipgloss.JoinHorizontal(lipgloss.Center, cells[0], cells[1], cells[2]),
lipgloss.JoinHorizontal(lipgloss.Left, cells[3], cells[4], cells[5]),
lipgloss.JoinHorizontal(lipgloss.Left, cells[6], cells[7], cells[8]))

// Wyświetlanie błędów, jeśli istnieją
errorMsg := ""
if m.errorMessage != "" {
errorMsg = errorStyle.Render(m.errorMessage)
}

// Joining all elements vertically
view := lipgloss.JoinVertical(lipgloss.Center,
header,
boardStyle.Render(board),
errorMsg,
footer,
)

centeredFullView := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, view)

return centeredFullView
}
58 changes: 58 additions & 0 deletions endgame.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package main

import (
"fmt"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)

var (
highlightStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#2ECC71")).Align(lipgloss.Center)
drawStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#F4D03F")).Align(lipgloss.Center)
)

type EndGameModel struct {
width int
height int
message string
isDraw bool
}

func NewEndGameModel(width, height int, endGameMessage string, isDraw bool) *EndGameModel {
return &EndGameModel{
width: width,
height: height,
message: endGameMessage,
isDraw: isDraw,
}
}

func (m *EndGameModel) Init() tea.Cmd {
return nil
}

func (m *EndGameModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "r":
return NewGameModel(m.width, m.height), nil
case "m":
return initialModel(m.width, m.height), nil
case "ctrl+c":
return m, tea.Quit
}
}
return m, nil
}

func (m *EndGameModel) View() string {
message := fmt.Sprintf("%s\n\nPress 'r' to restart or 'm' to return to menu.", m.message)
styledMessage := highlightStyle.Render(message)
if m.isDraw {
styledMessage = drawStyle.Render(message)
}
centeredMessage := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, styledMessage)
return centeredMessage
}
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
module Tic-Tac-Toe
module github.com/dziedzicgrzegorz/Tic-Tac-Toe

go 1.22

require (
github.com/charmbracelet/bubbletea v0.26.2
github.com/charmbracelet/bubbletea v0.26.4
github.com/charmbracelet/lipgloss v0.11.0
github.com/spf13/cobra v1.8.0
)

require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/x/ansi v0.1.1 // indirect
github.com/charmbracelet/x/ansi v0.1.2 // indirect
github.com/charmbracelet/x/input v0.1.0 // indirect
github.com/charmbracelet/x/term v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.1.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ github.com/charmbracelet/bubbletea v0.26.2 h1:Eeb+n75Om9gQ+I6YpbCXQRKHt5Pn4vMwus
github.com/charmbracelet/bubbletea v0.26.2/go.mod h1:6I0nZ3YHUrQj7YHIHlM8RySX4ZIthTliMY+W8X8b+Gs=
github.com/charmbracelet/bubbletea v0.26.3 h1:iXyGvI+FfOWqkB2V07m1DF3xxQijxjY2j8PqiXYqasg=
github.com/charmbracelet/bubbletea v0.26.3/go.mod h1:bpZHfDHTYJC5g+FBK+ptJRCQotRC+Dhh3AoMxa/2+3Q=
github.com/charmbracelet/bubbletea v0.26.4 h1:2gDkkzLZaTjMl/dQBpNVtnvcCxsh/FCkimep7FC9c40=
github.com/charmbracelet/bubbletea v0.26.4/go.mod h1:P+r+RRA5qtI1DOHNFn0otoNwB4rn+zNAzSj/EXz6xU0=
github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g=
github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8=
github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk=
github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY=
github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ=
github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28=
github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
Expand Down
24 changes: 14 additions & 10 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,7 @@ var (
Bold(true).
Foreground(lipgloss.Color("#FFD700")). // Gold
Align(lipgloss.Center).
Background(lipgloss.Color("#0000FF")).
Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("#FFD700"))
Padding(1, 2)

backgroundStyle = lipgloss.NewStyle().
Align(lipgloss.Center).
Expand All @@ -73,7 +70,7 @@ func initialModel(width, height int) model {
}

func (m model) Init() tea.Cmd {
return tea.EnterAltScreen
return tea.SetWindowTitle("Tic-Tac-Toe Game")
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
Expand All @@ -82,16 +79,24 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch m.mode {
case modeMenu:
switch msg.String() {
case "up", "k":
case "up":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
case "down":
if m.cursor < len(m.menuItems)-1 {
m.cursor++
}
case "enter":
m.mode = m.menuItems[m.cursor].mode
switch m.cursor {
case 0:
m.mode = modeSinglePlayer
case 1:
game := NewGameModel(m.width, m.height)
return game, nil
case 2:
m.mode = modeStats
}
case "ctrl+c", "q", "esc":
return m, tea.Quit
}
Expand Down Expand Up @@ -121,7 +126,7 @@ func choicesView(m model) string {

menuSelect := lipgloss.JoinVertical(lipgloss.Left, choices...)

footer := subtleStyle.Render("j/k, up/down: select | enter: choose | q, esc: quit")
footer := subtleStyle.Render("up ↑ / down: select | enter: choose | q, esc: quit")

view := lipgloss.JoinVertical(lipgloss.Center,
titleStyle.Render(title),
Expand Down Expand Up @@ -166,7 +171,6 @@ func main() {
Long: "A simple Tic-Tac-Toe game written in Go using the Bubble Tea library and the Lip Gloss library.",
Run: func(cmd *cobra.Command, args []string) {
p := tea.NewProgram(initialModel(0, 0), tea.WithAltScreen())
p.SetWindowTitle("Welcome to the game")
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
Expand Down

0 comments on commit 4109ee4

Please # to comment.