Skip to content

Commit

Permalink
Snail mode (#98)
Browse files Browse the repository at this point in the history
* Add snail-mode map

* snail-mode: cap max hazards to 7

- Ensure that no more than 7 hazards are added to a square. This
  fixes a bug where some squares were getting way too many hazards
  applied to them.  There must be some other bug at work here as
  well.
- Change author names to be github usernames instead of first names

* snail-mode: fix bug with eliminated snakes

- Ensure that hazard snail-trail is not added for eliminated snakes

* Update from Stream July 31

Added comments to most functions and important bits of code

Also changed the map so that instead of a fixed number of 7 hazards,
we add hazards equal to the length of the snake.

* snail-mode: add TAG_EXPERIMENTAL and TAG_HAZARD_PLACEMENT

* snail-mode: use Point as map key

Co-authored-by: Corey Alexander <[email protected]>
  • Loading branch information
jlafayette and coreyja committed Aug 19, 2022
1 parent f82cfe5 commit fbbec6a
Showing 1 changed file with 179 additions and 0 deletions.
179 changes: 179 additions & 0 deletions maps/snail_mode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package maps

import (
"github.com/BattlesnakeOfficial/rules"
)

type SnailModeMap struct{}

// init registers this map in the global registry.
func init() {
globalRegistry.RegisterMap("snail_mode", SnailModeMap{})
}

// ID returns a unique identifier for this map.
func (m SnailModeMap) ID() string {
return "snail_mode"
}

// Meta returns the non-functional metadata about this map.
func (m SnailModeMap) Meta() Metadata {
return Metadata{
Name: "Snail Mode",
Description: "Snakes leave behind a trail of hazards",
Author: "coreyja and jlafayette",
Version: 1,
MinPlayers: 1,
MaxPlayers: 16,
BoardSizes: OddSizes(rules.BoardSizeSmall, rules.BoardSizeXXLarge),
Tags: []string{TAG_EXPERIMENTAL, TAG_HAZARD_PLACEMENT},
}
}

// SetupBoard here is pretty 'standard' and doesn't do any special setup for this game mode
func (m SnailModeMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
rand := settings.GetRand(0)

if len(initialBoardState.Snakes) > int(m.Meta().MaxPlayers) {
return rules.ErrorTooManySnakes
}

snakeIDs := make([]string, 0, len(initialBoardState.Snakes))
for _, snake := range initialBoardState.Snakes {
snakeIDs = append(snakeIDs, snake.ID)
}

tempBoardState := rules.NewBoardState(initialBoardState.Width, initialBoardState.Height)
err := rules.PlaceSnakesAutomatically(rand, tempBoardState, snakeIDs)
if err != nil {
return err
}

// Copy snakes from temp board state
for _, snake := range tempBoardState.Snakes {
editor.PlaceSnake(snake.ID, snake.Body, snake.Health)
}

return nil
}

// storeTailLocation returns an offboard point that corresponds to the given point.
// This is useful for storing state that can be accessed next turn.
func storeTailLocation(point rules.Point, height int) rules.Point {
return rules.Point{X: point.X, Y: point.Y + height}
}

// getPrevTailLocation returns the onboard point that corresponds to an offboard point.
// This is useful for restoring state that was stored last turn.
func getPrevTailLocation(point rules.Point, height int) rules.Point {
return rules.Point{X: point.X, Y: point.Y - height}
}

// outOfBounds determines if the given point is out of bounds for the current board size
func outOfBounds(p rules.Point, w, h int) bool {
return p.X < 0 || p.Y < 0 || p.X >= w || p.Y >= h
}

// doubleTail determine if the snake has a double stacked tail currently
func doubleTail(snake *rules.Snake) bool {
almostTail := snake.Body[len(snake.Body)-2]
tail := snake.Body[len(snake.Body)-1]
return almostTail.X == tail.X && almostTail.Y == tail.Y
}

// isEliminated determines if the snake is already eliminated
func isEliminated(s *rules.Snake) bool {
return s.EliminatedCause != rules.NotEliminated
}

// UpdateBoard does the work of placing the hazards along the 'snail tail' of snakes
// This is responsible for saving the current tail location off the board
// and restoring the previous tail position. This also handles removing one hazards from
// the current stacks so the hazards tails fade as the snake moves away.
func (m SnailModeMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
if err != nil {
return err
}

// This map decrements the stack of hazards on a point each turn, so they
// need to be cleared first.
editor.ClearHazards()

// This is a list of all the hazards we want to add for the previous tails
// These were stored off board in the previous turn as a way to save state
// When we add the locations to this list we have already converted the off-board
// points to on-board points
tailLocations := make([]rules.Point, 0, len(lastBoardState.Snakes))

// Count the number of hazards for a given position
// Add non-double tail locations to a slice
hazardCounts := map[rules.Point]int{}
for _, hazard := range lastBoardState.Hazards {

// discard out of bound
if outOfBounds(hazard, lastBoardState.Width, lastBoardState.Height) {
onBoardTail := getPrevTailLocation(hazard, lastBoardState.Height)
tailLocations = append(tailLocations, onBoardTail)
} else {
hazardCounts[hazard]++
}
}

// Add back existing hazards, but with a stack of 1 less than before.
// This has the effect of making the snail-trail disappear over time.
for hazard, count := range hazardCounts {

for i := 0; i < count-1; i++ {
editor.AddHazard(hazard)
}
}

// Store a stack of hazards for the tail of each snake. This is stored out
// of bounds and then applied on the next turn. The stack count is equal
// the lenght of the snake.
for _, snake := range lastBoardState.Snakes {
if isEliminated(&snake) {
continue
}

// Double tail means that the tail will stay on the same square for more
// than one turn, so we don't want to spawn hazards
if doubleTail(&snake) {
continue
}

tail := snake.Body[len(snake.Body)-1]
offBoardTail := storeTailLocation(tail, lastBoardState.Height)
for i := 0; i < len(snake.Body); i++ {
editor.AddHazard(offBoardTail)
}
}

// Read offboard tails and move them to the board. The offboard tails are
// stacked based on the length of the snake
for _, p := range tailLocations {

// Skip position if a snakes head occupies it.
// Otherwise hazard shows up in the viewer on top of a snake head, but
// does not damage the snake, which is visually confusing.
isHead := false
for _, snake := range lastBoardState.Snakes {
if isEliminated(&snake) {
continue
}
head := snake.Body[0]
if p.X == head.X && p.Y == head.Y {
isHead = true
break
}
}
if isHead {
continue
}

editor.AddHazard(p)
}

return nil
}

0 comments on commit fbbec6a

Please sign in to comment.