add http handler into play.go
this allows for http game creation
This commit is contained in:
parent
442970f17c
commit
e7a4362a66
2 changed files with 250 additions and 48 deletions
|
|
@ -17,27 +17,45 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
// adding custom stuff here
|
// adding custom stuff here
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/BattlesnakeOfficial/rules"
|
"github.com/BattlesnakeOfficial/rules"
|
||||||
"github.com/BattlesnakeOfficial/rules/board"
|
"github.com/BattlesnakeOfficial/rules/board"
|
||||||
"github.com/BattlesnakeOfficial/rules/client"
|
"github.com/BattlesnakeOfficial/rules/client"
|
||||||
"github.com/BattlesnakeOfficial/rules/maps"
|
"github.com/BattlesnakeOfficial/rules/maps"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
_ "github.com/mattn/go-sqlite3" // the SQLite driver (underscore means we only init it)
|
|
||||||
"github.com/pkg/browser"
|
// _ "github.com/mattn/go-sqlite3" // the SQLite driver for local development
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
log "github.com/spf13/jwalterweatherman"
|
log "github.com/spf13/jwalterweatherman"
|
||||||
|
|
||||||
// adding custom stuff below here
|
"github.com/BattlesnakeOfficial/rules/db"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var db *pgxpool.Pool // global database connection pool
|
|
||||||
|
|
||||||
// store frames in memory for now
|
|
||||||
// will write to db at end of game
|
// will write to db at end of game
|
||||||
var frames []board.GameEvent
|
var frames []board.GameEvent
|
||||||
|
|
||||||
|
func setupDb(dsn string) *db.Database {
|
||||||
|
// Replace with your Postgres DSN or set DATABASE_URL
|
||||||
|
|
||||||
|
if dsn == "" {
|
||||||
|
print("Using default DSN for local development. Please set DATABASE_URL in production.\n")
|
||||||
|
dsn = "removeditdidnotpushtoproductionhaha" // DO NOT PUSH TO PRODUCTION
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to DB
|
||||||
|
database, err := db.Connect(dsn)
|
||||||
|
if err != nil {
|
||||||
|
log.ERROR.Fatalf("Failed to connect: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
print("SUCCESS: Connected to database\n")
|
||||||
|
// os.Exit(0) // Exit after successful connection for testing purposes
|
||||||
|
return database
|
||||||
|
}
|
||||||
|
|
||||||
// Used to store state for each SnakeState while running a local game
|
// Used to store state for each SnakeState while running a local game
|
||||||
type SnakeState struct {
|
type SnakeState struct {
|
||||||
URL string
|
URL string
|
||||||
|
|
@ -89,6 +107,172 @@ type GameState struct {
|
||||||
idGenerator func(int) string
|
idGenerator func(int) string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
activeGame *GameState
|
||||||
|
// Mutex to protect activeGame from concurrent access
|
||||||
|
activeGameMutex sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewHostCommand() *cobra.Command {
|
||||||
|
|
||||||
|
var hostCmd = &cobra.Command{
|
||||||
|
Use: "host",
|
||||||
|
Short: "Host an HTTP server to start Battlesnake games via /play endpoint.",
|
||||||
|
Long: "Runs an HTTP server that accepts /play requests with query parameters instead of CLI flags. Each request will initialize and run a new game in the background.",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
http.HandleFunc("/play", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
activeGameMutex.Lock()
|
||||||
|
|
||||||
|
defer activeGameMutex.Unlock()
|
||||||
|
|
||||||
|
if activeGame != nil {
|
||||||
|
log.WARN.Print("A game is already running. Please wait for it to finish before starting a new one.")
|
||||||
|
w.WriteHeader(http.StatusConflict) // 409 Conflict
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"status": "busy",
|
||||||
|
"message": "A game is already running. Please wait for it to finish before starting a new one.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new game state with defaults
|
||||||
|
gameState := &GameState{
|
||||||
|
Width: 11,
|
||||||
|
Height: 11,
|
||||||
|
Timeout: 500,
|
||||||
|
GameType: "standard",
|
||||||
|
MapName: "standard",
|
||||||
|
Seed: time.Now().UTC().UnixNano(),
|
||||||
|
TurnDelay: 0,
|
||||||
|
TurnDuration: 0,
|
||||||
|
OutputPath: "",
|
||||||
|
ViewInBrowser: true,
|
||||||
|
BoardURL: "https://board.battlesnake.com",
|
||||||
|
FoodSpawnChance: 15,
|
||||||
|
MinimumFood: 1,
|
||||||
|
HazardDamagePerTurn: 14,
|
||||||
|
ShrinkEveryNTurns: 25,
|
||||||
|
}
|
||||||
|
|
||||||
|
q := r.URL.Query()
|
||||||
|
|
||||||
|
// Map query params to fields
|
||||||
|
if v := q.Get("width"); v != "" {
|
||||||
|
if i, err := strconv.Atoi(v); err == nil {
|
||||||
|
gameState.Width = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := q.Get("height"); v != "" {
|
||||||
|
if i, err := strconv.Atoi(v); err == nil {
|
||||||
|
gameState.Height = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := q["name"]; len(v) > 0 {
|
||||||
|
gameState.Names = v
|
||||||
|
}
|
||||||
|
if v := q["url"]; len(v) > 0 {
|
||||||
|
gameState.URLs = v
|
||||||
|
}
|
||||||
|
if v := q.Get("timeout"); v != "" {
|
||||||
|
if i, err := strconv.Atoi(v); err == nil {
|
||||||
|
gameState.Timeout = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := q.Get("sequential"); v == "true" {
|
||||||
|
gameState.Sequential = true
|
||||||
|
}
|
||||||
|
if v := q.Get("gametype"); v != "" {
|
||||||
|
gameState.GameType = v
|
||||||
|
}
|
||||||
|
if v := q.Get("map"); v != "" {
|
||||||
|
gameState.MapName = v
|
||||||
|
}
|
||||||
|
if v := q.Get("viewmap"); v == "true" {
|
||||||
|
gameState.ViewMap = true
|
||||||
|
}
|
||||||
|
if v := q.Get("color"); v == "true" {
|
||||||
|
gameState.UseColor = true
|
||||||
|
}
|
||||||
|
if v := q.Get("seed"); v != "" {
|
||||||
|
if i, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||||||
|
gameState.Seed = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := q.Get("delay"); v != "" {
|
||||||
|
if i, err := strconv.Atoi(v); err == nil {
|
||||||
|
gameState.TurnDelay = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := q.Get("duration"); v != "" {
|
||||||
|
if i, err := strconv.Atoi(v); err == nil {
|
||||||
|
gameState.TurnDuration = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := q.Get("output"); v != "" {
|
||||||
|
gameState.OutputPath = v
|
||||||
|
}
|
||||||
|
if v := q.Get("browser"); v == "true" {
|
||||||
|
gameState.ViewInBrowser = true
|
||||||
|
}
|
||||||
|
if v := q.Get("board-url"); v != "" {
|
||||||
|
gameState.BoardURL = v
|
||||||
|
}
|
||||||
|
if v := q.Get("foodSpawnChance"); v != "" {
|
||||||
|
if i, err := strconv.Atoi(v); err == nil {
|
||||||
|
gameState.FoodSpawnChance = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := q.Get("minimumFood"); v != "" {
|
||||||
|
if i, err := strconv.Atoi(v); err == nil {
|
||||||
|
gameState.MinimumFood = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := q.Get("hazardDamagePerTurn"); v != "" {
|
||||||
|
if i, err := strconv.Atoi(v); err == nil {
|
||||||
|
gameState.HazardDamagePerTurn = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := q.Get("shrinkEveryNTurns"); v != "" {
|
||||||
|
if i, err := strconv.Atoi(v); err == nil {
|
||||||
|
gameState.ShrinkEveryNTurns = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gameState.Initialize(); err != nil {
|
||||||
|
log.FATAL.Printf("Error initializing game: %v", err)
|
||||||
|
http.Error(w, fmt.Sprintf("failed to initialize: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set active game
|
||||||
|
activeGame = gameState
|
||||||
|
|
||||||
|
// Run game asynchronously
|
||||||
|
go func(gs *GameState) {
|
||||||
|
|
||||||
|
if err := gs.Run(); err != nil {
|
||||||
|
log.FATAL.Printf("Error running game: %v", err)
|
||||||
|
}
|
||||||
|
}(gameState)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
resp := map[string]string{
|
||||||
|
"id": gameState.gameID,
|
||||||
|
}
|
||||||
|
print("gameState.gameID", gameState.gameID)
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
log.INFO.Print("Hosting Battlesnake HTTP server on :6969")
|
||||||
|
log.INFO.Print(http.ListenAndServe(":6969", nil))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return hostCmd
|
||||||
|
}
|
||||||
|
|
||||||
func NewPlayCommand() *cobra.Command {
|
func NewPlayCommand() *cobra.Command {
|
||||||
gameState := &GameState{}
|
gameState := &GameState{}
|
||||||
|
|
||||||
|
|
@ -187,6 +371,11 @@ func (gameState *GameState) Initialize() error {
|
||||||
|
|
||||||
// Setup and run a full game.
|
// Setup and run a full game.
|
||||||
func (gameState *GameState) Run() error {
|
func (gameState *GameState) Run() error {
|
||||||
|
|
||||||
|
var dbconnection string = os.Getenv("DATABASE_URL")
|
||||||
|
var ctx context.Context = context.Background()
|
||||||
|
var database *db.Database = setupDb(dbconnection)
|
||||||
|
defer database.Pool.Close()
|
||||||
var gameOver bool
|
var gameOver bool
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
|
@ -216,7 +405,7 @@ func (gameState *GameState) Run() error {
|
||||||
|
|
||||||
boardGame := board.Game{
|
boardGame := board.Game{
|
||||||
ID: gameState.gameID,
|
ID: gameState.gameID,
|
||||||
Status: "running",
|
Status: "running", // switch to comlpeted before pushing to prod
|
||||||
Width: gameState.Width,
|
Width: gameState.Width,
|
||||||
Height: gameState.Height,
|
Height: gameState.Height,
|
||||||
Ruleset: map[string]string{
|
Ruleset: map[string]string{
|
||||||
|
|
@ -228,23 +417,24 @@ func (gameState *GameState) Run() error {
|
||||||
}
|
}
|
||||||
boardServer := board.NewBoardServer(boardGame)
|
boardServer := board.NewBoardServer(boardGame)
|
||||||
|
|
||||||
if gameState.outputFile != nil {
|
// commented out for now, we will write to db at end of game, and we modified the functionality
|
||||||
// insert initial game start into database
|
// if gameState.outputFile != nil {
|
||||||
// direct db call since this is the first event
|
// // insert initial game start into database
|
||||||
|
// // direct db call since this is the first event
|
||||||
|
|
||||||
bytes, err := json.Marshal(boardGame)
|
// bytes, err := json.Marshal(boardGame) // remove this later
|
||||||
|
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
log.WARN.Printf("Failed to serialize frame event for turn%v", err)
|
// log.WARN.Printf("Failed to serialize frame event for turn%v", err)
|
||||||
} else {
|
// } else {
|
||||||
bytes = append(bytes, '\n') // write each event on its own line
|
// bytes = append(bytes, '\n') // write each event on its own line
|
||||||
if _, err := gameState.outputFile.Write(bytes); err != nil {
|
// if _, err := gameState.outputFile.Write(bytes); err != nil {
|
||||||
log.WARN.Printf("Failed to write frame event to file: %v", err)
|
// log.WARN.Printf("Failed to write frame event to file: %v", err)
|
||||||
} else {
|
// } else {
|
||||||
log.INFO.Printf("Wrote initial get state to output file")
|
// log.INFO.Printf("Wrote initial get state to output file")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if gameState.ViewInBrowser {
|
if gameState.ViewInBrowser {
|
||||||
serverURL, err := boardServer.Listen()
|
serverURL, err := boardServer.Listen()
|
||||||
|
|
@ -253,13 +443,13 @@ func (gameState *GameState) Run() error {
|
||||||
}
|
}
|
||||||
defer boardServer.Shutdown()
|
defer boardServer.Shutdown()
|
||||||
log.INFO.Printf("Board server listening on %s", serverURL)
|
log.INFO.Printf("Board server listening on %s", serverURL)
|
||||||
|
// no need to open the browser automatically since our primary use case is to run this headlessly
|
||||||
|
// boardURL := fmt.Sprintf(gameState.BoardURL+"?engine=%s&game=%s&autoplay=true", serverURL, gameState.gameID)
|
||||||
|
|
||||||
boardURL := fmt.Sprintf(gameState.BoardURL+"?engine=%s&game=%s&autoplay=true", serverURL, gameState.gameID)
|
// log.INFO.Printf("Opening board URL: %s", boardURL)
|
||||||
|
// if err := browser.OpenURL(boardURL); err != nil {
|
||||||
log.INFO.Printf("Opening board URL: %s", boardURL)
|
// log.ERROR.Printf("Failed to open browser: %v", err)
|
||||||
if err := browser.OpenURL(boardURL); err != nil {
|
// }
|
||||||
log.ERROR.Printf("Failed to open browser: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// send turn zero to websocket server
|
// send turn zero to websocket server
|
||||||
boardServer.SendEvent(gameState.buildFrameEvent(boardState))
|
boardServer.SendEvent(gameState.buildFrameEvent(boardState))
|
||||||
|
|
@ -273,6 +463,7 @@ func (gameState *GameState) Run() error {
|
||||||
gameState.printState(boardState)
|
gameState.printState(boardState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bhavnoor's notes: This is straight up a lie, the default output is nothing like a valid API request.
|
||||||
// Export game first, if enabled, so that we capture the request for turn zero.
|
// Export game first, if enabled, so that we capture the request for turn zero.
|
||||||
if exportGame {
|
if exportGame {
|
||||||
// The output file was designed in a way so that (nearly) every entry is equivalent to a valid API request.
|
// The output file was designed in a way so that (nearly) every entry is equivalent to a valid API request.
|
||||||
|
|
@ -364,17 +555,11 @@ func (gameState *GameState) Run() error {
|
||||||
Data: boardGame,
|
Data: boardGame,
|
||||||
}
|
}
|
||||||
|
|
||||||
// boardServer.SendEvent(board.GameEvent{
|
|
||||||
// EventType: board.EVENT_TYPE_GAME_END,
|
|
||||||
// Data: boardGame,
|
|
||||||
// })
|
|
||||||
|
|
||||||
boardServer.SendEvent(endEvent)
|
boardServer.SendEvent(endEvent)
|
||||||
|
frames = append(frames, endEvent) // add game end to frames array
|
||||||
|
|
||||||
if gameState.outputFile != nil {
|
if gameState.outputFile != nil {
|
||||||
// write frames array to db
|
|
||||||
|
|
||||||
// insert game end into database
|
|
||||||
bytes, err := json.Marshal(endEvent)
|
bytes, err := json.Marshal(endEvent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// log.WARN.Printf("Unable to serialize game end event: %v", err)
|
// log.WARN.Printf("Unable to serialize game end event: %v", err)
|
||||||
|
|
@ -386,23 +571,36 @@ func (gameState *GameState) Run() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// // log.INFO.Printf("wrote frame event for game end")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
// the magic happens here:
|
||||||
if exportGame {
|
err = database.WriteInfo(ctx, boardGame, frames)
|
||||||
// lines, err := gameExporter.FlushToFile(gameState.outputFile)
|
if err != nil {
|
||||||
|
log.WARN.Printf("Failed to write game end event to database: %v", err)
|
||||||
lines, err := gameExporter.FlushToFile(io.Discard)
|
} else {
|
||||||
|
log.INFO.Printf("Wrote game end event to database")
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Unable to export game: %w", err)
|
|
||||||
}
|
|
||||||
log.INFO.Printf("Fake Wrote %d lines to output file: %s", lines, gameState.OutputPath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activeGameMutex.Lock()
|
||||||
|
activeGame = nil
|
||||||
|
activeGameMutex.Unlock()
|
||||||
|
|
||||||
|
// if exportGame {
|
||||||
|
// // lines, err := gameExporter.FlushToFile(gameState.outputFile)
|
||||||
|
|
||||||
|
// lines, err := gameExporter.FlushToFile(io.Discard)
|
||||||
|
|
||||||
|
// if err != nil {
|
||||||
|
// return fmt.Errorf("Unable to export game: %w", err)
|
||||||
|
// }
|
||||||
|
// log.INFO.Printf("Fake Wrote %d lines to output file: %s", lines, gameState.OutputPath)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return nil
|
||||||
|
// os.Exit(0) // Exit after successful run for testing purposes
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gameState *GameState) initializeBoardFromArgs() (bool, *rules.BoardState, error) {
|
func (gameState *GameState) initializeBoardFromArgs() (bool, *rules.BoardState, error) {
|
||||||
|
|
@ -844,10 +1042,12 @@ func (gameState *GameState) buildFrameEvent(boardState *rules.BoardState) board.
|
||||||
Data: gameFrame,
|
Data: gameFrame,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frames = append(frames, gameEvent)
|
||||||
|
|
||||||
// === New: Write frame event to output file if set ===
|
// === New: Write frame event to output file if set ===
|
||||||
if gameState.outputFile != nil {
|
if gameState.outputFile != nil {
|
||||||
// add frame event to array for now, will write to db at end of game
|
// add frame event to array for now, will write to db at end of game
|
||||||
frames = append(frames, gameEvent)
|
// frames = append(frames, gameEvent)
|
||||||
|
|
||||||
bytes, err := json.Marshal(gameEvent)
|
bytes, err := json.Marshal(gameEvent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ var rootCmd = &cobra.Command{
|
||||||
}
|
}
|
||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
|
|
||||||
rootCmd.AddCommand(NewPlayCommand())
|
rootCmd.AddCommand(NewPlayCommand())
|
||||||
|
rootCmd.AddCommand(NewHostCommand())
|
||||||
|
|
||||||
mapCommand := NewMapCommand()
|
mapCommand := NewMapCommand()
|
||||||
mapCommand.AddCommand(NewMapListCommand())
|
mapCommand.AddCommand(NewMapListCommand())
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue