Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEV 1916: Fix output issues in CLI #114

Merged
merged 2 commits into from
Oct 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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