From bd149c3d5d05210e7f5c31d1a3fcc5f0ce677d09 Mon Sep 17 00:00:00 2001 From: Steve Date: Fri, 18 Feb 2022 18:36:18 -0500 Subject: [PATCH] initial skeleton --- board.go | 86 +++++++++++++++++++ command.go | 63 ++++++++++++++ game.go | 242 +++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 59 +++++++++++++ piece.go | 34 ++++++++ player.go | 35 ++++++++ tile.go | 103 +++++++++++++++++++++++ 7 files changed, 622 insertions(+) create mode 100644 board.go create mode 100644 command.go create mode 100644 game.go create mode 100644 main.go create mode 100644 piece.go create mode 100644 player.go create mode 100644 tile.go diff --git a/board.go b/board.go new file mode 100644 index 0000000..3c0d5f3 --- /dev/null +++ b/board.go @@ -0,0 +1,86 @@ +package main + +import "errors" + +//Board represents main game board +type Board struct { + board [][]*Tile + size int +} + +//NewBoard creates a new board instance +func NewBoard(size int) *Board { + b := make([][]*Tile, size) + var colour Colour + for i := 0; i < size; i++ { + b[i] = make([]*Tile, size) + if i < size/2 { + colour = Blue + } else { + colour = Red + } + for j := 0; j < size; j++ { + b[i][j] = &Tile{i, j, true, nil, colour} + } + } + return &Board{ + board: b, + size: size, + } +} + +func (b *Board) validatePoint(x, y int) bool { + if x < 0 || x >= len(b.board) { + return false + } + if y < 0 || y >= len(b.board) { + return false + } + return true +} + +//GetPiece returns the piece at a given location +func (b *Board) GetPiece(x, y int) (*Piece, error) { + if !b.validatePoint(x, y) { + return nil, errors.New("GetPiece invalid location") + } + return b.board[x][y].Piece(), nil +} + +//Place a piece on the board; returns false if a piece is already there +func (b *Board) Place(x, y int, p *Piece) (bool, error) { + if !b.validatePoint(x, y) { + return false, errors.New("Place invalid location") + } + if b.board[x][y].Piece() != nil { + return false, nil + } + err := b.board[x][y].Place(p) + if err != nil { + return false, err + } + return true, nil +} + +//Remove a piece from a tile +func (b *Board) Remove(x, y int) error { + if !b.validatePoint(x, y) { + return errors.New("Remove invalid location") + } + b.board[x][y].Remove() + return nil +} + +//GetColor returns color of tile +func (b *Board) GetColor(x, y int) Colour { + return b.board[x][y].Colour() +} + +//AddRiver puts a river tile at specified location +func (b *Board) AddRiver(x, y int) (bool, error) { + if !b.validatePoint(x, y) { + return false, errors.New("River invalid location") + } + b.board[x][y].AddTerrain() + return true, nil +} diff --git a/command.go b/command.go new file mode 100644 index 0000000..2b42705 --- /dev/null +++ b/command.go @@ -0,0 +1,63 @@ +package main + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" +) + +var cmdRegxp = regexp.MustCompile("([a-zA-Z])([1-9])(x|-)([a-zA-Z])([1-9])") +var ranks = "ABCDEFHI" + +//RawCommand is a game command, converted from algebraic notation +type RawCommand struct { + srcX int + srcY int + dstX int + dstY int + act string +} + +//NewRawCommand creates a RawCommand struct from a algebraic notation string +func NewRawCommand(cmd string) (*RawCommand, error) { + res := cmdRegxp.FindAllString(cmd, -1) + if res == nil { + return nil, errors.New("error creating command from string") + } + sx := strings.Index(ranks, res[0]) + dx := strings.Index(ranks, res[3]) + sy, err := strconv.Atoi(res[1]) + if err != nil { + return nil, err + } + dy, err := strconv.Atoi(res[4]) + if err != nil { + return nil, err + } + return &RawCommand{sx, sy, dx, dy, res[2]}, nil +} + +func (c *RawCommand) String() string { + return fmt.Sprintf("%s%d%s%s%d", string(ranks[c.srcX]), c.srcY, c.act, string(ranks[c.dstX]), c.dstY) +} + +//ParsedCommand is a game command after being run through the engine +type ParsedCommand struct { + srcX int + srcY int + srcRank string + dstX int + dstY int + dstRank string + act string +} + +func (c *ParsedCommand) String() string { + if c.dstRank != "" { + return fmt.Sprintf("%s %s%d %s %s %s%d", c.srcRank, string(ranks[c.srcX]), c.srcY, c.act, c.dstRank, string(ranks[c.dstX]), c.dstY) + } + return fmt.Sprintf("%s %s%d %s %s %s%d", c.srcRank, string(ranks[c.srcX]), c.srcY, c.act, "empty", string(ranks[c.dstX]), c.dstY) + +} diff --git a/game.go b/game.go new file mode 100644 index 0000000..e93d5a1 --- /dev/null +++ b/game.go @@ -0,0 +1,242 @@ +package main + +import ( + "errors" + "fmt" + "math" +) + +//GameState is an int representing the current state the game's in +type GameState int + +const ( + gameLobby GameState = iota + gameSetup + gameTurnRed + gameTurnBlue + gameWinRed + gameWinBlue +) + +//Colour reperensents player 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 true, 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 !g.board.validatePoint(x, y) { + return false, errors.New("Invalid location") + } + if p.Owner != g.board.GetColor(x, y) { + return false, errors.New("Can't setup piece on enemy board") + } + return g.board.Place(x, y, p) +} + +//Start start the game +func (g *Game) Start() { + g.state = gameTurnRed +} + +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 + } + res, err := g.board.Place(s, t, startPiece) + if err != nil { + return false, err + } + if res { + err = g.board.Remove(x, y) + if err != nil { + return false, err + } + } + return true, 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 + g.board.Remove(x, y) + case 0: + //tie + g.board.Remove(x, y) + g.board.Remove(s, t) + case 1: + //endPiece lost + g.board.Remove(s, t) + //scouts replace the piece that was destroyed + if startPiece.Rank == 1 { + g.board.Remove(x, y) + g.board.Place(s, t, startPiece) + } + } + 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 atk.Hidden { + return 0, errors.New("trying to attack with a piece we know nothing about?") + } + if def.Hidden { + return 0, errors.New("defender has not been revealed to us") + } + //handle special cases first + //miner hitting bomb + if atk.Rank == 3 && def.Rank == 12 { + return 1, nil + } + //spy hitting marshal + if atk.Rank == 0 && def.Rank == 10 { + 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 + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..cb52098 --- /dev/null +++ b/main.go @@ -0,0 +1,59 @@ +package main + +import "fmt" + +func main() { + red := NewDummyPlayer(Red) + blue := NewDummyPlayer(Blue) + game := NewGame() + game.state = gameSetup + addpiece(game, 0, Blue, 0, 0) + addpiece(game, 1, Blue, 0, 1) + addpiece(game, 2, Blue, 1, 2) + addpiece(game, 3, Blue, 2, 4) + addpiece(game, 3, Blue, 1, 3) + addpiece(game, 4, Blue, 2, 6) + addpiece(game, 5, Blue, 3, 3) + addpiece(game, 9, Blue, 0, 6) + addpiece(game, 9, Blue, 0, 7) + addpiece(game, 0, Blue, 3, 5) + addriver(game, 3, 2) + addriver(game, 4, 2) + addriver(game, 3, 6) + addriver(game, 4, 6) + addpiece(game, 0, Red, 4, 0) + addpiece(game, 1, Red, 4, 1) + addpiece(game, 2, Red, 5, 2) + addpiece(game, 3, Red, 6, 4) + addpiece(game, 3, Red, 5, 3) + addpiece(game, 4, Red, 6, 6) + addpiece(game, 5, Red, 7, 3) + addpiece(game, 9, Red, 4, 5) + addpiece(game, 9, Red, 4, 7) + addpiece(game, 0, Red, 7, 5) + + fmt.Printf("%v %v\n%v\n", red, blue, game) + game.Start() + + return +} + +func addpiece(game *Game, rank int, c Colour, x int, y int) { + res, err := game.SetupPiece(x, y, NewPiece(rank, c)) + if err != nil { + panic(err) + } + if !res { + panic("can't setup") + } +} + +func addriver(game *Game, x int, y int) { + res, err := game.board.AddRiver(x, y) + if err != nil { + panic(err) + } + if !res { + panic("can't river") + } +} diff --git a/piece.go b/piece.go new file mode 100644 index 0000000..2c53a81 --- /dev/null +++ b/piece.go @@ -0,0 +1,34 @@ +package main + +import "strconv" + +//Rank represents the rank of a piece +type Rank int + +func (r Rank) String() string { + return strconv.Itoa(int(r)) +} + +//Piece :game piece +type Piece struct { + Rank Rank + Owner Colour + Hidden bool +} + +//NewPiece creates a new piece +func NewPiece(r int, o Colour) *Piece { + return &Piece{ + Rank: Rank(r), + Owner: o, + Hidden: false, + } +} + +//NewHiddenPiece creates a new hidden piece +func NewHiddenPiece(o Colour) *Piece { + return &Piece{ + Owner: o, + Hidden: true, + } +} diff --git a/player.go b/player.go new file mode 100644 index 0000000..85988bf --- /dev/null +++ b/player.go @@ -0,0 +1,35 @@ +package main + +//Player represents a player of the game +type Player interface { + ID() int + Colour() Colour +} + +//DummyPlayer is the simplest implementation of a player +type DummyPlayer struct { + id Colour +} + +//ID returns player ID +func (d *DummyPlayer) ID() int { + return int(d.id) +} + +//Colour returns player colour (same as ID) +func (d *DummyPlayer) Colour() Colour { + return d.id +} + +//NewDummyPlayer creates a new player +func NewDummyPlayer(i Colour) *DummyPlayer { + return &DummyPlayer{i} +} + +func (d *DummyPlayer) String() string { + if d.id == Red { + return "Red Player" + } else { + return "Blue Player" + } +} diff --git a/tile.go b/tile.go new file mode 100644 index 0000000..654fad8 --- /dev/null +++ b/tile.go @@ -0,0 +1,103 @@ +package main + +import ( + "errors" + "fmt" +) + +//Tile represents a spot on the board +type Tile struct { + x int + y int + passable bool + entity *Piece + colour Colour +} + +func (t *Tile) String() string { + icon := "?" + if !t.passable { + icon = "X" + } else if t.entity != nil { + if !t.entity.Hidden { + icon = t.entity.Rank.String() + } else { + icon = "O" + } + } else { + icon = " " + } + return fmt.Sprintf("|%v|", icon) +} + +//Place a piece on the tile +func (t *Tile) Place(p *Piece) error { + if p == nil { + return errors.New("tried to place nil piece") + } + if !t.passable { + return errors.New("tile not passable") + } + if t.entity != nil { + return errors.New("entity already present") + } + t.entity = p + return nil +} + +//Remove the piece from a tile +func (t *Tile) Remove() { + t.entity = nil +} + +//Empty returns true if tile is empty +func (t *Tile) Empty() bool { + return t.entity == nil +} + +//Team returns the team of the piece currently occupying it, or 0 +func (t *Tile) Team() Colour { + if t.entity != nil { + return t.entity.Owner + } + return NoColour +} + +//Occupied returns true if the tile is currently occupied +func (t *Tile) Occupied() bool { + return t.entity != nil +} + +//Piece returns the current Piece if any +func (t *Tile) Piece() *Piece { + return t.entity +} + +//Passable returns if the tile is passable +func (t *Tile) Passable() bool { + return t.passable +} + +//Colour returns the tile colour +func (t *Tile) Colour() Colour { + return t.colour +} + +//X returns tile X position +func (t *Tile) X() int { + return t.x +} + +//Y returns tile y position +func (t *Tile) Y() int { + return t.y +} + +//AddTerrain adds specified terrain to position +func (t *Tile) AddTerrain() bool { + if t.entity != nil { + return false + } + t.passable = false + return true +}