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