gorbito/main.go

303 lines
7.5 KiB
Go

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)
}
}