diff --git a/cli/commands/play.go b/cli/commands/play.go index 622edad..40e26aa 100644 --- a/cli/commands/play.go +++ b/cli/commands/play.go @@ -15,100 +15,26 @@ import ( "time" "github.com/BattlesnakeOfficial/rules" + "github.com/BattlesnakeOfficial/rules/client" "github.com/google/uuid" "github.com/spf13/cobra" ) -type Battlesnake struct { +// Used to store state for each SnakeState while running a local game +type SnakeState struct { URL string Name string ID string - API string LastMove string Squad string Character rune -} - -type Coord struct { - X int32 `json:"x"` - Y int32 `json:"y"` -} - -type SnakeResponse struct { - Id string `json:"id"` - Name string `json:"name"` - Health int32 `json:"health"` - Body []Coord `json:"body"` - Latency string `json:"latency"` - Head Coord `json:"head"` - Length int32 `json:"length"` - Shout string `json:"shout"` - Squad string `json:"squad"` -} - -type BoardResponse struct { - Height int32 `json:"height"` - Width int32 `json:"width"` - Food []Coord `json:"food"` - Hazards []Coord `json:"hazards"` - Snakes []SnakeResponse `json:"snakes"` -} - -type GameResponseRulesetSettings struct { - HazardDamagePerTurn int32 `json:"hazardDamagePerTurn"` - FoodSpawnChance int32 `json:"foodSpawnChance"` - MinimumFood int32 `json:"minimumFood"` - RoyaleSettings RoyaleSettings `json:"royale"` - SquadSettings SquadSettings `json:"squad"` -} - -type RoyaleSettings struct { - ShrinkEveryNTurns int32 `json:"shrinkEveryNTurns"` -} - -type SquadSettings struct { - AllowBodyCollisions bool `json:"allowBodyCollisions"` - SharedElimination bool `json:"sharedElimination"` - SharedHealth bool `json:"sharedHealth"` - SharedLength bool `json:"sharedLength"` -} - -type GameResponseRuleset struct { - Name string `json:"name"` - Version string `json:"version"` - Settings GameResponseRulesetSettings `json:"settings"` -} - -type GameResponse struct { - Id string `json:"id"` - Timeout int32 `json:"timeout"` - Ruleset GameResponseRuleset `json:"ruleset"` -} - -type ResponsePayload struct { - Game GameResponse `json:"game"` - Turn int32 `json:"turn"` - Board BoardResponse `json:"board"` - You SnakeResponse `json:"you"` -} - -type PlayerResponse struct { - Move string `json:"move"` - Shout string `json:"shout"` -} - -type PingResponse struct { - APIVersion string `json:"apiversion"` - Author string `json:"author"` - Color string `json:"color"` - Head string `json:"head"` - Tail string `json:"tail"` - Version string `json:"version"` + Color string + Head string + Tail string } var GameId string var Turn int32 -var Battlesnakes map[string]Battlesnake var HttpClient http.Client var Width int32 var Height int32 @@ -162,24 +88,20 @@ func init() { var run = func(cmd *cobra.Command, args []string) { rand.Seed(Seed) - Battlesnakes = make(map[string]Battlesnake) GameId = uuid.New().String() Turn = 0 - snakes := buildSnakesFromOptions() + snakeStates := buildSnakesFromOptions() - ruleset := getRuleset(Seed, snakes) - state := initializeBoardFromArgs(ruleset, snakes) - for _, snake := range snakes { - Battlesnakes[snake.ID] = snake - } + ruleset := getRuleset(Seed, snakeStates) + state := initializeBoardFromArgs(ruleset, snakeStates) for v := false; !v; v, _ = ruleset.IsGameOver(state) { Turn++ - state = createNextBoardState(ruleset, state, snakes, Turn) + state = createNextBoardState(ruleset, state, snakeStates, Turn) if ViewMap { - printMap(state, Turn) + printMap(state, snakeStates, Turn) } else { log.Printf("[%v]: State: %v\n", Turn, state) } @@ -197,9 +119,9 @@ var run = func(cmd *cobra.Command, args []string) { for _, snake := range state.Snakes { if snake.EliminatedCause == rules.NotEliminated { isDraw = false - winner = Battlesnakes[snake.ID].Name + winner = snakeStates[snake.ID].Name } - sendEndRequest(ruleset, state, Battlesnakes[snake.ID]) + sendEndRequest(ruleset, state, snakeStates[snake.ID], snakeStates) } if isDraw { @@ -210,7 +132,7 @@ var run = func(cmd *cobra.Command, args []string) { } } -func getRuleset(seed int64, snakes []Battlesnake) rules.Ruleset { +func getRuleset(seed int64, snakeStates map[string]SnakeState) rules.Ruleset { var ruleset rules.Ruleset var royale rules.RoyaleRuleset @@ -231,8 +153,8 @@ func getRuleset(seed int64, snakes []Battlesnake) rules.Ruleset { ruleset = &royale case "squad": squadMap := map[string]string{} - for _, snake := range snakes { - squadMap[snake.ID] = snake.Squad + for _, snakeState := range snakeStates { + squadMap[snakeState.ID] = snakeState.Squad } ruleset = &rules.SquadRuleset{ StandardRuleset: standard, @@ -260,7 +182,7 @@ func getRuleset(seed int64, snakes []Battlesnake) rules.Ruleset { return ruleset } -func initializeBoardFromArgs(ruleset rules.Ruleset, snakes []Battlesnake) *rules.BoardState { +func initializeBoardFromArgs(ruleset rules.Ruleset, snakeStates map[string]SnakeState) *rules.BoardState { if Timeout == 0 { Timeout = 500 } @@ -269,8 +191,8 @@ func initializeBoardFromArgs(ruleset rules.Ruleset, snakes []Battlesnake) *rules } snakeIds := []string{} - for _, snake := range snakes { - snakeIds = append(snakeIds, snake.ID) + for _, snakeState := range snakeStates { + snakeIds = append(snakeIds, snakeState.ID) } state, err := rules.CreateDefaultBoardState(Width, Height, snakeIds) if err != nil { @@ -281,9 +203,9 @@ func initializeBoardFromArgs(ruleset rules.Ruleset, snakes []Battlesnake) *rules log.Panic("[PANIC]: Error Initializing Board State") } - for _, snake := range snakes { - requestBody := getIndividualBoardStateForSnake(state, snake, ruleset) - u, _ := url.ParseRequestURI(snake.URL) + for _, snakeState := range snakeStates { + requestBody := getIndividualBoardStateForSnake(state, snakeState, snakeStates, ruleset) + u, _ := url.ParseRequestURI(snakeState.URL) u.Path = path.Join(u.Path, "start") if DebugRequests { log.Printf("POST %s: %v", u, string(requestBody)) @@ -296,25 +218,28 @@ func initializeBoardFromArgs(ruleset rules.Ruleset, snakes []Battlesnake) *rules return state } -func createNextBoardState(ruleset rules.Ruleset, state *rules.BoardState, snakes []Battlesnake, turn int32) *rules.BoardState { +func createNextBoardState(ruleset rules.Ruleset, state *rules.BoardState, snakeStates map[string]SnakeState, turn int32) *rules.BoardState { var moves []rules.SnakeMove if Sequential { - for _, snake := range snakes { - for _, stateSnake := range state.Snakes { - if snake.ID == stateSnake.ID && stateSnake.EliminatedCause == rules.NotEliminated { - moves = append(moves, getMoveForSnake(ruleset, state, snake)) + for _, snakeState := range snakeStates { + for _, snake := range state.Snakes { + if snakeState.ID == snake.ID && snake.EliminatedCause == rules.NotEliminated { + moves = append(moves, getMoveForSnake(ruleset, state, snakeState, snakeStates)) } } } } else { var wg sync.WaitGroup - c := make(chan rules.SnakeMove, len(snakes)) + c := make(chan rules.SnakeMove, len(snakeStates)) - for _, snake := range snakes { - for _, stateSnake := range state.Snakes { - if snake.ID == stateSnake.ID && stateSnake.EliminatedCause == rules.NotEliminated { + for _, snakeState := range snakeStates { + for _, snake := range state.Snakes { + if snakeState.ID == snake.ID && snake.EliminatedCause == rules.NotEliminated { wg.Add(1) - go getConcurrentMoveForSnake(&wg, ruleset, state, snake, c) + go func(snakeState SnakeState) { + defer wg.Done() + c <- getMoveForSnake(ruleset, state, snakeState, snakeStates) + }(snakeState) } } } @@ -327,14 +252,13 @@ func createNextBoardState(ruleset rules.Ruleset, state *rules.BoardState, snakes } } for _, move := range moves { - snake := Battlesnakes[move.ID] - snake.LastMove = move.Move - Battlesnakes[move.ID] = snake + snakeState := snakeStates[move.ID] + snakeState.LastMove = move.Move + snakeStates[move.ID] = snakeState } state, err := ruleset.CreateNextBoardState(state, moves) if err != nil { - log.Panic("[PANIC]: Error Producing Next Board State") - panic(err) + log.Panicf("[PANIC]: Error Producing Next Board State: %v", err) } state.Turn = turn @@ -342,20 +266,15 @@ func createNextBoardState(ruleset rules.Ruleset, state *rules.BoardState, snakes return state } -func getConcurrentMoveForSnake(wg *sync.WaitGroup, ruleset rules.Ruleset, state *rules.BoardState, snake Battlesnake, c chan rules.SnakeMove) { - defer wg.Done() - c <- getMoveForSnake(ruleset, state, snake) -} - -func getMoveForSnake(ruleset rules.Ruleset, state *rules.BoardState, snake Battlesnake) rules.SnakeMove { - requestBody := getIndividualBoardStateForSnake(state, snake, ruleset) - u, _ := url.ParseRequestURI(snake.URL) +func getMoveForSnake(ruleset rules.Ruleset, state *rules.BoardState, snakeState SnakeState, snakeStates map[string]SnakeState) rules.SnakeMove { + requestBody := getIndividualBoardStateForSnake(state, snakeState, snakeStates, ruleset) + u, _ := url.ParseRequestURI(snakeState.URL) u.Path = path.Join(u.Path, "move") if DebugRequests { log.Printf("POST %s: %v", u, string(requestBody)) } res, err := HttpClient.Post(u.String(), "application/json", bytes.NewBuffer(requestBody)) - move := snake.LastMove + move := snakeState.LastMove if err != nil { log.Printf("[WARN]: Request to %v failed\n", u.String()) log.Printf("Body --> %v\n", string(requestBody)) @@ -365,7 +284,7 @@ func getMoveForSnake(ruleset rules.Ruleset, state *rules.BoardState, snake Battl if readErr != nil { log.Fatal(readErr) } else { - playerResponse := PlayerResponse{} + playerResponse := client.MoveResponse{} jsonErr := json.Unmarshal(body, &playerResponse) if jsonErr != nil { log.Fatal(jsonErr) @@ -374,12 +293,12 @@ func getMoveForSnake(ruleset rules.Ruleset, state *rules.BoardState, snake Battl } } } - return rules.SnakeMove{ID: snake.ID, Move: move} + return rules.SnakeMove{ID: snakeState.ID, Move: move} } -func sendEndRequest(ruleset rules.Ruleset, state *rules.BoardState, snake Battlesnake) { - requestBody := getIndividualBoardStateForSnake(state, snake, ruleset) - u, _ := url.ParseRequestURI(snake.URL) +func sendEndRequest(ruleset rules.Ruleset, state *rules.BoardState, snakeState SnakeState, snakeStates map[string]SnakeState) { + requestBody := getIndividualBoardStateForSnake(state, snakeState, snakeStates, ruleset) + u, _ := url.ParseRequestURI(snakeState.URL) u.Path = path.Join(u.Path, "end") if DebugRequests { log.Printf("POST %s: %v", u, string(requestBody)) @@ -390,26 +309,26 @@ func sendEndRequest(ruleset rules.Ruleset, state *rules.BoardState, snake Battle } } -func getIndividualBoardStateForSnake(state *rules.BoardState, snake Battlesnake, ruleset rules.Ruleset) []byte { +func getIndividualBoardStateForSnake(state *rules.BoardState, snakeState SnakeState, snakeStates map[string]SnakeState, ruleset rules.Ruleset) []byte { var youSnake rules.Snake for _, snk := range state.Snakes { - if snake.ID == snk.ID { + if snakeState.ID == snk.ID { youSnake = snk break } } - response := ResponsePayload{ - Game: GameResponse{Id: GameId, Timeout: Timeout, Ruleset: GameResponseRuleset{ + request := client.SnakeRequest{ + Game: client.Game{ID: GameId, Timeout: Timeout, Ruleset: client.Ruleset{ Name: ruleset.Name(), Version: "cli", // TODO: Use GitHub Release Version - Settings: GameResponseRulesetSettings{ + Settings: client.RulesetSettings{ HazardDamagePerTurn: HazardDamagePerTurn, FoodSpawnChance: FoodSpawnChance, MinimumFood: MinimumFood, - RoyaleSettings: RoyaleSettings{ + RoyaleSettings: client.RoyaleSettings{ ShrinkEveryNTurns: ShrinkEveryNTurns, }, - SquadSettings: SquadSettings{ + SquadSettings: client.SquadSettings{ AllowBodyCollisions: true, SharedElimination: true, SharedHealth: true, @@ -418,63 +337,56 @@ func getIndividualBoardStateForSnake(state *rules.BoardState, snake Battlesnake, }, }}, Turn: Turn, - Board: BoardResponse{ + Board: client.Board{ Height: state.Height, Width: state.Width, - Food: coordFromPointArray(state.Food), - Hazards: coordFromPointArray(state.Hazards), - Snakes: buildSnakesResponse(state.Snakes), + Food: client.CoordFromPointArray(state.Food), + Hazards: client.CoordFromPointArray(state.Hazards), + Snakes: convertRulesSnakes(state.Snakes, snakeStates), }, - You: snakeResponseFromSnake(youSnake), + You: convertRulesSnake(youSnake, snakeStates[youSnake.ID]), } - responseJson, err := json.Marshal(response) + requestJSON, err := json.Marshal(request) if err != nil { log.Panic("[PANIC]: Error Marshalling JSON from State") panic(err) } - return responseJson + return requestJSON } -func snakeResponseFromSnake(snake rules.Snake) SnakeResponse { - return SnakeResponse{ - Id: snake.ID, - Name: Battlesnakes[snake.ID].Name, +func convertRulesSnake(snake rules.Snake, snakeState SnakeState) client.Snake { + return client.Snake{ + ID: snake.ID, + Name: snakeState.Name, Health: snake.Health, - Body: coordFromPointArray(snake.Body), + Body: client.CoordFromPointArray(snake.Body), Latency: "0", - Head: coordFromPoint(snake.Body[0]), + Head: client.CoordFromPoint(snake.Body[0]), Length: int32(len(snake.Body)), Shout: "", - Squad: Battlesnakes[snake.ID].Squad, + Squad: snakeState.Squad, + Customizations: client.Customizations{ + Head: snakeState.Head, + Tail: snakeState.Tail, + Color: snakeState.Color, + }, } } -func buildSnakesResponse(snakes []rules.Snake) []SnakeResponse { - var a []SnakeResponse +func convertRulesSnakes(snakes []rules.Snake, snakeStates map[string]SnakeState) []client.Snake { + var a []client.Snake for _, snake := range snakes { if snake.EliminatedCause == rules.NotEliminated { - a = append(a, snakeResponseFromSnake(snake)) + a = append(a, convertRulesSnake(snake, snakeStates[snake.ID])) } } return a } -func coordFromPoint(pt rules.Point) Coord { - return Coord{X: pt.X, Y: pt.Y} -} - -func coordFromPointArray(ptArray []rules.Point) []Coord { - a := make([]Coord, 0) - for _, pt := range ptArray { - a = append(a, coordFromPoint(pt)) - } - return a -} - -func buildSnakesFromOptions() []Battlesnake { +func buildSnakesFromOptions() map[string]SnakeState { bodyChars := []rune{'■', '⌀', '●', '⍟', '◘', '☺', '□', '☻'} var numSnakes int - var snakes []Battlesnake + snakes := map[string]SnakeState{} numNames := len(Names) numURLs := len(URLs) numSquads := len(Squads) @@ -521,10 +433,12 @@ func buildSnakesFromOptions() []Battlesnake { snakeSquad = strconv.Itoa(i / 2) } } + snakeState := SnakeState{ + Name: snakeName, URL: snakeURL, ID: id, LastMove: "up", Character: bodyChars[i%8], + } res, err := HttpClient.Get(snakeURL) - api := "0" if err != nil { - log.Printf("[WARN]: Request to %v failed", snakeURL) + log.Printf("[WARN]: Request to %v failed: %v", snakeURL, err) } else if res.Body != nil { defer res.Body.Close() body, readErr := ioutil.ReadAll(res.Body) @@ -532,24 +446,25 @@ func buildSnakesFromOptions() []Battlesnake { log.Fatal(readErr) } - pingResponse := PingResponse{} + pingResponse := client.SnakeMetadataResponse{} jsonErr := json.Unmarshal(body, &pingResponse) if jsonErr != nil { - log.Fatal(jsonErr) + log.Printf("Error reading response from %v: %v", snakeURL, jsonErr) } else { - api = pingResponse.APIVersion + snakeState.Head = pingResponse.Head + snakeState.Tail = pingResponse.Tail + snakeState.Color = pingResponse.Color } } - snake := Battlesnake{Name: snakeName, URL: snakeURL, ID: id, API: api, LastMove: "up", Character: bodyChars[i%8]} if GameType == "squad" { - snake.Squad = snakeSquad + snakeState.Squad = snakeSquad } - snakes = append(snakes, snake) + snakes[snakeState.ID] = snakeState } return snakes } -func printMap(state *rules.BoardState, gameTurn int32) { +func printMap(state *rules.BoardState, snakeStates map[string]SnakeState, gameTurn int32) { var o bytes.Buffer o.WriteString(fmt.Sprintf("Ruleset: %s, Seed: %d, Turn: %v\n", GameType, Seed, gameTurn)) board := make([][]rune, state.Width) @@ -572,10 +487,10 @@ func printMap(state *rules.BoardState, gameTurn int32) { for _, s := range state.Snakes { for _, b := range s.Body { if b.X >= 0 && b.X < state.Width && b.Y >= 0 && b.Y < state.Height { - board[b.X][b.Y] = Battlesnakes[s.ID].Character + board[b.X][b.Y] = snakeStates[s.ID].Character } } - o.WriteString(fmt.Sprintf("%v %c: %v\n", Battlesnakes[s.ID].Name, Battlesnakes[s.ID].Character, s)) + 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-- { for x := int32(0); x < state.Width; x++ { diff --git a/cli/commands/play_test.go b/cli/commands/play_test.go index b50f8db..919be6f 100644 --- a/cli/commands/play_test.go +++ b/cli/commands/play_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/BattlesnakeOfficial/rules" + "github.com/BattlesnakeOfficial/rules/test" ) func TestGetIndividualBoardStateForSnake(t *testing.T) { @@ -14,8 +15,27 @@ func TestGetIndividualBoardStateForSnake(t *testing.T) { Width: 11, Snakes: []rules.Snake{s1, s2}, } - snake := Battlesnake{Name: "one", URL: "", ID: "one"} - requestBody := getIndividualBoardStateForSnake(state, snake, &rules.StandardRuleset{}) + s1State := SnakeState{ + ID: "one", + Name: "ONE", + URL: "http://example1.com", + Head: "safe", + Tail: "curled", + Color: "#123456", + } + s2State := SnakeState{ + ID: "two", + Name: "TWO", + URL: "http://example2.com", + Head: "silly", + Tail: "bolt", + Color: "#654321", + } + snakeStates := map[string]SnakeState{ + s1State.ID: s1State, + s2State.ID: s2State, + } + requestBody := getIndividualBoardStateForSnake(state, s1State, snakeStates, &rules.StandardRuleset{}) - rules.RequireJSONMatchesFixture(t, "testdata/snake_request_body.json", string(requestBody)) + test.RequireJSONMatchesFixture(t, "testdata/snake_request_body.json", string(requestBody)) } diff --git a/cli/commands/testdata/snake_request_body.json b/cli/commands/testdata/snake_request_body.json index 34c8a52..5e89ce7 100644 --- a/cli/commands/testdata/snake_request_body.json +++ b/cli/commands/testdata/snake_request_body.json @@ -1,14 +1,13 @@ { "game": { "id": "", - "timeout": 500, "ruleset": { "name": "standard", "version": "cli", "settings": { - "hazardDamagePerTurn": 14, "foodSpawnChance": 15, "minimumFood": 1, + "hazardDamagePerTurn": 14, "royale": { "shrinkEveryNTurns": 25 }, @@ -19,18 +18,19 @@ "sharedLength": true } } - } + }, + "timeout": 500, + "source": "" }, "turn": 0, "board": { "height": 11, "width": 11, - "food": [], - "hazards": [], "snakes": [ { "id": "one", - "name": "", + "name": "ONE", + "latency": "0", "health": 0, "body": [ { @@ -38,18 +38,23 @@ "y": 3 } ], - "latency": "0", "head": { "x": 3, "y": 3 }, "length": 1, "shout": "", - "squad": "" + "squad": "", + "customizations": { + "color": "#123456", + "head": "safe", + "tail": "curled" + } }, { "id": "two", - "name": "", + "name": "TWO", + "latency": "0", "health": 0, "body": [ { @@ -57,20 +62,27 @@ "y": 3 } ], - "latency": "0", "head": { "x": 4, "y": 3 }, "length": 1, "shout": "", - "squad": "" + "squad": "", + "customizations": { + "color": "#654321", + "head": "silly", + "tail": "bolt" + } } - ] + ], + "food": [], + "hazards": [] }, "you": { "id": "one", - "name": "", + "name": "ONE", + "latency": "0", "health": 0, "body": [ { @@ -78,13 +90,17 @@ "y": 3 } ], - "latency": "0", "head": { "x": 3, "y": 3 }, "length": 1, "shout": "", - "squad": "" + "squad": "", + "customizations": { + "color": "#123456", + "head": "safe", + "tail": "curled" + } } } \ No newline at end of file diff --git a/client/fixtures_test.go b/client/fixtures_test.go new file mode 100644 index 0000000..687ec3d --- /dev/null +++ b/client/fixtures_test.go @@ -0,0 +1,90 @@ +package client + +func exampleSnakeRequest() SnakeRequest { + return SnakeRequest{ + Game: Game{ + ID: "game-id", + Ruleset: Ruleset{ + Name: "test-ruleset-name", + Version: "cli", + Settings: exampleRulesetSettings, + }, + Timeout: 33, + Source: "league", + }, + Turn: 11, + Board: Board{ + Height: 22, + Width: 11, + Snakes: []Snake{ + { + ID: "snake-0", + Name: "snake-0-name", + Latency: "snake-0-latency", + Health: 100, + Body: []Coord{{X: 1, Y: 2}, {X: 1, Y: 3}, {X: 1, Y: 4}}, + Head: Coord{X: 1, Y: 2}, + Length: 3, + Shout: "snake-0-shout", + Squad: "", + Customizations: Customizations{ + Head: "safe", + Tail: "curled", + Color: "#123456", + }, + }, + { + ID: "snake-1", + Name: "snake-1-name", + Latency: "snake-1-latency", + Health: 200, + Body: []Coord{{X: 2, Y: 2}, {X: 2, Y: 3}, {X: 2, Y: 4}}, + Head: Coord{X: 2, Y: 2}, + Length: 3, + Shout: "snake-1-shout", + Squad: "snake-1-squad", + Customizations: Customizations{ + Head: "silly", + Tail: "bolt", + Color: "#654321", + }, + }, + }, + Food: []Coord{{X: 2, Y: 2}}, + Hazards: []Coord{{X: 8, Y: 8}, {X: 9, Y: 9}}, + }, + You: Snake{ + ID: "snake-1", + Name: "snake-1-name", + Latency: "snake-1-latency", + Health: 200, + Body: []Coord{{X: 2, Y: 2}, {X: 2, Y: 3}, {X: 2, Y: 4}}, + Head: Coord{X: 2, Y: 2}, + Length: 3, + Shout: "snake-1-shout", + Squad: "snake-1-squad", + Customizations: Customizations{ + Head: "silly", + Tail: "bolt", + Color: "#654321", + }, + }, + } +} + +var exampleRulesetSettings = RulesetSettings{ + FoodSpawnChance: 10, + MinimumFood: 20, + HazardDamagePerTurn: 30, + + RoyaleSettings: RoyaleSettings{ + ShrinkEveryNTurns: 40, + }, + + SquadSettings: SquadSettings{ + AllowBodyCollisions: true, + SharedElimination: true, + SharedHealth: true, + SharedLength: true, + }, +} diff --git a/client/models.go b/client/models.go new file mode 100644 index 0000000..e7d2893 --- /dev/null +++ b/client/models.go @@ -0,0 +1,107 @@ +package client + +import "github.com/BattlesnakeOfficial/rules" + +// The top-level message sent in /start, /move, and /end requests +type SnakeRequest struct { + Game Game `json:"game"` + Turn int32 `json:"turn"` + Board Board `json:"board"` + You Snake `json:"you"` +} + +// Game represents the current game state +type Game struct { + ID string `json:"id"` + Ruleset Ruleset `json:"ruleset"` + Timeout int32 `json:"timeout"` + Source string `json:"source"` +} + +// Board provides information about the game board +type Board struct { + Height int32 `json:"height"` + Width int32 `json:"width"` + Snakes []Snake `json:"snakes"` + Food []Coord `json:"food"` + Hazards []Coord `json:"hazards"` +} + +// Snake represents information about a snake in the game +type Snake struct { + ID string `json:"id"` + Name string `json:"name"` + Latency string `json:"latency"` + Health int32 `json:"health"` + Body []Coord `json:"body"` + Head Coord `json:"head"` + Length int32 `json:"length"` + Shout string `json:"shout"` + Squad string `json:"squad"` + Customizations Customizations `json:"customizations"` +} + +type Customizations struct { + Color string `json:"color"` + Head string `json:"head"` + Tail string `json:"tail"` +} + +type Ruleset struct { + Name string `json:"name"` + Version string `json:"version"` + Settings RulesetSettings `json:"settings"` +} + +type RulesetSettings struct { + FoodSpawnChance int32 `json:"foodSpawnChance"` + MinimumFood int32 `json:"minimumFood"` + HazardDamagePerTurn int32 `json:"hazardDamagePerTurn"` + RoyaleSettings RoyaleSettings `json:"royale"` + SquadSettings SquadSettings `json:"squad"` +} + +type RoyaleSettings struct { + ShrinkEveryNTurns int32 `json:"shrinkEveryNTurns"` +} + +type SquadSettings struct { + AllowBodyCollisions bool `json:"allowBodyCollisions"` + SharedElimination bool `json:"sharedElimination"` + SharedHealth bool `json:"sharedHealth"` + SharedLength bool `json:"sharedLength"` +} + +// Coord represents a point on the board +type Coord struct { + X int32 `json:"x"` + Y int32 `json:"y"` +} + +// The expected format of the response body from a /move request +type MoveResponse struct { + Move string `json:"move"` + Shout string `json:"shout"` +} + +// The expected format of the response body from a GET request to a Battlesnake's index URL +type SnakeMetadataResponse struct { + APIVersion string `json:"apiversion,omitempty"` + Author string `json:"author,omitempty"` + Color string `json:"color,omitempty"` + Head string `json:"head,omitempty"` + Tail string `json:"tail,omitempty"` + Version string `json:"version,omitempty"` +} + +func CoordFromPoint(pt rules.Point) Coord { + return Coord{X: pt.X, Y: pt.Y} +} + +func CoordFromPointArray(ptArray []rules.Point) []Coord { + a := make([]Coord, 0) + for _, pt := range ptArray { + a = append(a, CoordFromPoint(pt)) + } + return a +} diff --git a/client/models_test.go b/client/models_test.go new file mode 100644 index 0000000..5253867 --- /dev/null +++ b/client/models_test.go @@ -0,0 +1,26 @@ +package client + +import ( + "encoding/json" + "testing" + + "github.com/BattlesnakeOfficial/rules/test" + "github.com/stretchr/testify/require" +) + +func TestBuildSnakeRequestJSON(t *testing.T) { + snakeRequest := exampleSnakeRequest() + data, err := json.MarshalIndent(snakeRequest, "", " ") + require.NoError(t, err) + + test.RequireJSONMatchesFixture(t, "testdata/snake_request.json", string(data)) +} + +func TestBuildSnakeRequestJSONEmptyRulesetSettings(t *testing.T) { + snakeRequest := exampleSnakeRequest() + snakeRequest.Game.Ruleset.Settings = RulesetSettings{} + data, err := json.MarshalIndent(snakeRequest, "", " ") + require.NoError(t, err) + + test.RequireJSONMatchesFixture(t, "testdata/snake_request_empty_ruleset_settings.json", string(data)) +} diff --git a/client/testdata/snake_request.json b/client/testdata/snake_request.json new file mode 100644 index 0000000..0e7557d --- /dev/null +++ b/client/testdata/snake_request.json @@ -0,0 +1,144 @@ +{ + "game": { + "id": "game-id", + "ruleset": { + "name": "test-ruleset-name", + "version": "cli", + "settings": { + "foodSpawnChance": 10, + "minimumFood": 20, + "hazardDamagePerTurn": 30, + "royale": { + "shrinkEveryNTurns": 40 + }, + "squad": { + "allowBodyCollisions": true, + "sharedElimination": true, + "sharedHealth": true, + "sharedLength": true + } + } + }, + "timeout": 33, + "source": "league" + }, + "turn": 11, + "board": { + "height": 22, + "width": 11, + "snakes": [ + { + "id": "snake-0", + "name": "snake-0-name", + "latency": "snake-0-latency", + "health": 100, + "body": [ + { + "x": 1, + "y": 2 + }, + { + "x": 1, + "y": 3 + }, + { + "x": 1, + "y": 4 + } + ], + "head": { + "x": 1, + "y": 2 + }, + "length": 3, + "shout": "snake-0-shout", + "squad": "", + "customizations": { + "color": "#123456", + "head": "safe", + "tail": "curled" + } + }, + { + "id": "snake-1", + "name": "snake-1-name", + "latency": "snake-1-latency", + "health": 200, + "body": [ + { + "x": 2, + "y": 2 + }, + { + "x": 2, + "y": 3 + }, + { + "x": 2, + "y": 4 + } + ], + "head": { + "x": 2, + "y": 2 + }, + "length": 3, + "shout": "snake-1-shout", + "squad": "snake-1-squad", + "customizations": { + "color": "#654321", + "head": "silly", + "tail": "bolt" + } + } + ], + "food": [ + { + "x": 2, + "y": 2 + } + ], + "hazards": [ + { + "x": 8, + "y": 8 + }, + { + "x": 9, + "y": 9 + } + ] + }, + "you": { + "id": "snake-1", + "name": "snake-1-name", + "latency": "snake-1-latency", + "health": 200, + "body": [ + { + "x": 2, + "y": 2 + }, + { + "x": 2, + "y": 3 + }, + { + "x": 2, + "y": 4 + } + ], + "head": { + "x": 2, + "y": 2 + }, + "length": 3, + "shout": "snake-1-shout", + "squad": "snake-1-squad", + "customizations": { + "color": "#654321", + "head": "silly", + "tail": "bolt" + } + } +} \ No newline at end of file diff --git a/client/testdata/snake_request_empty_ruleset_settings.json b/client/testdata/snake_request_empty_ruleset_settings.json new file mode 100644 index 0000000..2755619 --- /dev/null +++ b/client/testdata/snake_request_empty_ruleset_settings.json @@ -0,0 +1,144 @@ +{ + "game": { + "id": "game-id", + "ruleset": { + "name": "test-ruleset-name", + "version": "cli", + "settings": { + "foodSpawnChance": 0, + "minimumFood": 0, + "hazardDamagePerTurn": 0, + "royale": { + "shrinkEveryNTurns": 0 + }, + "squad": { + "allowBodyCollisions": false, + "sharedElimination": false, + "sharedHealth": false, + "sharedLength": false + } + } + }, + "timeout": 33, + "source": "league" + }, + "turn": 11, + "board": { + "height": 22, + "width": 11, + "snakes": [ + { + "id": "snake-0", + "name": "snake-0-name", + "latency": "snake-0-latency", + "health": 100, + "body": [ + { + "x": 1, + "y": 2 + }, + { + "x": 1, + "y": 3 + }, + { + "x": 1, + "y": 4 + } + ], + "head": { + "x": 1, + "y": 2 + }, + "length": 3, + "shout": "snake-0-shout", + "squad": "", + "customizations": { + "color": "#123456", + "head": "safe", + "tail": "curled" + } + }, + { + "id": "snake-1", + "name": "snake-1-name", + "latency": "snake-1-latency", + "health": 200, + "body": [ + { + "x": 2, + "y": 2 + }, + { + "x": 2, + "y": 3 + }, + { + "x": 2, + "y": 4 + } + ], + "head": { + "x": 2, + "y": 2 + }, + "length": 3, + "shout": "snake-1-shout", + "squad": "snake-1-squad", + "customizations": { + "color": "#654321", + "head": "silly", + "tail": "bolt" + } + } + ], + "food": [ + { + "x": 2, + "y": 2 + } + ], + "hazards": [ + { + "x": 8, + "y": 8 + }, + { + "x": 9, + "y": 9 + } + ] + }, + "you": { + "id": "snake-1", + "name": "snake-1-name", + "latency": "snake-1-latency", + "health": 200, + "body": [ + { + "x": 2, + "y": 2 + }, + { + "x": 2, + "y": 3 + }, + { + "x": 2, + "y": 4 + } + ], + "head": { + "x": 2, + "y": 2 + }, + "length": 3, + "shout": "snake-1-shout", + "squad": "snake-1-squad", + "customizations": { + "color": "#654321", + "head": "silly", + "tail": "bolt" + } + } +} \ No newline at end of file diff --git a/ruleset_test.go b/ruleset_test.go index 61ca220..f5192b8 100644 --- a/ruleset_test.go +++ b/ruleset_test.go @@ -4,6 +4,9 @@ import ( "testing" "github.com/stretchr/testify/require" + + // included to allow using -update-fixtures for every package without errors + _ "github.com/BattlesnakeOfficial/rules/test" ) func TestRulesetError(t *testing.T) { diff --git a/test_utils.go b/test/test_utils.go similarity index 94% rename from test_utils.go rename to test/test_utils.go index 7913efc..06119ed 100644 --- a/test_utils.go +++ b/test/test_utils.go @@ -1,10 +1,11 @@ -package rules +package test import ( "bytes" "encoding/json" "flag" "io/ioutil" + "log" "testing" "github.com/stretchr/testify/require" @@ -26,6 +27,8 @@ func RequireJSONMatchesFixture(t *testing.T, filename string, actual string) { require.NoError(t, err, "Failed to indent JSON") err = ioutil.WriteFile(filename, indented.Bytes(), 0644) require.NoError(t, err, "Failed to update fixture", filename) + + log.Printf("Updating fixture file %#v", filename) } expectedData, err := ioutil.ReadFile(filename)