DEV 559: Refactor CLI and add customizations (#57)

* move snake API structs into a new client package

* add customizations to snake objects

* refactor and add support for passing snake customizations in games
This commit is contained in:
Rob O'Dwyer 2021-11-25 14:07:56 -08:00 committed by GitHub
parent 6140f232c2
commit 4a9dbbcaef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 665 additions and 197 deletions

View file

@ -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++ {

View file

@ -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))
}

View file

@ -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"
}
}
}

90
client/fixtures_test.go Normal file
View file

@ -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,
},
}

107
client/models.go Normal file
View file

@ -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
}

26
client/models_test.go Normal file
View file

@ -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))
}

144
client/testdata/snake_request.json vendored Normal file
View file

@ -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"
}
}
}

View file

@ -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"
}
}
}

View file

@ -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) {

View file

@ -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)