From fbbec6a7f50b55e4bc14eb63a1d4fa85b2e47af4 Mon Sep 17 00:00:00 2001 From: Josh LaFayette Date: Fri, 19 Aug 2022 10:23:33 -0700 Subject: [PATCH] Snail mode (#98) * 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 --- maps/snail_mode.go | 179 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 maps/snail_mode.go diff --git a/maps/snail_mode.go b/maps/snail_mode.go new file mode 100644 index 0000000..5a843ae --- /dev/null +++ b/maps/snail_mode.go @@ -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 +}