diff --git a/standard.go b/standard.go index 23672f5..07bf73b 100644 --- a/standard.go +++ b/standard.go @@ -1,13 +1,18 @@ package rulesets import ( -// log "github.com/sirupsen/logrus" + "errors" + "math/rand" ) type StandardRuleset struct{} const ( - SNAKE_MAX_HEALTH = 100 + BOARD_SIZE_SMALL = 7 + BOARD_SIZE_MEDIUM = 11 + BOARD_SIZE_LARGE = 19 + FOOD_SPAWN_CHANCE = 0.1 + SNAKE_MAX_HEALTH = 100 // bvanvugt - TODO: Just return formatted strings instead of codes? ELIMINATED_COLLISION = "snake-collision" ELIMINATED_SELF_COLLISION = "snake-self-collision" @@ -16,50 +21,158 @@ const ( ELIMINATED_OUT_OF_BOUNDS = "wall-collision" ) -func (r *StandardRuleset) ResolveMoves(g *Game, prevGameState *GameState, moves []*SnakeMove) (nextGameState *GameState, err error) { - nextGameState = &GameState{ - Snakes: prevGameState.Snakes, - Food: prevGameState.Food, +func (r *StandardRuleset) CreateInitialBoard(width int32, height int32, snakeIDs []string) (*BoardState, error) { + var err error + + 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, + } - // TODO: LOG? - err = r.moveSnakes(nextGameState, moves) + // Place Snakes + if r.isKnownBoardSize(initialBoardState) { + err = r.placeSnakesFixed(initialBoardState) + } else { + err = r.placeSnakesRandomly(initialBoardState) + } if err != nil { return nil, err } - // TODO: LOG? - err = r.reduceSnakeHealth(nextGameState) + // Place Food + err = r.placeInitialFood(initialBoardState) if err != nil { return nil, err } - // TODO: LOG? - // bvanvugt: we specifically want this to happen before elimination - // so that head-to-head collisions on food still remove the food. - err = r.feedSnakes(nextGameState) - if err != nil { - return nil, err - } - - // TODO: LOG? - err = r.eliminateSnakes(nextGameState, g.Width, g.Height) - if err != nil { - return nil, err - } - - // TODO: LOG? - err = r.maybeSpawnFood(nextGameState) - if err != nil { - return nil, err - } - - return nextGameState, nil + return initialBoardState, nil } -func (r *StandardRuleset) moveSnakes(gs *GameState, moves []*SnakeMove) error { +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? + err := r.moveSnakes(nextState, moves) + if err != nil { + return nil, err + } + + // TODO: LOG? + err = r.reduceSnakeHealth(nextState) + if err != nil { + return nil, err + } + + // TODO + // bvanvugt: we specifically want this to happen before elimination + // so that head-to-head collisions on food still remove the food. + // 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 { + return nil, err + } + + // TODO: LOG? + err = r.maybeSpawnFood(nextState, 1) + if err != nil { + return nil, err + } + + // TODO: LOG? + err = r.eliminateSnakes(nextState) + if err != nil { + return nil, err + } + + return nextState, nil +} + +func (r *StandardRuleset) moveSnakes(b *BoardState, moves []*SnakeMove) error { for _, move := range moves { var newHead = &Point{} switch move.Move { @@ -98,21 +211,21 @@ func (r *StandardRuleset) moveSnakes(gs *GameState, moves []*SnakeMove) error { return nil } -func (r *StandardRuleset) reduceSnakeHealth(gs *GameState) error { - for _, snake := range gs.Snakes { +func (r *StandardRuleset) reduceSnakeHealth(b *BoardState) error { + for _, snake := range b.Snakes { snake.Health = snake.Health - 1 } return nil } -func (r *StandardRuleset) eliminateSnakes(gs *GameState, boardWidth int32, boardHeight int32) error { - for _, snake := range gs.Snakes { +func (r *StandardRuleset) eliminateSnakes(b *BoardState) error { + for _, snake := range b.Snakes { if r.snakeHasStarved(snake) { 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 } else { - for _, other := range gs.Snakes { + for _, other := range b.Snakes { if r.snakeHasBodyCollided(snake, other) { if snake.ID == other.ID { snake.EliminatedCause = ELIMINATED_SELF_COLLISION @@ -165,13 +278,13 @@ func (r *StandardRuleset) snakeHasLostHeadToHead(s *Snake, other *Snake) bool { return false } -func (r *StandardRuleset) feedSnakes(gs *GameState) error { +func (r *StandardRuleset) feedSnakes(b *BoardState) error { var newFood []*Point var tail *Point - for _, food := range gs.Food { + for _, food := range b.Food { 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 { foodHasBeenEaten = true // 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}) } } - // Persist food to next GameState if not eaten + // Persist food to next BoardState if not eaten if !foodHasBeenEaten { newFood = append(newFood, food) } } - gs.Food = newFood + b.Food = newFood return nil } -func (r *StandardRuleset) maybeSpawnFood(gs *GameState) error { - // TODO +func (r *StandardRuleset) maybeSpawnFood(b *BoardState, n int) error { + if rand.Float32() <= FOOD_SPAWN_CHANCE { + r.spawnFood(b, n) + } 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 +}