Skip to content

Commit

Permalink
DEV 1916: Fix output issues in CLI (#114)
Browse files Browse the repository at this point in the history
* adding testing of output file and write line for /end

* fix regression in latency rounding for board
  • Loading branch information
robbles authored Oct 20, 2022
1 parent 5f60ccb commit 639362e
Show file tree
Hide file tree
Showing 7 changed files with 396 additions and 49 deletions.
33 changes: 7 additions & 26 deletions cli/commands/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ package commands
import (
"encoding/json"
"fmt"
"os"

log "github.com/spf13/jwalterweatherman"
"io"

"github.com/BattlesnakeOfficial/rules/client"
)
Expand All @@ -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) {
Expand Down
55 changes: 45 additions & 10 deletions cli/commands/play.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net/http"
"net/url"
"os"
"path"
"strconv"
"strings"
Expand Down Expand Up @@ -56,7 +58,7 @@ type GameState struct {
UseColor bool
Seed int64
TurnDelay int
Output string
OutputPath string
ViewInBrowser bool
BoardURL string
FoodSpawnChance int
Expand All @@ -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 {
Expand All @@ -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()
},
}
Expand All @@ -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")

Expand All @@ -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()

Expand All @@ -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

Expand All @@ -153,26 +160,37 @@ 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(),
snakeRequests: make([]client.SnakeRequest, 0),
winner: SnakeState{},
isDraw: false,
}
exportGame := gameState.outputFile != nil
if exportGame {
defer gameState.outputFile.Close()
}

boardGame := board.Game{
ID: gameState.gameID,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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: "",
Expand Down
Loading

0 comments on commit 639362e

Please sign in to comment.