-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(Multiplayer): ✨Add Multiplayer functionality and End Game model
- Loading branch information
1 parent
c7ff43a
commit 4109ee4
Showing
5 changed files
with
313 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters