2020-12-10 15:02:29 -08:00
package commands
2020-12-10 17:35:52 -05:00
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
2020-12-10 15:56:22 -08:00
"math/rand"
2020-12-10 17:35:52 -05:00
"net/http"
"net/url"
2021-12-02 18:59:20 +01:00
"os"
2020-12-10 17:35:52 -05:00
"path"
"strconv"
2021-07-02 20:36:37 -07:00
"sync"
2020-12-10 17:35:52 -05:00
"time"
2021-07-03 06:42:16 +03:00
"github.com/BattlesnakeOfficial/rules"
2021-11-25 14:07:56 -08:00
"github.com/BattlesnakeOfficial/rules/client"
2022-05-25 11:24:27 -07:00
"github.com/BattlesnakeOfficial/rules/maps"
2021-07-03 06:42:16 +03:00
"github.com/google/uuid"
"github.com/spf13/cobra"
2020-12-10 17:35:52 -05:00
)
2021-11-25 14:07:56 -08:00
// Used to store state for each SnakeState while running a local game
type SnakeState struct {
2020-12-10 17:35:52 -05:00
URL string
Name string
ID string
LastMove string
Character rune
2021-11-25 14:07:56 -08:00
Color string
Head string
Tail string
2020-12-10 17:35:52 -05:00
}
2022-05-25 11:24:27 -07:00
type GameState struct {
// Options
Width int
Height int
Names [ ] string
URLs [ ] string
Timeout int
TurnDuration int
Sequential bool
GameType string
MapName string
ViewMap bool
UseColor bool
Seed int64
TurnDelay int
DebugRequests bool
Output string
FoodSpawnChance int
MinimumFood int
HazardDamagePerTurn int
ShrinkEveryNTurns int
// Internal game state
settings map [ string ] string
snakeStates map [ string ] SnakeState
gameID string
httpClient http . Client
ruleset rules . Ruleset
gameMap maps . GameMap
2022-03-16 16:58:05 -07:00
}
2022-05-25 11:24:27 -07:00
func NewPlayCommand ( ) * cobra . Command {
gameState := & GameState { }
2020-12-10 17:35:52 -05:00
2022-05-25 11:24:27 -07:00
var playCmd = & cobra . Command {
Use : "play" ,
Short : "Play a game of Battlesnake locally." ,
Long : "Play a game of Battlesnake locally." ,
Run : func ( cmd * cobra . Command , args [ ] string ) {
gameState . Run ( )
} ,
}
playCmd . Flags ( ) . IntVarP ( & gameState . Width , "width" , "W" , 11 , "Width of Board" )
playCmd . Flags ( ) . IntVarP ( & gameState . Height , "height" , "H" , 11 , "Height of Board" )
playCmd . Flags ( ) . StringArrayVarP ( & gameState . Names , "name" , "n" , nil , "Name of Snake" )
playCmd . Flags ( ) . StringArrayVarP ( & gameState . URLs , "url" , "u" , nil , "URL of Snake" )
playCmd . Flags ( ) . IntVarP ( & gameState . Timeout , "timeout" , "t" , 500 , "Request Timeout" )
playCmd . Flags ( ) . BoolVarP ( & gameState . Sequential , "sequential" , "s" , false , "Use Sequential Processing" )
playCmd . Flags ( ) . StringVarP ( & gameState . GameType , "gametype" , "g" , "standard" , "Type of Game Rules" )
playCmd . Flags ( ) . StringVarP ( & gameState . MapName , "map" , "m" , "standard" , "Game map to use to populate the board" )
playCmd . Flags ( ) . BoolVarP ( & gameState . ViewMap , "viewmap" , "v" , false , "View the Map Each Turn" )
playCmd . Flags ( ) . BoolVarP ( & gameState . UseColor , "color" , "c" , false , "Use color to draw the map" )
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 ( ) . BoolVar ( & gameState . DebugRequests , "debug-requests" , false , "Log body of all requests sent" )
playCmd . Flags ( ) . StringVarP ( & gameState . Output , "output" , "o" , "" , "File path to output game state to. Existing files will be overwritten" )
playCmd . Flags ( ) . IntVar ( & gameState . FoodSpawnChance , "foodSpawnChance" , 15 , "Percentage chance of spawning a new food every round" )
playCmd . Flags ( ) . IntVar ( & gameState . MinimumFood , "minimumFood" , 1 , "Minimum food to keep on the board every turn" )
playCmd . Flags ( ) . IntVar ( & gameState . HazardDamagePerTurn , "hazardDamagePerTurn" , 14 , "Health damage a snake will take when ending its turn in a hazard" )
playCmd . Flags ( ) . IntVar ( & gameState . ShrinkEveryNTurns , "shrinkEveryNTurns" , 25 , "In Royale mode, the number of turns between generating new hazards" )
2021-09-02 15:32:46 -07:00
playCmd . Flags ( ) . SortFlags = false
2022-05-25 11:24:27 -07:00
return playCmd
2020-12-10 17:35:52 -05:00
}
2022-05-25 11:24:27 -07:00
// Setup a GameState once all the fields have been parsed from the command-line.
func ( gameState * GameState ) initialize ( ) {
// Generate game ID
gameState . gameID = uuid . New ( ) . String ( )
// Set up HTTP client with request timeout
if gameState . Timeout == 0 {
gameState . Timeout = 500
}
gameState . httpClient = http . Client {
Timeout : time . Duration ( gameState . Timeout ) * time . Millisecond ,
}
// Load game map
gameMap , err := maps . GetMap ( gameState . MapName )
if err != nil {
log . Fatalf ( "Failed to load game map %#v: %v" , gameState . MapName , err )
}
gameState . gameMap = gameMap
// Create settings object
gameState . settings = map [ string ] string {
rules . ParamGameType : gameState . GameType ,
rules . ParamFoodSpawnChance : fmt . Sprint ( gameState . FoodSpawnChance ) ,
rules . ParamMinimumFood : fmt . Sprint ( gameState . MinimumFood ) ,
rules . ParamHazardDamagePerTurn : fmt . Sprint ( gameState . HazardDamagePerTurn ) ,
rules . ParamShrinkEveryNTurns : fmt . Sprint ( gameState . ShrinkEveryNTurns ) ,
}
// Build ruleset from settings
2022-06-08 15:45:20 -07:00
ruleset := rules . NewRulesetBuilder ( ) .
WithSeed ( gameState . Seed ) .
WithParams ( gameState . settings ) .
WithSolo ( len ( gameState . URLs ) < 2 ) .
Ruleset ( )
2022-05-25 11:24:27 -07:00
gameState . ruleset = ruleset
// Initialize snake states as empty until we can ping the snake URLs
gameState . snakeStates = map [ string ] SnakeState { }
2022-03-16 16:58:05 -07:00
}
2022-05-25 11:24:27 -07:00
// Setup and run a full game.
func ( gameState * GameState ) Run ( ) {
gameState . initialize ( )
2020-12-10 15:56:22 -08:00
2022-05-25 11:24:27 -07:00
// Setup local state for snakes
gameState . snakeStates = gameState . buildSnakesFromOptions ( )
2020-12-10 17:35:52 -05:00
2022-05-25 11:24:27 -07:00
rand . Seed ( gameState . Seed )
2020-12-10 17:35:52 -05:00
2022-05-25 11:24:27 -07:00
boardState := gameState . initializeBoardFromArgs ( )
exportGame := gameState . Output != ""
2021-12-02 18:59:20 +01:00
gameExporter := GameExporter {
2022-05-25 11:24:27 -07:00
game : gameState . createClientGame ( ) ,
2021-12-02 18:59:20 +01:00
snakeRequests : make ( [ ] client . SnakeRequest , 0 ) ,
winner : SnakeState { } ,
isDraw : false ,
}
2020-12-10 17:35:52 -05:00
2022-05-25 11:24:27 -07:00
if gameState . ViewMap {
gameState . printMap ( boardState )
}
2021-12-02 18:59:20 +01:00
2022-05-25 11:24:27 -07:00
var endTime time . Time
for v := false ; ! v ; v , _ = gameState . ruleset . IsGameOver ( boardState ) {
if gameState . TurnDuration > 0 {
endTime = time . Now ( ) . Add ( time . Duration ( gameState . TurnDuration ) * time . Millisecond )
2022-03-28 13:22:51 -04:00
}
2022-05-25 11:24:27 -07:00
// Export game first, if enabled, so that we save the board on turn zero
2021-12-02 18:59:20 +01:00
if exportGame {
// The output file was designed in a way so that (nearly) every entry is equivalent to a valid API request.
// This is meant to help unlock further development of tools such as replaying a saved game by simply copying each line and sending it as a POST request.
// There was a design choice to be made here: the difference between SnakeRequest and BoardState is the `you` key.
// We could choose to either store the SnakeRequest of each snake OR to omit the `you` key OR fill the `you` key with one of the snakes
// In all cases the API request is technically non-compliant with how the actual API request should be.
// The third option (filling the `you` key with an arbitrary snake) is the closest to the actual API request that would need the least manipulation to
// be adjusted to look like an API call for a specific snake in the game.
2022-05-25 11:24:27 -07:00
for _ , snakeState := range gameState . snakeStates {
snakeRequest := gameState . getRequestBodyForSnake ( boardState , snakeState )
gameExporter . AddSnakeRequest ( snakeRequest )
break
}
}
boardState = gameState . createNextBoardState ( boardState )
if gameState . ViewMap {
gameState . printMap ( boardState )
} else {
log . Printf ( "[%v]: State: %v\n" , boardState . Turn , boardState )
}
if gameState . TurnDelay > 0 {
time . Sleep ( time . Duration ( gameState . TurnDelay ) * time . Millisecond )
}
if gameState . TurnDuration > 0 {
time . Sleep ( time . Until ( endTime ) )
2021-12-02 18:59:20 +01:00
}
2022-05-25 11:24:27 -07:00
2020-12-10 17:35:52 -05:00
}
2021-12-02 18:59:20 +01:00
isDraw := true
2022-05-25 11:24:27 -07:00
if gameState . GameType == "solo" {
log . Printf ( "[DONE]: Game completed after %v turns." , boardState . Turn )
2021-12-02 18:59:20 +01:00
if exportGame {
// These checks for exportGame are present to avoid vacuuming up RAM when an export is not requred.
2022-05-25 11:24:27 -07:00
for _ , snakeState := range gameState . snakeStates {
gameExporter . winner = snakeState
break
}
2021-12-02 18:59:20 +01:00
}
2020-12-10 15:56:22 -08:00
} else {
2021-12-02 18:59:20 +01:00
var winner SnakeState
2022-05-25 11:24:27 -07:00
for _ , snake := range boardState . Snakes {
snakeState := gameState . snakeStates [ snake . ID ]
2020-12-10 15:56:22 -08:00
if snake . EliminatedCause == rules . NotEliminated {
isDraw = false
2021-12-02 18:59:20 +01:00
winner = snakeState
2020-12-10 15:56:22 -08:00
}
2022-05-25 11:24:27 -07:00
gameState . sendEndRequest ( boardState , snakeState )
2020-12-10 17:35:52 -05:00
}
2020-12-10 15:56:22 -08:00
if isDraw {
2022-05-25 11:24:27 -07:00
log . Printf ( "[DONE]: Game completed after %v turns. It was a draw." , boardState . Turn )
2020-12-10 15:56:22 -08:00
} else {
2022-05-25 11:24:27 -07:00
log . Printf ( "[DONE]: Game completed after %v turns. %v is the winner." , boardState . Turn , winner . Name )
2021-12-02 18:59:20 +01:00
}
if exportGame {
gameExporter . winner = winner
2022-05-30 15:16:06 -07:00
gameExporter . isDraw = isDraw
2021-12-02 18:59:20 +01:00
}
}
if exportGame {
2022-05-25 11:24:27 -07:00
err := gameExporter . FlushToFile ( gameState . Output , "JSONL" )
2021-12-02 18:59:20 +01:00
if err != nil {
log . Printf ( "[WARN]: Unable to export game. Reason: %v\n" , err . Error ( ) )
os . Exit ( 1 )
2020-12-10 15:56:22 -08:00
}
2020-12-10 17:35:52 -05:00
}
}
2022-05-25 11:24:27 -07:00
func ( gameState * GameState ) initializeBoardFromArgs ( ) * rules . BoardState {
2020-12-10 17:35:52 -05:00
snakeIds := [ ] string { }
2022-05-25 11:24:27 -07:00
for _ , snakeState := range gameState . snakeStates {
2021-11-25 14:07:56 -08:00
snakeIds = append ( snakeIds , snakeState . ID )
2020-12-10 17:35:52 -05:00
}
2022-05-25 11:24:27 -07:00
boardState , err := maps . SetupBoard ( gameState . gameMap . ID ( ) , gameState . ruleset . Settings ( ) , gameState . Width , gameState . Height , snakeIds )
2020-12-10 17:35:52 -05:00
if err != nil {
2022-05-25 11:24:27 -07:00
log . Fatalf ( "Error Initializing Board State: %v" , err )
2020-12-10 17:35:52 -05:00
}
2022-05-25 11:24:27 -07:00
boardState , err = gameState . ruleset . ModifyInitialBoardState ( boardState )
2021-08-23 17:13:58 -07:00
if err != nil {
2022-05-25 11:24:27 -07:00
log . Fatalf ( "Error Initializing Board State: %v" , err )
2021-08-23 17:13:58 -07:00
}
2022-05-25 11:24:27 -07:00
for _ , snakeState := range gameState . snakeStates {
snakeRequest := gameState . getRequestBodyForSnake ( boardState , snakeState )
2021-12-02 18:59:20 +01:00
requestBody := serialiseSnakeRequest ( snakeRequest )
2021-11-25 14:07:56 -08:00
u , _ := url . ParseRequestURI ( snakeState . URL )
2020-12-10 17:35:52 -05:00
u . Path = path . Join ( u . Path , "start" )
2022-05-25 11:24:27 -07:00
if gameState . DebugRequests {
2021-09-07 14:58:10 -07:00
log . Printf ( "POST %s: %v" , u , string ( requestBody ) )
}
2022-05-25 11:24:27 -07:00
_ , err = gameState . httpClient . Post ( u . String ( ) , "application/json" , bytes . NewBuffer ( requestBody ) )
2020-12-10 17:35:52 -05:00
if err != nil {
log . Printf ( "[WARN]: Request to %v failed" , u . String ( ) )
}
}
2022-05-25 11:24:27 -07:00
return boardState
2020-12-10 17:35:52 -05:00
}
2022-05-25 11:24:27 -07:00
func ( gameState * GameState ) createNextBoardState ( boardState * rules . BoardState ) * rules . BoardState {
2020-12-10 17:35:52 -05:00
var moves [ ] rules . SnakeMove
2022-05-25 11:24:27 -07:00
if gameState . Sequential {
for _ , snakeState := range gameState . snakeStates {
for _ , snake := range boardState . Snakes {
2021-11-25 14:07:56 -08:00
if snakeState . ID == snake . ID && snake . EliminatedCause == rules . NotEliminated {
2022-05-25 11:24:27 -07:00
moves = append ( moves , gameState . getMoveForSnake ( boardState , snakeState ) )
2021-07-02 20:36:37 -07:00
}
}
2020-12-10 17:35:52 -05:00
}
} else {
2021-07-02 20:36:37 -07:00
var wg sync . WaitGroup
2022-05-25 11:24:27 -07:00
c := make ( chan rules . SnakeMove , len ( gameState . snakeStates ) )
2021-07-02 20:36:37 -07:00
2022-05-25 11:24:27 -07:00
for _ , snakeState := range gameState . snakeStates {
for _ , snake := range boardState . Snakes {
2021-11-25 14:07:56 -08:00
if snakeState . ID == snake . ID && snake . EliminatedCause == rules . NotEliminated {
2021-07-02 20:36:37 -07:00
wg . Add ( 1 )
2021-11-25 14:07:56 -08:00
go func ( snakeState SnakeState ) {
defer wg . Done ( )
2022-05-25 11:24:27 -07:00
c <- gameState . getMoveForSnake ( boardState , snakeState )
2021-11-25 14:07:56 -08:00
} ( snakeState )
2021-07-02 20:36:37 -07:00
}
}
2020-12-10 17:35:52 -05:00
}
2021-07-02 20:36:37 -07:00
wg . Wait ( )
close ( c )
for move := range c {
moves = append ( moves , move )
2020-12-10 17:35:52 -05:00
}
}
for _ , move := range moves {
2022-05-25 11:24:27 -07:00
snakeState := gameState . snakeStates [ move . ID ]
2021-11-25 14:07:56 -08:00
snakeState . LastMove = move . Move
2022-05-25 11:24:27 -07:00
gameState . snakeStates [ move . ID ] = snakeState
}
boardState , err := gameState . ruleset . CreateNextBoardState ( boardState , moves )
if err != nil {
log . Fatalf ( "Error producing next board state: %v" , err )
2020-12-10 17:35:52 -05:00
}
2022-05-25 11:24:27 -07:00
boardState , err = maps . UpdateBoard ( gameState . gameMap . ID ( ) , boardState , gameState . ruleset . Settings ( ) )
2020-12-10 17:35:52 -05:00
if err != nil {
2022-05-25 11:24:27 -07:00
log . Fatalf ( "Error updating board with game map: %v" , err )
2020-12-10 17:35:52 -05:00
}
2021-08-27 13:28:12 -07:00
2022-05-25 11:24:27 -07:00
boardState . Turn += 1
2021-08-27 13:28:12 -07:00
2022-05-25 11:24:27 -07:00
return boardState
2020-12-10 17:35:52 -05:00
}
2022-05-25 11:24:27 -07:00
func ( gameState * GameState ) getMoveForSnake ( boardState * rules . BoardState , snakeState SnakeState ) rules . SnakeMove {
snakeRequest := gameState . getRequestBodyForSnake ( boardState , snakeState )
2021-12-02 18:59:20 +01:00
requestBody := serialiseSnakeRequest ( snakeRequest )
2021-11-25 14:07:56 -08:00
u , _ := url . ParseRequestURI ( snakeState . URL )
2020-12-10 17:35:52 -05:00
u . Path = path . Join ( u . Path , "move" )
2022-05-25 11:24:27 -07:00
if gameState . DebugRequests {
2021-09-07 14:58:10 -07:00
log . Printf ( "POST %s: %v" , u , string ( requestBody ) )
}
2022-05-25 11:24:27 -07:00
res , err := gameState . httpClient . Post ( u . String ( ) , "application/json" , bytes . NewBuffer ( requestBody ) )
2021-11-25 14:07:56 -08:00
move := snakeState . LastMove
2020-12-10 17:35:52 -05:00
if err != nil {
log . Printf ( "[WARN]: Request to %v failed\n" , u . String ( ) )
log . Printf ( "Body --> %v\n" , string ( requestBody ) )
} else if res . Body != nil {
defer res . Body . Close ( )
body , readErr := ioutil . ReadAll ( res . Body )
if readErr != nil {
log . Fatal ( readErr )
} else {
2021-11-25 14:07:56 -08:00
playerResponse := client . MoveResponse { }
2020-12-10 17:35:52 -05:00
jsonErr := json . Unmarshal ( body , & playerResponse )
if jsonErr != nil {
log . Fatal ( jsonErr )
} else {
move = playerResponse . Move
}
}
}
2021-11-25 14:07:56 -08:00
return rules . SnakeMove { ID : snakeState . ID , Move : move }
2020-12-10 17:35:52 -05:00
}
2022-05-25 11:24:27 -07:00
func ( gameState * GameState ) sendEndRequest ( boardState * rules . BoardState , snakeState SnakeState ) {
snakeRequest := gameState . getRequestBodyForSnake ( boardState , snakeState )
2021-12-02 18:59:20 +01:00
requestBody := serialiseSnakeRequest ( snakeRequest )
2021-11-25 14:07:56 -08:00
u , _ := url . ParseRequestURI ( snakeState . URL )
2020-12-10 17:35:52 -05:00
u . Path = path . Join ( u . Path , "end" )
2022-05-25 11:24:27 -07:00
if gameState . DebugRequests {
2021-09-07 14:58:10 -07:00
log . Printf ( "POST %s: %v" , u , string ( requestBody ) )
}
2022-05-25 11:24:27 -07:00
_ , err := gameState . httpClient . Post ( u . String ( ) , "application/json" , bytes . NewBuffer ( requestBody ) )
2020-12-10 17:35:52 -05:00
if err != nil {
log . Printf ( "[WARN]: Request to %v failed" , u . String ( ) )
}
}
2022-05-25 11:24:27 -07:00
func ( gameState * GameState ) getRequestBodyForSnake ( boardState * rules . BoardState , snakeState SnakeState ) client . SnakeRequest {
2020-12-10 17:35:52 -05:00
var youSnake rules . Snake
2022-05-25 11:24:27 -07:00
for _ , snk := range boardState . Snakes {
2021-11-25 14:07:56 -08:00
if snakeState . ID == snk . ID {
2020-12-10 17:35:52 -05:00
youSnake = snk
break
}
}
2021-11-25 14:07:56 -08:00
request := client . SnakeRequest {
2022-05-25 11:24:27 -07:00
Game : gameState . createClientGame ( ) ,
Turn : boardState . Turn ,
Board : convertStateToBoard ( boardState , gameState . snakeStates ) ,
You : convertRulesSnake ( youSnake , snakeState ) ,
2020-12-10 17:35:52 -05:00
}
2021-12-02 18:59:20 +01:00
return request
}
2022-05-25 11:24:27 -07:00
func ( gameState * GameState ) createClientGame ( ) client . Game {
return client . Game {
ID : gameState . gameID ,
Timeout : gameState . Timeout ,
Ruleset : client . Ruleset {
Name : gameState . ruleset . Name ( ) ,
Version : "cli" , // TODO: Use GitHub Release Version
Settings : gameState . ruleset . Settings ( ) ,
2021-11-25 14:07:56 -08:00
} ,
2022-05-25 11:24:27 -07:00
Map : gameState . gameMap . ID ( ) ,
2020-12-10 17:35:52 -05:00
}
}
2022-05-25 11:24:27 -07:00
func ( gameState * GameState ) buildSnakesFromOptions ( ) map [ string ] SnakeState {
2021-12-08 10:43:40 -08:00
bodyChars := [ ] rune { '■' , '⌀' , '●' , '☻' , '◘' , '☺' , '□' , '⍟' }
2020-12-10 17:35:52 -05:00
var numSnakes int
2021-11-25 14:07:56 -08:00
snakes := map [ string ] SnakeState { }
2022-05-25 11:24:27 -07:00
numNames := len ( gameState . Names )
numURLs := len ( gameState . URLs )
2020-12-10 17:35:52 -05:00
if numNames > numURLs {
numSnakes = numNames
} else {
numSnakes = numURLs
}
if numNames != numURLs {
log . Println ( "[WARN]: Number of Names and URLs do not match: defaults will be applied to missing values" )
}
for i := int ( 0 ) ; i < numSnakes ; i ++ {
var snakeName string
var snakeURL string
id := uuid . New ( ) . String ( )
if i < numNames {
2022-05-25 11:24:27 -07:00
snakeName = gameState . Names [ i ]
2020-12-10 17:35:52 -05:00
} else {
2022-05-25 11:24:27 -07:00
log . Printf ( "[WARN]: Name for URL %v is missing: a default name will be applied\n" , gameState . URLs [ i ] )
2020-12-10 17:35:52 -05:00
snakeName = id
}
if i < numURLs {
2022-05-25 11:24:27 -07:00
u , err := url . ParseRequestURI ( gameState . URLs [ i ] )
2020-12-10 17:35:52 -05:00
if err != nil {
2022-05-25 11:24:27 -07:00
log . Printf ( "[WARN]: URL %v is not valid: a default will be applied\n" , gameState . URLs [ i ] )
2020-12-10 17:35:52 -05:00
snakeURL = "https://example.com"
} else {
snakeURL = u . String ( )
}
} else {
2022-05-25 11:24:27 -07:00
log . Printf ( "[WARN]: URL for Name %v is missing: a default URL will be applied\n" , gameState . Names [ i ] )
2020-12-10 17:35:52 -05:00
snakeURL = "https://example.com"
}
2021-11-25 14:07:56 -08:00
snakeState := SnakeState {
Name : snakeName , URL : snakeURL , ID : id , LastMove : "up" , Character : bodyChars [ i % 8 ] ,
}
2022-05-25 11:24:27 -07:00
res , err := gameState . httpClient . Get ( snakeURL )
2020-12-10 17:35:52 -05:00
if err != nil {
2021-11-25 14:07:56 -08:00
log . Printf ( "[WARN]: Request to %v failed: %v" , snakeURL , err )
2020-12-10 17:35:52 -05:00
} else if res . Body != nil {
defer res . Body . Close ( )
body , readErr := ioutil . ReadAll ( res . Body )
if readErr != nil {
log . Fatal ( readErr )
}
2021-11-25 14:07:56 -08:00
pingResponse := client . SnakeMetadataResponse { }
2020-12-10 17:35:52 -05:00
jsonErr := json . Unmarshal ( body , & pingResponse )
if jsonErr != nil {
2021-11-25 14:07:56 -08:00
log . Printf ( "Error reading response from %v: %v" , snakeURL , jsonErr )
2020-12-10 17:35:52 -05:00
} else {
2021-11-25 14:07:56 -08:00
snakeState . Head = pingResponse . Head
snakeState . Tail = pingResponse . Tail
snakeState . Color = pingResponse . Color
2020-12-10 17:35:52 -05:00
}
}
2021-11-25 14:07:56 -08:00
snakes [ snakeState . ID ] = snakeState
2020-12-10 17:35:52 -05:00
}
return snakes
}
2022-05-25 11:24:27 -07:00
func ( gameState * GameState ) printMap ( boardState * rules . BoardState ) {
2020-12-10 17:35:52 -05:00
var o bytes . Buffer
2022-05-25 11:24:27 -07:00
o . WriteString ( fmt . Sprintf ( "Ruleset: %s, Seed: %d, Turn: %v\n" , gameState . GameType , gameState . Seed , boardState . Turn ) )
board := make ( [ ] [ ] string , boardState . Width )
2020-12-10 17:35:52 -05:00
for i := range board {
2022-05-25 11:24:27 -07:00
board [ i ] = make ( [ ] string , boardState . Height )
2020-12-10 17:35:52 -05:00
}
2022-05-25 11:24:27 -07:00
for y := int ( 0 ) ; y < boardState . Height ; y ++ {
for x := int ( 0 ) ; x < boardState . Width ; x ++ {
if gameState . UseColor {
2021-12-08 10:43:40 -08:00
board [ x ] [ y ] = TERM_FG_LIGHTGRAY + "□"
} else {
board [ x ] [ y ] = "◦"
}
2020-12-10 17:35:52 -05:00
}
}
2022-05-25 11:24:27 -07:00
for _ , oob := range boardState . Hazards {
if gameState . UseColor {
2021-12-08 10:43:40 -08:00
board [ oob . X ] [ oob . Y ] = TERM_BG_GRAY + " " + TERM_BG_WHITE
} else {
board [ oob . X ] [ oob . Y ] = "░"
}
}
2022-05-25 11:24:27 -07:00
if gameState . UseColor {
o . WriteString ( fmt . Sprintf ( "Hazards " + TERM_BG_GRAY + " " + TERM_RESET + ": %v\n" , boardState . Hazards ) )
2021-12-08 10:43:40 -08:00
} else {
2022-05-25 11:24:27 -07:00
o . WriteString ( fmt . Sprintf ( "Hazards ░: %v\n" , boardState . Hazards ) )
2020-12-10 17:35:52 -05:00
}
2022-05-25 11:24:27 -07:00
for _ , f := range boardState . Food {
if gameState . UseColor {
2021-12-08 10:43:40 -08:00
board [ f . X ] [ f . Y ] = TERM_FG_FOOD + "●"
} else {
board [ f . X ] [ f . Y ] = "⚕"
}
}
2022-05-25 11:24:27 -07:00
if gameState . UseColor {
o . WriteString ( fmt . Sprintf ( "Food " + TERM_FG_FOOD + TERM_BG_WHITE + "●" + TERM_RESET + ": %v\n" , boardState . Food ) )
2021-12-08 10:43:40 -08:00
} else {
2022-05-25 11:24:27 -07:00
o . WriteString ( fmt . Sprintf ( "Food ⚕: %v\n" , boardState . Food ) )
2020-12-10 17:35:52 -05:00
}
2022-05-25 11:24:27 -07:00
for _ , s := range boardState . Snakes {
red , green , blue := parseSnakeColor ( gameState . snakeStates [ s . ID ] . Color )
2020-12-10 17:35:52 -05:00
for _ , b := range s . Body {
2022-05-25 11:24:27 -07:00
if b . X >= 0 && b . X < boardState . Width && b . Y >= 0 && b . Y < boardState . Height {
if gameState . UseColor {
2021-12-08 10:43:40 -08:00
board [ b . X ] [ b . Y ] = fmt . Sprintf ( TERM_FG_RGB + "■" , red , green , blue )
} else {
2022-05-25 11:24:27 -07:00
board [ b . X ] [ b . Y ] = string ( gameState . snakeStates [ s . ID ] . Character )
2021-12-08 10:43:40 -08:00
}
2021-07-03 06:42:16 +03:00
}
2020-12-10 17:35:52 -05:00
}
2022-05-25 11:24:27 -07:00
if gameState . UseColor {
o . WriteString ( fmt . Sprintf ( "%v " + TERM_FG_RGB + TERM_BG_WHITE + "■■■" + TERM_RESET + ": %v\n" , gameState . snakeStates [ s . ID ] . Name , red , green , blue , s ) )
2021-12-08 10:43:40 -08:00
} else {
2022-05-25 11:24:27 -07:00
o . WriteString ( fmt . Sprintf ( "%v %c: %v\n" , gameState . snakeStates [ s . ID ] . Name , gameState . snakeStates [ s . ID ] . Character , s ) )
2021-12-08 10:43:40 -08:00
}
2020-12-10 17:35:52 -05:00
}
2022-05-25 11:24:27 -07:00
for y := boardState . Height - 1 ; y >= 0 ; y -- {
if gameState . UseColor {
2021-12-08 10:43:40 -08:00
o . WriteString ( TERM_BG_WHITE )
}
2022-05-25 11:24:27 -07:00
for x := int ( 0 ) ; x < boardState . Width ; x ++ {
2021-12-08 10:43:40 -08:00
o . WriteString ( board [ x ] [ y ] )
}
2022-05-25 11:24:27 -07:00
if gameState . UseColor {
2021-12-08 10:43:40 -08:00
o . WriteString ( TERM_RESET )
2020-12-10 17:35:52 -05:00
}
o . WriteString ( "\n" )
}
log . Print ( o . String ( ) )
}
2022-05-25 11:24:27 -07:00
func serialiseSnakeRequest ( snakeRequest client . SnakeRequest ) [ ] byte {
requestJSON , err := json . Marshal ( snakeRequest )
if err != nil {
log . Fatalf ( "Error marshalling JSON from State: %v" , err )
}
return requestJSON
}
func convertRulesSnake ( snake rules . Snake , snakeState SnakeState ) client . Snake {
return client . Snake {
ID : snake . ID ,
Name : snakeState . Name ,
Health : snake . Health ,
Body : client . CoordFromPointArray ( snake . Body ) ,
Latency : "0" ,
Head : client . CoordFromPoint ( snake . Body [ 0 ] ) ,
Length : int ( len ( snake . Body ) ) ,
Shout : "" ,
Customizations : client . Customizations {
Head : snakeState . Head ,
Tail : snakeState . Tail ,
Color : snakeState . Color ,
} ,
}
}
func convertRulesSnakes ( snakes [ ] rules . Snake , snakeStates map [ string ] SnakeState ) [ ] client . Snake {
a := make ( [ ] client . Snake , 0 )
for _ , snake := range snakes {
if snake . EliminatedCause == rules . NotEliminated {
a = append ( a , convertRulesSnake ( snake , snakeStates [ snake . ID ] ) )
}
}
return a
}
func convertStateToBoard ( boardState * rules . BoardState , snakeStates map [ string ] SnakeState ) client . Board {
return client . Board {
Height : boardState . Height ,
Width : boardState . Width ,
Food : client . CoordFromPointArray ( boardState . Food ) ,
Hazards : client . CoordFromPointArray ( boardState . Hazards ) ,
Snakes : convertRulesSnakes ( boardState . Snakes , snakeStates ) ,
}
}
// Parses a color string like "#ef03d3" to rgb values from 0 to 255 or returns
// the default gray if any errors occure
func parseSnakeColor ( color string ) ( int64 , int64 , int64 ) {
if len ( color ) == 7 {
red , err_r := strconv . ParseInt ( color [ 1 : 3 ] , 16 , 64 )
green , err_g := strconv . ParseInt ( color [ 3 : 5 ] , 16 , 64 )
blue , err_b := strconv . ParseInt ( color [ 5 : ] , 16 , 64 )
if err_r == nil && err_g == nil && err_b == nil {
return red , green , blue
}
}
// Default gray color from Battlesnake board
return 136 , 136 , 136
}