-
Notifications
You must be signed in to change notification settings - Fork 52
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
Snail mode #98
Changes from 5 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
cbdf52b
Add snail-mode map
jlafayette 4e771d3
snail-mode: cap max hazards to 7
jlafayette 68ccc0c
snail-mode: fix bug with eliminated snakes
jlafayette b61973d
Merge branch 'main' into snail-mode
coreyja ad38a12
Update from Stream July 31
coreyja c11249f
Merge branch 'BattlesnakeOfficial:main' into snail-mode
jlafayette c8f374d
snail-mode: add TAG_EXPERIMENTAL and TAG_HAZARD_PLACEMENT
jlafayette 2cd3a2d
snail-mode: use Point as map key
jlafayette File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
||
// 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 | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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!