Update standard.go

This commit is contained in:
Brad Van Vugt 2020-01-01 17:23:01 -08:00 committed by GitHub
parent d477201b1e
commit 29a8fd1dd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -1,12 +1,17 @@
package rulesets package rulesets
import ( import (
// log "github.com/sirupsen/logrus" "errors"
"math/rand"
) )
type StandardRuleset struct{} type StandardRuleset struct{}
const ( const (
BOARD_SIZE_SMALL = 7
BOARD_SIZE_MEDIUM = 11
BOARD_SIZE_LARGE = 19
FOOD_SPAWN_CHANCE = 0.1
SNAKE_MAX_HEALTH = 100 SNAKE_MAX_HEALTH = 100
// bvanvugt - TODO: Just return formatted strings instead of codes? // bvanvugt - TODO: Just return formatted strings instead of codes?
ELIMINATED_COLLISION = "snake-collision" ELIMINATED_COLLISION = "snake-collision"
@ -16,50 +21,158 @@ const (
ELIMINATED_OUT_OF_BOUNDS = "wall-collision" ELIMINATED_OUT_OF_BOUNDS = "wall-collision"
) )
func (r *StandardRuleset) ResolveMoves(g *Game, prevGameState *GameState, moves []*SnakeMove) (nextGameState *GameState, err error) { func (r *StandardRuleset) CreateInitialBoard(width int32, height int32, snakeIDs []string) (*BoardState, error) {
nextGameState = &GameState{ var err error
Snakes: prevGameState.Snakes,
Food: prevGameState.Food, snakes := []*Snake{}
for _, id := range snakeIDs {
snakes = append(snakes,
&Snake{
ID: id,
Health: SNAKE_MAX_HEALTH,
},
)
} }
// TODO: Gut check the GameState? initialBoardState := &BoardState{
Height: height,
Width: width,
Snakes: snakes,
}
// Place Snakes
if r.isKnownBoardSize(initialBoardState) {
err = r.placeSnakesFixed(initialBoardState)
} else {
err = r.placeSnakesRandomly(initialBoardState)
}
if err != nil {
return nil, err
}
// Place Food
err = r.placeInitialFood(initialBoardState)
if err != nil {
return nil, err
}
return initialBoardState, nil
}
func (r *StandardRuleset) placeSnakesFixed(b *BoardState) error {
// Sanity check
if len(b.Snakes) >= 8 {
return errors.New("too many snakes for fixed start positions")
}
// Create start points
mn, md, mx := int32(1), (b.Width-1)/2, b.Width-2
startPoints := []Point{
{mn, mn},
{mn, md},
{mn, mx},
{md, mn},
{md, mx},
{mx, mn},
{mx, md},
{mx, mx},
}
// Randomly order them
rand.Shuffle(len(startPoints), func(i int, j int) {
startPoints[i], startPoints[j] = startPoints[j], startPoints[i]
})
// Assign to snakes in order given
for i, snake := range b.Snakes {
p := startPoints[i]
for j := 0; j < 3; j++ {
snake.Body = append(snake.Body, &Point{p.X, p.Y})
}
}
return nil
}
func (r *StandardRuleset) placeSnakesRandomly(b *BoardState) error {
for _, snake := range b.Snakes {
unoccupiedPoints := r.getUnoccupiedPoints(b)
p := unoccupiedPoints[rand.Intn(len(unoccupiedPoints))]
for j := 0; j < 3; j++ {
snake.Body = append(snake.Body, &Point{p.X, p.Y})
}
}
return nil
}
func (r *StandardRuleset) isKnownBoardSize(b *BoardState) bool {
if b.Height == BOARD_SIZE_SMALL && b.Width == BOARD_SIZE_SMALL {
return true
}
if b.Height == BOARD_SIZE_MEDIUM && b.Width == BOARD_SIZE_MEDIUM {
return true
}
if b.Height == BOARD_SIZE_LARGE && b.Width == BOARD_SIZE_LARGE {
return true
}
return false
}
func (r *StandardRuleset) placeInitialFood(b *BoardState) error {
r.spawnFood(b, len(b.Snakes))
return nil
}
func (r *StandardRuleset) ResolveMoves(prevState *BoardState, moves []*SnakeMove) (*BoardState, error) {
// TODO: DO NOT REFERENCE prevState directly!!!!
// we're technically altering both states
nextState := &BoardState{
Snakes: prevState.Snakes,
Food: prevState.Food,
}
// TODO: Gut check the BoardState?
// TODO: LOG? // TODO: LOG?
err = r.moveSnakes(nextGameState, moves) err := r.moveSnakes(nextState, moves)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO: LOG? // TODO: LOG?
err = r.reduceSnakeHealth(nextGameState) err = r.reduceSnakeHealth(nextState)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO: LOG? // TODO
// bvanvugt: we specifically want this to happen before elimination // bvanvugt: we specifically want this to happen before elimination
// so that head-to-head collisions on food still remove the food. // so that head-to-head collisions on food still remove the food.
err = r.feedSnakes(nextGameState) // It does create an artifact though, where head-to-head collisions
// of equal length actually show length + 1
// TODO: LOG?
err = r.feedSnakes(nextState)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO: LOG? // TODO: LOG?
err = r.eliminateSnakes(nextGameState, g.Width, g.Height) err = r.maybeSpawnFood(nextState, 1)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO: LOG? // TODO: LOG?
err = r.maybeSpawnFood(nextGameState) err = r.eliminateSnakes(nextState)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return nextGameState, nil return nextState, nil
} }
func (r *StandardRuleset) moveSnakes(gs *GameState, moves []*SnakeMove) error { func (r *StandardRuleset) moveSnakes(b *BoardState, moves []*SnakeMove) error {
for _, move := range moves { for _, move := range moves {
var newHead = &Point{} var newHead = &Point{}
switch move.Move { switch move.Move {
@ -98,21 +211,21 @@ func (r *StandardRuleset) moveSnakes(gs *GameState, moves []*SnakeMove) error {
return nil return nil
} }
func (r *StandardRuleset) reduceSnakeHealth(gs *GameState) error { func (r *StandardRuleset) reduceSnakeHealth(b *BoardState) error {
for _, snake := range gs.Snakes { for _, snake := range b.Snakes {
snake.Health = snake.Health - 1 snake.Health = snake.Health - 1
} }
return nil return nil
} }
func (r *StandardRuleset) eliminateSnakes(gs *GameState, boardWidth int32, boardHeight int32) error { func (r *StandardRuleset) eliminateSnakes(b *BoardState) error {
for _, snake := range gs.Snakes { for _, snake := range b.Snakes {
if r.snakeHasStarved(snake) { if r.snakeHasStarved(snake) {
snake.EliminatedCause = ELIMINATED_STARVATION snake.EliminatedCause = ELIMINATED_STARVATION
} else if r.snakeIsOutOfBounds(snake, boardWidth, boardHeight) { } else if r.snakeIsOutOfBounds(snake, b.Width, b.Height) {
snake.EliminatedCause = ELIMINATED_OUT_OF_BOUNDS snake.EliminatedCause = ELIMINATED_OUT_OF_BOUNDS
} else { } else {
for _, other := range gs.Snakes { for _, other := range b.Snakes {
if r.snakeHasBodyCollided(snake, other) { if r.snakeHasBodyCollided(snake, other) {
if snake.ID == other.ID { if snake.ID == other.ID {
snake.EliminatedCause = ELIMINATED_SELF_COLLISION snake.EliminatedCause = ELIMINATED_SELF_COLLISION
@ -165,13 +278,13 @@ func (r *StandardRuleset) snakeHasLostHeadToHead(s *Snake, other *Snake) bool {
return false return false
} }
func (r *StandardRuleset) feedSnakes(gs *GameState) error { func (r *StandardRuleset) feedSnakes(b *BoardState) error {
var newFood []*Point var newFood []*Point
var tail *Point var tail *Point
for _, food := range gs.Food { for _, food := range b.Food {
foodHasBeenEaten := false foodHasBeenEaten := false
for _, snake := range gs.Snakes { for _, snake := range b.Snakes {
if snake.Body[0].X == food.X && snake.Body[0].Y == food.Y { if snake.Body[0].X == food.X && snake.Body[0].Y == food.Y {
foodHasBeenEaten = true foodHasBeenEaten = true
// Update snake // Update snake
@ -180,17 +293,62 @@ func (r *StandardRuleset) feedSnakes(gs *GameState) error {
snake.Body = append(snake.Body, &Point{X: tail.X, Y: tail.Y}) snake.Body = append(snake.Body, &Point{X: tail.X, Y: tail.Y})
} }
} }
// Persist food to next GameState if not eaten // Persist food to next BoardState if not eaten
if !foodHasBeenEaten { if !foodHasBeenEaten {
newFood = append(newFood, food) newFood = append(newFood, food)
} }
} }
gs.Food = newFood b.Food = newFood
return nil return nil
} }
func (r *StandardRuleset) maybeSpawnFood(gs *GameState) error { func (r *StandardRuleset) maybeSpawnFood(b *BoardState, n int) error {
// TODO if rand.Float32() <= FOOD_SPAWN_CHANCE {
r.spawnFood(b, n)
}
return nil return nil
} }
func (r *StandardRuleset) spawnFood(b *BoardState, n int) {
for i := 0; i < n; i++ {
unoccupiedPoints := r.getUnoccupiedPoints(b)
if len(unoccupiedPoints) > 0 {
newFood := unoccupiedPoints[rand.Intn(len(unoccupiedPoints))]
b.Food = append(b.Food, newFood)
}
}
}
func (r *StandardRuleset) getUnoccupiedPoints(b *BoardState) []*Point {
pointIsOccupied := map[int32]map[int32]bool{}
for _, p := range b.Food {
if _, xExists := pointIsOccupied[p.X]; !xExists {
pointIsOccupied[p.X] = map[int32]bool{}
}
pointIsOccupied[p.X][p.Y] = true
}
for _, snake := range b.Snakes {
for _, p := range snake.Body {
if _, xExists := pointIsOccupied[p.X]; !xExists {
pointIsOccupied[p.X] = map[int32]bool{}
}
pointIsOccupied[p.X][p.Y] = true
}
}
unoccupiedPoints := []*Point{}
for x := int32(0); x < b.Width; x++ {
for y := int32(0); y < b.Height; y++ {
if _, xExists := pointIsOccupied[x]; xExists {
if isOccupied, yExists := pointIsOccupied[x][y]; yExists {
if isOccupied {
continue
}
}
}
unoccupiedPoints = append(unoccupiedPoints, &Point{X: x, Y: y})
}
}
return unoccupiedPoints
}