DEV 1916: Fix output issues in CLI (#114)
* adding testing of output file and write line for /end * fix regression in latency rounding for board
This commit is contained in:
parent
5f60ccbba8
commit
639362ef46
7 changed files with 396 additions and 49 deletions
|
|
@ -3,9 +3,7 @@ package commands
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
log "github.com/spf13/jwalterweatherman"
|
||||
"io"
|
||||
|
||||
"github.com/BattlesnakeOfficial/rules/client"
|
||||
)
|
||||
|
|
@ -23,37 +21,20 @@ type result struct {
|
|||
IsDraw bool `json:"isDraw"`
|
||||
}
|
||||
|
||||
func (ge *GameExporter) FlushToFile(filepath string, format string) error {
|
||||
var formattedOutput []string
|
||||
var formattingErr error
|
||||
|
||||
// TODO: Support more formats
|
||||
if format == "JSONL" {
|
||||
formattedOutput, formattingErr = ge.ConvertToJSON()
|
||||
} else {
|
||||
log.ERROR.Fatalf("Invalid output format passed: %s", format)
|
||||
}
|
||||
|
||||
if formattingErr != nil {
|
||||
return formattingErr
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(filepath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
func (ge *GameExporter) FlushToFile(outputFile io.Writer) (int, error) {
|
||||
formattedOutput, err := ge.ConvertToJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
for _, line := range formattedOutput {
|
||||
_, err := f.WriteString(fmt.Sprintf("%s\n", line))
|
||||
_, err := io.WriteString(outputFile, fmt.Sprintf("%s\n", line))
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
log.DEBUG.Printf("Written %d lines of output to file: %s\n", len(formattedOutput), filepath)
|
||||
|
||||
return nil
|
||||
return len(formattedOutput), nil
|
||||
}
|
||||
|
||||
func (ge *GameExporter) ConvertToJSON() ([]string, error) {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -56,7 +58,7 @@ type GameState struct {
|
|||
UseColor bool
|
||||
Seed int64
|
||||
TurnDelay int
|
||||
Output string
|
||||
OutputPath string
|
||||
ViewInBrowser bool
|
||||
BoardURL string
|
||||
FoodSpawnChance int
|
||||
|
|
@ -71,6 +73,8 @@ type GameState struct {
|
|||
httpClient TimedHttpClient
|
||||
ruleset rules.Ruleset
|
||||
gameMap maps.GameMap
|
||||
outputFile io.WriteCloser
|
||||
idGenerator func(int) string
|
||||
}
|
||||
|
||||
func NewPlayCommand() *cobra.Command {
|
||||
|
|
@ -81,6 +85,9 @@ func NewPlayCommand() *cobra.Command {
|
|||
Short: "Play a game of Battlesnake locally.",
|
||||
Long: "Play a game of Battlesnake locally.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := gameState.Initialize(); err != nil {
|
||||
log.ERROR.Fatalf("Error initializing game: %v", err)
|
||||
}
|
||||
gameState.Run()
|
||||
},
|
||||
}
|
||||
|
|
@ -98,7 +105,7 @@ func NewPlayCommand() *cobra.Command {
|
|||
playCmd.Flags().Int64VarP(&gameState.Seed, "seed", "r", time.Now().UTC().UnixNano(), "Random Seed")
|
||||
playCmd.Flags().IntVarP(&gameState.TurnDelay, "delay", "d", 0, "Turn Delay in Milliseconds")
|
||||
playCmd.Flags().IntVarP(&gameState.TurnDuration, "duration", "D", 0, "Minimum Turn Duration in Milliseconds")
|
||||
playCmd.Flags().StringVarP(&gameState.Output, "output", "o", "", "File path to output game state to. Existing files will be overwritten")
|
||||
playCmd.Flags().StringVarP(&gameState.OutputPath, "output", "o", "", "File path to output game state to. Existing files will be overwritten")
|
||||
playCmd.Flags().BoolVar(&gameState.ViewInBrowser, "browser", false, "View the game in the browser using the Battlesnake game board")
|
||||
playCmd.Flags().StringVar(&gameState.BoardURL, "board-url", "https://board.battlesnake.com", "Base URL for the game board when using --browser")
|
||||
|
||||
|
|
@ -113,7 +120,7 @@ func NewPlayCommand() *cobra.Command {
|
|||
}
|
||||
|
||||
// Setup a GameState once all the fields have been parsed from the command-line.
|
||||
func (gameState *GameState) initialize() {
|
||||
func (gameState *GameState) Initialize() error {
|
||||
// Generate game ID
|
||||
gameState.gameID = uuid.New().String()
|
||||
|
||||
|
|
@ -130,7 +137,7 @@ func (gameState *GameState) initialize() {
|
|||
// Load game map
|
||||
gameMap, err := maps.GetMap(gameState.MapName)
|
||||
if err != nil {
|
||||
log.ERROR.Fatalf("Failed to load game map %#v: %v", gameState.MapName, err)
|
||||
return fmt.Errorf("Failed to load game map %#v: %v", gameState.MapName, err)
|
||||
}
|
||||
gameState.gameMap = gameMap
|
||||
|
||||
|
|
@ -153,19 +160,26 @@ func (gameState *GameState) initialize() {
|
|||
|
||||
// Initialize snake states as empty until we can ping the snake URLs
|
||||
gameState.snakeStates = map[string]SnakeState{}
|
||||
|
||||
if gameState.OutputPath != "" {
|
||||
f, err := os.OpenFile(gameState.OutputPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to open output file: %w", err)
|
||||
}
|
||||
gameState.outputFile = f
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Setup and run a full game.
|
||||
func (gameState *GameState) Run() {
|
||||
gameState.initialize()
|
||||
|
||||
// Setup local state for snakes
|
||||
gameState.snakeStates = gameState.buildSnakesFromOptions()
|
||||
|
||||
rand.Seed(gameState.Seed)
|
||||
|
||||
boardState := gameState.initializeBoardFromArgs()
|
||||
exportGame := gameState.Output != ""
|
||||
|
||||
gameExporter := GameExporter{
|
||||
game: gameState.createClientGame(),
|
||||
|
|
@ -173,6 +187,10 @@ func (gameState *GameState) Run() {
|
|||
winner: SnakeState{},
|
||||
isDraw: false,
|
||||
}
|
||||
exportGame := gameState.outputFile != nil
|
||||
if exportGame {
|
||||
defer gameState.outputFile.Close()
|
||||
}
|
||||
|
||||
boardGame := board.Game{
|
||||
ID: gameState.gameID,
|
||||
|
|
@ -258,6 +276,15 @@ func (gameState *GameState) Run() {
|
|||
}
|
||||
}
|
||||
|
||||
// Export final turn
|
||||
if exportGame {
|
||||
for _, snakeState := range gameState.snakeStates {
|
||||
snakeRequest := gameState.getRequestBodyForSnake(boardState, snakeState)
|
||||
gameExporter.AddSnakeRequest(snakeRequest)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
gameExporter.isDraw = false
|
||||
|
||||
if len(gameState.snakeStates) > 1 {
|
||||
|
|
@ -291,10 +318,11 @@ func (gameState *GameState) Run() {
|
|||
}
|
||||
|
||||
if exportGame {
|
||||
err := gameExporter.FlushToFile(gameState.Output, "JSONL")
|
||||
lines, err := gameExporter.FlushToFile(gameState.outputFile)
|
||||
if err != nil {
|
||||
log.ERROR.Fatalf("Unable to export game. Reason: %v", err)
|
||||
}
|
||||
log.INFO.Printf("Wrote %d lines to output file: %s", lines, gameState.OutputPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -515,7 +543,12 @@ func (gameState *GameState) buildSnakesFromOptions() map[string]SnakeState {
|
|||
var snakeName string
|
||||
var snakeURL string
|
||||
|
||||
id := uuid.New().String()
|
||||
var id string
|
||||
if gameState.idGenerator != nil {
|
||||
id = gameState.idGenerator(i)
|
||||
} else {
|
||||
id = uuid.New().String()
|
||||
}
|
||||
|
||||
if i < numNames {
|
||||
snakeName = gameState.Names[i]
|
||||
|
|
@ -677,6 +710,7 @@ func (gameState *GameState) buildFrameEvent(boardState *rules.BoardState) board.
|
|||
snakeState := gameState.snakeStates[snake.ID]
|
||||
|
||||
latencyMS := snakeState.Latency.Milliseconds()
|
||||
// round up latency of 0 to 1, to avoid legacy error display in board
|
||||
if latencyMS == 0 {
|
||||
latencyMS = 1
|
||||
}
|
||||
|
|
@ -734,12 +768,13 @@ func serialiseSnakeRequest(snakeRequest client.SnakeRequest) []byte {
|
|||
}
|
||||
|
||||
func convertRulesSnake(snake rules.Snake, snakeState SnakeState) client.Snake {
|
||||
latencyMS := snakeState.Latency.Milliseconds()
|
||||
return client.Snake{
|
||||
ID: snake.ID,
|
||||
Name: snakeState.Name,
|
||||
Health: snake.Health,
|
||||
Body: client.CoordFromPointArray(snake.Body),
|
||||
Latency: "0",
|
||||
Latency: fmt.Sprint(latencyMS),
|
||||
Head: client.CoordFromPoint(snake.Body[0]),
|
||||
Length: int(len(snake.Body)),
|
||||
Shout: "",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -31,7 +32,7 @@ func buildDefaultGameState() *GameState {
|
|||
Seed: 1,
|
||||
TurnDelay: 0,
|
||||
TurnDuration: 0,
|
||||
Output: "",
|
||||
OutputPath: "",
|
||||
FoodSpawnChance: 15,
|
||||
MinimumFood: 1,
|
||||
HazardDamagePerTurn: 14,
|
||||
|
|
@ -67,7 +68,8 @@ func TestGetIndividualBoardStateForSnake(t *testing.T) {
|
|||
}
|
||||
|
||||
gameState := buildDefaultGameState()
|
||||
gameState.initialize()
|
||||
err := gameState.Initialize()
|
||||
require.NoError(t, err)
|
||||
gameState.gameID = "GAME_ID"
|
||||
gameState.snakeStates = map[string]SnakeState{
|
||||
s1State.ID: s1State,
|
||||
|
|
@ -118,7 +120,8 @@ func TestSettingsRequestSerialization(t *testing.T) {
|
|||
gameState.ShrinkEveryNTurns = 17
|
||||
gameState.GameType = gt
|
||||
|
||||
gameState.initialize()
|
||||
err := gameState.Initialize()
|
||||
require.NoError(t, err)
|
||||
gameState.gameID = "GAME_ID"
|
||||
gameState.snakeStates = map[string]SnakeState{s1State.ID: s1State, s2State.ID: s2State}
|
||||
|
||||
|
|
@ -159,13 +162,14 @@ func TestConvertRulesSnakes(t *testing.T) {
|
|||
Color: "#012345",
|
||||
LastMove: "up",
|
||||
Character: '+',
|
||||
Latency: time.Millisecond * 42,
|
||||
},
|
||||
},
|
||||
expected: []client.Snake{
|
||||
{
|
||||
ID: "one",
|
||||
Name: "ONE",
|
||||
Latency: "0",
|
||||
Latency: "42",
|
||||
Health: 100,
|
||||
Body: []client.Coord{{X: 3, Y: 3}, {X: 2, Y: 3}},
|
||||
Head: client.Coord{X: 3, Y: 3},
|
||||
|
|
@ -507,9 +511,10 @@ func TestGetMoveForSnake(t *testing.T) {
|
|||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
gameState := buildDefaultGameState()
|
||||
gameState.initialize()
|
||||
err := gameState.Initialize()
|
||||
require.NoError(t, err)
|
||||
gameState.snakeStates = map[string]SnakeState{test.snakeState.ID: test.snakeState}
|
||||
gameState.httpClient = stubHTTPClient{test.responseErr, test.responseCode, test.responseBody, test.responseLatency}
|
||||
gameState.httpClient = stubHTTPClient{test.responseErr, test.responseCode, func(_ string) string { return test.responseBody }, test.responseLatency}
|
||||
|
||||
nextSnakeState := gameState.getSnakeUpdate(test.boardState, test.snakeState)
|
||||
if test.expectedSnakeState.Error != nil {
|
||||
|
|
@ -539,9 +544,10 @@ func TestCreateNextBoardState(t *testing.T) {
|
|||
t.Run(fmt.Sprintf("sequential_%v", sequential), func(t *testing.T) {
|
||||
gameState := buildDefaultGameState()
|
||||
gameState.Sequential = sequential
|
||||
gameState.initialize()
|
||||
err := gameState.Initialize()
|
||||
require.NoError(t, err)
|
||||
gameState.snakeStates = map[string]SnakeState{s1.ID: snakeState}
|
||||
gameState.httpClient = stubHTTPClient{nil, 200, `{"move": "right"}`, 54 * time.Millisecond}
|
||||
gameState.httpClient = stubHTTPClient{nil, 200, func(_ string) string { return `{"move": "right"}` }, 54 * time.Millisecond}
|
||||
|
||||
nextBoardState := gameState.createNextBoardState(boardState)
|
||||
snakeState = gameState.snakeStates[s1.ID]
|
||||
|
|
@ -556,18 +562,92 @@ func TestCreateNextBoardState(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestOutputFile(t *testing.T) {
|
||||
gameState := buildDefaultGameState()
|
||||
gameState.Names = []string{"example snake"}
|
||||
gameState.URLs = []string{"http://example.com"}
|
||||
err := gameState.Initialize()
|
||||
require.NoError(t, err)
|
||||
|
||||
gameState.gameID = "GAME_ID"
|
||||
gameState.idGenerator = func(index int) string { return fmt.Sprintf("snk_%d", index) }
|
||||
|
||||
gameState.httpClient = stubHTTPClient{nil, http.StatusOK, func(url string) string {
|
||||
switch url {
|
||||
case "http://example.com":
|
||||
return `
|
||||
{
|
||||
"apiversion": "1",
|
||||
"author": "author",
|
||||
"color": "#123456",
|
||||
"head": "safe",
|
||||
"tail": "curled",
|
||||
"version": "0.0.1-beta"
|
||||
}
|
||||
`
|
||||
case "http://example.com/move":
|
||||
return `{"move": "left"}`
|
||||
}
|
||||
return ""
|
||||
}, time.Millisecond * 42}
|
||||
outputFile := new(closableBuffer)
|
||||
gameState.outputFile = outputFile
|
||||
|
||||
gameState.ruleset = StubRuleset{maxTurns: 1, settings: rules.Settings{
|
||||
FoodSpawnChance: 1,
|
||||
MinimumFood: 2,
|
||||
HazardDamagePerTurn: 3,
|
||||
RoyaleSettings: rules.RoyaleSettings{
|
||||
ShrinkEveryNTurns: 4,
|
||||
},
|
||||
}}
|
||||
|
||||
gameState.Run()
|
||||
|
||||
lines := strings.Split(outputFile.String(), "\n")
|
||||
require.Len(t, lines, 5)
|
||||
test.RequireJSONMatchesFixture(t, "testdata/jsonl_game.json", lines[0])
|
||||
test.RequireJSONMatchesFixture(t, "testdata/jsonl_turn_0.json", lines[1])
|
||||
test.RequireJSONMatchesFixture(t, "testdata/jsonl_turn_1.json", lines[2])
|
||||
test.RequireJSONMatchesFixture(t, "testdata/jsonl_game_complete.json", lines[3])
|
||||
require.Equal(t, "", lines[4])
|
||||
}
|
||||
|
||||
type closableBuffer struct {
|
||||
bytes.Buffer
|
||||
}
|
||||
|
||||
func (closableBuffer) Close() error { return nil }
|
||||
|
||||
type StubRuleset struct {
|
||||
maxTurns int
|
||||
settings rules.Settings
|
||||
}
|
||||
|
||||
func (ruleset StubRuleset) Name() string { return "standard" }
|
||||
func (ruleset StubRuleset) Settings() rules.Settings { return ruleset.settings }
|
||||
func (ruleset StubRuleset) ModifyInitialBoardState(initialState *rules.BoardState) (*rules.BoardState, error) {
|
||||
return initialState, nil
|
||||
}
|
||||
func (ruleset StubRuleset) CreateNextBoardState(prevState *rules.BoardState, moves []rules.SnakeMove) (*rules.BoardState, error) {
|
||||
return prevState, nil
|
||||
}
|
||||
func (ruleset StubRuleset) IsGameOver(state *rules.BoardState) (bool, error) {
|
||||
return state.Turn >= ruleset.maxTurns, nil
|
||||
}
|
||||
|
||||
type stubHTTPClient struct {
|
||||
err error
|
||||
statusCode int
|
||||
body string
|
||||
body func(url string) string
|
||||
latency time.Duration
|
||||
}
|
||||
|
||||
func (client stubHTTPClient) request() (*http.Response, time.Duration, error) {
|
||||
func (client stubHTTPClient) request(url string) (*http.Response, time.Duration, error) {
|
||||
if client.err != nil {
|
||||
return nil, client.latency, client.err
|
||||
}
|
||||
body := ioutil.NopCloser(bytes.NewBufferString(client.body))
|
||||
body := ioutil.NopCloser(bytes.NewBufferString(client.body(url)))
|
||||
|
||||
response := &http.Response{
|
||||
Header: make(http.Header),
|
||||
|
|
@ -579,9 +659,9 @@ func (client stubHTTPClient) request() (*http.Response, time.Duration, error) {
|
|||
}
|
||||
|
||||
func (client stubHTTPClient) Get(url string) (*http.Response, time.Duration, error) {
|
||||
return client.request()
|
||||
return client.request(url)
|
||||
}
|
||||
|
||||
func (client stubHTTPClient) Post(url string, contentType string, body io.Reader) (*http.Response, time.Duration, error) {
|
||||
return client.request()
|
||||
return client.request(url)
|
||||
}
|
||||
|
|
|
|||
26
cli/commands/testdata/jsonl_game.json
vendored
Normal file
26
cli/commands/testdata/jsonl_game.json
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"id": "GAME_ID",
|
||||
"ruleset": {
|
||||
"name": "standard",
|
||||
"version": "cli",
|
||||
"settings": {
|
||||
"foodSpawnChance": 1,
|
||||
"minimumFood": 2,
|
||||
"hazardDamagePerTurn": 3,
|
||||
"hazardMap": "",
|
||||
"hazardMapAuthor": "",
|
||||
"royale": {
|
||||
"shrinkEveryNTurns": 4
|
||||
},
|
||||
"squad": {
|
||||
"allowBodyCollisions": false,
|
||||
"sharedElimination": false,
|
||||
"sharedHealth": false,
|
||||
"sharedLength": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"map": "standard",
|
||||
"timeout": 500,
|
||||
"source": ""
|
||||
}
|
||||
5
cli/commands/testdata/jsonl_game_complete.json
vendored
Normal file
5
cli/commands/testdata/jsonl_game_complete.json
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"winnerId": "snk_0",
|
||||
"winnerName": "example snake",
|
||||
"isDraw": false
|
||||
}
|
||||
110
cli/commands/testdata/jsonl_turn_0.json
vendored
Normal file
110
cli/commands/testdata/jsonl_turn_0.json
vendored
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
{
|
||||
"game": {
|
||||
"id": "GAME_ID",
|
||||
"ruleset": {
|
||||
"name": "standard",
|
||||
"version": "cli",
|
||||
"settings": {
|
||||
"foodSpawnChance": 1,
|
||||
"minimumFood": 2,
|
||||
"hazardDamagePerTurn": 3,
|
||||
"hazardMap": "",
|
||||
"hazardMapAuthor": "",
|
||||
"royale": {
|
||||
"shrinkEveryNTurns": 4
|
||||
},
|
||||
"squad": {
|
||||
"allowBodyCollisions": false,
|
||||
"sharedElimination": false,
|
||||
"sharedHealth": false,
|
||||
"sharedLength": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"map": "standard",
|
||||
"timeout": 500,
|
||||
"source": ""
|
||||
},
|
||||
"turn": 0,
|
||||
"board": {
|
||||
"height": 11,
|
||||
"width": 11,
|
||||
"snakes": [
|
||||
{
|
||||
"id": "snk_0",
|
||||
"name": "example snake",
|
||||
"latency": "0",
|
||||
"health": 100,
|
||||
"body": [
|
||||
{
|
||||
"x": 1,
|
||||
"y": 5
|
||||
},
|
||||
{
|
||||
"x": 1,
|
||||
"y": 5
|
||||
},
|
||||
{
|
||||
"x": 1,
|
||||
"y": 5
|
||||
}
|
||||
],
|
||||
"head": {
|
||||
"x": 1,
|
||||
"y": 5
|
||||
},
|
||||
"length": 3,
|
||||
"shout": "",
|
||||
"squad": "",
|
||||
"customizations": {
|
||||
"color": "#123456",
|
||||
"head": "safe",
|
||||
"tail": "curled"
|
||||
}
|
||||
}
|
||||
],
|
||||
"food": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 4
|
||||
},
|
||||
{
|
||||
"x": 5,
|
||||
"y": 5
|
||||
}
|
||||
],
|
||||
"hazards": []
|
||||
},
|
||||
"you": {
|
||||
"id": "snk_0",
|
||||
"name": "example snake",
|
||||
"latency": "0",
|
||||
"health": 100,
|
||||
"body": [
|
||||
{
|
||||
"x": 1,
|
||||
"y": 5
|
||||
},
|
||||
{
|
||||
"x": 1,
|
||||
"y": 5
|
||||
},
|
||||
{
|
||||
"x": 1,
|
||||
"y": 5
|
||||
}
|
||||
],
|
||||
"head": {
|
||||
"x": 1,
|
||||
"y": 5
|
||||
},
|
||||
"length": 3,
|
||||
"shout": "",
|
||||
"squad": "",
|
||||
"customizations": {
|
||||
"color": "#123456",
|
||||
"head": "safe",
|
||||
"tail": "curled"
|
||||
}
|
||||
}
|
||||
}
|
||||
110
cli/commands/testdata/jsonl_turn_1.json
vendored
Normal file
110
cli/commands/testdata/jsonl_turn_1.json
vendored
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
{
|
||||
"game": {
|
||||
"id": "GAME_ID",
|
||||
"ruleset": {
|
||||
"name": "standard",
|
||||
"version": "cli",
|
||||
"settings": {
|
||||
"foodSpawnChance": 1,
|
||||
"minimumFood": 2,
|
||||
"hazardDamagePerTurn": 3,
|
||||
"hazardMap": "",
|
||||
"hazardMapAuthor": "",
|
||||
"royale": {
|
||||
"shrinkEveryNTurns": 4
|
||||
},
|
||||
"squad": {
|
||||
"allowBodyCollisions": false,
|
||||
"sharedElimination": false,
|
||||
"sharedHealth": false,
|
||||
"sharedLength": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"map": "standard",
|
||||
"timeout": 500,
|
||||
"source": ""
|
||||
},
|
||||
"turn": 1,
|
||||
"board": {
|
||||
"height": 11,
|
||||
"width": 11,
|
||||
"snakes": [
|
||||
{
|
||||
"id": "snk_0",
|
||||
"name": "example snake",
|
||||
"latency": "42",
|
||||
"health": 100,
|
||||
"body": [
|
||||
{
|
||||
"x": 1,
|
||||
"y": 5
|
||||
},
|
||||
{
|
||||
"x": 1,
|
||||
"y": 5
|
||||
},
|
||||
{
|
||||
"x": 1,
|
||||
"y": 5
|
||||
}
|
||||
],
|
||||
"head": {
|
||||
"x": 1,
|
||||
"y": 5
|
||||
},
|
||||
"length": 3,
|
||||
"shout": "",
|
||||
"squad": "",
|
||||
"customizations": {
|
||||
"color": "#123456",
|
||||
"head": "safe",
|
||||
"tail": "curled"
|
||||
}
|
||||
}
|
||||
],
|
||||
"food": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 4
|
||||
},
|
||||
{
|
||||
"x": 5,
|
||||
"y": 5
|
||||
}
|
||||
],
|
||||
"hazards": []
|
||||
},
|
||||
"you": {
|
||||
"id": "snk_0",
|
||||
"name": "example snake",
|
||||
"latency": "42",
|
||||
"health": 100,
|
||||
"body": [
|
||||
{
|
||||
"x": 1,
|
||||
"y": 5
|
||||
},
|
||||
{
|
||||
"x": 1,
|
||||
"y": 5
|
||||
},
|
||||
{
|
||||
"x": 1,
|
||||
"y": 5
|
||||
}
|
||||
],
|
||||
"head": {
|
||||
"x": 1,
|
||||
"y": 5
|
||||
},
|
||||
"length": 3,
|
||||
"shout": "",
|
||||
"squad": "",
|
||||
"customizations": {
|
||||
"color": "#123456",
|
||||
"head": "safe",
|
||||
"tail": "curled"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue