Initial setup, basic game logic
This commit is contained in:
parent
7fecd7f572
commit
70b45671a8
@ -1,3 +1,7 @@
|
|||||||
# gorbito
|
# Gorbito
|
||||||
|
|
||||||
"Go" with the flow, in the board gameOrbito!
|
"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.
|
||||||
|
24
go.mod
Normal file
24
go.mod
Normal file
@ -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
|
||||||
|
)
|
37
go.sum
Normal file
37
go.sum
Normal file
@ -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=
|
302
main.go
Normal file
302
main.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user