Skip to content

Commit

Permalink
Revert SnailMode to not use PreUpdate (#121)
Browse files Browse the repository at this point in the history
In the fall me and Jonathan worked on an update to the Engine
specifically with SnailMode in mind, and refactored SnailMode to use
this new `PreUpdate` method

This allowed us to not have to use off-board hazards as state!

However we want to use Snail Mode for our first Community Tournament!
And the web engine doesn't support PreUpdate yet, so we are reverting
Snail Mode to its old Pre-PreUpdate version :lol:
  • Loading branch information
coreyja authored Feb 10, 2023
1 parent 932f541 commit ef9c766
Showing 1 changed file with 79 additions and 35 deletions.
114 changes: 79 additions & 35 deletions maps/snail_mode.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,20 @@ import (
"github.com/BattlesnakeOfficial/rules"
)

type SnailModeMap struct {
lastTailPositions map[rules.Point]int // local state is preserved during the turn
}
type SnailModeMap struct{}

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

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

// Meta returns the non-functional metadata about this map.
func (m *SnailModeMap) Meta() Metadata {
func (m SnailModeMap) Meta() Metadata {
return Metadata{
Name: "Snail Mode",
Description: "Snakes leave behind a trail of hazards",
Expand All @@ -33,7 +31,7 @@ func (m *SnailModeMap) Meta() Metadata {
}

// 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 {
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) {
Expand All @@ -59,6 +57,23 @@ func (m *SnailModeMap) SetupBoard(initialBoardState *rules.BoardState, settings
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]
Expand All @@ -71,27 +86,15 @@ func isEliminated(s *rules.Snake) bool {
return s.EliminatedCause != rules.NotEliminated
}

// PreUpdateBoard stores the tail position of each snake in memory, to be
// able to place hazards there after the snakes move.
func (m *SnailModeMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
m.lastTailPositions = make(map[rules.Point]int)
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
}
m.lastTailPositions[snake.Body[len(snake.Body)-1]] = len(snake.Body)
}
func (m SnailModeMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
return nil
}

// PostUpdateBoard does the work of placing the hazards along the 'snail tail' of snakes
// This also handles removing one hazards from the current stacks so the hazards tails fade as the snake moves away.
func (m *SnailModeMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
// 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) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
if err != nil {
return err
Expand All @@ -101,38 +104,79 @@ func (m *SnailModeMap) PostUpdateBoard(lastBoardState *rules.BoardState, setting
// 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 {
hazardCounts[hazard]++

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

// Place a new stack of hazards where each snake's tail used to be
NewHazardLoop:
for location, count := range m.lastTailPositions {
// 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 location.X == head.X && location.Y == head.Y {
// 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.
continue NewHazardLoop
if p.X == head.X && p.Y == head.Y {
isHead = true
break
}
}
for i := 0; i < count; i++ {
editor.AddHazard(location)
if isHead {
continue
}

editor.AddHazard(p)
}

return nil
Expand Down

0 comments on commit ef9c766

Please sign in to comment.