Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Snail mode #98

Merged
merged 8 commits into from
Aug 19, 2022
Merged
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions maps/snail_mode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
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),
}
}

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

// hash takes in a Point and the height of the board, and returns a unique integer to be used in maps
// Convert the given point into an integer that can be used as a map key
// This is takes the X and Y coordinates and combines them into a single integer
// that uniquely identifies the point given the current board size
func hash(point rules.Point, height int) int {
return point.X + point.Y*height
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably not worth changing, but you can actually use a rules.Point as a map key! I think a few other maps use this to look up tables of locations for a given WxH board.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know that was possible to use a struct as a map key - thanks!


// dehash converts an integer back onto a Point.
// Storing points as int allows using them as a map key.
func dehash(hash int, height int) rules.Point {
x := hash % height
y := hash / height

return rules.Point{X: x, Y: y}
}

// 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[int]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[hash(hazard, lastBoardState.Height)]++
}
}

// 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 hazardHash, count := range hazardCounts {
hazard := dehash(hazardHash, lastBoardState.Height)

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
}