DEV 953: Add basic maps support to CLI (#74)
* remove squad support and switch to using pipelines only in RulesBuilder * remove spawn_food.standard from legacy ruleset definitions * bugfix: Royale map generates Standard food * add maps support to CLI * add automated tests for all registered GameMap implementations * update README
This commit is contained in:
parent
3bd1e47bb4
commit
1adbc79168
27 changed files with 565 additions and 1371 deletions
|
|
@ -36,10 +36,10 @@ Flags:
|
||||||
-H, --height int Height of Board (default 11)
|
-H, --height int Height of Board (default 11)
|
||||||
-n, --name stringArray Name of Snake
|
-n, --name stringArray Name of Snake
|
||||||
-u, --url stringArray URL of Snake
|
-u, --url stringArray URL of Snake
|
||||||
-S, --squad stringArray Squad of Snake
|
|
||||||
-t, --timeout int Request Timeout (default 500)
|
-t, --timeout int Request Timeout (default 500)
|
||||||
-s, --sequential Use Sequential Processing
|
-s, --sequential Use Sequential Processing
|
||||||
-g, --gametype string Type of Game Rules (default "standard")
|
-g, --gametype string Type of Game Rules (default "standard")
|
||||||
|
-m, --map string Game map to use to populate the board (default "standard")
|
||||||
-v, --viewmap View the Map Each Turn
|
-v, --viewmap View the Map Each Turn
|
||||||
-c, --color Use color to draw the map
|
-c, --color Use color to draw the map
|
||||||
-r, --seed int Random Seed (default 1649588785026867900)
|
-r, --seed int Random Seed (default 1649588785026867900)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import (
|
||||||
|
|
||||||
"github.com/BattlesnakeOfficial/rules"
|
"github.com/BattlesnakeOfficial/rules"
|
||||||
"github.com/BattlesnakeOfficial/rules/client"
|
"github.com/BattlesnakeOfficial/rules/client"
|
||||||
|
"github.com/BattlesnakeOfficial/rules/maps"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -27,128 +28,148 @@ type SnakeState struct {
|
||||||
Name string
|
Name string
|
||||||
ID string
|
ID string
|
||||||
LastMove string
|
LastMove string
|
||||||
Squad string
|
|
||||||
Character rune
|
Character rune
|
||||||
Color string
|
Color string
|
||||||
Head string
|
Head string
|
||||||
Tail string
|
Tail string
|
||||||
}
|
}
|
||||||
|
|
||||||
var GameId string
|
type GameState struct {
|
||||||
var Turn int
|
// Options
|
||||||
var HttpClient http.Client
|
Width int
|
||||||
var Width int
|
Height int
|
||||||
var Height int
|
Names []string
|
||||||
var Names []string
|
URLs []string
|
||||||
var URLs []string
|
Timeout int
|
||||||
var Squads []string
|
TurnDuration int
|
||||||
var Timeout int
|
Sequential bool
|
||||||
var TurnDuration int
|
GameType string
|
||||||
var Sequential bool
|
MapName string
|
||||||
var GameType string
|
ViewMap bool
|
||||||
var ViewMap bool
|
UseColor bool
|
||||||
var UseColor bool
|
Seed int64
|
||||||
var Seed int64
|
TurnDelay int
|
||||||
var TurnDelay int
|
DebugRequests bool
|
||||||
var DebugRequests bool
|
Output string
|
||||||
var Output string
|
FoodSpawnChance int
|
||||||
|
MinimumFood int
|
||||||
|
HazardDamagePerTurn int
|
||||||
|
ShrinkEveryNTurns int
|
||||||
|
|
||||||
var FoodSpawnChance int
|
// Internal game state
|
||||||
var MinimumFood int
|
settings map[string]string
|
||||||
var HazardDamagePerTurn int
|
snakeStates map[string]SnakeState
|
||||||
var ShrinkEveryNTurns int
|
gameID string
|
||||||
|
httpClient http.Client
|
||||||
var defaultConfig = map[string]string{
|
ruleset rules.Ruleset
|
||||||
// default to standard ruleset
|
gameMap maps.GameMap
|
||||||
rules.ParamGameType: "standard",
|
|
||||||
// squad settings default to true (not zero value)
|
|
||||||
rules.ParamSharedElimination: "true",
|
|
||||||
rules.ParamSharedHealth: "true",
|
|
||||||
rules.ParamSharedLength: "true",
|
|
||||||
rules.ParamAllowBodyCollisions: "true",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var playCmd = &cobra.Command{
|
func NewPlayCommand() *cobra.Command {
|
||||||
|
gameState := &GameState{}
|
||||||
|
|
||||||
|
var playCmd = &cobra.Command{
|
||||||
Use: "play",
|
Use: "play",
|
||||||
Short: "Play a game of Battlesnake locally.",
|
Short: "Play a game of Battlesnake locally.",
|
||||||
Long: "Play a game of Battlesnake locally.",
|
Long: "Play a game of Battlesnake locally.",
|
||||||
Run: run,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
PreRun: playPreRun,
|
gameState.Run()
|
||||||
}
|
},
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
playCmd.Flags().IntVarP(&gameState.Width, "width", "W", 11, "Width of Board")
|
||||||
rootCmd.AddCommand(playCmd)
|
playCmd.Flags().IntVarP(&gameState.Height, "height", "H", 11, "Height of Board")
|
||||||
|
playCmd.Flags().StringArrayVarP(&gameState.Names, "name", "n", nil, "Name of Snake")
|
||||||
|
playCmd.Flags().StringArrayVarP(&gameState.URLs, "url", "u", nil, "URL of Snake")
|
||||||
|
playCmd.Flags().IntVarP(&gameState.Timeout, "timeout", "t", 500, "Request Timeout")
|
||||||
|
playCmd.Flags().BoolVarP(&gameState.Sequential, "sequential", "s", false, "Use Sequential Processing")
|
||||||
|
playCmd.Flags().StringVarP(&gameState.GameType, "gametype", "g", "standard", "Type of Game Rules")
|
||||||
|
playCmd.Flags().StringVarP(&gameState.MapName, "map", "m", "standard", "Game map to use to populate the board")
|
||||||
|
playCmd.Flags().BoolVarP(&gameState.ViewMap, "viewmap", "v", false, "View the Map Each Turn")
|
||||||
|
playCmd.Flags().BoolVarP(&gameState.UseColor, "color", "c", false, "Use color to draw the map")
|
||||||
|
playCmd.Flags().Int64VarP(&gameState.Seed, "seed", "r", time.Now().UTC().UnixNano(), "Random Seed")
|
||||||
|
playCmd.Flags().IntVarP(&gameState.TurnDelay, "delay", "d", 0, "Turn Delay in Milliseconds")
|
||||||
|
playCmd.Flags().IntVarP(&gameState.TurnDuration, "duration", "D", 0, "Minimum Turn Duration in Milliseconds")
|
||||||
|
playCmd.Flags().BoolVar(&gameState.DebugRequests, "debug-requests", false, "Log body of all requests sent")
|
||||||
|
playCmd.Flags().StringVarP(&gameState.Output, "output", "o", "", "File path to output game state to. Existing files will be overwritten")
|
||||||
|
|
||||||
playCmd.Flags().IntVarP(&Width, "width", "W", 11, "Width of Board")
|
playCmd.Flags().IntVar(&gameState.FoodSpawnChance, "foodSpawnChance", 15, "Percentage chance of spawning a new food every round")
|
||||||
playCmd.Flags().IntVarP(&Height, "height", "H", 11, "Height of Board")
|
playCmd.Flags().IntVar(&gameState.MinimumFood, "minimumFood", 1, "Minimum food to keep on the board every turn")
|
||||||
playCmd.Flags().StringArrayVarP(&Names, "name", "n", nil, "Name of Snake")
|
playCmd.Flags().IntVar(&gameState.HazardDamagePerTurn, "hazardDamagePerTurn", 14, "Health damage a snake will take when ending its turn in a hazard")
|
||||||
playCmd.Flags().StringArrayVarP(&URLs, "url", "u", nil, "URL of Snake")
|
playCmd.Flags().IntVar(&gameState.ShrinkEveryNTurns, "shrinkEveryNTurns", 25, "In Royale mode, the number of turns between generating new hazards")
|
||||||
playCmd.Flags().StringArrayVarP(&Names, "squad", "S", nil, "Squad of Snake")
|
|
||||||
playCmd.Flags().IntVarP(&Timeout, "timeout", "t", 500, "Request Timeout")
|
|
||||||
playCmd.Flags().BoolVarP(&Sequential, "sequential", "s", false, "Use Sequential Processing")
|
|
||||||
playCmd.Flags().StringVarP(&GameType, "gametype", "g", "standard", "Type of Game Rules")
|
|
||||||
playCmd.Flags().BoolVarP(&ViewMap, "viewmap", "v", false, "View the Map Each Turn")
|
|
||||||
playCmd.Flags().BoolVarP(&UseColor, "color", "c", false, "Use color to draw the map")
|
|
||||||
playCmd.Flags().Int64VarP(&Seed, "seed", "r", time.Now().UTC().UnixNano(), "Random Seed")
|
|
||||||
playCmd.Flags().IntVarP(&TurnDelay, "delay", "d", 0, "Turn Delay in Milliseconds")
|
|
||||||
playCmd.Flags().IntVarP(&TurnDuration, "duration", "D", 0, "Minimum Turn Duration in Milliseconds")
|
|
||||||
playCmd.Flags().BoolVar(&DebugRequests, "debug-requests", false, "Log body of all requests sent")
|
|
||||||
playCmd.Flags().StringVarP(&Output, "output", "o", "", "File path to output game state to. Existing files will be overwritten")
|
|
||||||
|
|
||||||
playCmd.Flags().IntVar(&FoodSpawnChance, "foodSpawnChance", 15, "Percentage chance of spawning a new food every round")
|
|
||||||
playCmd.Flags().IntVar(&MinimumFood, "minimumFood", 1, "Minimum food to keep on the board every turn")
|
|
||||||
playCmd.Flags().IntVar(&HazardDamagePerTurn, "hazardDamagePerTurn", 14, "Health damage a snake will take when ending its turn in a hazard")
|
|
||||||
playCmd.Flags().IntVar(&ShrinkEveryNTurns, "shrinkEveryNTurns", 25, "In Royale mode, the number of turns between generating new hazards")
|
|
||||||
|
|
||||||
playCmd.Flags().SortFlags = false
|
playCmd.Flags().SortFlags = false
|
||||||
|
|
||||||
|
return playCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func playPreRun(cmd *cobra.Command, args []string) {
|
// Setup a GameState once all the fields have been parsed from the command-line.
|
||||||
initialiseGameConfig()
|
func (gameState *GameState) initialize() {
|
||||||
|
// Generate game ID
|
||||||
|
gameState.gameID = uuid.New().String()
|
||||||
|
|
||||||
|
// Set up HTTP client with request timeout
|
||||||
|
if gameState.Timeout == 0 {
|
||||||
|
gameState.Timeout = 500
|
||||||
|
}
|
||||||
|
gameState.httpClient = http.Client{
|
||||||
|
Timeout: time.Duration(gameState.Timeout) * time.Millisecond,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load game map
|
||||||
|
gameMap, err := maps.GetMap(gameState.MapName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load game map %#v: %v", gameState.MapName, err)
|
||||||
|
}
|
||||||
|
gameState.gameMap = gameMap
|
||||||
|
|
||||||
|
// Create settings object
|
||||||
|
gameState.settings = map[string]string{
|
||||||
|
rules.ParamGameType: gameState.GameType,
|
||||||
|
rules.ParamFoodSpawnChance: fmt.Sprint(gameState.FoodSpawnChance),
|
||||||
|
rules.ParamMinimumFood: fmt.Sprint(gameState.MinimumFood),
|
||||||
|
rules.ParamHazardDamagePerTurn: fmt.Sprint(gameState.HazardDamagePerTurn),
|
||||||
|
rules.ParamShrinkEveryNTurns: fmt.Sprint(gameState.ShrinkEveryNTurns),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build ruleset from settings
|
||||||
|
ruleset := rules.NewRulesetBuilder().WithSeed(gameState.Seed).WithParams(gameState.settings).Ruleset()
|
||||||
|
gameState.ruleset = ruleset
|
||||||
|
|
||||||
|
// Initialize snake states as empty until we can ping the snake URLs
|
||||||
|
gameState.snakeStates = map[string]SnakeState{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var run = func(cmd *cobra.Command, args []string) {
|
// Setup and run a full game.
|
||||||
rand.Seed(Seed)
|
func (gameState *GameState) Run() {
|
||||||
|
gameState.initialize()
|
||||||
|
|
||||||
GameId = uuid.New().String()
|
// Setup local state for snakes
|
||||||
Turn = 0
|
gameState.snakeStates = gameState.buildSnakesFromOptions()
|
||||||
|
|
||||||
var endTime time.Time
|
rand.Seed(gameState.Seed)
|
||||||
snakeStates := buildSnakesFromOptions()
|
|
||||||
|
|
||||||
ruleset := getRuleset(Seed, snakeStates)
|
boardState := gameState.initializeBoardFromArgs()
|
||||||
state := initializeBoardFromArgs(ruleset, snakeStates)
|
exportGame := gameState.Output != ""
|
||||||
exportGame := Output != ""
|
|
||||||
|
|
||||||
gameExporter := GameExporter{
|
gameExporter := GameExporter{
|
||||||
game: createClientGame(ruleset),
|
game: gameState.createClientGame(),
|
||||||
snakeRequests: make([]client.SnakeRequest, 0),
|
snakeRequests: make([]client.SnakeRequest, 0),
|
||||||
winner: SnakeState{},
|
winner: SnakeState{},
|
||||||
isDraw: false,
|
isDraw: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
for v := false; !v; v, _ = ruleset.IsGameOver(state) {
|
if gameState.ViewMap {
|
||||||
if TurnDuration > 0 {
|
gameState.printMap(boardState)
|
||||||
endTime = time.Now().Add(time.Duration(TurnDuration) * time.Millisecond)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Turn++
|
var endTime time.Time
|
||||||
state = createNextBoardState(ruleset, state, snakeStates, Turn)
|
for v := false; !v; v, _ = gameState.ruleset.IsGameOver(boardState) {
|
||||||
|
if gameState.TurnDuration > 0 {
|
||||||
if ViewMap {
|
endTime = time.Now().Add(time.Duration(gameState.TurnDuration) * time.Millisecond)
|
||||||
printMap(state, snakeStates, Turn)
|
|
||||||
} else {
|
|
||||||
log.Printf("[%v]: State: %v\n", Turn, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
if TurnDelay > 0 {
|
|
||||||
time.Sleep(time.Duration(TurnDelay) * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
if TurnDuration > 0 {
|
|
||||||
time.Sleep(time.Until(endTime))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export game first, if enabled, so that we save the board on turn zero
|
||||||
if exportGame {
|
if exportGame {
|
||||||
// The output file was designed in a way so that (nearly) every entry is equivalent to a valid API request.
|
// The output file was designed in a way so that (nearly) every entry is equivalent to a valid API request.
|
||||||
// This is meant to help unlock further development of tools such as replaying a saved game by simply copying each line and sending it as a POST request.
|
// This is meant to help unlock further development of tools such as replaying a saved game by simply copying each line and sending it as a POST request.
|
||||||
|
|
@ -157,34 +178,56 @@ var run = func(cmd *cobra.Command, args []string) {
|
||||||
// In all cases the API request is technically non-compliant with how the actual API request should be.
|
// In all cases the API request is technically non-compliant with how the actual API request should be.
|
||||||
// The third option (filling the `you` key with an arbitrary snake) is the closest to the actual API request that would need the least manipulation to
|
// The third option (filling the `you` key with an arbitrary snake) is the closest to the actual API request that would need the least manipulation to
|
||||||
// be adjusted to look like an API call for a specific snake in the game.
|
// be adjusted to look like an API call for a specific snake in the game.
|
||||||
snakeState := snakeStates[state.Snakes[0].ID]
|
for _, snakeState := range gameState.snakeStates {
|
||||||
snakeRequest := getIndividualBoardStateForSnake(state, snakeState, snakeStates, ruleset)
|
snakeRequest := gameState.getRequestBodyForSnake(boardState, snakeState)
|
||||||
gameExporter.AddSnakeRequest(snakeRequest)
|
gameExporter.AddSnakeRequest(snakeRequest)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boardState = gameState.createNextBoardState(boardState)
|
||||||
|
|
||||||
|
if gameState.ViewMap {
|
||||||
|
gameState.printMap(boardState)
|
||||||
|
} else {
|
||||||
|
log.Printf("[%v]: State: %v\n", boardState.Turn, boardState)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gameState.TurnDelay > 0 {
|
||||||
|
time.Sleep(time.Duration(gameState.TurnDelay) * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gameState.TurnDuration > 0 {
|
||||||
|
time.Sleep(time.Until(endTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
isDraw := true
|
isDraw := true
|
||||||
if GameType == "solo" {
|
if gameState.GameType == "solo" {
|
||||||
log.Printf("[DONE]: Game completed after %v turns.", Turn)
|
log.Printf("[DONE]: Game completed after %v turns.", boardState.Turn)
|
||||||
if exportGame {
|
if exportGame {
|
||||||
// These checks for exportGame are present to avoid vacuuming up RAM when an export is not requred.
|
// These checks for exportGame are present to avoid vacuuming up RAM when an export is not requred.
|
||||||
gameExporter.winner = snakeStates[state.Snakes[0].ID]
|
for _, snakeState := range gameState.snakeStates {
|
||||||
|
gameExporter.winner = snakeState
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var winner SnakeState
|
var winner SnakeState
|
||||||
for _, snake := range state.Snakes {
|
for _, snake := range boardState.Snakes {
|
||||||
snakeState := snakeStates[snake.ID]
|
snakeState := gameState.snakeStates[snake.ID]
|
||||||
if snake.EliminatedCause == rules.NotEliminated {
|
if snake.EliminatedCause == rules.NotEliminated {
|
||||||
isDraw = false
|
isDraw = false
|
||||||
winner = snakeState
|
winner = snakeState
|
||||||
}
|
}
|
||||||
sendEndRequest(ruleset, state, snakeState, snakeStates)
|
gameState.sendEndRequest(boardState, snakeState)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isDraw {
|
if isDraw {
|
||||||
log.Printf("[DONE]: Game completed after %v turns. It was a draw.", Turn)
|
log.Printf("[DONE]: Game completed after %v turns. It was a draw.", boardState.Turn)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[DONE]: Game completed after %v turns. %v is the winner.", Turn, winner.Name)
|
log.Printf("[DONE]: Game completed after %v turns. %v is the winner.", boardState.Turn, winner.Name)
|
||||||
}
|
}
|
||||||
if exportGame {
|
if exportGame {
|
||||||
gameExporter.winner = winner
|
gameExporter.winner = winner
|
||||||
|
|
@ -192,7 +235,7 @@ var run = func(cmd *cobra.Command, args []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if exportGame {
|
if exportGame {
|
||||||
err := gameExporter.FlushToFile(Output, "JSONL")
|
err := gameExporter.FlushToFile(gameState.Output, "JSONL")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[WARN]: Unable to export game. Reason: %v\n", err.Error())
|
log.Printf("[WARN]: Unable to export game. Reason: %v\n", err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
@ -200,83 +243,57 @@ var run = func(cmd *cobra.Command, args []string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initialiseGameConfig() {
|
func (gameState *GameState) initializeBoardFromArgs() *rules.BoardState {
|
||||||
defaultConfig[rules.ParamGameType] = GameType
|
|
||||||
defaultConfig[rules.ParamFoodSpawnChance] = fmt.Sprint(FoodSpawnChance)
|
|
||||||
defaultConfig[rules.ParamMinimumFood] = fmt.Sprint(MinimumFood)
|
|
||||||
defaultConfig[rules.ParamHazardDamagePerTurn] = fmt.Sprint(HazardDamagePerTurn)
|
|
||||||
defaultConfig[rules.ParamShrinkEveryNTurns] = fmt.Sprint(ShrinkEveryNTurns)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRuleset(seed int64, snakeStates map[string]SnakeState) rules.Ruleset {
|
|
||||||
rb := rules.NewRulesetBuilder().WithSeed(seed).WithParams(defaultConfig)
|
|
||||||
|
|
||||||
for _, s := range snakeStates {
|
|
||||||
rb.AddSnakeToSquad(s.ID, s.Squad)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rb.Ruleset()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func initializeBoardFromArgs(ruleset rules.Ruleset, snakeStates map[string]SnakeState) *rules.BoardState {
|
|
||||||
if Timeout == 0 {
|
|
||||||
Timeout = 500
|
|
||||||
}
|
|
||||||
HttpClient = http.Client{
|
|
||||||
Timeout: time.Duration(Timeout) * time.Millisecond,
|
|
||||||
}
|
|
||||||
|
|
||||||
snakeIds := []string{}
|
snakeIds := []string{}
|
||||||
for _, snakeState := range snakeStates {
|
for _, snakeState := range gameState.snakeStates {
|
||||||
snakeIds = append(snakeIds, snakeState.ID)
|
snakeIds = append(snakeIds, snakeState.ID)
|
||||||
}
|
}
|
||||||
state, err := rules.CreateDefaultBoardState(rules.GlobalRand, Width, Height, snakeIds)
|
boardState, err := maps.SetupBoard(gameState.gameMap.ID(), gameState.ruleset.Settings(), gameState.Width, gameState.Height, snakeIds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic("[PANIC]: Error Initializing Board State")
|
log.Fatalf("Error Initializing Board State: %v", err)
|
||||||
}
|
}
|
||||||
state, err = ruleset.ModifyInitialBoardState(state)
|
boardState, err = gameState.ruleset.ModifyInitialBoardState(boardState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic("[PANIC]: Error Initializing Board State")
|
log.Fatalf("Error Initializing Board State: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, snakeState := range snakeStates {
|
for _, snakeState := range gameState.snakeStates {
|
||||||
snakeRequest := getIndividualBoardStateForSnake(state, snakeState, snakeStates, ruleset)
|
snakeRequest := gameState.getRequestBodyForSnake(boardState, snakeState)
|
||||||
requestBody := serialiseSnakeRequest(snakeRequest)
|
requestBody := serialiseSnakeRequest(snakeRequest)
|
||||||
u, _ := url.ParseRequestURI(snakeState.URL)
|
u, _ := url.ParseRequestURI(snakeState.URL)
|
||||||
u.Path = path.Join(u.Path, "start")
|
u.Path = path.Join(u.Path, "start")
|
||||||
if DebugRequests {
|
if gameState.DebugRequests {
|
||||||
log.Printf("POST %s: %v", u, string(requestBody))
|
log.Printf("POST %s: %v", u, string(requestBody))
|
||||||
}
|
}
|
||||||
_, err = HttpClient.Post(u.String(), "application/json", bytes.NewBuffer(requestBody))
|
_, err = gameState.httpClient.Post(u.String(), "application/json", bytes.NewBuffer(requestBody))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[WARN]: Request to %v failed", u.String())
|
log.Printf("[WARN]: Request to %v failed", u.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state
|
return boardState
|
||||||
}
|
}
|
||||||
|
|
||||||
func createNextBoardState(ruleset rules.Ruleset, state *rules.BoardState, snakeStates map[string]SnakeState, turn int) *rules.BoardState {
|
func (gameState *GameState) createNextBoardState(boardState *rules.BoardState) *rules.BoardState {
|
||||||
var moves []rules.SnakeMove
|
var moves []rules.SnakeMove
|
||||||
if Sequential {
|
if gameState.Sequential {
|
||||||
for _, snakeState := range snakeStates {
|
for _, snakeState := range gameState.snakeStates {
|
||||||
for _, snake := range state.Snakes {
|
for _, snake := range boardState.Snakes {
|
||||||
if snakeState.ID == snake.ID && snake.EliminatedCause == rules.NotEliminated {
|
if snakeState.ID == snake.ID && snake.EliminatedCause == rules.NotEliminated {
|
||||||
moves = append(moves, getMoveForSnake(ruleset, state, snakeState, snakeStates))
|
moves = append(moves, gameState.getMoveForSnake(boardState, snakeState))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
c := make(chan rules.SnakeMove, len(snakeStates))
|
c := make(chan rules.SnakeMove, len(gameState.snakeStates))
|
||||||
|
|
||||||
for _, snakeState := range snakeStates {
|
for _, snakeState := range gameState.snakeStates {
|
||||||
for _, snake := range state.Snakes {
|
for _, snake := range boardState.Snakes {
|
||||||
if snakeState.ID == snake.ID && snake.EliminatedCause == rules.NotEliminated {
|
if snakeState.ID == snake.ID && snake.EliminatedCause == rules.NotEliminated {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(snakeState SnakeState) {
|
go func(snakeState SnakeState) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
c <- getMoveForSnake(ruleset, state, snakeState, snakeStates)
|
c <- gameState.getMoveForSnake(boardState, snakeState)
|
||||||
}(snakeState)
|
}(snakeState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -290,29 +307,34 @@ func createNextBoardState(ruleset rules.Ruleset, state *rules.BoardState, snakeS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, move := range moves {
|
for _, move := range moves {
|
||||||
snakeState := snakeStates[move.ID]
|
snakeState := gameState.snakeStates[move.ID]
|
||||||
snakeState.LastMove = move.Move
|
snakeState.LastMove = move.Move
|
||||||
snakeStates[move.ID] = snakeState
|
gameState.snakeStates[move.ID] = snakeState
|
||||||
}
|
}
|
||||||
state, err := ruleset.CreateNextBoardState(state, moves)
|
boardState, err := gameState.ruleset.CreateNextBoardState(boardState, moves)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panicf("[PANIC]: Error Producing Next Board State: %v", err)
|
log.Fatalf("Error producing next board state: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
state.Turn = turn
|
boardState, err = maps.UpdateBoard(gameState.gameMap.ID(), boardState, gameState.ruleset.Settings())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error updating board with game map: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return state
|
boardState.Turn += 1
|
||||||
|
|
||||||
|
return boardState
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMoveForSnake(ruleset rules.Ruleset, state *rules.BoardState, snakeState SnakeState, snakeStates map[string]SnakeState) rules.SnakeMove {
|
func (gameState *GameState) getMoveForSnake(boardState *rules.BoardState, snakeState SnakeState) rules.SnakeMove {
|
||||||
snakeRequest := getIndividualBoardStateForSnake(state, snakeState, snakeStates, ruleset)
|
snakeRequest := gameState.getRequestBodyForSnake(boardState, snakeState)
|
||||||
requestBody := serialiseSnakeRequest(snakeRequest)
|
requestBody := serialiseSnakeRequest(snakeRequest)
|
||||||
u, _ := url.ParseRequestURI(snakeState.URL)
|
u, _ := url.ParseRequestURI(snakeState.URL)
|
||||||
u.Path = path.Join(u.Path, "move")
|
u.Path = path.Join(u.Path, "move")
|
||||||
if DebugRequests {
|
if gameState.DebugRequests {
|
||||||
log.Printf("POST %s: %v", u, string(requestBody))
|
log.Printf("POST %s: %v", u, string(requestBody))
|
||||||
}
|
}
|
||||||
res, err := HttpClient.Post(u.String(), "application/json", bytes.NewBuffer(requestBody))
|
res, err := gameState.httpClient.Post(u.String(), "application/json", bytes.NewBuffer(requestBody))
|
||||||
move := snakeState.LastMove
|
move := snakeState.LastMove
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[WARN]: Request to %v failed\n", u.String())
|
log.Printf("[WARN]: Request to %v failed\n", u.String())
|
||||||
|
|
@ -335,100 +357,56 @@ func getMoveForSnake(ruleset rules.Ruleset, state *rules.BoardState, snakeState
|
||||||
return rules.SnakeMove{ID: snakeState.ID, Move: move}
|
return rules.SnakeMove{ID: snakeState.ID, Move: move}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendEndRequest(ruleset rules.Ruleset, state *rules.BoardState, snakeState SnakeState, snakeStates map[string]SnakeState) {
|
func (gameState *GameState) sendEndRequest(boardState *rules.BoardState, snakeState SnakeState) {
|
||||||
snakeRequest := getIndividualBoardStateForSnake(state, snakeState, snakeStates, ruleset)
|
snakeRequest := gameState.getRequestBodyForSnake(boardState, snakeState)
|
||||||
requestBody := serialiseSnakeRequest(snakeRequest)
|
requestBody := serialiseSnakeRequest(snakeRequest)
|
||||||
u, _ := url.ParseRequestURI(snakeState.URL)
|
u, _ := url.ParseRequestURI(snakeState.URL)
|
||||||
u.Path = path.Join(u.Path, "end")
|
u.Path = path.Join(u.Path, "end")
|
||||||
if DebugRequests {
|
if gameState.DebugRequests {
|
||||||
log.Printf("POST %s: %v", u, string(requestBody))
|
log.Printf("POST %s: %v", u, string(requestBody))
|
||||||
}
|
}
|
||||||
_, err := HttpClient.Post(u.String(), "application/json", bytes.NewBuffer(requestBody))
|
_, err := gameState.httpClient.Post(u.String(), "application/json", bytes.NewBuffer(requestBody))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[WARN]: Request to %v failed", u.String())
|
log.Printf("[WARN]: Request to %v failed", u.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIndividualBoardStateForSnake(state *rules.BoardState, snakeState SnakeState, snakeStates map[string]SnakeState, ruleset rules.Ruleset) client.SnakeRequest {
|
func (gameState *GameState) getRequestBodyForSnake(boardState *rules.BoardState, snakeState SnakeState) client.SnakeRequest {
|
||||||
var youSnake rules.Snake
|
var youSnake rules.Snake
|
||||||
for _, snk := range state.Snakes {
|
for _, snk := range boardState.Snakes {
|
||||||
if snakeState.ID == snk.ID {
|
if snakeState.ID == snk.ID {
|
||||||
youSnake = snk
|
youSnake = snk
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
request := client.SnakeRequest{
|
request := client.SnakeRequest{
|
||||||
Game: createClientGame(ruleset),
|
Game: gameState.createClientGame(),
|
||||||
Turn: Turn,
|
Turn: boardState.Turn,
|
||||||
Board: convertStateToBoard(state, snakeStates),
|
Board: convertStateToBoard(boardState, gameState.snakeStates),
|
||||||
You: convertRulesSnake(youSnake, snakeStates[youSnake.ID]),
|
You: convertRulesSnake(youSnake, snakeState),
|
||||||
}
|
}
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
func serialiseSnakeRequest(snakeRequest client.SnakeRequest) []byte {
|
func (gameState *GameState) createClientGame() client.Game {
|
||||||
requestJSON, err := json.Marshal(snakeRequest)
|
return client.Game{
|
||||||
if err != nil {
|
ID: gameState.gameID,
|
||||||
log.Panic("[PANIC]: Error Marshalling JSON from State")
|
Timeout: gameState.Timeout,
|
||||||
panic(err)
|
Ruleset: client.Ruleset{
|
||||||
}
|
Name: gameState.ruleset.Name(),
|
||||||
return requestJSON
|
|
||||||
}
|
|
||||||
|
|
||||||
func createClientGame(ruleset rules.Ruleset) client.Game {
|
|
||||||
return client.Game{ID: GameId, Timeout: Timeout, Ruleset: client.Ruleset{
|
|
||||||
Name: ruleset.Name(),
|
|
||||||
Version: "cli", // TODO: Use GitHub Release Version
|
Version: "cli", // TODO: Use GitHub Release Version
|
||||||
Settings: ruleset.Settings(),
|
Settings: gameState.ruleset.Settings(),
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertRulesSnake(snake rules.Snake, snakeState SnakeState) client.Snake {
|
|
||||||
return client.Snake{
|
|
||||||
ID: snake.ID,
|
|
||||||
Name: snakeState.Name,
|
|
||||||
Health: snake.Health,
|
|
||||||
Body: client.CoordFromPointArray(snake.Body),
|
|
||||||
Latency: "0",
|
|
||||||
Head: client.CoordFromPoint(snake.Body[0]),
|
|
||||||
Length: len(snake.Body),
|
|
||||||
Shout: "",
|
|
||||||
Squad: snakeState.Squad,
|
|
||||||
Customizations: client.Customizations{
|
|
||||||
Head: snakeState.Head,
|
|
||||||
Tail: snakeState.Tail,
|
|
||||||
Color: snakeState.Color,
|
|
||||||
},
|
},
|
||||||
|
Map: gameState.gameMap.ID(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertRulesSnakes(snakes []rules.Snake, snakeStates map[string]SnakeState) []client.Snake {
|
func (gameState *GameState) buildSnakesFromOptions() map[string]SnakeState {
|
||||||
a := make([]client.Snake, 0)
|
|
||||||
for _, snake := range snakes {
|
|
||||||
if snake.EliminatedCause == rules.NotEliminated {
|
|
||||||
a = append(a, convertRulesSnake(snake, snakeStates[snake.ID]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertStateToBoard(state *rules.BoardState, snakeStates map[string]SnakeState) client.Board {
|
|
||||||
return client.Board{
|
|
||||||
Height: state.Height,
|
|
||||||
Width: state.Width,
|
|
||||||
Food: client.CoordFromPointArray(state.Food),
|
|
||||||
Hazards: client.CoordFromPointArray(state.Hazards),
|
|
||||||
Snakes: convertRulesSnakes(state.Snakes, snakeStates),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildSnakesFromOptions() map[string]SnakeState {
|
|
||||||
bodyChars := []rune{'■', '⌀', '●', '☻', '◘', '☺', '□', '⍟'}
|
bodyChars := []rune{'■', '⌀', '●', '☻', '◘', '☺', '□', '⍟'}
|
||||||
var numSnakes int
|
var numSnakes int
|
||||||
snakes := map[string]SnakeState{}
|
snakes := map[string]SnakeState{}
|
||||||
numNames := len(Names)
|
numNames := len(gameState.Names)
|
||||||
numURLs := len(URLs)
|
numURLs := len(gameState.URLs)
|
||||||
numSquads := len(Squads)
|
|
||||||
if numNames > numURLs {
|
if numNames > numURLs {
|
||||||
numSnakes = numNames
|
numSnakes = numNames
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -440,42 +418,33 @@ func buildSnakesFromOptions() map[string]SnakeState {
|
||||||
for i := int(0); i < numSnakes; i++ {
|
for i := int(0); i < numSnakes; i++ {
|
||||||
var snakeName string
|
var snakeName string
|
||||||
var snakeURL string
|
var snakeURL string
|
||||||
var snakeSquad string
|
|
||||||
|
|
||||||
id := uuid.New().String()
|
id := uuid.New().String()
|
||||||
|
|
||||||
if i < numNames {
|
if i < numNames {
|
||||||
snakeName = Names[i]
|
snakeName = gameState.Names[i]
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[WARN]: Name for URL %v is missing: a default name will be applied\n", URLs[i])
|
log.Printf("[WARN]: Name for URL %v is missing: a default name will be applied\n", gameState.URLs[i])
|
||||||
snakeName = id
|
snakeName = id
|
||||||
}
|
}
|
||||||
|
|
||||||
if i < numURLs {
|
if i < numURLs {
|
||||||
u, err := url.ParseRequestURI(URLs[i])
|
u, err := url.ParseRequestURI(gameState.URLs[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[WARN]: URL %v is not valid: a default will be applied\n", URLs[i])
|
log.Printf("[WARN]: URL %v is not valid: a default will be applied\n", gameState.URLs[i])
|
||||||
snakeURL = "https://example.com"
|
snakeURL = "https://example.com"
|
||||||
} else {
|
} else {
|
||||||
snakeURL = u.String()
|
snakeURL = u.String()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[WARN]: URL for Name %v is missing: a default URL will be applied\n", Names[i])
|
log.Printf("[WARN]: URL for Name %v is missing: a default URL will be applied\n", gameState.Names[i])
|
||||||
snakeURL = "https://example.com"
|
snakeURL = "https://example.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
if GameType == "squad" {
|
|
||||||
if i < numSquads {
|
|
||||||
snakeSquad = Squads[i]
|
|
||||||
} else {
|
|
||||||
log.Printf("[WARN]: Squad for URL %v is missing: a default squad will be applied\n", URLs[i])
|
|
||||||
snakeSquad = strconv.Itoa(i / 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
snakeState := SnakeState{
|
snakeState := SnakeState{
|
||||||
Name: snakeName, URL: snakeURL, ID: id, LastMove: "up", Character: bodyChars[i%8],
|
Name: snakeName, URL: snakeURL, ID: id, LastMove: "up", Character: bodyChars[i%8],
|
||||||
}
|
}
|
||||||
res, err := HttpClient.Get(snakeURL)
|
res, err := gameState.httpClient.Get(snakeURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[WARN]: Request to %v failed: %v", snakeURL, err)
|
log.Printf("[WARN]: Request to %v failed: %v", snakeURL, err)
|
||||||
} else if res.Body != nil {
|
} else if res.Body != nil {
|
||||||
|
|
@ -495,14 +464,129 @@ func buildSnakesFromOptions() map[string]SnakeState {
|
||||||
snakeState.Color = pingResponse.Color
|
snakeState.Color = pingResponse.Color
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if GameType == "squad" {
|
|
||||||
snakeState.Squad = snakeSquad
|
|
||||||
}
|
|
||||||
snakes[snakeState.ID] = snakeState
|
snakes[snakeState.ID] = snakeState
|
||||||
}
|
}
|
||||||
return snakes
|
return snakes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gameState *GameState) printMap(boardState *rules.BoardState) {
|
||||||
|
var o bytes.Buffer
|
||||||
|
o.WriteString(fmt.Sprintf("Ruleset: %s, Seed: %d, Turn: %v\n", gameState.GameType, gameState.Seed, boardState.Turn))
|
||||||
|
board := make([][]string, boardState.Width)
|
||||||
|
for i := range board {
|
||||||
|
board[i] = make([]string, boardState.Height)
|
||||||
|
}
|
||||||
|
for y := int(0); y < boardState.Height; y++ {
|
||||||
|
for x := int(0); x < boardState.Width; x++ {
|
||||||
|
if gameState.UseColor {
|
||||||
|
board[x][y] = TERM_FG_LIGHTGRAY + "□"
|
||||||
|
} else {
|
||||||
|
board[x][y] = "◦"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, oob := range boardState.Hazards {
|
||||||
|
if gameState.UseColor {
|
||||||
|
board[oob.X][oob.Y] = TERM_BG_GRAY + " " + TERM_BG_WHITE
|
||||||
|
} else {
|
||||||
|
board[oob.X][oob.Y] = "░"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if gameState.UseColor {
|
||||||
|
o.WriteString(fmt.Sprintf("Hazards "+TERM_BG_GRAY+" "+TERM_RESET+": %v\n", boardState.Hazards))
|
||||||
|
} else {
|
||||||
|
o.WriteString(fmt.Sprintf("Hazards ░: %v\n", boardState.Hazards))
|
||||||
|
}
|
||||||
|
for _, f := range boardState.Food {
|
||||||
|
if gameState.UseColor {
|
||||||
|
board[f.X][f.Y] = TERM_FG_FOOD + "●"
|
||||||
|
} else {
|
||||||
|
board[f.X][f.Y] = "⚕"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if gameState.UseColor {
|
||||||
|
o.WriteString(fmt.Sprintf("Food "+TERM_FG_FOOD+TERM_BG_WHITE+"●"+TERM_RESET+": %v\n", boardState.Food))
|
||||||
|
} else {
|
||||||
|
o.WriteString(fmt.Sprintf("Food ⚕: %v\n", boardState.Food))
|
||||||
|
}
|
||||||
|
for _, s := range boardState.Snakes {
|
||||||
|
red, green, blue := parseSnakeColor(gameState.snakeStates[s.ID].Color)
|
||||||
|
for _, b := range s.Body {
|
||||||
|
if b.X >= 0 && b.X < boardState.Width && b.Y >= 0 && b.Y < boardState.Height {
|
||||||
|
if gameState.UseColor {
|
||||||
|
board[b.X][b.Y] = fmt.Sprintf(TERM_FG_RGB+"■", red, green, blue)
|
||||||
|
} else {
|
||||||
|
board[b.X][b.Y] = string(gameState.snakeStates[s.ID].Character)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if gameState.UseColor {
|
||||||
|
o.WriteString(fmt.Sprintf("%v "+TERM_FG_RGB+TERM_BG_WHITE+"■■■"+TERM_RESET+": %v\n", gameState.snakeStates[s.ID].Name, red, green, blue, s))
|
||||||
|
} else {
|
||||||
|
o.WriteString(fmt.Sprintf("%v %c: %v\n", gameState.snakeStates[s.ID].Name, gameState.snakeStates[s.ID].Character, s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for y := boardState.Height - 1; y >= 0; y-- {
|
||||||
|
if gameState.UseColor {
|
||||||
|
o.WriteString(TERM_BG_WHITE)
|
||||||
|
}
|
||||||
|
for x := int(0); x < boardState.Width; x++ {
|
||||||
|
o.WriteString(board[x][y])
|
||||||
|
}
|
||||||
|
if gameState.UseColor {
|
||||||
|
o.WriteString(TERM_RESET)
|
||||||
|
}
|
||||||
|
o.WriteString("\n")
|
||||||
|
}
|
||||||
|
log.Print(o.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func serialiseSnakeRequest(snakeRequest client.SnakeRequest) []byte {
|
||||||
|
requestJSON, err := json.Marshal(snakeRequest)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error marshalling JSON from State: %v", err)
|
||||||
|
}
|
||||||
|
return requestJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertRulesSnake(snake rules.Snake, snakeState SnakeState) client.Snake {
|
||||||
|
return client.Snake{
|
||||||
|
ID: snake.ID,
|
||||||
|
Name: snakeState.Name,
|
||||||
|
Health: snake.Health,
|
||||||
|
Body: client.CoordFromPointArray(snake.Body),
|
||||||
|
Latency: "0",
|
||||||
|
Head: client.CoordFromPoint(snake.Body[0]),
|
||||||
|
Length: int(len(snake.Body)),
|
||||||
|
Shout: "",
|
||||||
|
Customizations: client.Customizations{
|
||||||
|
Head: snakeState.Head,
|
||||||
|
Tail: snakeState.Tail,
|
||||||
|
Color: snakeState.Color,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertRulesSnakes(snakes []rules.Snake, snakeStates map[string]SnakeState) []client.Snake {
|
||||||
|
a := make([]client.Snake, 0)
|
||||||
|
for _, snake := range snakes {
|
||||||
|
if snake.EliminatedCause == rules.NotEliminated {
|
||||||
|
a = append(a, convertRulesSnake(snake, snakeStates[snake.ID]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertStateToBoard(boardState *rules.BoardState, snakeStates map[string]SnakeState) client.Board {
|
||||||
|
return client.Board{
|
||||||
|
Height: boardState.Height,
|
||||||
|
Width: boardState.Width,
|
||||||
|
Food: client.CoordFromPointArray(boardState.Food),
|
||||||
|
Hazards: client.CoordFromPointArray(boardState.Hazards),
|
||||||
|
Snakes: convertRulesSnakes(boardState.Snakes, snakeStates),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Parses a color string like "#ef03d3" to rgb values from 0 to 255 or returns
|
// Parses a color string like "#ef03d3" to rgb values from 0 to 255 or returns
|
||||||
// the default gray if any errors occure
|
// the default gray if any errors occure
|
||||||
func parseSnakeColor(color string) (int64, int64, int64) {
|
func parseSnakeColor(color string) (int64, int64, int64) {
|
||||||
|
|
@ -517,75 +601,3 @@ func parseSnakeColor(color string) (int64, int64, int64) {
|
||||||
// Default gray color from Battlesnake board
|
// Default gray color from Battlesnake board
|
||||||
return 136, 136, 136
|
return 136, 136, 136
|
||||||
}
|
}
|
||||||
|
|
||||||
func printMap(state *rules.BoardState, snakeStates map[string]SnakeState, gameTurn int) {
|
|
||||||
var o bytes.Buffer
|
|
||||||
o.WriteString(fmt.Sprintf("Ruleset: %s, Seed: %d, Turn: %v\n", GameType, Seed, gameTurn))
|
|
||||||
board := make([][]string, state.Width)
|
|
||||||
for i := range board {
|
|
||||||
board[i] = make([]string, state.Height)
|
|
||||||
}
|
|
||||||
for y := 0; y < state.Height; y++ {
|
|
||||||
for x := 0; x < state.Width; x++ {
|
|
||||||
if UseColor {
|
|
||||||
board[x][y] = TERM_FG_LIGHTGRAY + "□"
|
|
||||||
} else {
|
|
||||||
board[x][y] = "◦"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, oob := range state.Hazards {
|
|
||||||
if UseColor {
|
|
||||||
board[oob.X][oob.Y] = TERM_BG_GRAY + " " + TERM_BG_WHITE
|
|
||||||
} else {
|
|
||||||
board[oob.X][oob.Y] = "░"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if UseColor {
|
|
||||||
o.WriteString(fmt.Sprintf("Hazards "+TERM_BG_GRAY+" "+TERM_RESET+": %v\n", state.Hazards))
|
|
||||||
} else {
|
|
||||||
o.WriteString(fmt.Sprintf("Hazards ░: %v\n", state.Hazards))
|
|
||||||
}
|
|
||||||
for _, f := range state.Food {
|
|
||||||
if UseColor {
|
|
||||||
board[f.X][f.Y] = TERM_FG_FOOD + "●"
|
|
||||||
} else {
|
|
||||||
board[f.X][f.Y] = "⚕"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if UseColor {
|
|
||||||
o.WriteString(fmt.Sprintf("Food "+TERM_FG_FOOD+TERM_BG_WHITE+"●"+TERM_RESET+": %v\n", state.Food))
|
|
||||||
} else {
|
|
||||||
o.WriteString(fmt.Sprintf("Food ⚕: %v\n", state.Food))
|
|
||||||
}
|
|
||||||
for _, s := range state.Snakes {
|
|
||||||
red, green, blue := parseSnakeColor(snakeStates[s.ID].Color)
|
|
||||||
for _, b := range s.Body {
|
|
||||||
if b.X >= 0 && b.X < state.Width && b.Y >= 0 && b.Y < state.Height {
|
|
||||||
if UseColor {
|
|
||||||
board[b.X][b.Y] = fmt.Sprintf(TERM_FG_RGB+"■", red, green, blue)
|
|
||||||
} else {
|
|
||||||
board[b.X][b.Y] = string(snakeStates[s.ID].Character)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if UseColor {
|
|
||||||
o.WriteString(fmt.Sprintf("%v "+TERM_FG_RGB+TERM_BG_WHITE+"■■■"+TERM_RESET+": %v\n", snakeStates[s.ID].Name, red, green, blue, s))
|
|
||||||
} else {
|
|
||||||
o.WriteString(fmt.Sprintf("%v %c: %v\n", snakeStates[s.ID].Name, snakeStates[s.ID].Character, s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for y := state.Height - 1; y >= 0; y-- {
|
|
||||||
if UseColor {
|
|
||||||
o.WriteString(TERM_BG_WHITE)
|
|
||||||
}
|
|
||||||
for x := 0; x < state.Width; x++ {
|
|
||||||
o.WriteString(board[x][y])
|
|
||||||
}
|
|
||||||
if UseColor {
|
|
||||||
o.WriteString(TERM_RESET)
|
|
||||||
}
|
|
||||||
o.WriteString("\n")
|
|
||||||
}
|
|
||||||
log.Print(o.String())
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,31 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func buildDefaultGameState() *GameState {
|
||||||
|
gameState := &GameState{
|
||||||
|
Width: 11,
|
||||||
|
Height: 11,
|
||||||
|
Names: nil,
|
||||||
|
Timeout: 500,
|
||||||
|
Sequential: false,
|
||||||
|
GameType: "standard",
|
||||||
|
MapName: "standard",
|
||||||
|
ViewMap: false,
|
||||||
|
UseColor: false,
|
||||||
|
Seed: 1,
|
||||||
|
TurnDelay: 0,
|
||||||
|
TurnDuration: 0,
|
||||||
|
DebugRequests: false,
|
||||||
|
Output: "",
|
||||||
|
FoodSpawnChance: 15,
|
||||||
|
MinimumFood: 1,
|
||||||
|
HazardDamagePerTurn: 14,
|
||||||
|
ShrinkEveryNTurns: 25,
|
||||||
|
}
|
||||||
|
|
||||||
|
return gameState
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetIndividualBoardStateForSnake(t *testing.T) {
|
func TestGetIndividualBoardStateForSnake(t *testing.T) {
|
||||||
s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}}
|
s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}}
|
||||||
s2 := rules.Snake{ID: "two", Body: []rules.Point{{X: 4, Y: 3}}}
|
s2 := rules.Snake{ID: "two", Body: []rules.Point{{X: 4, Y: 3}}}
|
||||||
|
|
@ -34,12 +59,16 @@ func TestGetIndividualBoardStateForSnake(t *testing.T) {
|
||||||
Tail: "bolt",
|
Tail: "bolt",
|
||||||
Color: "#654321",
|
Color: "#654321",
|
||||||
}
|
}
|
||||||
snakeStates := map[string]SnakeState{
|
|
||||||
|
gameState := buildDefaultGameState()
|
||||||
|
gameState.initialize()
|
||||||
|
gameState.gameID = "GAME_ID"
|
||||||
|
gameState.snakeStates = map[string]SnakeState{
|
||||||
s1State.ID: s1State,
|
s1State.ID: s1State,
|
||||||
s2State.ID: s2State,
|
s2State.ID: s2State,
|
||||||
}
|
}
|
||||||
initialiseGameConfig() // initialise default config
|
|
||||||
snakeRequest := getIndividualBoardStateForSnake(state, s1State, snakeStates, getRuleset(0, snakeStates))
|
snakeRequest := gameState.getRequestBodyForSnake(state, s1State)
|
||||||
requestBody := serialiseSnakeRequest(snakeRequest)
|
requestBody := serialiseSnakeRequest(snakeRequest)
|
||||||
|
|
||||||
test.RequireJSONMatchesFixture(t, "testdata/snake_request_body.json", string(requestBody))
|
test.RequireJSONMatchesFixture(t, "testdata/snake_request_body.json", string(requestBody))
|
||||||
|
|
@ -69,34 +98,25 @@ func TestSettingsRequestSerialization(t *testing.T) {
|
||||||
Tail: "bolt",
|
Tail: "bolt",
|
||||||
Color: "#654321",
|
Color: "#654321",
|
||||||
}
|
}
|
||||||
snakeStates := map[string]SnakeState{s1State.ID: s1State, s2State.ID: s2State}
|
|
||||||
|
|
||||||
rsb := rules.NewRulesetBuilder().
|
|
||||||
WithParams(map[string]string{
|
|
||||||
// standard
|
|
||||||
rules.ParamFoodSpawnChance: "11",
|
|
||||||
rules.ParamMinimumFood: "7",
|
|
||||||
rules.ParamHazardDamagePerTurn: "19",
|
|
||||||
rules.ParamHazardMap: "hz_spiral",
|
|
||||||
rules.ParamHazardMapAuthor: "altersaddle",
|
|
||||||
// squad
|
|
||||||
rules.ParamAllowBodyCollisions: "true",
|
|
||||||
rules.ParamSharedElimination: "false",
|
|
||||||
rules.ParamSharedHealth: "true",
|
|
||||||
rules.ParamSharedLength: "false",
|
|
||||||
// royale
|
|
||||||
rules.ParamShrinkEveryNTurns: "17",
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, gt := range []string{
|
for _, gt := range []string{
|
||||||
rules.GameTypeStandard, rules.GameTypeRoyale, rules.GameTypeSolo,
|
rules.GameTypeStandard, rules.GameTypeRoyale, rules.GameTypeSolo,
|
||||||
rules.GameTypeWrapped, rules.GameTypeSquad, rules.GameTypeConstrictor,
|
rules.GameTypeWrapped, rules.GameTypeConstrictor,
|
||||||
} {
|
} {
|
||||||
t.Run(gt, func(t *testing.T) {
|
t.Run(gt, func(t *testing.T) {
|
||||||
// apply game type
|
gameState := buildDefaultGameState()
|
||||||
ruleset := rsb.WithParams(map[string]string{rules.ParamGameType: gt}).Ruleset()
|
|
||||||
|
|
||||||
snakeRequest := getIndividualBoardStateForSnake(state, s1State, snakeStates, ruleset)
|
gameState.FoodSpawnChance = 11
|
||||||
|
gameState.MinimumFood = 7
|
||||||
|
gameState.HazardDamagePerTurn = 19
|
||||||
|
gameState.ShrinkEveryNTurns = 17
|
||||||
|
gameState.GameType = gt
|
||||||
|
|
||||||
|
gameState.initialize()
|
||||||
|
gameState.gameID = "GAME_ID"
|
||||||
|
gameState.snakeStates = map[string]SnakeState{s1State.ID: s1State, s2State.ID: s2State}
|
||||||
|
|
||||||
|
snakeRequest := gameState.getRequestBodyForSnake(state, s1State)
|
||||||
requestBody := serialiseSnakeRequest(snakeRequest)
|
requestBody := serialiseSnakeRequest(snakeRequest)
|
||||||
t.Log(string(requestBody))
|
t.Log(string(requestBody))
|
||||||
|
|
||||||
|
|
@ -128,7 +148,6 @@ func TestConvertRulesSnakes(t *testing.T) {
|
||||||
ID: "one",
|
ID: "one",
|
||||||
Name: "ONE",
|
Name: "ONE",
|
||||||
URL: "http://example1.com",
|
URL: "http://example1.com",
|
||||||
Squad: "squadA",
|
|
||||||
Head: "a",
|
Head: "a",
|
||||||
Tail: "b",
|
Tail: "b",
|
||||||
Color: "#012345",
|
Color: "#012345",
|
||||||
|
|
@ -146,7 +165,6 @@ func TestConvertRulesSnakes(t *testing.T) {
|
||||||
Head: client.Coord{X: 3, Y: 3},
|
Head: client.Coord{X: 3, Y: 3},
|
||||||
Length: 2,
|
Length: 2,
|
||||||
Shout: "",
|
Shout: "",
|
||||||
Squad: "squadA",
|
|
||||||
Customizations: client.Customizations{
|
Customizations: client.Customizations{
|
||||||
Color: "#012345",
|
Color: "#012345",
|
||||||
Head: "a",
|
Head: "a",
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
homedir "github.com/mitchellh/go-homedir"
|
homedir "github.com/mitchellh/go-homedir"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
@ -18,6 +19,8 @@ var rootCmd = &cobra.Command{
|
||||||
}
|
}
|
||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
|
rootCmd.AddCommand(NewPlayCommand())
|
||||||
|
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"game": {
|
"game": {
|
||||||
"id": "",
|
"id": "GAME_ID",
|
||||||
"ruleset": {
|
"ruleset": {
|
||||||
"name": "standard",
|
"name": "standard",
|
||||||
"version": "cli",
|
"version": "cli",
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
"hazardMap": "",
|
"hazardMap": "",
|
||||||
"hazardMapAuthor": "",
|
"hazardMapAuthor": "",
|
||||||
"royale": {
|
"royale": {
|
||||||
"shrinkEveryNTurns": 0
|
"shrinkEveryNTurns": 25
|
||||||
},
|
},
|
||||||
"squad": {
|
"squad": {
|
||||||
"allowBodyCollisions": false,
|
"allowBodyCollisions": false,
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"map": "",
|
"map": "standard",
|
||||||
"timeout": 500,
|
"timeout": 500,
|
||||||
"source": ""
|
"source": ""
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"game": {
|
"game": {
|
||||||
"id": "",
|
"id": "GAME_ID",
|
||||||
"ruleset": {
|
"ruleset": {
|
||||||
"name": "constrictor",
|
"name": "constrictor",
|
||||||
"version": "cli",
|
"version": "cli",
|
||||||
|
|
@ -8,10 +8,10 @@
|
||||||
"foodSpawnChance": 11,
|
"foodSpawnChance": 11,
|
||||||
"minimumFood": 7,
|
"minimumFood": 7,
|
||||||
"hazardDamagePerTurn": 19,
|
"hazardDamagePerTurn": 19,
|
||||||
"hazardMap": "hz_spiral",
|
"hazardMap": "",
|
||||||
"hazardMapAuthor": "altersaddle",
|
"hazardMapAuthor": "",
|
||||||
"royale": {
|
"royale": {
|
||||||
"shrinkEveryNTurns": 0
|
"shrinkEveryNTurns": 17
|
||||||
},
|
},
|
||||||
"squad": {
|
"squad": {
|
||||||
"allowBodyCollisions": false,
|
"allowBodyCollisions": false,
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"map": "",
|
"map": "standard",
|
||||||
"timeout": 500,
|
"timeout": 500,
|
||||||
"source": ""
|
"source": ""
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"game": {
|
"game": {
|
||||||
"id": "",
|
"id": "GAME_ID",
|
||||||
"ruleset": {
|
"ruleset": {
|
||||||
"name": "royale",
|
"name": "royale",
|
||||||
"version": "cli",
|
"version": "cli",
|
||||||
|
|
@ -8,8 +8,8 @@
|
||||||
"foodSpawnChance": 11,
|
"foodSpawnChance": 11,
|
||||||
"minimumFood": 7,
|
"minimumFood": 7,
|
||||||
"hazardDamagePerTurn": 19,
|
"hazardDamagePerTurn": 19,
|
||||||
"hazardMap": "hz_spiral",
|
"hazardMap": "",
|
||||||
"hazardMapAuthor": "altersaddle",
|
"hazardMapAuthor": "",
|
||||||
"royale": {
|
"royale": {
|
||||||
"shrinkEveryNTurns": 17
|
"shrinkEveryNTurns": 17
|
||||||
},
|
},
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"map": "",
|
"map": "standard",
|
||||||
"timeout": 500,
|
"timeout": 500,
|
||||||
"source": ""
|
"source": ""
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"game": {
|
"game": {
|
||||||
"id": "",
|
"id": "GAME_ID",
|
||||||
"ruleset": {
|
"ruleset": {
|
||||||
"name": "solo",
|
"name": "solo",
|
||||||
"version": "cli",
|
"version": "cli",
|
||||||
|
|
@ -8,10 +8,10 @@
|
||||||
"foodSpawnChance": 11,
|
"foodSpawnChance": 11,
|
||||||
"minimumFood": 7,
|
"minimumFood": 7,
|
||||||
"hazardDamagePerTurn": 19,
|
"hazardDamagePerTurn": 19,
|
||||||
"hazardMap": "hz_spiral",
|
"hazardMap": "",
|
||||||
"hazardMapAuthor": "altersaddle",
|
"hazardMapAuthor": "",
|
||||||
"royale": {
|
"royale": {
|
||||||
"shrinkEveryNTurns": 0
|
"shrinkEveryNTurns": 17
|
||||||
},
|
},
|
||||||
"squad": {
|
"squad": {
|
||||||
"allowBodyCollisions": false,
|
"allowBodyCollisions": false,
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"map": "",
|
"map": "standard",
|
||||||
"timeout": 500,
|
"timeout": 500,
|
||||||
"source": ""
|
"source": ""
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
"hazardMap": "hz_spiral",
|
"hazardMap": "hz_spiral",
|
||||||
"hazardMapAuthor": "altersaddle",
|
"hazardMapAuthor": "altersaddle",
|
||||||
"royale": {
|
"royale": {
|
||||||
"shrinkEveryNTurns": 0
|
"shrinkEveryNTurns": 17
|
||||||
},
|
},
|
||||||
"squad": {
|
"squad": {
|
||||||
"allowBodyCollisions": true,
|
"allowBodyCollisions": true,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"game": {
|
"game": {
|
||||||
"id": "",
|
"id": "GAME_ID",
|
||||||
"ruleset": {
|
"ruleset": {
|
||||||
"name": "standard",
|
"name": "standard",
|
||||||
"version": "cli",
|
"version": "cli",
|
||||||
|
|
@ -8,10 +8,10 @@
|
||||||
"foodSpawnChance": 11,
|
"foodSpawnChance": 11,
|
||||||
"minimumFood": 7,
|
"minimumFood": 7,
|
||||||
"hazardDamagePerTurn": 19,
|
"hazardDamagePerTurn": 19,
|
||||||
"hazardMap": "hz_spiral",
|
"hazardMap": "",
|
||||||
"hazardMapAuthor": "altersaddle",
|
"hazardMapAuthor": "",
|
||||||
"royale": {
|
"royale": {
|
||||||
"shrinkEveryNTurns": 0
|
"shrinkEveryNTurns": 17
|
||||||
},
|
},
|
||||||
"squad": {
|
"squad": {
|
||||||
"allowBodyCollisions": false,
|
"allowBodyCollisions": false,
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"map": "",
|
"map": "standard",
|
||||||
"timeout": 500,
|
"timeout": 500,
|
||||||
"source": ""
|
"source": ""
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"game": {
|
"game": {
|
||||||
"id": "",
|
"id": "GAME_ID",
|
||||||
"ruleset": {
|
"ruleset": {
|
||||||
"name": "wrapped",
|
"name": "wrapped",
|
||||||
"version": "cli",
|
"version": "cli",
|
||||||
|
|
@ -8,10 +8,10 @@
|
||||||
"foodSpawnChance": 11,
|
"foodSpawnChance": 11,
|
||||||
"minimumFood": 7,
|
"minimumFood": 7,
|
||||||
"hazardDamagePerTurn": 19,
|
"hazardDamagePerTurn": 19,
|
||||||
"hazardMap": "hz_spiral",
|
"hazardMap": "",
|
||||||
"hazardMapAuthor": "altersaddle",
|
"hazardMapAuthor": "",
|
||||||
"royale": {
|
"royale": {
|
||||||
"shrinkEveryNTurns": 0
|
"shrinkEveryNTurns": 17
|
||||||
},
|
},
|
||||||
"squad": {
|
"squad": {
|
||||||
"allowBodyCollisions": false,
|
"allowBodyCollisions": false,
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"map": "",
|
"map": "standard",
|
||||||
"timeout": 500,
|
"timeout": 500,
|
||||||
"source": ""
|
"source": ""
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ const (
|
||||||
EliminatedByOutOfHealth = "out-of-health"
|
EliminatedByOutOfHealth = "out-of-health"
|
||||||
EliminatedByHeadToHeadCollision = "head-collision"
|
EliminatedByHeadToHeadCollision = "head-collision"
|
||||||
EliminatedByOutOfBounds = "wall-collision"
|
EliminatedByOutOfBounds = "wall-collision"
|
||||||
EliminatedBySquad = "squad-eliminated"
|
|
||||||
|
|
||||||
// Error constants
|
// Error constants
|
||||||
ErrorTooManySnakes = RulesetError("too many snakes for fixed start positions")
|
ErrorTooManySnakes = RulesetError("too many snakes for fixed start positions")
|
||||||
|
|
@ -41,7 +40,6 @@ const (
|
||||||
GameTypeConstrictor = "constrictor"
|
GameTypeConstrictor = "constrictor"
|
||||||
GameTypeRoyale = "royale"
|
GameTypeRoyale = "royale"
|
||||||
GameTypeSolo = "solo"
|
GameTypeSolo = "solo"
|
||||||
GameTypeSquad = "squad"
|
|
||||||
GameTypeStandard = "standard"
|
GameTypeStandard = "standard"
|
||||||
GameTypeWrapped = "wrapped"
|
GameTypeWrapped = "wrapped"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,10 @@ func TestSetupBoard_Error(t *testing.T) {
|
||||||
Id: t.Name(),
|
Id: t.Name(),
|
||||||
Error: errors.New("bad map update"),
|
Error: errors.New("bad map update"),
|
||||||
}
|
}
|
||||||
RegisterMap(testMap.ID(), testMap)
|
TestMap(testMap.ID(), testMap, func() {
|
||||||
|
|
||||||
_, err := SetupBoard(testMap.ID(), rules.Settings{}, 10, 10, []string{})
|
_, err := SetupBoard(testMap.ID(), rules.Settings{}, 10, 10, []string{})
|
||||||
|
|
||||||
require.EqualError(t, err, "bad map update")
|
require.EqualError(t, err, "bad map update")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSetupBoard(t *testing.T) {
|
func TestSetupBoard(t *testing.T) {
|
||||||
|
|
@ -42,8 +41,8 @@ func TestSetupBoard(t *testing.T) {
|
||||||
{X: 2, Y: 2},
|
{X: 2, Y: 2},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
RegisterMap(testMap.ID(), testMap)
|
|
||||||
|
|
||||||
|
TestMap(testMap.ID(), testMap, func() {
|
||||||
boardState, err := SetupBoard(testMap.ID(), rules.Settings{}, 10, 10, []string{"1", "2"})
|
boardState, err := SetupBoard(testMap.ID(), rules.Settings{}, 10, 10, []string{"1", "2"})
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
@ -62,6 +61,7 @@ func TestSetupBoard(t *testing.T) {
|
||||||
}, boardState.Snakes[1])
|
}, boardState.Snakes[1])
|
||||||
require.Equal(t, []rules.Point{{X: 1, Y: 1}, {X: 5, Y: 3}}, boardState.Food)
|
require.Equal(t, []rules.Point{{X: 1, Y: 1}, {X: 5, Y: 3}}, boardState.Food)
|
||||||
require.Equal(t, []rules.Point{{X: 3, Y: 5}, {X: 2, Y: 2}}, boardState.Hazards)
|
require.Equal(t, []rules.Point{{X: 3, Y: 5}, {X: 2, Y: 2}}, boardState.Hazards)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateBoard(t *testing.T) {
|
func TestUpdateBoard(t *testing.T) {
|
||||||
|
|
@ -80,7 +80,6 @@ func TestUpdateBoard(t *testing.T) {
|
||||||
{X: 2, Y: 2},
|
{X: 2, Y: 2},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
RegisterMap(testMap.ID(), testMap)
|
|
||||||
|
|
||||||
previousBoardState := &rules.BoardState{
|
previousBoardState := &rules.BoardState{
|
||||||
Turn: 0,
|
Turn: 0,
|
||||||
|
|
@ -98,6 +97,8 @@ func TestUpdateBoard(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TestMap(testMap.ID(), testMap, func() {
|
||||||
boardState, err := UpdateBoard(testMap.ID(), previousBoardState, rules.Settings{})
|
boardState, err := UpdateBoard(testMap.ID(), previousBoardState, rules.Settings{})
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
@ -111,4 +112,5 @@ func TestUpdateBoard(t *testing.T) {
|
||||||
}, boardState.Snakes[0])
|
}, boardState.Snakes[0])
|
||||||
require.Equal(t, []rules.Point{{X: 0, Y: 1}, {X: 1, Y: 1}, {X: 5, Y: 3}}, boardState.Food)
|
require.Equal(t, []rules.Point{{X: 0, Y: 1}, {X: 1, Y: 1}, {X: 5, Y: 3}}, boardState.Food)
|
||||||
require.Equal(t, []rules.Point{{X: 3, Y: 4}, {X: 3, Y: 5}, {X: 2, Y: 2}}, boardState.Hazards)
|
require.Equal(t, []rules.Point{{X: 3, Y: 4}, {X: 3, Y: 5}, {X: 2, Y: 2}}, boardState.Hazards)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,3 +38,9 @@ func GetMap(id string) (GameMap, error) {
|
||||||
func RegisterMap(id string, m GameMap) {
|
func RegisterMap(id string, m GameMap) {
|
||||||
globalRegistry.RegisterMap(id, m)
|
globalRegistry.RegisterMap(id, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMap(id string, m GameMap, callback func()) {
|
||||||
|
globalRegistry[id] = m
|
||||||
|
callback()
|
||||||
|
delete(globalRegistry, id)
|
||||||
|
}
|
||||||
|
|
|
||||||
68
maps/registry_test.go
Normal file
68
maps/registry_test.go
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
package maps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/BattlesnakeOfficial/rules"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxBoardWidth, maxBoardHeight = 25, 25
|
||||||
|
|
||||||
|
var testSettings rules.Settings = rules.Settings{
|
||||||
|
FoodSpawnChance: 25,
|
||||||
|
MinimumFood: 1,
|
||||||
|
HazardDamagePerTurn: 14,
|
||||||
|
RoyaleSettings: rules.RoyaleSettings{
|
||||||
|
ShrinkEveryNTurns: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisteredMaps(t *testing.T) {
|
||||||
|
for mapName, gameMap := range globalRegistry {
|
||||||
|
t.Run(mapName, func(t *testing.T) {
|
||||||
|
require.Equalf(t, mapName, gameMap.ID(), "%#v game map doesn't return its own ID", mapName)
|
||||||
|
|
||||||
|
var setupBoardState *rules.BoardState
|
||||||
|
|
||||||
|
for width := 0; width < maxBoardWidth; width++ {
|
||||||
|
for height := 0; height < maxBoardHeight; height++ {
|
||||||
|
initialBoardState := rules.NewBoardState(width, height)
|
||||||
|
initialBoardState.Snakes = append(initialBoardState.Snakes, rules.Snake{ID: "1", Body: []rules.Point{}})
|
||||||
|
initialBoardState.Snakes = append(initialBoardState.Snakes, rules.Snake{ID: "2", Body: []rules.Point{}})
|
||||||
|
passedBoardState := initialBoardState.Clone()
|
||||||
|
tempBoardState := initialBoardState.Clone()
|
||||||
|
err := gameMap.SetupBoard(passedBoardState, testSettings, NewBoardStateEditor(tempBoardState))
|
||||||
|
if err == nil {
|
||||||
|
setupBoardState = tempBoardState
|
||||||
|
require.Equal(t, initialBoardState, passedBoardState, "BoardState should not be modified directly by GameMap.SetupBoard")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotNil(t, setupBoardState, "Map does not successfully setup the board at any supported combination of width and height")
|
||||||
|
require.NotNil(t, setupBoardState.Food)
|
||||||
|
require.NotNil(t, setupBoardState.Hazards)
|
||||||
|
require.NotNil(t, setupBoardState.Snakes)
|
||||||
|
for _, snake := range setupBoardState.Snakes {
|
||||||
|
require.NotEmpty(t, snake.Body, "Map should place all snakes by initializing their body")
|
||||||
|
}
|
||||||
|
|
||||||
|
previousBoardState := rules.NewBoardState(rules.BoardSizeMedium, rules.BoardSizeMedium)
|
||||||
|
previousBoardState.Food = append(previousBoardState.Food, []rules.Point{{X: 1, Y: 2}, {X: 3, Y: 4}}...)
|
||||||
|
previousBoardState.Hazards = append(previousBoardState.Food, []rules.Point{{X: 4, Y: 3}, {X: 2, Y: 1}}...)
|
||||||
|
previousBoardState.Snakes = append(previousBoardState.Snakes, rules.Snake{
|
||||||
|
ID: "1",
|
||||||
|
Body: []rules.Point{{X: 5, Y: 5}, {X: 5, Y: 4}, {X: 5, Y: 3}},
|
||||||
|
Health: 100,
|
||||||
|
})
|
||||||
|
previousBoardState.Turn = 0
|
||||||
|
|
||||||
|
passedBoardState := previousBoardState.Clone()
|
||||||
|
tempBoardState := previousBoardState.Clone()
|
||||||
|
err := gameMap.UpdateBoard(passedBoardState, testSettings, NewBoardStateEditor(tempBoardState))
|
||||||
|
require.NoError(t, err, "GameMap.UpdateBoard returned an error")
|
||||||
|
require.Equal(t, previousBoardState, passedBoardState, "BoardState should not be modified directly by GameMap.UpdateBoard")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,6 +29,11 @@ func (m RoyaleHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m RoyaleHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m RoyaleHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
// Use StandardMap to populate food
|
||||||
|
if err := (StandardMap{}).UpdateBoard(lastBoardState, settings, editor); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Royale uses the current turn to generate hazards, not the previous turn that's in the board state
|
// Royale uses the current turn to generate hazards, not the previous turn that's in the board state
|
||||||
turn := lastBoardState.Turn + 1
|
turn := lastBoardState.Turn + 1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,8 @@ const (
|
||||||
StageEliminationStandard = "elimination.standard"
|
StageEliminationStandard = "elimination.standard"
|
||||||
|
|
||||||
StageGameOverSoloSnake = "game_over.solo_snake"
|
StageGameOverSoloSnake = "game_over.solo_snake"
|
||||||
StageGameOverBySquad = "game_over.by_squad"
|
|
||||||
StageSpawnFoodNoFood = "spawn_food.no_food"
|
StageSpawnFoodNoFood = "spawn_food.no_food"
|
||||||
StageSpawnHazardsShrinkMap = "spawn_hazards.shrink_map"
|
StageSpawnHazardsShrinkMap = "spawn_hazards.shrink_map"
|
||||||
StageEliminationResurrectSquadCollisions = "elimination.resurrect_squad_collisions"
|
|
||||||
StageModifySnakesAlwaysGrow = "modify_snakes.always_grow"
|
StageModifySnakesAlwaysGrow = "modify_snakes.always_grow"
|
||||||
StageMovementWrapBoundaries = "movement.wrap_boundaries"
|
StageMovementWrapBoundaries = "movement.wrap_boundaries"
|
||||||
StageModifySnakesShareAttributes = "modify_snakes.share_attributes"
|
StageModifySnakesShareAttributes = "modify_snakes.share_attributes"
|
||||||
|
|
@ -29,18 +27,15 @@ var globalRegistry = StageRegistry{
|
||||||
StageSpawnFoodNoFood: RemoveFoodConstrictor,
|
StageSpawnFoodNoFood: RemoveFoodConstrictor,
|
||||||
StageSpawnFoodStandard: SpawnFoodStandard,
|
StageSpawnFoodStandard: SpawnFoodStandard,
|
||||||
StageGameOverSoloSnake: GameOverSolo,
|
StageGameOverSoloSnake: GameOverSolo,
|
||||||
StageGameOverBySquad: GameOverSquad,
|
|
||||||
StageGameOverStandard: GameOverStandard,
|
StageGameOverStandard: GameOverStandard,
|
||||||
StageHazardDamageStandard: DamageHazardsStandard,
|
StageHazardDamageStandard: DamageHazardsStandard,
|
||||||
StageSpawnHazardsShrinkMap: PopulateHazardsRoyale,
|
StageSpawnHazardsShrinkMap: PopulateHazardsRoyale,
|
||||||
StageStarvationStandard: ReduceSnakeHealthStandard,
|
StageStarvationStandard: ReduceSnakeHealthStandard,
|
||||||
StageEliminationResurrectSquadCollisions: ResurrectSnakesSquad,
|
|
||||||
StageFeedSnakesStandard: FeedSnakesStandard,
|
StageFeedSnakesStandard: FeedSnakesStandard,
|
||||||
StageEliminationStandard: EliminateSnakesStandard,
|
StageEliminationStandard: EliminateSnakesStandard,
|
||||||
StageModifySnakesAlwaysGrow: GrowSnakesConstrictor,
|
StageModifySnakesAlwaysGrow: GrowSnakesConstrictor,
|
||||||
StageMovementStandard: MoveSnakesStandard,
|
StageMovementStandard: MoveSnakesStandard,
|
||||||
StageMovementWrapBoundaries: MoveSnakesWrapped,
|
StageMovementWrapBoundaries: MoveSnakesWrapped,
|
||||||
StageModifySnakesShareAttributes: ShareAttributesSquad,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StageFunc represents a single stage of an ordered pipeline and applies custom logic to the board state each turn.
|
// StageFunc represents a single stage of an ordered pipeline and applies custom logic to the board state each turn.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package rules
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"math/rand"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var royaleRulesetStages = []string{
|
var royaleRulesetStages = []string{
|
||||||
|
|
@ -10,7 +9,6 @@ var royaleRulesetStages = []string{
|
||||||
StageStarvationStandard,
|
StageStarvationStandard,
|
||||||
StageHazardDamageStandard,
|
StageHazardDamageStandard,
|
||||||
StageFeedSnakesStandard,
|
StageFeedSnakesStandard,
|
||||||
StageSpawnFoodStandard,
|
|
||||||
StageEliminationStandard,
|
StageEliminationStandard,
|
||||||
StageSpawnHazardsShrinkMap,
|
StageSpawnHazardsShrinkMap,
|
||||||
StageGameOverStandard,
|
StageGameOverStandard,
|
||||||
|
|
@ -19,8 +17,6 @@ var royaleRulesetStages = []string{
|
||||||
type RoyaleRuleset struct {
|
type RoyaleRuleset struct {
|
||||||
StandardRuleset
|
StandardRuleset
|
||||||
|
|
||||||
Seed int64
|
|
||||||
|
|
||||||
ShrinkEveryNTurns int
|
ShrinkEveryNTurns int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,7 +51,7 @@ func PopulateHazardsRoyale(b *BoardState, settings Settings, moves []SnakeMove)
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
randGenerator := rand.New(rand.NewSource(settings.RoyaleSettings.seed))
|
randGenerator := settings.GetRand(0)
|
||||||
|
|
||||||
numShrinks := turn / settings.RoyaleSettings.ShrinkEveryNTurns
|
numShrinks := turn / settings.RoyaleSettings.ShrinkEveryNTurns
|
||||||
minX, maxX := 0, b.Width-1
|
minX, maxX := 0, b.Width-1
|
||||||
|
|
@ -91,7 +87,6 @@ func (r *RoyaleRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||||
func (r RoyaleRuleset) Settings() Settings {
|
func (r RoyaleRuleset) Settings() Settings {
|
||||||
s := r.StandardRuleset.Settings()
|
s := r.StandardRuleset.Settings()
|
||||||
s.RoyaleSettings = RoyaleSettings{
|
s.RoyaleSettings = RoyaleSettings{
|
||||||
seed: r.Seed,
|
|
||||||
ShrinkEveryNTurns: r.ShrinkEveryNTurns,
|
ShrinkEveryNTurns: r.ShrinkEveryNTurns,
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
|
|
|
||||||
|
|
@ -94,15 +94,14 @@ func TestRoyaleHazards(t *testing.T) {
|
||||||
Width: test.Width,
|
Width: test.Width,
|
||||||
Height: test.Height,
|
Height: test.Height,
|
||||||
}
|
}
|
||||||
r := RoyaleRuleset{
|
settings := Settings{
|
||||||
StandardRuleset: StandardRuleset{
|
|
||||||
HazardDamagePerTurn: 1,
|
HazardDamagePerTurn: 1,
|
||||||
},
|
RoyaleSettings: RoyaleSettings{
|
||||||
Seed: seed,
|
|
||||||
ShrinkEveryNTurns: test.ShrinkEveryNTurns,
|
ShrinkEveryNTurns: test.ShrinkEveryNTurns,
|
||||||
}
|
},
|
||||||
|
}.WithSeed(seed)
|
||||||
|
|
||||||
_, err := PopulateHazardsRoyale(b, r.Settings(), mockSnakeMoves())
|
_, err := PopulateHazardsRoyale(b, settings, mockSnakeMoves())
|
||||||
require.Equal(t, test.Error, err)
|
require.Equal(t, test.Error, err)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Obstacles should match
|
// Obstacles should match
|
||||||
|
|
@ -121,64 +120,6 @@ func TestRoyaleHazards(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRoyalDamageNextTurn(t *testing.T) {
|
|
||||||
seed := int64(45897034512311)
|
|
||||||
|
|
||||||
base := &BoardState{Width: 10, Height: 10, Snakes: []Snake{{ID: "one", Health: 100, Body: []Point{{9, 1}, {9, 1}, {9, 1}}}}}
|
|
||||||
r := RoyaleRuleset{StandardRuleset: StandardRuleset{HazardDamagePerTurn: 30}, Seed: seed, ShrinkEveryNTurns: 10}
|
|
||||||
m := []SnakeMove{{ID: "one", Move: "down"}}
|
|
||||||
|
|
||||||
stateAfterTurn := func(prevState *BoardState, turn int) *BoardState {
|
|
||||||
nextState := prevState.Clone()
|
|
||||||
nextState.Turn = turn - 1
|
|
||||||
_, err := PopulateHazardsRoyale(nextState, r.Settings(), nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
nextState.Turn = turn
|
|
||||||
return nextState
|
|
||||||
}
|
|
||||||
|
|
||||||
prevState := stateAfterTurn(base, 9)
|
|
||||||
next, err := r.CreateNextBoardState(prevState, m)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, NotEliminated, next.Snakes[0].EliminatedCause)
|
|
||||||
require.Equal(t, 99, next.Snakes[0].Health)
|
|
||||||
require.Equal(t, Point{9, 0}, next.Snakes[0].Body[0])
|
|
||||||
require.Equal(t, 10, len(next.Hazards)) // X = 0
|
|
||||||
|
|
||||||
prevState = stateAfterTurn(base, 19)
|
|
||||||
next, err = r.CreateNextBoardState(prevState, m)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, NotEliminated, next.Snakes[0].EliminatedCause)
|
|
||||||
require.Equal(t, 99, next.Snakes[0].Health)
|
|
||||||
require.Equal(t, Point{9, 0}, next.Snakes[0].Body[0])
|
|
||||||
require.Equal(t, 20, len(next.Hazards)) // X = 9
|
|
||||||
|
|
||||||
prevState = stateAfterTurn(base, 20)
|
|
||||||
next, err = r.CreateNextBoardState(prevState, m)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, NotEliminated, next.Snakes[0].EliminatedCause)
|
|
||||||
require.Equal(t, 69, next.Snakes[0].Health)
|
|
||||||
require.Equal(t, Point{9, 0}, next.Snakes[0].Body[0])
|
|
||||||
require.Equal(t, 20, len(next.Hazards))
|
|
||||||
|
|
||||||
prevState.Snakes[0].Health = 15
|
|
||||||
next, err = r.CreateNextBoardState(prevState, m)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, EliminatedByOutOfHealth, next.Snakes[0].EliminatedCause)
|
|
||||||
require.Equal(t, 0, next.Snakes[0].Health)
|
|
||||||
require.Equal(t, Point{9, 0}, next.Snakes[0].Body[0])
|
|
||||||
require.Equal(t, 20, len(next.Hazards))
|
|
||||||
|
|
||||||
prevState.Food = append(prevState.Food, Point{9, 0})
|
|
||||||
next, err = r.CreateNextBoardState(prevState, m)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, Point{9, 0}, next.Snakes[0].Body[0])
|
|
||||||
require.Equal(t, NotEliminated, next.Snakes[0].EliminatedCause)
|
|
||||||
require.Equal(t, 100, next.Snakes[0].Health)
|
|
||||||
require.Equal(t, Point{9, 0}, next.Snakes[0].Body[0])
|
|
||||||
require.Equal(t, 20, len(next.Hazards))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks that hazards get placed
|
// Checks that hazards get placed
|
||||||
// also that:
|
// also that:
|
||||||
// - snakes move properly
|
// - snakes move properly
|
||||||
|
|
@ -264,13 +205,13 @@ func TestRoyaleCreateNextBoardState(t *testing.T) {
|
||||||
},
|
},
|
||||||
ShrinkEveryNTurns: 1,
|
ShrinkEveryNTurns: 1,
|
||||||
}
|
}
|
||||||
rand.Seed(0)
|
|
||||||
rb := NewRulesetBuilder().WithParams(map[string]string{
|
rb := NewRulesetBuilder().WithParams(map[string]string{
|
||||||
ParamGameType: GameTypeRoyale,
|
ParamGameType: GameTypeRoyale,
|
||||||
ParamHazardDamagePerTurn: "1",
|
ParamHazardDamagePerTurn: "1",
|
||||||
ParamShrinkEveryNTurns: "1",
|
ParamShrinkEveryNTurns: "1",
|
||||||
})
|
}).WithSeed(1234)
|
||||||
for _, gc := range cases {
|
for _, gc := range cases {
|
||||||
|
rand.Seed(1234)
|
||||||
gc.requireValidNextState(t, &r)
|
gc.requireValidNextState(t, &r)
|
||||||
// also test a RulesBuilder constructed instance
|
// also test a RulesBuilder constructed instance
|
||||||
gc.requireValidNextState(t, rb.Ruleset())
|
gc.requireValidNextState(t, rb.Ruleset())
|
||||||
|
|
|
||||||
73
ruleset.go
73
ruleset.go
|
|
@ -27,7 +27,7 @@ type Settings struct {
|
||||||
HazardMap string `json:"hazardMap"`
|
HazardMap string `json:"hazardMap"`
|
||||||
HazardMapAuthor string `json:"hazardMapAuthor"`
|
HazardMapAuthor string `json:"hazardMapAuthor"`
|
||||||
RoyaleSettings RoyaleSettings `json:"royale"`
|
RoyaleSettings RoyaleSettings `json:"royale"`
|
||||||
SquadSettings SquadSettings `json:"squad"`
|
SquadSettings SquadSettings `json:"squad"` // Deprecated, provided with default fields for API compatibility
|
||||||
|
|
||||||
rand Rand
|
rand Rand
|
||||||
seed int64
|
seed int64
|
||||||
|
|
@ -41,7 +41,7 @@ func (settings Settings) GetRand(turn int) Rand {
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.seed != 0 {
|
if settings.seed != 0 {
|
||||||
return NewSeedRand(settings.seed + int64(turn+1))
|
return NewSeedRand(settings.seed + int64(turn))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to global random number generator if neither seed or rand are set.
|
// Default to global random number generator if neither seed or rand are set.
|
||||||
|
|
@ -64,13 +64,11 @@ func (settings Settings) WithSeed(seed int64) Settings {
|
||||||
|
|
||||||
// RoyaleSettings contains settings that are specific to the "royale" game mode
|
// RoyaleSettings contains settings that are specific to the "royale" game mode
|
||||||
type RoyaleSettings struct {
|
type RoyaleSettings struct {
|
||||||
seed int64
|
|
||||||
ShrinkEveryNTurns int `json:"shrinkEveryNTurns"`
|
ShrinkEveryNTurns int `json:"shrinkEveryNTurns"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SquadSettings contains settings that are specific to the "squad" game mode
|
// SquadSettings contains settings that are specific to the "squad" game mode
|
||||||
type SquadSettings struct {
|
type SquadSettings struct {
|
||||||
squadMap map[string]string
|
|
||||||
AllowBodyCollisions bool `json:"allowBodyCollisions"`
|
AllowBodyCollisions bool `json:"allowBodyCollisions"`
|
||||||
SharedElimination bool `json:"sharedElimination"`
|
SharedElimination bool `json:"sharedElimination"`
|
||||||
SharedHealth bool `json:"sharedHealth"`
|
SharedHealth bool `json:"sharedHealth"`
|
||||||
|
|
@ -81,14 +79,12 @@ type rulesetBuilder struct {
|
||||||
params map[string]string // game customisation parameters
|
params map[string]string // game customisation parameters
|
||||||
seed int64 // used for random events in games
|
seed int64 // used for random events in games
|
||||||
rand Rand // used for random number generation
|
rand Rand // used for random number generation
|
||||||
squads map[string]string // Snake ID -> Squad Name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRulesetBuilder returns an instance of a builder for the Ruleset types.
|
// NewRulesetBuilder returns an instance of a builder for the Ruleset types.
|
||||||
func NewRulesetBuilder() *rulesetBuilder {
|
func NewRulesetBuilder() *rulesetBuilder {
|
||||||
return &rulesetBuilder{
|
return &rulesetBuilder{
|
||||||
params: map[string]string{},
|
params: map[string]string{},
|
||||||
squads: map[string]string{},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,66 +118,27 @@ func (rb *rulesetBuilder) WithRand(rand Rand) *rulesetBuilder {
|
||||||
return rb
|
return rb
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddSnakeToSquad adds the specified snake (by ID) to a squad with the given name.
|
|
||||||
// This configuration may be ignored by game modes if they do not support squads.
|
|
||||||
func (rb *rulesetBuilder) AddSnakeToSquad(snakeID, squadName string) *rulesetBuilder {
|
|
||||||
rb.squads[snakeID] = squadName
|
|
||||||
return rb
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ruleset constructs a customised ruleset using the parameters passed to the builder.
|
// Ruleset constructs a customised ruleset using the parameters passed to the builder.
|
||||||
func (rb rulesetBuilder) Ruleset() PipelineRuleset {
|
func (rb rulesetBuilder) Ruleset() PipelineRuleset {
|
||||||
standardRuleset := &StandardRuleset{
|
|
||||||
FoodSpawnChance: paramsInt(rb.params, ParamFoodSpawnChance, 0),
|
|
||||||
MinimumFood: paramsInt(rb.params, ParamMinimumFood, 0),
|
|
||||||
HazardDamagePerTurn: paramsInt(rb.params, ParamHazardDamagePerTurn, 0),
|
|
||||||
HazardMap: rb.params[ParamHazardMap],
|
|
||||||
HazardMapAuthor: rb.params[ParamHazardMapAuthor],
|
|
||||||
}
|
|
||||||
|
|
||||||
name, ok := rb.params[ParamGameType]
|
name, ok := rb.params[ParamGameType]
|
||||||
if !ok {
|
if !ok {
|
||||||
return standardRuleset
|
name = GameTypeStandard
|
||||||
}
|
}
|
||||||
|
|
||||||
switch name {
|
switch name {
|
||||||
|
case GameTypeStandard:
|
||||||
|
return rb.PipelineRuleset(name, NewPipeline(standardRulesetStages...))
|
||||||
case GameTypeConstrictor:
|
case GameTypeConstrictor:
|
||||||
return &ConstrictorRuleset{
|
return rb.PipelineRuleset(name, NewPipeline(constrictorRulesetStages...))
|
||||||
StandardRuleset: *standardRuleset,
|
|
||||||
}
|
|
||||||
case GameTypeRoyale:
|
case GameTypeRoyale:
|
||||||
return &RoyaleRuleset{
|
return rb.PipelineRuleset(name, NewPipeline(royaleRulesetStages...))
|
||||||
StandardRuleset: *standardRuleset,
|
|
||||||
Seed: rb.seed,
|
|
||||||
ShrinkEveryNTurns: paramsInt(rb.params, ParamShrinkEveryNTurns, 0),
|
|
||||||
}
|
|
||||||
case GameTypeSolo:
|
case GameTypeSolo:
|
||||||
return &SoloRuleset{
|
return rb.PipelineRuleset(name, NewPipeline(soloRulesetStages...))
|
||||||
StandardRuleset: *standardRuleset,
|
|
||||||
}
|
|
||||||
case GameTypeWrapped:
|
case GameTypeWrapped:
|
||||||
return &WrappedRuleset{
|
return rb.PipelineRuleset(name, NewPipeline(wrappedRulesetStages...))
|
||||||
StandardRuleset: *standardRuleset,
|
default:
|
||||||
|
return rb.PipelineRuleset(name, NewPipeline(standardRulesetStages...))
|
||||||
}
|
}
|
||||||
case GameTypeSquad:
|
|
||||||
return &SquadRuleset{
|
|
||||||
StandardRuleset: *standardRuleset,
|
|
||||||
SquadMap: rb.squadMap(),
|
|
||||||
AllowBodyCollisions: paramsBool(rb.params, ParamAllowBodyCollisions, false),
|
|
||||||
SharedElimination: paramsBool(rb.params, ParamSharedElimination, false),
|
|
||||||
SharedHealth: paramsBool(rb.params, ParamSharedHealth, false),
|
|
||||||
SharedLength: paramsBool(rb.params, ParamSharedLength, false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return standardRuleset
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rb rulesetBuilder) squadMap() map[string]string {
|
|
||||||
squadMap := map[string]string{}
|
|
||||||
for id, squad := range rb.squads {
|
|
||||||
squadMap[id] = squad
|
|
||||||
}
|
|
||||||
return squadMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PipelineRuleset provides an implementation of the Ruleset using a pipeline with a name.
|
// PipelineRuleset provides an implementation of the Ruleset using a pipeline with a name.
|
||||||
|
|
@ -198,16 +155,8 @@ func (rb rulesetBuilder) PipelineRuleset(name string, p Pipeline) PipelineRulese
|
||||||
HazardMap: rb.params[ParamHazardMap],
|
HazardMap: rb.params[ParamHazardMap],
|
||||||
HazardMapAuthor: rb.params[ParamHazardMapAuthor],
|
HazardMapAuthor: rb.params[ParamHazardMapAuthor],
|
||||||
RoyaleSettings: RoyaleSettings{
|
RoyaleSettings: RoyaleSettings{
|
||||||
seed: rb.seed,
|
|
||||||
ShrinkEveryNTurns: paramsInt(rb.params, ParamShrinkEveryNTurns, 0),
|
ShrinkEveryNTurns: paramsInt(rb.params, ParamShrinkEveryNTurns, 0),
|
||||||
},
|
},
|
||||||
SquadSettings: SquadSettings{
|
|
||||||
squadMap: rb.squadMap(),
|
|
||||||
AllowBodyCollisions: paramsBool(rb.params, ParamAllowBodyCollisions, false),
|
|
||||||
SharedElimination: paramsBool(rb.params, ParamSharedElimination, false),
|
|
||||||
SharedHealth: paramsBool(rb.params, ParamSharedHealth, false),
|
|
||||||
SharedLength: paramsBool(rb.params, ParamSharedLength, false),
|
|
||||||
},
|
|
||||||
rand: rb.rand,
|
rand: rb.rand,
|
||||||
seed: rb.seed,
|
seed: rb.seed,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -41,30 +41,11 @@ func TestRulesetError(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRulesetBuilderInternals(t *testing.T) {
|
func TestRulesetBuilderInternals(t *testing.T) {
|
||||||
|
|
||||||
// test Royale with seed
|
// test Royale with seed
|
||||||
rsb := NewRulesetBuilder().WithSeed(3).WithParams(map[string]string{ParamGameType: GameTypeRoyale})
|
rsb := NewRulesetBuilder().WithSeed(3).WithParams(map[string]string{ParamGameType: GameTypeRoyale})
|
||||||
require.Equal(t, int64(3), rsb.seed)
|
require.Equal(t, int64(3), rsb.seed)
|
||||||
require.Equal(t, GameTypeRoyale, rsb.Ruleset().Name())
|
require.Equal(t, GameTypeRoyale, rsb.Ruleset().Name())
|
||||||
require.Equal(t, int64(3), rsb.Ruleset().(*RoyaleRuleset).Seed)
|
require.Equal(t, int64(3), rsb.Ruleset().Settings().Seed())
|
||||||
|
|
||||||
// test squad configuration
|
|
||||||
rsb = NewRulesetBuilder().
|
|
||||||
WithParams(map[string]string{
|
|
||||||
ParamGameType: GameTypeSquad,
|
|
||||||
}).
|
|
||||||
AddSnakeToSquad("snek1", "squad1").
|
|
||||||
AddSnakeToSquad("snek2", "squad1").
|
|
||||||
AddSnakeToSquad("snek3", "squad2").
|
|
||||||
AddSnakeToSquad("snek4", "squad2")
|
|
||||||
|
|
||||||
require.NotNil(t, rsb.Ruleset())
|
|
||||||
require.Equal(t, GameTypeSquad, rsb.Ruleset().Name())
|
|
||||||
require.Equal(t, 4, len(rsb.squads))
|
|
||||||
require.Equal(t, "squad1", rsb.Ruleset().(*SquadRuleset).SquadMap["snek1"])
|
|
||||||
require.Equal(t, "squad1", rsb.Ruleset().(*SquadRuleset).SquadMap["snek2"])
|
|
||||||
require.Equal(t, "squad2", rsb.Ruleset().(*SquadRuleset).SquadMap["snek3"])
|
|
||||||
require.Equal(t, "squad2", rsb.Ruleset().(*SquadRuleset).SquadMap["snek4"])
|
|
||||||
|
|
||||||
// test parameter merging
|
// test parameter merging
|
||||||
rsb = NewRulesetBuilder().
|
rsb = NewRulesetBuilder().
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,6 @@ func TestSoloRulesetSettings(t *testing.T) {
|
||||||
|
|
||||||
func TestRoyaleRulesetSettings(t *testing.T) {
|
func TestRoyaleRulesetSettings(t *testing.T) {
|
||||||
ruleset := rules.RoyaleRuleset{
|
ruleset := rules.RoyaleRuleset{
|
||||||
Seed: 30,
|
|
||||||
ShrinkEveryNTurns: 12,
|
ShrinkEveryNTurns: 12,
|
||||||
StandardRuleset: rules.StandardRuleset{
|
StandardRuleset: rules.StandardRuleset{
|
||||||
MinimumFood: 5,
|
MinimumFood: 5,
|
||||||
|
|
@ -94,32 +93,6 @@ func TestConstrictorRulesetSettings(t *testing.T) {
|
||||||
assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor)
|
assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSquadRulesetSettings(t *testing.T) {
|
|
||||||
ruleset := rules.SquadRuleset{
|
|
||||||
AllowBodyCollisions: true,
|
|
||||||
SharedElimination: false,
|
|
||||||
SharedHealth: true,
|
|
||||||
SharedLength: false,
|
|
||||||
StandardRuleset: rules.StandardRuleset{
|
|
||||||
MinimumFood: 5,
|
|
||||||
FoodSpawnChance: 10,
|
|
||||||
HazardDamagePerTurn: 10,
|
|
||||||
HazardMap: "hz_spiral",
|
|
||||||
HazardMapAuthor: "altersaddle",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert.Equal(t, ruleset.AllowBodyCollisions, ruleset.Settings().SquadSettings.AllowBodyCollisions)
|
|
||||||
assert.Equal(t, ruleset.SharedElimination, ruleset.Settings().SquadSettings.SharedElimination)
|
|
||||||
assert.Equal(t, ruleset.SharedHealth, ruleset.Settings().SquadSettings.SharedHealth)
|
|
||||||
assert.Equal(t, ruleset.SharedLength, ruleset.Settings().SquadSettings.SharedLength)
|
|
||||||
|
|
||||||
assert.Equal(t, ruleset.MinimumFood, ruleset.Settings().MinimumFood)
|
|
||||||
assert.Equal(t, ruleset.FoodSpawnChance, ruleset.Settings().FoodSpawnChance)
|
|
||||||
assert.Equal(t, ruleset.HazardDamagePerTurn, ruleset.Settings().HazardDamagePerTurn)
|
|
||||||
assert.Equal(t, ruleset.HazardMap, ruleset.Settings().HazardMap)
|
|
||||||
assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRulesetBuilder(t *testing.T) {
|
func TestRulesetBuilder(t *testing.T) {
|
||||||
// Test that a fresh instance can produce a Ruleset
|
// Test that a fresh instance can produce a Ruleset
|
||||||
require.NotNil(t, rules.NewRulesetBuilder().Ruleset())
|
require.NotNil(t, rules.NewRulesetBuilder().Ruleset())
|
||||||
|
|
@ -131,22 +104,11 @@ func TestRulesetBuilder(t *testing.T) {
|
||||||
// make sure it works okay for lots of game types
|
// make sure it works okay for lots of game types
|
||||||
expectedResults := []struct {
|
expectedResults := []struct {
|
||||||
GameType string
|
GameType string
|
||||||
Snakes map[string]string
|
|
||||||
}{
|
}{
|
||||||
{GameType: rules.GameTypeStandard},
|
{GameType: rules.GameTypeStandard},
|
||||||
{GameType: rules.GameTypeWrapped},
|
{GameType: rules.GameTypeWrapped},
|
||||||
{GameType: rules.GameTypeRoyale},
|
{GameType: rules.GameTypeRoyale},
|
||||||
{GameType: rules.GameTypeSolo},
|
{GameType: rules.GameTypeSolo},
|
||||||
{GameType: rules.GameTypeSquad, Snakes: map[string]string{
|
|
||||||
"one": "s1",
|
|
||||||
"two": "s1",
|
|
||||||
"three": "s2",
|
|
||||||
"four": "s2",
|
|
||||||
"five": "s3",
|
|
||||||
"six": "s3",
|
|
||||||
"seven": "s4",
|
|
||||||
"eight": "s4",
|
|
||||||
}},
|
|
||||||
{GameType: rules.GameTypeConstrictor},
|
{GameType: rules.GameTypeConstrictor},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,11 +126,6 @@ func TestRulesetBuilder(t *testing.T) {
|
||||||
rules.ParamHazardMapAuthor: "tester",
|
rules.ParamHazardMapAuthor: "tester",
|
||||||
})
|
})
|
||||||
|
|
||||||
// add any snake squads
|
|
||||||
for id, squad := range expected.Snakes {
|
|
||||||
rsb = rsb.AddSnakeToSquad(id, squad)
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NotNil(t, rsb.Ruleset())
|
require.NotNil(t, rsb.Ruleset())
|
||||||
require.Equal(t, expected.GameType, rsb.Ruleset().Name())
|
require.Equal(t, expected.GameType, rsb.Ruleset().Name())
|
||||||
// All the standard settings should always be copied over
|
// All the standard settings should always be copied over
|
||||||
|
|
@ -200,8 +157,8 @@ func TestRulesetBuilderGetRand(t *testing.T) {
|
||||||
rand1 := ruleset.Settings().GetRand(turn)
|
rand1 := ruleset.Settings().GetRand(turn)
|
||||||
|
|
||||||
// Should produce a predictable series of numbers based on a seed
|
// Should produce a predictable series of numbers based on a seed
|
||||||
require.Equal(t, 80, rand1.Intn(100))
|
require.Equal(t, 83, rand1.Intn(100))
|
||||||
require.Equal(t, 94, rand1.Intn(100))
|
require.Equal(t, 15, rand1.Intn(100))
|
||||||
|
|
||||||
// Should produce the same number if re-initialized
|
// Should produce the same number if re-initialized
|
||||||
require.Equal(
|
require.Equal(
|
||||||
|
|
@ -211,6 +168,6 @@ func TestRulesetBuilderGetRand(t *testing.T) {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Should produce a different series of numbers for another turn
|
// Should produce a different series of numbers for another turn
|
||||||
require.Equal(t, 22, rand1.Intn(100))
|
require.Equal(t, 69, rand1.Intn(100))
|
||||||
require.Equal(t, 16, rand1.Intn(100))
|
require.Equal(t, 86, rand1.Intn(100))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
solo.go
1
solo.go
|
|
@ -5,7 +5,6 @@ var soloRulesetStages = []string{
|
||||||
StageStarvationStandard,
|
StageStarvationStandard,
|
||||||
StageHazardDamageStandard,
|
StageHazardDamageStandard,
|
||||||
StageFeedSnakesStandard,
|
StageFeedSnakesStandard,
|
||||||
StageSpawnFoodStandard,
|
|
||||||
StageEliminationStandard,
|
StageEliminationStandard,
|
||||||
StageGameOverSoloSnake,
|
StageGameOverSoloSnake,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
152
squad.go
152
squad.go
|
|
@ -1,152 +0,0 @@
|
||||||
package rules
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var squadRulesetStages = []string{
|
|
||||||
StageMovementStandard,
|
|
||||||
StageStarvationStandard,
|
|
||||||
StageHazardDamageStandard,
|
|
||||||
StageFeedSnakesStandard,
|
|
||||||
StageSpawnFoodStandard,
|
|
||||||
StageEliminationStandard,
|
|
||||||
StageEliminationResurrectSquadCollisions,
|
|
||||||
StageModifySnakesShareAttributes,
|
|
||||||
StageGameOverBySquad,
|
|
||||||
}
|
|
||||||
|
|
||||||
type SquadRuleset struct {
|
|
||||||
StandardRuleset
|
|
||||||
|
|
||||||
SquadMap map[string]string
|
|
||||||
|
|
||||||
// These are intentionally designed so that they default to a standard game.
|
|
||||||
AllowBodyCollisions bool
|
|
||||||
SharedElimination bool
|
|
||||||
SharedHealth bool
|
|
||||||
SharedLength bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *SquadRuleset) Name() string { return GameTypeSquad }
|
|
||||||
|
|
||||||
func (r SquadRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) {
|
|
||||||
return NewPipeline(squadRulesetStages...).Execute(bs, s, sm)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *SquadRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
|
||||||
_, nextState, err := r.Execute(prevState, r.Settings(), moves)
|
|
||||||
return nextState, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func areSnakesOnSameSquad(squadMap map[string]string, snake *Snake, other *Snake) bool {
|
|
||||||
return areSnakeIDsOnSameSquad(squadMap, snake.ID, other.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func areSnakeIDsOnSameSquad(squadMap map[string]string, snakeID string, otherID string) bool {
|
|
||||||
return squadMap[snakeID] == squadMap[otherID]
|
|
||||||
}
|
|
||||||
|
|
||||||
func ResurrectSnakesSquad(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
|
||||||
if IsInitialization(b, settings, moves) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
if !settings.SquadSettings.AllowBodyCollisions {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
|
||||||
snake := &b.Snakes[i]
|
|
||||||
if snake.EliminatedCause == EliminatedByCollision {
|
|
||||||
if snake.EliminatedBy == "" {
|
|
||||||
return false, errors.New("snake eliminated by collision and eliminatedby is not set")
|
|
||||||
}
|
|
||||||
if snake.ID != snake.EliminatedBy && areSnakeIDsOnSameSquad(settings.SquadSettings.squadMap, snake.ID, snake.EliminatedBy) {
|
|
||||||
snake.EliminatedCause = NotEliminated
|
|
||||||
snake.EliminatedBy = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ShareAttributesSquad(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
|
||||||
if IsInitialization(b, settings, moves) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
squadSettings := settings.SquadSettings
|
|
||||||
|
|
||||||
if !(squadSettings.SharedElimination || squadSettings.SharedLength || squadSettings.SharedHealth) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
|
||||||
snake := &b.Snakes[i]
|
|
||||||
if snake.EliminatedCause != NotEliminated {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for j := 0; j < len(b.Snakes); j++ {
|
|
||||||
other := &b.Snakes[j]
|
|
||||||
if areSnakesOnSameSquad(squadSettings.squadMap, snake, other) {
|
|
||||||
if squadSettings.SharedHealth {
|
|
||||||
if snake.Health < other.Health {
|
|
||||||
snake.Health = other.Health
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if squadSettings.SharedLength {
|
|
||||||
if len(snake.Body) == 0 || len(other.Body) == 0 {
|
|
||||||
return false, errors.New("found snake of zero length")
|
|
||||||
}
|
|
||||||
for len(snake.Body) < len(other.Body) {
|
|
||||||
growSnake(snake)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if squadSettings.SharedElimination {
|
|
||||||
if snake.EliminatedCause == NotEliminated && other.EliminatedCause != NotEliminated {
|
|
||||||
snake.EliminatedCause = EliminatedBySquad
|
|
||||||
// We intentionally do not set snake.EliminatedBy because there might be multiple culprits.
|
|
||||||
snake.EliminatedBy = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *SquadRuleset) IsGameOver(b *BoardState) (bool, error) {
|
|
||||||
return GameOverSquad(b, r.Settings(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GameOverSquad(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
|
||||||
snakesRemaining := []*Snake{}
|
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
|
||||||
if b.Snakes[i].EliminatedCause == NotEliminated {
|
|
||||||
snakesRemaining = append(snakesRemaining, &b.Snakes[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(snakesRemaining); i++ {
|
|
||||||
if !areSnakesOnSameSquad(settings.SquadSettings.squadMap, snakesRemaining[i], snakesRemaining[0]) {
|
|
||||||
// There are multiple squads remaining
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// no snakes or single squad remaining
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r SquadRuleset) Settings() Settings {
|
|
||||||
s := r.StandardRuleset.Settings()
|
|
||||||
s.SquadSettings = SquadSettings{
|
|
||||||
squadMap: r.SquadMap,
|
|
||||||
AllowBodyCollisions: r.AllowBodyCollisions,
|
|
||||||
SharedElimination: r.SharedElimination,
|
|
||||||
SharedHealth: r.SharedHealth,
|
|
||||||
SharedLength: r.SharedLength,
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
582
squad_test.go
582
squad_test.go
|
|
@ -1,582 +0,0 @@
|
||||||
package rules
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSquadRulesetInterface(t *testing.T) {
|
|
||||||
var _ Ruleset = (*SquadRuleset)(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSquadName(t *testing.T) {
|
|
||||||
r := SquadRuleset{}
|
|
||||||
require.Equal(t, "squad", r.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSquadCreateNextBoardStateSanity(t *testing.T) {
|
|
||||||
boardState := &BoardState{}
|
|
||||||
r := SquadRuleset{}
|
|
||||||
_, err := r.CreateNextBoardState(boardState, []SnakeMove{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSquadResurrectSquadBodyCollisionsSanity(t *testing.T) {
|
|
||||||
boardState := &BoardState{}
|
|
||||||
r := SquadRuleset{}
|
|
||||||
_, err := ResurrectSnakesSquad(boardState, r.Settings(), nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSquadSharedAttributesSanity(t *testing.T) {
|
|
||||||
boardState := &BoardState{}
|
|
||||||
r := SquadRuleset{}
|
|
||||||
_, err := ShareAttributesSquad(boardState, r.Settings(), nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSquadAllowBodyCollisions(t *testing.T) {
|
|
||||||
testSnakes := []struct {
|
|
||||||
SnakeID string
|
|
||||||
SquadID string
|
|
||||||
EliminatedCause string
|
|
||||||
EliminatedBy string
|
|
||||||
ExpectedCause string
|
|
||||||
ExpectedBy string
|
|
||||||
}{
|
|
||||||
// Red Squad
|
|
||||||
{"R1", "red", NotEliminated, "", NotEliminated, ""},
|
|
||||||
{"R2", "red", EliminatedByCollision, "R1", NotEliminated, ""},
|
|
||||||
// Blue Squad
|
|
||||||
{"B1", "blue", EliminatedByCollision, "R1", EliminatedByCollision, "R1"},
|
|
||||||
{"B2", "blue", EliminatedBySelfCollision, "B1", EliminatedBySelfCollision, "B1"},
|
|
||||||
{"B4", "blue", EliminatedByOutOfBounds, "", EliminatedByOutOfBounds, ""},
|
|
||||||
{"B3", "blue", NotEliminated, "", NotEliminated, ""},
|
|
||||||
// More Red Squad
|
|
||||||
{"R3", "red", NotEliminated, "", NotEliminated, ""},
|
|
||||||
{"R4", "red", EliminatedByCollision, "R4", EliminatedByCollision, "R4"}, // this is an error case but worth testing
|
|
||||||
{"R5", "red", EliminatedByCollision, "R4", NotEliminated, ""},
|
|
||||||
// Green Squad
|
|
||||||
{"G1", "green", EliminatedByOutOfHealth, "x", EliminatedByOutOfHealth, "x"},
|
|
||||||
// Yellow Squad
|
|
||||||
{"Y1", "yellow", EliminatedByCollision, "B4", EliminatedByCollision, "B4"},
|
|
||||||
}
|
|
||||||
|
|
||||||
boardState := &BoardState{}
|
|
||||||
squadMap := make(map[string]string)
|
|
||||||
for _, testSnake := range testSnakes {
|
|
||||||
boardState.Snakes = append(boardState.Snakes, Snake{
|
|
||||||
ID: testSnake.SnakeID,
|
|
||||||
EliminatedCause: testSnake.EliminatedCause,
|
|
||||||
EliminatedBy: testSnake.EliminatedBy,
|
|
||||||
})
|
|
||||||
squadMap[testSnake.SnakeID] = testSnake.SquadID
|
|
||||||
}
|
|
||||||
require.Equal(t, len(squadMap), len(boardState.Snakes), "squad map is wrong size, error in test setup")
|
|
||||||
|
|
||||||
r := SquadRuleset{SquadMap: squadMap, AllowBodyCollisions: true}
|
|
||||||
_, err := ResurrectSnakesSquad(boardState, r.Settings(), mockSnakeMoves())
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, len(boardState.Snakes), len(testSnakes))
|
|
||||||
for i := 0; i < len(boardState.Snakes); i++ {
|
|
||||||
require.Equal(
|
|
||||||
t,
|
|
||||||
testSnakes[i].ExpectedCause,
|
|
||||||
boardState.Snakes[i].EliminatedCause,
|
|
||||||
"snake %s failed shared eliminated cause",
|
|
||||||
testSnakes[i].SnakeID,
|
|
||||||
)
|
|
||||||
require.Equal(
|
|
||||||
t,
|
|
||||||
testSnakes[i].ExpectedBy,
|
|
||||||
boardState.Snakes[i].EliminatedBy,
|
|
||||||
"snake %s failed shared eliminated by",
|
|
||||||
testSnakes[i].SnakeID,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSquadAllowBodyCollisionsEliminatedByNotSet(t *testing.T) {
|
|
||||||
boardState := &BoardState{
|
|
||||||
Snakes: []Snake{
|
|
||||||
{ID: "1", EliminatedCause: EliminatedByCollision},
|
|
||||||
{ID: "2"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
r := SquadRuleset{
|
|
||||||
AllowBodyCollisions: true,
|
|
||||||
SquadMap: map[string]string{
|
|
||||||
"1": "red",
|
|
||||||
"2": "red",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
_, err := ResurrectSnakesSquad(boardState, r.Settings(), mockSnakeMoves())
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSquadShareSquadHealth(t *testing.T) {
|
|
||||||
testSnakes := []struct {
|
|
||||||
SnakeID string
|
|
||||||
SquadID string
|
|
||||||
Health int
|
|
||||||
ExpectedHealth int
|
|
||||||
}{
|
|
||||||
// Red Squad
|
|
||||||
{"R1", "red", 11, 88},
|
|
||||||
{"R2", "red", 22, 88},
|
|
||||||
// Blue Squad
|
|
||||||
{"B1", "blue", 33, 333},
|
|
||||||
{"B2", "blue", 333, 333},
|
|
||||||
{"B3", "blue", 3, 333},
|
|
||||||
// More Red Squad
|
|
||||||
{"R3", "red", 77, 88},
|
|
||||||
{"R4", "red", 88, 88},
|
|
||||||
// Green Squad
|
|
||||||
{"G1", "green", 100, 100},
|
|
||||||
// Yellow Squad
|
|
||||||
{"Y1", "yellow", 1, 1},
|
|
||||||
}
|
|
||||||
|
|
||||||
boardState := &BoardState{}
|
|
||||||
squadMap := make(map[string]string)
|
|
||||||
for _, testSnake := range testSnakes {
|
|
||||||
boardState.Snakes = append(boardState.Snakes, Snake{
|
|
||||||
ID: testSnake.SnakeID,
|
|
||||||
Health: testSnake.Health,
|
|
||||||
})
|
|
||||||
squadMap[testSnake.SnakeID] = testSnake.SquadID
|
|
||||||
}
|
|
||||||
require.Equal(t, len(squadMap), len(boardState.Snakes), "squad map is wrong size, error in test setup")
|
|
||||||
|
|
||||||
r := SquadRuleset{SharedHealth: true, SquadMap: squadMap}
|
|
||||||
_, err := ShareAttributesSquad(boardState, r.Settings(), mockSnakeMoves())
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, len(boardState.Snakes), len(testSnakes))
|
|
||||||
for i := 0; i < len(boardState.Snakes); i++ {
|
|
||||||
require.Equal(
|
|
||||||
t,
|
|
||||||
testSnakes[i].ExpectedHealth,
|
|
||||||
boardState.Snakes[i].Health,
|
|
||||||
"snake %s failed shared health",
|
|
||||||
testSnakes[i].SnakeID,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSquadSharedLength(t *testing.T) {
|
|
||||||
testSnakes := []struct {
|
|
||||||
SnakeID string
|
|
||||||
SquadID string
|
|
||||||
Body []Point
|
|
||||||
ExpectedBody []Point
|
|
||||||
}{
|
|
||||||
// Red Squad
|
|
||||||
{"R1", "red", []Point{{1, 1}}, []Point{{1, 1}, {1, 1}, {1, 1}, {1, 1}, {1, 1}}},
|
|
||||||
{"R2", "red", []Point{{2, 2}, {2, 2}}, []Point{{2, 2}, {2, 2}, {2, 2}, {2, 2}, {2, 2}}},
|
|
||||||
// Blue Squad
|
|
||||||
{"B1", "blue", []Point{{1, 1}, {1, 2}}, []Point{{1, 1}, {1, 2}}},
|
|
||||||
{"B2", "blue", []Point{{2, 1}}, []Point{{2, 1}, {2, 1}}},
|
|
||||||
{"B3", "blue", []Point{{3, 3}}, []Point{{3, 3}, {3, 3}}},
|
|
||||||
// More Red Squad
|
|
||||||
{"R3", "red", []Point{{1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}}, []Point{{1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}}},
|
|
||||||
{"R4", "red", []Point{{4, 4}}, []Point{{4, 4}, {4, 4}, {4, 4}, {4, 4}, {4, 4}}},
|
|
||||||
// Green Squad
|
|
||||||
{"G1", "green", []Point{{1, 1}}, []Point{{1, 1}}},
|
|
||||||
// Yellow Squad
|
|
||||||
{"Y1", "yellow", []Point{{1, 3}, {1, 4}, {1, 5}, {1, 6}}, []Point{{1, 3}, {1, 4}, {1, 5}, {1, 6}}},
|
|
||||||
}
|
|
||||||
|
|
||||||
boardState := &BoardState{}
|
|
||||||
squadMap := make(map[string]string)
|
|
||||||
for _, testSnake := range testSnakes {
|
|
||||||
boardState.Snakes = append(boardState.Snakes, Snake{
|
|
||||||
ID: testSnake.SnakeID,
|
|
||||||
Body: testSnake.Body,
|
|
||||||
})
|
|
||||||
squadMap[testSnake.SnakeID] = testSnake.SquadID
|
|
||||||
}
|
|
||||||
require.Equal(t, len(squadMap), len(boardState.Snakes), "squad map is wrong size, error in test setup")
|
|
||||||
|
|
||||||
r := SquadRuleset{SharedLength: true, SquadMap: squadMap}
|
|
||||||
_, err := ShareAttributesSquad(boardState, r.Settings(), mockSnakeMoves())
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, len(boardState.Snakes), len(testSnakes))
|
|
||||||
for i := 0; i < len(boardState.Snakes); i++ {
|
|
||||||
require.Equal(
|
|
||||||
t,
|
|
||||||
testSnakes[i].ExpectedBody,
|
|
||||||
boardState.Snakes[i].Body,
|
|
||||||
"snake %s failed shared length",
|
|
||||||
testSnakes[i].SnakeID,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSquadSharedElimination(t *testing.T) {
|
|
||||||
testSnakes := []struct {
|
|
||||||
SnakeID string
|
|
||||||
SquadID string
|
|
||||||
EliminatedCause string
|
|
||||||
EliminatedBy string
|
|
||||||
ExpectedCause string
|
|
||||||
ExpectedBy string
|
|
||||||
}{
|
|
||||||
// Red Squad
|
|
||||||
{"R1", "red", NotEliminated, "", EliminatedBySquad, ""},
|
|
||||||
{"R2", "red", EliminatedByHeadToHeadCollision, "y", EliminatedByHeadToHeadCollision, "y"},
|
|
||||||
// Blue Squad
|
|
||||||
{"B1", "blue", EliminatedByOutOfBounds, "z", EliminatedByOutOfBounds, "z"},
|
|
||||||
{"B2", "blue", NotEliminated, "", EliminatedBySquad, ""},
|
|
||||||
{"B3", "blue", NotEliminated, "", EliminatedBySquad, ""},
|
|
||||||
// More Red Squad
|
|
||||||
{"R3", "red", NotEliminated, "", EliminatedBySquad, ""},
|
|
||||||
{"R4", "red", EliminatedByCollision, "B1", EliminatedByCollision, "B1"},
|
|
||||||
// Green Squad
|
|
||||||
{"G1", "green", EliminatedByOutOfHealth, "x", EliminatedByOutOfHealth, "x"},
|
|
||||||
// Yellow Squad
|
|
||||||
{"Y1", "yellow", NotEliminated, "", NotEliminated, ""},
|
|
||||||
}
|
|
||||||
|
|
||||||
boardState := &BoardState{}
|
|
||||||
squadMap := make(map[string]string)
|
|
||||||
for _, testSnake := range testSnakes {
|
|
||||||
boardState.Snakes = append(boardState.Snakes, Snake{
|
|
||||||
ID: testSnake.SnakeID,
|
|
||||||
EliminatedCause: testSnake.EliminatedCause,
|
|
||||||
EliminatedBy: testSnake.EliminatedBy,
|
|
||||||
})
|
|
||||||
squadMap[testSnake.SnakeID] = testSnake.SquadID
|
|
||||||
}
|
|
||||||
require.Equal(t, len(squadMap), len(boardState.Snakes), "squad map is wrong size, error in test setup")
|
|
||||||
|
|
||||||
r := SquadRuleset{SharedElimination: true, SquadMap: squadMap}
|
|
||||||
_, err := ShareAttributesSquad(boardState, r.Settings(), mockSnakeMoves())
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, len(boardState.Snakes), len(testSnakes))
|
|
||||||
for i := 0; i < len(boardState.Snakes); i++ {
|
|
||||||
require.Equal(
|
|
||||||
t,
|
|
||||||
testSnakes[i].ExpectedCause,
|
|
||||||
boardState.Snakes[i].EliminatedCause,
|
|
||||||
"snake %s failed shared eliminated cause",
|
|
||||||
testSnakes[i].SnakeID,
|
|
||||||
)
|
|
||||||
require.Equal(
|
|
||||||
t,
|
|
||||||
testSnakes[i].ExpectedBy,
|
|
||||||
boardState.Snakes[i].EliminatedBy,
|
|
||||||
"snake %s failed shared eliminated by",
|
|
||||||
testSnakes[i].SnakeID,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSquadSharedAttributesErrorLengthZero(t *testing.T) {
|
|
||||||
boardState := &BoardState{
|
|
||||||
Snakes: []Snake{
|
|
||||||
{ID: "1"},
|
|
||||||
{ID: "2"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
r := SquadRuleset{
|
|
||||||
SharedLength: true,
|
|
||||||
SquadMap: map[string]string{
|
|
||||||
"1": "red",
|
|
||||||
"2": "red",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
_, err := ShareAttributesSquad(boardState, r.Settings(), mockSnakeMoves())
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSquadIsGameOver(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
Snakes []Snake
|
|
||||||
SquadMap map[string]string
|
|
||||||
Expected bool
|
|
||||||
}{
|
|
||||||
{[]Snake{}, map[string]string{}, true},
|
|
||||||
{[]Snake{{ID: "R1"}}, map[string]string{"R1": "red"}, true},
|
|
||||||
{
|
|
||||||
[]Snake{{ID: "R1"}, {ID: "R2"}, {ID: "R3"}},
|
|
||||||
map[string]string{"R1": "red", "R2": "red", "R3": "red"},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]Snake{{ID: "R1"}, {ID: "B1"}},
|
|
||||||
map[string]string{"R1": "red", "B1": "blue"},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]Snake{{ID: "R1"}, {ID: "B1"}, {ID: "B2"}, {ID: "G1"}},
|
|
||||||
map[string]string{"R1": "red", "B1": "blue", "B2": "blue", "G1": "green"},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]Snake{
|
|
||||||
{ID: "R1", EliminatedCause: EliminatedByOutOfBounds},
|
|
||||||
{ID: "B1", EliminatedCause: EliminatedBySelfCollision, EliminatedBy: "B1"},
|
|
||||||
{ID: "B2", EliminatedCause: EliminatedByCollision, EliminatedBy: "B2"},
|
|
||||||
{ID: "G1"},
|
|
||||||
},
|
|
||||||
map[string]string{"R1": "red", "B1": "blue", "B2": "blue", "G1": "green"},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
b := &BoardState{
|
|
||||||
Height: 11,
|
|
||||||
Width: 11,
|
|
||||||
Snakes: test.Snakes,
|
|
||||||
Food: []Point{},
|
|
||||||
}
|
|
||||||
r := SquadRuleset{SquadMap: test.SquadMap}
|
|
||||||
|
|
||||||
actual, err := r.IsGameOver(b)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, test.Expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegressionIssue16(t *testing.T) {
|
|
||||||
// This is a specific test case to detect this issue:
|
|
||||||
// https://github.com/BattlesnakeOfficial/rules/issues/16
|
|
||||||
boardState := &BoardState{
|
|
||||||
Width: 11,
|
|
||||||
Height: 11,
|
|
||||||
Snakes: []Snake{
|
|
||||||
{ID: "teamBoi", Health: 10, Body: []Point{{1, 4}, {1, 3}, {0, 3}, {0, 2}, {1, 2}, {2, 2}}},
|
|
||||||
{ID: "Node-Red-Bellied-Black-Snake", Health: 10, Body: []Point{{1, 8}, {2, 8}, {2, 9}, {3, 9}, {4, 9}, {4, 10}}},
|
|
||||||
{ID: "Crash Override", Health: 10, Body: []Point{{2, 7}, {2, 6}, {3, 6}, {4, 6}, {4, 5}, {5, 5}, {6, 5}}},
|
|
||||||
{ID: "Zero Cool", Health: 10, Body: []Point{{6, 5}, {5, 5}, {5, 4}, {5, 3}, {4, 3}, {3, 3}, {3, 4}}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
squadMap := map[string]string{
|
|
||||||
"teamBoi": "BirdSnakers",
|
|
||||||
"Node-Red-Bellied-Black-Snake": "BirdSnakers",
|
|
||||||
"Crash Override": "Hackers",
|
|
||||||
"Zero Cool": "Hackers",
|
|
||||||
}
|
|
||||||
snakeMoves := []SnakeMove{
|
|
||||||
{ID: "teamBoi", Move: "up"},
|
|
||||||
{ID: "Node-Red-Bellied-Black-Snake", Move: "left"},
|
|
||||||
{ID: "Crash Override", Move: "left"},
|
|
||||||
{ID: "Zero Cool", Move: "left"},
|
|
||||||
}
|
|
||||||
|
|
||||||
require.Equal(t, len(squadMap), len(boardState.Snakes), "squad map is wrong size, error in test setup")
|
|
||||||
|
|
||||||
r := SquadRuleset{
|
|
||||||
AllowBodyCollisions: true,
|
|
||||||
SquadMap: squadMap,
|
|
||||||
}
|
|
||||||
|
|
||||||
nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
|
||||||
|
|
||||||
expectedSnakes := []Snake{
|
|
||||||
{ID: "teamBoi", Body: []Point{{1, 5}, {1, 4}, {1, 3}, {0, 3}, {0, 2}, {1, 2}}},
|
|
||||||
{ID: "Node-Red-Bellied-Black-Snake", Body: []Point{{0, 8}, {1, 8}, {2, 8}, {2, 9}, {3, 9}, {4, 9}}},
|
|
||||||
{ID: "Crash Override", Body: []Point{{1, 7}, {2, 7}, {2, 6}, {3, 6}, {4, 6}, {4, 5}, {5, 5}}},
|
|
||||||
{ID: "Zero Cool", Body: []Point{{5, 5}, {6, 5}, {5, 5}, {5, 4}, {5, 3}, {4, 3}, {3, 3}}, EliminatedCause: EliminatedBySelfCollision, EliminatedBy: "Zero Cool"},
|
|
||||||
}
|
|
||||||
for i, snake := range nextBoardState.Snakes {
|
|
||||||
require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID)
|
|
||||||
require.Equal(t, expectedSnakes[i].Body, snake.Body, snake.ID)
|
|
||||||
require.Equal(t, expectedSnakes[i].EliminatedCause, snake.EliminatedCause, snake.ID)
|
|
||||||
require.Equal(t, expectedSnakes[i].EliminatedBy, snake.EliminatedBy, snake.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks that snakes on the same squad don't get eliminated
|
|
||||||
// when the allow squad collisions setting is enabled
|
|
||||||
// Both squads have snakes that move into each other.
|
|
||||||
var squadCaseMoveSquadCollisions = gameTestCase{
|
|
||||||
"Squad Case Move Squad Collisions",
|
|
||||||
&BoardState{
|
|
||||||
Width: 10,
|
|
||||||
Height: 10,
|
|
||||||
Snakes: []Snake{
|
|
||||||
{
|
|
||||||
ID: "snake1squad1",
|
|
||||||
Body: []Point{{1, 1}, {2, 1}},
|
|
||||||
Health: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "snake2squad1",
|
|
||||||
Body: []Point{{1, 2}, {2, 2}},
|
|
||||||
Health: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "snake3squad2",
|
|
||||||
Body: []Point{{4, 4}, {4, 5}},
|
|
||||||
Health: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "snake4squad2",
|
|
||||||
Body: []Point{{5, 4}, {5, 5}},
|
|
||||||
Health: 100,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Food: []Point{},
|
|
||||||
Hazards: []Point{},
|
|
||||||
},
|
|
||||||
[]SnakeMove{
|
|
||||||
{ID: "snake1squad1", Move: MoveUp},
|
|
||||||
{ID: "snake2squad1", Move: MoveDown},
|
|
||||||
{ID: "snake3squad2", Move: MoveRight},
|
|
||||||
{ID: "snake4squad2", Move: MoveLeft},
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
&BoardState{Width: 10,
|
|
||||||
Height: 10,
|
|
||||||
Snakes: []Snake{
|
|
||||||
{
|
|
||||||
ID: "snake1squad1",
|
|
||||||
Body: []Point{{1, 2}, {1, 1}},
|
|
||||||
Health: 99,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "snake2squad1",
|
|
||||||
Body: []Point{{1, 1}, {1, 2}},
|
|
||||||
Health: 99,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "snake3squad2",
|
|
||||||
Body: []Point{{5, 4}, {4, 4}},
|
|
||||||
Health: 99,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "snake4squad2",
|
|
||||||
Body: []Point{{4, 4}, {5, 4}},
|
|
||||||
Health: 99,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Food: []Point{},
|
|
||||||
Hazards: []Point{}},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks snakes on the same squad share health (assuming the setting is enabled)
|
|
||||||
var squadCaseEatFoodAndShareHealth = gameTestCase{
|
|
||||||
"Squad Case Move Squad Collisions",
|
|
||||||
&BoardState{
|
|
||||||
Width: 10,
|
|
||||||
Height: 10,
|
|
||||||
Snakes: []Snake{
|
|
||||||
{
|
|
||||||
ID: "snake1squad1",
|
|
||||||
Body: []Point{{1, 1}, {2, 1}},
|
|
||||||
Health: 80,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "snake2squad1",
|
|
||||||
Body: []Point{{7, 7}, {7, 8}},
|
|
||||||
Health: 50,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "snake3squad2",
|
|
||||||
Body: []Point{{4, 4}, {4, 5}},
|
|
||||||
Health: 60,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "snake4squad2",
|
|
||||||
Body: []Point{{5, 4}, {5, 5}},
|
|
||||||
Health: 71,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Food: []Point{{1, 2}},
|
|
||||||
Hazards: []Point{},
|
|
||||||
},
|
|
||||||
[]SnakeMove{
|
|
||||||
{ID: "snake1squad1", Move: MoveUp},
|
|
||||||
{ID: "snake2squad1", Move: MoveDown},
|
|
||||||
{ID: "snake3squad2", Move: MoveRight},
|
|
||||||
{ID: "snake4squad2", Move: MoveLeft},
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
&BoardState{
|
|
||||||
Width: 10,
|
|
||||||
Height: 10,
|
|
||||||
Snakes: []Snake{
|
|
||||||
{
|
|
||||||
ID: "snake1squad1",
|
|
||||||
Body: []Point{{1, 2}, {1, 1}, {1, 1}},
|
|
||||||
Health: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "snake2squad1",
|
|
||||||
Body: []Point{{7, 6}, {7, 7}},
|
|
||||||
Health: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "snake3squad2",
|
|
||||||
Body: []Point{{5, 4}, {4, 4}},
|
|
||||||
Health: 70,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "snake4squad2",
|
|
||||||
Body: []Point{{4, 4}, {5, 4}},
|
|
||||||
Health: 70,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Food: []Point{},
|
|
||||||
Hazards: []Point{}},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSquadCreateNextBoardState(t *testing.T) {
|
|
||||||
standardCases := []gameTestCase{
|
|
||||||
// inherits these test cases from standard
|
|
||||||
standardCaseErrNoMoveFound,
|
|
||||||
standardCaseErrZeroLengthSnake,
|
|
||||||
standardCaseMoveEatAndGrow,
|
|
||||||
}
|
|
||||||
r := SquadRuleset{
|
|
||||||
SquadMap: map[string]string{
|
|
||||||
"snake1squad1": "squad1",
|
|
||||||
"snake2squad1": "squad1",
|
|
||||||
"snake3squad2": "squad2",
|
|
||||||
"snake4squad2": "squad2",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
rand.Seed(0)
|
|
||||||
rb := NewRulesetBuilder().WithParams(map[string]string{
|
|
||||||
ParamGameType: GameTypeSquad,
|
|
||||||
})
|
|
||||||
rb.WithSeed(0)
|
|
||||||
for s, ss := range r.SquadMap {
|
|
||||||
rb = rb.AddSnakeToSquad(s, ss)
|
|
||||||
}
|
|
||||||
for _, gc := range standardCases {
|
|
||||||
gc.requireValidNextState(t, &r)
|
|
||||||
// also test a RulesBuilder constructed instance
|
|
||||||
gc.requireValidNextState(t, rb.Ruleset())
|
|
||||||
// also test a pipeline with the same settings
|
|
||||||
gc.requireValidNextState(t, rb.PipelineRuleset(GameTypeSquad, NewPipeline(squadRulesetStages...)))
|
|
||||||
}
|
|
||||||
|
|
||||||
extendedCases := []gameTestCase{
|
|
||||||
squadCaseMoveSquadCollisions,
|
|
||||||
squadCaseEatFoodAndShareHealth,
|
|
||||||
}
|
|
||||||
r.SharedHealth = true
|
|
||||||
r.AllowBodyCollisions = true
|
|
||||||
rb = rb.WithParams(map[string]string{
|
|
||||||
ParamSharedHealth: "true",
|
|
||||||
ParamAllowBodyCollisions: "true",
|
|
||||||
})
|
|
||||||
for _, gc := range extendedCases {
|
|
||||||
gc.requireValidNextState(t, &r)
|
|
||||||
// also test a RulesBuilder constructed instance
|
|
||||||
gc.requireValidNextState(t, rb.Ruleset())
|
|
||||||
// also test a pipeline with the same settings
|
|
||||||
gc.requireValidNextState(t, rb.PipelineRuleset(GameTypeSquad, NewPipeline(squadRulesetStages...)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -18,7 +18,6 @@ var standardRulesetStages = []string{
|
||||||
StageStarvationStandard,
|
StageStarvationStandard,
|
||||||
StageHazardDamageStandard,
|
StageHazardDamageStandard,
|
||||||
StageFeedSnakesStandard,
|
StageFeedSnakesStandard,
|
||||||
StageSpawnFoodStandard,
|
|
||||||
StageEliminationStandard,
|
StageEliminationStandard,
|
||||||
StageGameOverStandard,
|
StageGameOverStandard,
|
||||||
}
|
}
|
||||||
|
|
@ -387,6 +386,7 @@ func growSnake(snake *Snake) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated: handled by maps.Standard
|
||||||
func SpawnFoodStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
func SpawnFoodStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
if IsInitialization(b, settings, moves) {
|
if IsInitialization(b, settings, moves) {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ var wrappedRulesetStages = []string{
|
||||||
StageStarvationStandard,
|
StageStarvationStandard,
|
||||||
StageHazardDamageStandard,
|
StageHazardDamageStandard,
|
||||||
StageFeedSnakesStandard,
|
StageFeedSnakesStandard,
|
||||||
StageSpawnFoodStandard,
|
|
||||||
StageEliminationStandard,
|
StageEliminationStandard,
|
||||||
StageGameOverStandard,
|
StageGameOverStandard,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue