diff --git a/cli/README.md b/cli/README.md index 1245923..00cc1dd 100644 --- a/cli/README.md +++ b/cli/README.md @@ -32,14 +32,14 @@ Usage: battlesnake play [flags] Flags: - -W, --width int Width of Board (default 11) - -H, --height int Height of Board (default 11) + -W, --width int Width of Board (default 11) + -H, --height int Height of Board (default 11) -n, --name stringArray Name of Snake -u, --url stringArray URL of Snake - -S, --squad stringArray Squad of Snake -t, --timeout int Request Timeout (default 500) -s, --sequential Use Sequential Processing -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 -c, --color Use color to draw the map -r, --seed int Random Seed (default 1649588785026867900) diff --git a/cli/commands/play.go b/cli/commands/play.go index 4dbfc03..8257abe 100644 --- a/cli/commands/play.go +++ b/cli/commands/play.go @@ -17,6 +17,7 @@ import ( "github.com/BattlesnakeOfficial/rules" "github.com/BattlesnakeOfficial/rules/client" + "github.com/BattlesnakeOfficial/rules/maps" "github.com/google/uuid" "github.com/spf13/cobra" ) @@ -27,128 +28,148 @@ type SnakeState struct { Name string ID string LastMove string - Squad string Character rune Color string Head string Tail string } -var GameId string -var Turn int -var HttpClient http.Client -var Width int -var Height int -var Names []string -var URLs []string -var Squads []string -var Timeout int -var TurnDuration int -var Sequential bool -var GameType string -var ViewMap bool -var UseColor bool -var Seed int64 -var TurnDelay int -var DebugRequests bool -var Output string +type GameState struct { + // Options + Width int + Height int + Names []string + URLs []string + Timeout int + TurnDuration int + Sequential bool + GameType string + MapName string + ViewMap bool + UseColor bool + Seed int64 + TurnDelay int + DebugRequests bool + Output string + FoodSpawnChance int + MinimumFood int + HazardDamagePerTurn int + ShrinkEveryNTurns int -var FoodSpawnChance int -var MinimumFood int -var HazardDamagePerTurn int -var ShrinkEveryNTurns int - -var defaultConfig = map[string]string{ - // default to standard ruleset - rules.ParamGameType: "standard", - // squad settings default to true (not zero value) - rules.ParamSharedElimination: "true", - rules.ParamSharedHealth: "true", - rules.ParamSharedLength: "true", - rules.ParamAllowBodyCollisions: "true", + // Internal game state + settings map[string]string + snakeStates map[string]SnakeState + gameID string + httpClient http.Client + ruleset rules.Ruleset + gameMap maps.GameMap } -var playCmd = &cobra.Command{ - Use: "play", - Short: "Play a game of Battlesnake locally.", - Long: "Play a game of Battlesnake locally.", - Run: run, - PreRun: playPreRun, -} +func NewPlayCommand() *cobra.Command { + gameState := &GameState{} -func init() { - rootCmd.AddCommand(playCmd) + var playCmd = &cobra.Command{ + Use: "play", + Short: "Play a game of Battlesnake locally.", + Long: "Play a game of Battlesnake locally.", + Run: func(cmd *cobra.Command, args []string) { + gameState.Run() + }, + } - playCmd.Flags().IntVarP(&Width, "width", "W", 11, "Width of Board") - playCmd.Flags().IntVarP(&Height, "height", "H", 11, "Height of Board") - playCmd.Flags().StringArrayVarP(&Names, "name", "n", nil, "Name of Snake") - playCmd.Flags().StringArrayVarP(&URLs, "url", "u", nil, "URL of Snake") - 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().IntVarP(&gameState.Width, "width", "W", 11, "Width of Board") + 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().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().IntVar(&gameState.FoodSpawnChance, "foodSpawnChance", 15, "Percentage chance of spawning a new food every round") + playCmd.Flags().IntVar(&gameState.MinimumFood, "minimumFood", 1, "Minimum food to keep on the board every turn") + playCmd.Flags().IntVar(&gameState.HazardDamagePerTurn, "hazardDamagePerTurn", 14, "Health damage a snake will take when ending its turn in a hazard") + playCmd.Flags().IntVar(&gameState.ShrinkEveryNTurns, "shrinkEveryNTurns", 25, "In Royale mode, the number of turns between generating new hazards") playCmd.Flags().SortFlags = false + + return playCmd } -func playPreRun(cmd *cobra.Command, args []string) { - initialiseGameConfig() +// Setup a GameState once all the fields have been parsed from the command-line. +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) { - rand.Seed(Seed) +// Setup and run a full game. +func (gameState *GameState) Run() { + gameState.initialize() - GameId = uuid.New().String() - Turn = 0 + // Setup local state for snakes + gameState.snakeStates = gameState.buildSnakesFromOptions() - var endTime time.Time - snakeStates := buildSnakesFromOptions() + rand.Seed(gameState.Seed) - ruleset := getRuleset(Seed, snakeStates) - state := initializeBoardFromArgs(ruleset, snakeStates) - exportGame := Output != "" + boardState := gameState.initializeBoardFromArgs() + exportGame := gameState.Output != "" gameExporter := GameExporter{ - game: createClientGame(ruleset), + game: gameState.createClientGame(), snakeRequests: make([]client.SnakeRequest, 0), winner: SnakeState{}, isDraw: false, } - for v := false; !v; v, _ = ruleset.IsGameOver(state) { - if TurnDuration > 0 { - endTime = time.Now().Add(time.Duration(TurnDuration) * time.Millisecond) - } - - Turn++ - state = createNextBoardState(ruleset, state, snakeStates, Turn) - - if ViewMap { - 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)) + if gameState.ViewMap { + gameState.printMap(boardState) + } + + var endTime time.Time + for v := false; !v; v, _ = gameState.ruleset.IsGameOver(boardState) { + if gameState.TurnDuration > 0 { + endTime = time.Now().Add(time.Duration(gameState.TurnDuration) * time.Millisecond) } + // Export game first, if enabled, so that we save the board on turn zero if exportGame { // 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. @@ -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. // 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. - snakeState := snakeStates[state.Snakes[0].ID] - snakeRequest := getIndividualBoardStateForSnake(state, snakeState, snakeStates, ruleset) - gameExporter.AddSnakeRequest(snakeRequest) + for _, snakeState := range gameState.snakeStates { + snakeRequest := gameState.getRequestBodyForSnake(boardState, snakeState) + 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 - if GameType == "solo" { - log.Printf("[DONE]: Game completed after %v turns.", Turn) + if gameState.GameType == "solo" { + log.Printf("[DONE]: Game completed after %v turns.", boardState.Turn) if exportGame { // 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 { var winner SnakeState - for _, snake := range state.Snakes { - snakeState := snakeStates[snake.ID] + for _, snake := range boardState.Snakes { + snakeState := gameState.snakeStates[snake.ID] if snake.EliminatedCause == rules.NotEliminated { isDraw = false winner = snakeState } - sendEndRequest(ruleset, state, snakeState, snakeStates) + gameState.sendEndRequest(boardState, snakeState) } 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 { - 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 { gameExporter.winner = winner @@ -192,7 +235,7 @@ var run = func(cmd *cobra.Command, args []string) { } if exportGame { - err := gameExporter.FlushToFile(Output, "JSONL") + err := gameExporter.FlushToFile(gameState.Output, "JSONL") if err != nil { log.Printf("[WARN]: Unable to export game. Reason: %v\n", err.Error()) os.Exit(1) @@ -200,83 +243,57 @@ var run = func(cmd *cobra.Command, args []string) { } } -func initialiseGameConfig() { - 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, - } - +func (gameState *GameState) initializeBoardFromArgs() *rules.BoardState { snakeIds := []string{} - for _, snakeState := range snakeStates { + for _, snakeState := range gameState.snakeStates { 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 { - 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 { - log.Panic("[PANIC]: Error Initializing Board State") + log.Fatalf("Error Initializing Board State: %v", err) } - for _, snakeState := range snakeStates { - snakeRequest := getIndividualBoardStateForSnake(state, snakeState, snakeStates, ruleset) + for _, snakeState := range gameState.snakeStates { + snakeRequest := gameState.getRequestBodyForSnake(boardState, snakeState) requestBody := serialiseSnakeRequest(snakeRequest) u, _ := url.ParseRequestURI(snakeState.URL) u.Path = path.Join(u.Path, "start") - if DebugRequests { + if gameState.DebugRequests { 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 { 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 - if Sequential { - for _, snakeState := range snakeStates { - for _, snake := range state.Snakes { + if gameState.Sequential { + for _, snakeState := range gameState.snakeStates { + for _, snake := range boardState.Snakes { 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 { var wg sync.WaitGroup - c := make(chan rules.SnakeMove, len(snakeStates)) + c := make(chan rules.SnakeMove, len(gameState.snakeStates)) - for _, snakeState := range snakeStates { - for _, snake := range state.Snakes { + for _, snakeState := range gameState.snakeStates { + for _, snake := range boardState.Snakes { if snakeState.ID == snake.ID && snake.EliminatedCause == rules.NotEliminated { wg.Add(1) go func(snakeState SnakeState) { defer wg.Done() - c <- getMoveForSnake(ruleset, state, snakeState, snakeStates) + c <- gameState.getMoveForSnake(boardState, snakeState) }(snakeState) } } @@ -290,29 +307,34 @@ func createNextBoardState(ruleset rules.Ruleset, state *rules.BoardState, snakeS } } for _, move := range moves { - snakeState := snakeStates[move.ID] + snakeState := gameState.snakeStates[move.ID] 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 { - 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 { - snakeRequest := getIndividualBoardStateForSnake(state, snakeState, snakeStates, ruleset) +func (gameState *GameState) getMoveForSnake(boardState *rules.BoardState, snakeState SnakeState) rules.SnakeMove { + snakeRequest := gameState.getRequestBodyForSnake(boardState, snakeState) requestBody := serialiseSnakeRequest(snakeRequest) u, _ := url.ParseRequestURI(snakeState.URL) u.Path = path.Join(u.Path, "move") - if DebugRequests { + if gameState.DebugRequests { 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 if err != nil { 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} } -func sendEndRequest(ruleset rules.Ruleset, state *rules.BoardState, snakeState SnakeState, snakeStates map[string]SnakeState) { - snakeRequest := getIndividualBoardStateForSnake(state, snakeState, snakeStates, ruleset) +func (gameState *GameState) sendEndRequest(boardState *rules.BoardState, snakeState SnakeState) { + snakeRequest := gameState.getRequestBodyForSnake(boardState, snakeState) requestBody := serialiseSnakeRequest(snakeRequest) u, _ := url.ParseRequestURI(snakeState.URL) u.Path = path.Join(u.Path, "end") - if DebugRequests { + if gameState.DebugRequests { 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 { 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 - for _, snk := range state.Snakes { + for _, snk := range boardState.Snakes { if snakeState.ID == snk.ID { youSnake = snk break } } request := client.SnakeRequest{ - Game: createClientGame(ruleset), - Turn: Turn, - Board: convertStateToBoard(state, snakeStates), - You: convertRulesSnake(youSnake, snakeStates[youSnake.ID]), + Game: gameState.createClientGame(), + Turn: boardState.Turn, + Board: convertStateToBoard(boardState, gameState.snakeStates), + You: convertRulesSnake(youSnake, snakeState), } return request } -func serialiseSnakeRequest(snakeRequest client.SnakeRequest) []byte { - requestJSON, err := json.Marshal(snakeRequest) - if err != nil { - log.Panic("[PANIC]: Error Marshalling JSON from State") - panic(err) - } - 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 - Settings: 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, +func (gameState *GameState) createClientGame() client.Game { + return client.Game{ + ID: gameState.gameID, + Timeout: gameState.Timeout, + Ruleset: client.Ruleset{ + Name: gameState.ruleset.Name(), + Version: "cli", // TODO: Use GitHub Release Version + Settings: gameState.ruleset.Settings(), }, + Map: gameState.gameMap.ID(), } } -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(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 { +func (gameState *GameState) buildSnakesFromOptions() map[string]SnakeState { bodyChars := []rune{'■', '⌀', '●', '☻', '◘', '☺', '□', '⍟'} var numSnakes int snakes := map[string]SnakeState{} - numNames := len(Names) - numURLs := len(URLs) - numSquads := len(Squads) + numNames := len(gameState.Names) + numURLs := len(gameState.URLs) if numNames > numURLs { numSnakes = numNames } else { @@ -440,42 +418,33 @@ func buildSnakesFromOptions() map[string]SnakeState { for i := int(0); i < numSnakes; i++ { var snakeName string var snakeURL string - var snakeSquad string id := uuid.New().String() if i < numNames { - snakeName = Names[i] + snakeName = gameState.Names[i] } 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 } if i < numURLs { - u, err := url.ParseRequestURI(URLs[i]) + u, err := url.ParseRequestURI(gameState.URLs[i]) 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" } else { snakeURL = u.String() } } 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" } - 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{ 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 { log.Printf("[WARN]: Request to %v failed: %v", snakeURL, err) } else if res.Body != nil { @@ -495,14 +464,129 @@ func buildSnakesFromOptions() map[string]SnakeState { snakeState.Color = pingResponse.Color } } - if GameType == "squad" { - snakeState.Squad = snakeSquad - } snakes[snakeState.ID] = snakeState } 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 // the default gray if any errors occure func parseSnakeColor(color string) (int64, int64, int64) { @@ -517,75 +601,3 @@ func parseSnakeColor(color string) (int64, int64, int64) { // Default gray color from Battlesnake board 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()) -} diff --git a/cli/commands/play_test.go b/cli/commands/play_test.go index fb2e725..6c2836e 100644 --- a/cli/commands/play_test.go +++ b/cli/commands/play_test.go @@ -10,6 +10,31 @@ import ( "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) { s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, 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", Color: "#654321", } - snakeStates := map[string]SnakeState{ + + gameState := buildDefaultGameState() + gameState.initialize() + gameState.gameID = "GAME_ID" + gameState.snakeStates = map[string]SnakeState{ s1State.ID: s1State, s2State.ID: s2State, } - initialiseGameConfig() // initialise default config - snakeRequest := getIndividualBoardStateForSnake(state, s1State, snakeStates, getRuleset(0, snakeStates)) + + snakeRequest := gameState.getRequestBodyForSnake(state, s1State) requestBody := serialiseSnakeRequest(snakeRequest) test.RequireJSONMatchesFixture(t, "testdata/snake_request_body.json", string(requestBody)) @@ -69,34 +98,25 @@ func TestSettingsRequestSerialization(t *testing.T) { Tail: "bolt", 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{ rules.GameTypeStandard, rules.GameTypeRoyale, rules.GameTypeSolo, - rules.GameTypeWrapped, rules.GameTypeSquad, rules.GameTypeConstrictor, + rules.GameTypeWrapped, rules.GameTypeConstrictor, } { t.Run(gt, func(t *testing.T) { - // apply game type - ruleset := rsb.WithParams(map[string]string{rules.ParamGameType: gt}).Ruleset() + gameState := buildDefaultGameState() - 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) t.Log(string(requestBody)) @@ -128,7 +148,6 @@ func TestConvertRulesSnakes(t *testing.T) { ID: "one", Name: "ONE", URL: "http://example1.com", - Squad: "squadA", Head: "a", Tail: "b", Color: "#012345", @@ -146,7 +165,6 @@ func TestConvertRulesSnakes(t *testing.T) { Head: client.Coord{X: 3, Y: 3}, Length: 2, Shout: "", - Squad: "squadA", Customizations: client.Customizations{ Color: "#012345", Head: "a", diff --git a/cli/commands/root.go b/cli/commands/root.go index c8dc846..df8ec66 100644 --- a/cli/commands/root.go +++ b/cli/commands/root.go @@ -2,9 +2,10 @@ package commands import ( "fmt" - "github.com/spf13/cobra" "os" + "github.com/spf13/cobra" + homedir "github.com/mitchellh/go-homedir" "github.com/spf13/viper" ) @@ -18,6 +19,8 @@ var rootCmd = &cobra.Command{ } func Execute() { + rootCmd.AddCommand(NewPlayCommand()) + if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) diff --git a/cli/commands/testdata/snake_request_body.json b/cli/commands/testdata/snake_request_body.json index 058c596..bed79c3 100644 --- a/cli/commands/testdata/snake_request_body.json +++ b/cli/commands/testdata/snake_request_body.json @@ -1,6 +1,6 @@ { "game": { - "id": "", + "id": "GAME_ID", "ruleset": { "name": "standard", "version": "cli", @@ -11,7 +11,7 @@ "hazardMap": "", "hazardMapAuthor": "", "royale": { - "shrinkEveryNTurns": 0 + "shrinkEveryNTurns": 25 }, "squad": { "allowBodyCollisions": false, @@ -21,7 +21,7 @@ } } }, - "map": "", + "map": "standard", "timeout": 500, "source": "" }, diff --git a/cli/commands/testdata/snake_request_body_constrictor.json b/cli/commands/testdata/snake_request_body_constrictor.json index f57032d..f7aa878 100644 --- a/cli/commands/testdata/snake_request_body_constrictor.json +++ b/cli/commands/testdata/snake_request_body_constrictor.json @@ -1,6 +1,6 @@ { "game": { - "id": "", + "id": "GAME_ID", "ruleset": { "name": "constrictor", "version": "cli", @@ -8,10 +8,10 @@ "foodSpawnChance": 11, "minimumFood": 7, "hazardDamagePerTurn": 19, - "hazardMap": "hz_spiral", - "hazardMapAuthor": "altersaddle", + "hazardMap": "", + "hazardMapAuthor": "", "royale": { - "shrinkEveryNTurns": 0 + "shrinkEveryNTurns": 17 }, "squad": { "allowBodyCollisions": false, @@ -21,7 +21,7 @@ } } }, - "map": "", + "map": "standard", "timeout": 500, "source": "" }, diff --git a/cli/commands/testdata/snake_request_body_royale.json b/cli/commands/testdata/snake_request_body_royale.json index 9e5cbf7..17e5120 100644 --- a/cli/commands/testdata/snake_request_body_royale.json +++ b/cli/commands/testdata/snake_request_body_royale.json @@ -1,6 +1,6 @@ { "game": { - "id": "", + "id": "GAME_ID", "ruleset": { "name": "royale", "version": "cli", @@ -8,8 +8,8 @@ "foodSpawnChance": 11, "minimumFood": 7, "hazardDamagePerTurn": 19, - "hazardMap": "hz_spiral", - "hazardMapAuthor": "altersaddle", + "hazardMap": "", + "hazardMapAuthor": "", "royale": { "shrinkEveryNTurns": 17 }, @@ -21,7 +21,7 @@ } } }, - "map": "", + "map": "standard", "timeout": 500, "source": "" }, diff --git a/cli/commands/testdata/snake_request_body_solo.json b/cli/commands/testdata/snake_request_body_solo.json index 05a2934..de20fef 100644 --- a/cli/commands/testdata/snake_request_body_solo.json +++ b/cli/commands/testdata/snake_request_body_solo.json @@ -1,6 +1,6 @@ { "game": { - "id": "", + "id": "GAME_ID", "ruleset": { "name": "solo", "version": "cli", @@ -8,10 +8,10 @@ "foodSpawnChance": 11, "minimumFood": 7, "hazardDamagePerTurn": 19, - "hazardMap": "hz_spiral", - "hazardMapAuthor": "altersaddle", + "hazardMap": "", + "hazardMapAuthor": "", "royale": { - "shrinkEveryNTurns": 0 + "shrinkEveryNTurns": 17 }, "squad": { "allowBodyCollisions": false, @@ -21,7 +21,7 @@ } } }, - "map": "", + "map": "standard", "timeout": 500, "source": "" }, diff --git a/cli/commands/testdata/snake_request_body_squad.json b/cli/commands/testdata/snake_request_body_squad.json index 8d54f17..c053ac4 100644 --- a/cli/commands/testdata/snake_request_body_squad.json +++ b/cli/commands/testdata/snake_request_body_squad.json @@ -11,7 +11,7 @@ "hazardMap": "hz_spiral", "hazardMapAuthor": "altersaddle", "royale": { - "shrinkEveryNTurns": 0 + "shrinkEveryNTurns": 17 }, "squad": { "allowBodyCollisions": true, diff --git a/cli/commands/testdata/snake_request_body_standard.json b/cli/commands/testdata/snake_request_body_standard.json index a5e9111..35599fd 100644 --- a/cli/commands/testdata/snake_request_body_standard.json +++ b/cli/commands/testdata/snake_request_body_standard.json @@ -1,6 +1,6 @@ { "game": { - "id": "", + "id": "GAME_ID", "ruleset": { "name": "standard", "version": "cli", @@ -8,10 +8,10 @@ "foodSpawnChance": 11, "minimumFood": 7, "hazardDamagePerTurn": 19, - "hazardMap": "hz_spiral", - "hazardMapAuthor": "altersaddle", + "hazardMap": "", + "hazardMapAuthor": "", "royale": { - "shrinkEveryNTurns": 0 + "shrinkEveryNTurns": 17 }, "squad": { "allowBodyCollisions": false, @@ -21,7 +21,7 @@ } } }, - "map": "", + "map": "standard", "timeout": 500, "source": "" }, diff --git a/cli/commands/testdata/snake_request_body_wrapped.json b/cli/commands/testdata/snake_request_body_wrapped.json index 7012b5a..502b2c1 100644 --- a/cli/commands/testdata/snake_request_body_wrapped.json +++ b/cli/commands/testdata/snake_request_body_wrapped.json @@ -1,6 +1,6 @@ { "game": { - "id": "", + "id": "GAME_ID", "ruleset": { "name": "wrapped", "version": "cli", @@ -8,10 +8,10 @@ "foodSpawnChance": 11, "minimumFood": 7, "hazardDamagePerTurn": 19, - "hazardMap": "hz_spiral", - "hazardMapAuthor": "altersaddle", + "hazardMap": "", + "hazardMapAuthor": "", "royale": { - "shrinkEveryNTurns": 0 + "shrinkEveryNTurns": 17 }, "squad": { "allowBodyCollisions": false, @@ -21,7 +21,7 @@ } } }, - "map": "", + "map": "standard", "timeout": 500, "source": "" }, diff --git a/constants.go b/constants.go index 3116d88..0ef46f2 100644 --- a/constants.go +++ b/constants.go @@ -24,7 +24,6 @@ const ( EliminatedByOutOfHealth = "out-of-health" EliminatedByHeadToHeadCollision = "head-collision" EliminatedByOutOfBounds = "wall-collision" - EliminatedBySquad = "squad-eliminated" // Error constants ErrorTooManySnakes = RulesetError("too many snakes for fixed start positions") @@ -41,7 +40,6 @@ const ( GameTypeConstrictor = "constrictor" GameTypeRoyale = "royale" GameTypeSolo = "solo" - GameTypeSquad = "squad" GameTypeStandard = "standard" GameTypeWrapped = "wrapped" diff --git a/maps/helpers_test.go b/maps/helpers_test.go index 3aa2c5d..5c37df2 100644 --- a/maps/helpers_test.go +++ b/maps/helpers_test.go @@ -19,11 +19,10 @@ func TestSetupBoard_Error(t *testing.T) { Id: t.Name(), Error: errors.New("bad map update"), } - RegisterMap(testMap.ID(), testMap) - - _, err := SetupBoard(testMap.ID(), rules.Settings{}, 10, 10, []string{}) - - require.EqualError(t, err, "bad map update") + TestMap(testMap.ID(), testMap, func() { + _, err := SetupBoard(testMap.ID(), rules.Settings{}, 10, 10, []string{}) + require.EqualError(t, err, "bad map update") + }) } func TestSetupBoard(t *testing.T) { @@ -42,26 +41,27 @@ func TestSetupBoard(t *testing.T) { {X: 2, Y: 2}, }, } - RegisterMap(testMap.ID(), testMap) - boardState, err := SetupBoard(testMap.ID(), rules.Settings{}, 10, 10, []string{"1", "2"}) + TestMap(testMap.ID(), testMap, func() { + boardState, err := SetupBoard(testMap.ID(), rules.Settings{}, 10, 10, []string{"1", "2"}) - require.NoError(t, err) + require.NoError(t, err) - require.Len(t, boardState.Snakes, 2) + require.Len(t, boardState.Snakes, 2) - require.Equal(t, rules.Snake{ - ID: "1", - Body: []rules.Point{{X: 3, Y: 4}, {X: 3, Y: 4}, {X: 3, Y: 4}}, - Health: rules.SnakeMaxHealth, - }, boardState.Snakes[0]) - require.Equal(t, rules.Snake{ - ID: "2", - Body: []rules.Point{{X: 6, Y: 2}, {X: 6, Y: 2}, {X: 6, Y: 2}}, - Health: rules.SnakeMaxHealth, - }, boardState.Snakes[1]) - 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.Snake{ + ID: "1", + Body: []rules.Point{{X: 3, Y: 4}, {X: 3, Y: 4}, {X: 3, Y: 4}}, + Health: rules.SnakeMaxHealth, + }, boardState.Snakes[0]) + require.Equal(t, rules.Snake{ + ID: "2", + Body: []rules.Point{{X: 6, Y: 2}, {X: 6, Y: 2}, {X: 6, Y: 2}}, + Health: rules.SnakeMaxHealth, + }, boardState.Snakes[1]) + 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) + }) } func TestUpdateBoard(t *testing.T) { @@ -80,7 +80,6 @@ func TestUpdateBoard(t *testing.T) { {X: 2, Y: 2}, }, } - RegisterMap(testMap.ID(), testMap) previousBoardState := &rules.BoardState{ Turn: 0, @@ -98,17 +97,20 @@ func TestUpdateBoard(t *testing.T) { }, }, } - boardState, err := UpdateBoard(testMap.ID(), previousBoardState, rules.Settings{}) - require.NoError(t, err) + TestMap(testMap.ID(), testMap, func() { + boardState, err := UpdateBoard(testMap.ID(), previousBoardState, rules.Settings{}) - require.Len(t, boardState.Snakes, 1) + require.NoError(t, err) - require.Equal(t, rules.Snake{ - ID: "1", - Body: []rules.Point{{X: 6, Y: 4}, {X: 6, Y: 3}, {X: 6, Y: 2}}, - Health: rules.SnakeMaxHealth, - }, 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: 3, Y: 4}, {X: 3, Y: 5}, {X: 2, Y: 2}}, boardState.Hazards) + require.Len(t, boardState.Snakes, 1) + + require.Equal(t, rules.Snake{ + ID: "1", + Body: []rules.Point{{X: 6, Y: 4}, {X: 6, Y: 3}, {X: 6, Y: 2}}, + Health: rules.SnakeMaxHealth, + }, 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: 3, Y: 4}, {X: 3, Y: 5}, {X: 2, Y: 2}}, boardState.Hazards) + }) } diff --git a/maps/registry.go b/maps/registry.go index 3c9fff8..de79de3 100644 --- a/maps/registry.go +++ b/maps/registry.go @@ -38,3 +38,9 @@ func GetMap(id string) (GameMap, error) { func RegisterMap(id string, m GameMap) { globalRegistry.RegisterMap(id, m) } + +func TestMap(id string, m GameMap, callback func()) { + globalRegistry[id] = m + callback() + delete(globalRegistry, id) +} diff --git a/maps/registry_test.go b/maps/registry_test.go new file mode 100644 index 0000000..6016108 --- /dev/null +++ b/maps/registry_test.go @@ -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") + }) + } +} diff --git a/maps/royale.go b/maps/royale.go index c1a5d6d..59f1e66 100644 --- a/maps/royale.go +++ b/maps/royale.go @@ -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 { + // 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 turn := lastBoardState.Turn + 1 diff --git a/pipeline.go b/pipeline.go index 711772b..b11e6fd 100644 --- a/pipeline.go +++ b/pipeline.go @@ -11,14 +11,12 @@ const ( StageHazardDamageStandard = "hazard_damage.standard" StageEliminationStandard = "elimination.standard" - StageGameOverSoloSnake = "game_over.solo_snake" - StageGameOverBySquad = "game_over.by_squad" - StageSpawnFoodNoFood = "spawn_food.no_food" - StageSpawnHazardsShrinkMap = "spawn_hazards.shrink_map" - StageEliminationResurrectSquadCollisions = "elimination.resurrect_squad_collisions" - StageModifySnakesAlwaysGrow = "modify_snakes.always_grow" - StageMovementWrapBoundaries = "movement.wrap_boundaries" - StageModifySnakesShareAttributes = "modify_snakes.share_attributes" + StageGameOverSoloSnake = "game_over.solo_snake" + StageSpawnFoodNoFood = "spawn_food.no_food" + StageSpawnHazardsShrinkMap = "spawn_hazards.shrink_map" + StageModifySnakesAlwaysGrow = "modify_snakes.always_grow" + StageMovementWrapBoundaries = "movement.wrap_boundaries" + StageModifySnakesShareAttributes = "modify_snakes.share_attributes" ) // globalRegistry is a global, default mapping of stage names to stage functions. @@ -26,21 +24,18 @@ const ( // Plugins that wish to extend the available game stages should call RegisterPipelineStageError // to add additional stages. var globalRegistry = StageRegistry{ - StageSpawnFoodNoFood: RemoveFoodConstrictor, - StageSpawnFoodStandard: SpawnFoodStandard, - StageGameOverSoloSnake: GameOverSolo, - StageGameOverBySquad: GameOverSquad, - StageGameOverStandard: GameOverStandard, - StageHazardDamageStandard: DamageHazardsStandard, - StageSpawnHazardsShrinkMap: PopulateHazardsRoyale, - StageStarvationStandard: ReduceSnakeHealthStandard, - StageEliminationResurrectSquadCollisions: ResurrectSnakesSquad, - StageFeedSnakesStandard: FeedSnakesStandard, - StageEliminationStandard: EliminateSnakesStandard, - StageModifySnakesAlwaysGrow: GrowSnakesConstrictor, - StageMovementStandard: MoveSnakesStandard, - StageMovementWrapBoundaries: MoveSnakesWrapped, - StageModifySnakesShareAttributes: ShareAttributesSquad, + StageSpawnFoodNoFood: RemoveFoodConstrictor, + StageSpawnFoodStandard: SpawnFoodStandard, + StageGameOverSoloSnake: GameOverSolo, + StageGameOverStandard: GameOverStandard, + StageHazardDamageStandard: DamageHazardsStandard, + StageSpawnHazardsShrinkMap: PopulateHazardsRoyale, + StageStarvationStandard: ReduceSnakeHealthStandard, + StageFeedSnakesStandard: FeedSnakesStandard, + StageEliminationStandard: EliminateSnakesStandard, + StageModifySnakesAlwaysGrow: GrowSnakesConstrictor, + StageMovementStandard: MoveSnakesStandard, + StageMovementWrapBoundaries: MoveSnakesWrapped, } // StageFunc represents a single stage of an ordered pipeline and applies custom logic to the board state each turn. diff --git a/royale.go b/royale.go index 2b8e7ce..ddec548 100644 --- a/royale.go +++ b/royale.go @@ -2,7 +2,6 @@ package rules import ( "errors" - "math/rand" ) var royaleRulesetStages = []string{ @@ -10,7 +9,6 @@ var royaleRulesetStages = []string{ StageStarvationStandard, StageHazardDamageStandard, StageFeedSnakesStandard, - StageSpawnFoodStandard, StageEliminationStandard, StageSpawnHazardsShrinkMap, StageGameOverStandard, @@ -19,8 +17,6 @@ var royaleRulesetStages = []string{ type RoyaleRuleset struct { StandardRuleset - Seed int64 - ShrinkEveryNTurns int } @@ -55,7 +51,7 @@ func PopulateHazardsRoyale(b *BoardState, settings Settings, moves []SnakeMove) return false, nil } - randGenerator := rand.New(rand.NewSource(settings.RoyaleSettings.seed)) + randGenerator := settings.GetRand(0) numShrinks := turn / settings.RoyaleSettings.ShrinkEveryNTurns minX, maxX := 0, b.Width-1 @@ -91,7 +87,6 @@ func (r *RoyaleRuleset) IsGameOver(b *BoardState) (bool, error) { func (r RoyaleRuleset) Settings() Settings { s := r.StandardRuleset.Settings() s.RoyaleSettings = RoyaleSettings{ - seed: r.Seed, ShrinkEveryNTurns: r.ShrinkEveryNTurns, } return s diff --git a/royale_test.go b/royale_test.go index 29bb49d..52d3174 100644 --- a/royale_test.go +++ b/royale_test.go @@ -94,15 +94,14 @@ func TestRoyaleHazards(t *testing.T) { Width: test.Width, Height: test.Height, } - r := RoyaleRuleset{ - StandardRuleset: StandardRuleset{ - HazardDamagePerTurn: 1, + settings := Settings{ + HazardDamagePerTurn: 1, + RoyaleSettings: RoyaleSettings{ + ShrinkEveryNTurns: test.ShrinkEveryNTurns, }, - Seed: seed, - ShrinkEveryNTurns: test.ShrinkEveryNTurns, - } + }.WithSeed(seed) - _, err := PopulateHazardsRoyale(b, r.Settings(), mockSnakeMoves()) + _, err := PopulateHazardsRoyale(b, settings, mockSnakeMoves()) require.Equal(t, test.Error, err) if err == nil { // 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 // also that: // - snakes move properly @@ -264,13 +205,13 @@ func TestRoyaleCreateNextBoardState(t *testing.T) { }, ShrinkEveryNTurns: 1, } - rand.Seed(0) rb := NewRulesetBuilder().WithParams(map[string]string{ ParamGameType: GameTypeRoyale, ParamHazardDamagePerTurn: "1", ParamShrinkEveryNTurns: "1", - }) + }).WithSeed(1234) for _, gc := range cases { + rand.Seed(1234) gc.requireValidNextState(t, &r) // also test a RulesBuilder constructed instance gc.requireValidNextState(t, rb.Ruleset()) diff --git a/ruleset.go b/ruleset.go index 0da8a2f..1b28ec0 100644 --- a/ruleset.go +++ b/ruleset.go @@ -27,7 +27,7 @@ type Settings struct { HazardMap string `json:"hazardMap"` HazardMapAuthor string `json:"hazardMapAuthor"` RoyaleSettings RoyaleSettings `json:"royale"` - SquadSettings SquadSettings `json:"squad"` + SquadSettings SquadSettings `json:"squad"` // Deprecated, provided with default fields for API compatibility rand Rand seed int64 @@ -41,7 +41,7 @@ func (settings Settings) GetRand(turn int) Rand { } 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. @@ -64,13 +64,11 @@ func (settings Settings) WithSeed(seed int64) Settings { // RoyaleSettings contains settings that are specific to the "royale" game mode type RoyaleSettings struct { - seed int64 ShrinkEveryNTurns int `json:"shrinkEveryNTurns"` } // SquadSettings contains settings that are specific to the "squad" game mode type SquadSettings struct { - squadMap map[string]string AllowBodyCollisions bool `json:"allowBodyCollisions"` SharedElimination bool `json:"sharedElimination"` SharedHealth bool `json:"sharedHealth"` @@ -81,14 +79,12 @@ type rulesetBuilder struct { params map[string]string // game customisation parameters seed int64 // used for random events in games 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. func NewRulesetBuilder() *rulesetBuilder { return &rulesetBuilder{ params: map[string]string{}, - squads: map[string]string{}, } } @@ -122,66 +118,27 @@ func (rb *rulesetBuilder) WithRand(rand Rand) *rulesetBuilder { 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. 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] if !ok { - return standardRuleset + name = GameTypeStandard } switch name { + case GameTypeStandard: + return rb.PipelineRuleset(name, NewPipeline(standardRulesetStages...)) case GameTypeConstrictor: - return &ConstrictorRuleset{ - StandardRuleset: *standardRuleset, - } + return rb.PipelineRuleset(name, NewPipeline(constrictorRulesetStages...)) case GameTypeRoyale: - return &RoyaleRuleset{ - StandardRuleset: *standardRuleset, - Seed: rb.seed, - ShrinkEveryNTurns: paramsInt(rb.params, ParamShrinkEveryNTurns, 0), - } + return rb.PipelineRuleset(name, NewPipeline(royaleRulesetStages...)) case GameTypeSolo: - return &SoloRuleset{ - StandardRuleset: *standardRuleset, - } + return rb.PipelineRuleset(name, NewPipeline(soloRulesetStages...)) case GameTypeWrapped: - return &WrappedRuleset{ - StandardRuleset: *standardRuleset, - } - 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 rb.PipelineRuleset(name, NewPipeline(wrappedRulesetStages...)) + default: + return rb.PipelineRuleset(name, NewPipeline(standardRulesetStages...)) } - 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. @@ -198,16 +155,8 @@ func (rb rulesetBuilder) PipelineRuleset(name string, p Pipeline) PipelineRulese HazardMap: rb.params[ParamHazardMap], HazardMapAuthor: rb.params[ParamHazardMapAuthor], RoyaleSettings: RoyaleSettings{ - seed: rb.seed, 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, seed: rb.seed, }, diff --git a/ruleset_internal_test.go b/ruleset_internal_test.go index 55f009f..bbd47e7 100644 --- a/ruleset_internal_test.go +++ b/ruleset_internal_test.go @@ -41,30 +41,11 @@ func TestRulesetError(t *testing.T) { } func TestRulesetBuilderInternals(t *testing.T) { - // test Royale with seed rsb := NewRulesetBuilder().WithSeed(3).WithParams(map[string]string{ParamGameType: GameTypeRoyale}) require.Equal(t, int64(3), rsb.seed) require.Equal(t, GameTypeRoyale, rsb.Ruleset().Name()) - require.Equal(t, int64(3), rsb.Ruleset().(*RoyaleRuleset).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"]) + require.Equal(t, int64(3), rsb.Ruleset().Settings().Seed()) // test parameter merging rsb = NewRulesetBuilder(). diff --git a/ruleset_test.go b/ruleset_test.go index a8d3d1b..5eb6500 100644 --- a/ruleset_test.go +++ b/ruleset_test.go @@ -59,7 +59,6 @@ func TestSoloRulesetSettings(t *testing.T) { func TestRoyaleRulesetSettings(t *testing.T) { ruleset := rules.RoyaleRuleset{ - Seed: 30, ShrinkEveryNTurns: 12, StandardRuleset: rules.StandardRuleset{ MinimumFood: 5, @@ -94,32 +93,6 @@ func TestConstrictorRulesetSettings(t *testing.T) { 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) { // Test that a fresh instance can produce a 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 expectedResults := []struct { GameType string - Snakes map[string]string }{ {GameType: rules.GameTypeStandard}, {GameType: rules.GameTypeWrapped}, {GameType: rules.GameTypeRoyale}, {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}, } @@ -164,11 +126,6 @@ func TestRulesetBuilder(t *testing.T) { rules.ParamHazardMapAuthor: "tester", }) - // add any snake squads - for id, squad := range expected.Snakes { - rsb = rsb.AddSnakeToSquad(id, squad) - } - require.NotNil(t, rsb.Ruleset()) require.Equal(t, expected.GameType, rsb.Ruleset().Name()) // All the standard settings should always be copied over @@ -200,8 +157,8 @@ func TestRulesetBuilderGetRand(t *testing.T) { rand1 := ruleset.Settings().GetRand(turn) // Should produce a predictable series of numbers based on a seed - require.Equal(t, 80, rand1.Intn(100)) - require.Equal(t, 94, rand1.Intn(100)) + require.Equal(t, 83, rand1.Intn(100)) + require.Equal(t, 15, rand1.Intn(100)) // Should produce the same number if re-initialized require.Equal( @@ -211,6 +168,6 @@ func TestRulesetBuilderGetRand(t *testing.T) { ) // Should produce a different series of numbers for another turn - require.Equal(t, 22, rand1.Intn(100)) - require.Equal(t, 16, rand1.Intn(100)) + require.Equal(t, 69, rand1.Intn(100)) + require.Equal(t, 86, rand1.Intn(100)) } diff --git a/solo.go b/solo.go index 68dc8c5..bf8b969 100644 --- a/solo.go +++ b/solo.go @@ -5,7 +5,6 @@ var soloRulesetStages = []string{ StageStarvationStandard, StageHazardDamageStandard, StageFeedSnakesStandard, - StageSpawnFoodStandard, StageEliminationStandard, StageGameOverSoloSnake, } diff --git a/squad.go b/squad.go deleted file mode 100644 index 0ee852d..0000000 --- a/squad.go +++ /dev/null @@ -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 -} diff --git a/squad_test.go b/squad_test.go deleted file mode 100644 index 825dd03..0000000 --- a/squad_test.go +++ /dev/null @@ -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...))) - } -} diff --git a/standard.go b/standard.go index 7ae9502..28804e5 100644 --- a/standard.go +++ b/standard.go @@ -18,7 +18,6 @@ var standardRulesetStages = []string{ StageStarvationStandard, StageHazardDamageStandard, StageFeedSnakesStandard, - StageSpawnFoodStandard, StageEliminationStandard, StageGameOverStandard, } @@ -387,6 +386,7 @@ func growSnake(snake *Snake) { } } +// Deprecated: handled by maps.Standard func SpawnFoodStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) { if IsInitialization(b, settings, moves) { return false, nil diff --git a/wrapped.go b/wrapped.go index f29ae37..d2d7070 100644 --- a/wrapped.go +++ b/wrapped.go @@ -5,7 +5,6 @@ var wrappedRulesetStages = []string{ StageStarvationStandard, StageHazardDamageStandard, StageFeedSnakesStandard, - StageSpawnFoodStandard, StageEliminationStandard, StageGameOverStandard, }