From 70b45671a871e5c5d8f2e279f41f901955f3ae42 Mon Sep 17 00:00:00 2001 From: Felix Albrigtsen Date: Wed, 25 Dec 2024 22:19:02 +0100 Subject: [PATCH] Initial setup, basic game logic --- README.md | 8 +- go.mod | 24 +++++ go.sum | 37 +++++++ main.go | 302 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/README.md b/README.md index 5bfadf2..8cca78b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ -# gorbito +# Gorbito -"Go" with the flow, in the board gameOrbito! \ No newline at end of file +"Go" with the flow, in the board game Orbito! + +Gorbito is a basic TUI implementation of the 2-player strategy game [Orbito](https://flexiqgames.com/en/product/orbito/). + +I am writing this game to learn the Go programming language, and both the code and product might turn out messy. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c69c387 --- /dev/null +++ b/go.mod @@ -0,0 +1,24 @@ +module felixalb/gorbito + +go 1.23.3 + +require github.com/charmbracelet/bubbletea v1.2.4 + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/lipgloss v1.0.0 // indirect + github.com/charmbracelet/x/ansi v0.4.5 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6ef777c --- /dev/null +++ b/go.sum @@ -0,0 +1,37 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= +github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= +github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= +github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= +github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= +github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..8d8bbe4 --- /dev/null +++ b/main.go @@ -0,0 +1,302 @@ +package main + +import ( + "fmt" + "os" + "strings" + tea "github.com/charmbracelet/bubbletea" +) + +const BOARD_SIZE = 4 + +type GameState int8 +const ( + StartTurn GameState = iota + IsMoving + HasMoved + HasPlaced + GameOver +) + +type Player int8 +const ( + PNone Player = iota + POne + PTwo +) + +type Position struct { + x int + y int +} + +type GameModel struct { + board [][]Player + playerTurn Player + + cursor Position + moveCursor Position + state GameState + message string +} + +func (m GameModel) PrintBoard() string { + rowSeparator := "+" + strings.Repeat("---+", len(m.board)) + "\n" + playerSymbols := map[Player]byte{ + PNone: '_', + POne: 'X', + PTwo: 'O', + } + + lines := make([][]byte, 0) + for _, row := range m.board { + // Create the grid row + line := []byte("|" + strings.Repeat(" |", len(row))) + // Populate the player symbols + for i, p := range row { + line[(i*4)+2] = playerSymbols[p] + } + lines = append(lines, line) + } + + lines[m.cursor.y][(m.cursor.x*4)+1] = '>' + lines[m.cursor.y][(m.cursor.x*4)+3] = '<' + + if m.state == IsMoving { + lines[m.moveCursor.y][(m.moveCursor.x*4)+1] = '[' + lines[m.moveCursor.y][(m.moveCursor.x*4)+3] = ']' + } + + s := rowSeparator + for _, line := range lines { + s += string(line) + "\n" + s += rowSeparator + } + + return s +} + +func RotateBoard(board [][]Player) [][]Player { + // Clone the board + nBoard := make([][]Player, len(board)) + for i := range board { + nBoard[i] = make([]Player, len(board[i])) + copy(nBoard[i], board[i]) + } + + // Rotate each ring of the board counter-clockwise + for ringStart := range len(board)/2 { + ringEnd := len(board)-ringStart-1 + + // Top + for x := ringStart; x < ringEnd; x++ { + nBoard[ringStart][x] = board[ringStart][x+1] + } + // Bottom + for x := ringStart+1; x <= ringEnd; x++ { + nBoard[ringEnd][x] = board[ringEnd][x-1] + } + // Left + for y := ringStart+1; y <= ringEnd; y++ { + nBoard[y][ringStart] = board[y-1][ringStart] + } + // Right + for y := ringStart; y < ringEnd; y++ { + nBoard[y][ringEnd] = board[y+1][ringEnd] + } + } + + return nBoard +} + +func (m GameModel) Move() GameModel { + m.message = "" + + switch m.state { + case IsMoving: + if m.board[m.cursor.y][m.cursor.x] != PNone { + m.message = "You cannot move to an occupied space" + break + } + // TODO: Valid that cursor is out of bounds? + xDiff := m.cursor.x - m.moveCursor.x + yDiff := m.cursor.y - m.moveCursor.y + + if !( + (xDiff == 0 && yDiff == -1) || // Up + (xDiff == 0 && yDiff == 1) || // Down + (xDiff == -1 && yDiff == 0) || // Left + (xDiff == 1 && yDiff == 0)) { // Right + m.message = "You cannot move here. You can only move one square laterally." + break + } + + m.board[m.cursor.y][m.cursor.x] = m.board[m.moveCursor.x][m.moveCursor.y] + m.board[m.moveCursor.x][m.moveCursor.y] = PNone + m.state = HasMoved + + case HasPlaced: + m.board = RotateBoard(m.board) + m.message = "The board has been rotated." + if m.playerTurn == POne { + m.playerTurn = PTwo + } else { + m.playerTurn = POne + } + m.state = StartTurn + + default: + switch m.board[m.cursor.y][m.cursor.x] { + case PNone: + // Place piece + m.board[m.cursor.y][m.cursor.x] = m.playerTurn + m.state = HasPlaced + case m.playerTurn: + m.message = "You cannot move your own piece." + default: + m.state = IsMoving + m.moveCursor = m.cursor + } + } + + return m +} + +func initialModel() GameModel { + initialBoard := make([][]Player, BOARD_SIZE) + for i := range BOARD_SIZE { + initialBoard[i] = make([]Player, BOARD_SIZE) + for j := range initialBoard[i] { + initialBoard[i][j] = PNone + } + } + + return GameModel{ + board: initialBoard, + playerTurn: POne, + } +} + +func (m GameModel) Init() tea.Cmd { + return nil +} + +func (m GameModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + + case tea.KeyMsg: + + switch msg.String() { + + case "ctrl+c", "q": + return m, tea.Quit + + case "up", "k": + switch m.state { + case GameOver: + break + case HasPlaced: + break + case IsMoving: + if m.moveCursor.y > 0 { + m.cursor.x = m.moveCursor.x + m.cursor.y = m.moveCursor.y-1 + } + default: + m.cursor.y = (m.cursor.y + len(m.board) - 1) % len(m.board) + } + + case "down", "j": + switch m.state { + case GameOver: + break + case HasPlaced: + break + case IsMoving: + if m.moveCursor.y < len(m.board)-1 { + m.cursor.x = m.moveCursor.x + m.cursor.y = m.moveCursor.y+1 + } + default: + m.cursor.y = (m.cursor.y + 1) % len(m.board) + } + + case "left", "h": + switch m.state { + case GameOver: + break + case HasPlaced: + break + case IsMoving: + if m.moveCursor.x > 0 { + m.cursor.x = m.moveCursor.x-1 + m.cursor.y = m.moveCursor.y + } + default: + m.cursor.x = (m.cursor.x + len(m.board) - 1) % len(m.board) + } + + case "right", "l": + switch m.state { + case GameOver: + break + case HasPlaced: + break + case IsMoving: + if m.moveCursor.x < len(m.board)-1 { + m.cursor.x = m.moveCursor.x+1 + m.cursor.y = m.moveCursor.y + } + default: + m.cursor.x = (m.cursor.x + 1) % len(m.board) + } + + case "enter", " ": + m = m.Move() + } + } + + return m, nil +} + +func (m GameModel) View() string { + s := m.PrintBoard() + msg := "" + + switch m.playerTurn { + case POne: + msg += "Player 1 (X) plays\n" + case PTwo: + msg += "Player 2 (O) plays\n" + default: + // Something is wrong! + msg += "DEBUG: \n" + msg += fmt.Sprintf("Current player: %v\nCurrent MoveState: %v\nCurrent Board: %v\nCurrent Cursor: %#v\n", m.playerTurn, m.state, m.board, m.cursor) + } + + switch m.state { + case StartTurn: + msg += "Place a new piece or move an opponent piece >_< \n" + case IsMoving: + msg += "Move opponent piece from [_] to >_<. \n" + case HasMoved: + msg += "Place your piece at >_< \n" + case HasPlaced: + msg += "Finish your turn, rotating the board \n" + } + + s += "\n" + msg + s += "\n" + m.message + return s +} + +func main() { + fmt.Println("==== Gorbito ====") + + p := tea.NewProgram(initialModel()) + if _, err := p.Run(); err != nil { + fmt.Printf("Alas, there's been an error: %v", err) + os.Exit(1) + } +} +