freego/game.go

282 lines
6.1 KiB
Go

package freego
import (
"errors"
"fmt"
"math"
)
//GameState is an int representing the current state the game's in
//go:generate enumer -type=GameState
type GameState int
const (
gameLobby GameState = iota
gameSetup
gameTurnRed
gameTurnBlue
gameWinRed
gameWinBlue
)
//Colour reperensents player colour
//go:generate enumer -type=Colour
type Colour int
//Colour consts
const (
Red Colour = iota
Blue
NoColour
)
//Game represents general game state
type Game struct {
Board *Board
State GameState
}
//NewGame creates a new game and sets the state to lobby
func NewGame() *Game {
return &Game{
Board: NewBoard(8),
State: gameLobby,
}
}
func (g *Game) String() string {
var board string
for i := 0; i < g.Board.size; i++ {
board = board + "|"
for j := 0; j < g.Board.size; j++ {
board = board + g.Board.board[i][j].String()
}
board = board + "|\n"
}
return fmt.Sprintf("Status: %v\n%v", g.State, board)
}
//Parse a given RawCommand, return a ParsedCommand or an error
func (g *Game) Parse(p Player, cmd *RawCommand) (*ParsedCommand, error) {
if g.State != gameTurnRed && g.State != gameTurnBlue {
return nil, errors.New("game has not started")
}
if !g.Board.validatePoint(cmd.srcX, cmd.srcY) || !g.Board.validatePoint(cmd.dstX, cmd.dstY) {
return nil, errors.New("invalid location in command")
}
if (p.Colour() == Red && g.State != gameTurnRed) || (p.Colour() == Blue && g.State != gameTurnBlue) {
return nil, errors.New("not your turn")
}
start, err := g.Board.GetPiece(cmd.srcX, cmd.srcY)
if err != nil {
return nil, err
}
if cmd.act != "-" && cmd.act != "x" {
return nil, errors.New("invalid command action")
}
end, err := g.Board.GetPiece(cmd.dstX, cmd.dstY)
if err != nil {
return nil, err
}
if start == nil {
return nil, errors.New("empty start piece")
}
startRank := start.Rank.String()
endRank := ""
if end != nil {
endRank = end.Rank.String()
}
return &ParsedCommand{cmd.srcX, cmd.srcY, startRank, cmd.dstX, cmd.dstY, endRank, cmd.act}, nil
}
//Mutate the game state given a ParsedCommand
func (g *Game) Mutate(cmd *ParsedCommand) (bool, error) {
var res bool
var err error
switch cmd.act {
case "-":
res, err = g.move(cmd.srcX, cmd.srcY, cmd.dstX, cmd.dstY)
case "x":
res, err = g.strike(cmd.srcX, cmd.srcY, cmd.dstX, cmd.dstY)
default:
return false, errors.New("invalid mutate action")
}
if err != nil {
return false, err
}
if res {
if g.State == gameTurnRed {
g.State = gameTurnBlue
} else if g.State == gameTurnBlue {
g.State = gameTurnRed
} else {
return false, errors.New("bad game state")
}
}
return res, nil
}
//SetupPiece adds a piece to the board during setup
func (g *Game) SetupPiece(x, y int, p *Piece) (bool, error) {
if g.State != gameSetup {
return false, errors.New("Trying to setup piece when not in setup")
}
if p == nil {
return false, errors.New("Tried to setup a nil piece")
}
if !g.Board.validatePoint(x, y) {
return false, errors.New("Invalid location")
}
if p.Owner != g.Board.GetColor(x, y) {
return false, fmt.Errorf("Can't setup piece on enemy board: %v != %v", p.Owner, g.Board.GetColor(x, y))
}
return g.Board.Place(x, y, p)
}
//Start start the game
func (g *Game) Start() bool {
if g.State == gameSetup {
g.State = gameTurnRed
return true
}
return false
}
//Setup puts the game in setup mode
func (g *Game) Setup() bool {
if g.State == gameLobby {
g.State = gameSetup
return true
}
return false
}
func (g *Game) move(x, y, s, t int) (bool, error) {
startPiece, err := g.Board.GetPiece(x, y)
if err != nil {
return false, err
}
endPiece, err := g.Board.GetPiece(s, t)
if err != nil {
return false, err
}
if startPiece == nil {
return false, errors.New("invalid start piece for move")
}
if endPiece != nil {
return false, nil
}
//attempt to remove starting piece first
err = g.Board.Remove(x, y)
if err != nil {
return false, err
}
// then place piece in new location
res, err := g.Board.Place(s, t, startPiece)
if err != nil {
return false, err
}
return res, nil
}
func (g *Game) strike(x, y, s, t int) (bool, error) {
startPiece, err := g.Board.GetPiece(x, y)
if err != nil {
return false, err
}
endPiece, err := g.Board.GetPiece(s, t)
if err != nil {
return false, err
}
if startPiece == nil {
return false, errors.New("invalid start piece for strike")
}
if endPiece == nil {
return false, nil
}
distance := math.Sqrt(math.Pow(float64(s-x), 2) + math.Pow(float64(t-y), 2))
if distance > 1 && startPiece.Rank != 1 {
//trying to attack a piece more then 1 square away when not a scout
return false, nil
}
//bombs can't attack
if startPiece.Rank == 12 {
return false, nil
}
r, err := g.combat(startPiece, endPiece)
if err != nil {
return false, err
}
switch r {
case -1:
//startPiece lost
err = g.Board.Remove(x, y)
if err != nil {
return true, err
}
case 0:
//tie
err = g.Board.Remove(x, y)
err2 := g.Board.Remove(s, t)
if err != nil || err2 != nil {
return true, fmt.Errorf("Errors: %v %v", err, err2)
}
case 1:
//endPiece lost
err := g.Board.Remove(s, t)
if err != nil {
return true, err
}
//scouts replace the piece that was destroyed
if startPiece.Rank == Scout {
err = g.Board.Remove(x, y)
if err != nil {
return true, err
}
res, err := g.Board.Place(s, t, startPiece)
if err != nil {
return true, err
}
if !res {
return false, errors.New("Combat was valid but somehow placing the new piece was not")
}
}
}
return true, nil
}
func (g *Game) combat(atk, def *Piece) (int, error) {
if atk == nil || def == nil {
return 0, errors.New("invalid attacker or defender")
}
if def.Hidden && def.Rank == Unknown {
return 0, errors.New("defender has not been revealed to us")
}
//reveal attacker
atk.Hidden = false
//handle special cases first
//miner hitting bomb
if atk.Rank == Miner && def.Rank == Bomb {
return 1, nil
}
//anyone else hitting bomb
if def.Rank == Bomb {
return -1, nil
}
//spy hitting marshal
if atk.Rank == Spy && def.Rank == Marshal {
return 1, nil
}
//normal cases
if atk.Rank > def.Rank {
return 1, nil
} else if atk.Rank == def.Rank {
return 0, nil
} else {
return -1, nil
}
}