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:
parent
6140f232c2
commit
4a9dbbcaef
10 changed files with 665 additions and 197 deletions
|
|
@ -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++ {
|
||||
|
|
|
|||
|
|
@ -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{})
|
||||
|
||||
rules.RequireJSONMatchesFixture(t, "testdata/snake_request_body.json", string(requestBody))
|
||||
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{})
|
||||
|
||||
test.RequireJSONMatchesFixture(t, "testdata/snake_request_body.json", string(requestBody))
|
||||
}
|
||||
|
|
|
|||
46
cli/commands/testdata/snake_request_body.json
vendored
46
cli/commands/testdata/snake_request_body.json
vendored
|
|
@ -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
90
client/fixtures_test.go
Normal 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
107
client/models.go
Normal 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
26
client/models_test.go
Normal 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
144
client/testdata/snake_request.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
144
client/testdata/snake_request_empty_ruleset_settings.json
vendored
Normal file
144
client/testdata/snake_request_empty_ruleset_settings.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue