initial skeleton

This commit is contained in:
stryan 2022-02-18 18:36:18 -05:00
commit bd149c3d5d
7 changed files with 622 additions and 0 deletions

86
board.go Normal file
View File

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

63
command.go Normal file
View File

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

242
game.go Normal file
View File

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

59
main.go Normal file
View File

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

34
piece.go Normal file
View File

@ -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,
}
}

35
player.go Normal file
View File

@ -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"
}
}

103
tile.go Normal file
View File

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