Merge branch 'master' of git.saintnet.tech:stryan/freego_api into dockerize-api

This commit is contained in:
fry 2022-03-07 20:55:49 -05:00
commit 9969775f9d
9 changed files with 337 additions and 19 deletions

View File

@ -11,4 +11,6 @@ Currently stores no state and has no user authentication, so using it should jus
POST /game POST /game
GET /game/{id} GET /game/{id}
GET /game/{id}/status GET /game/{id}/status
POST /game/{id}/move
GET /game/{id}/move/{movenum}
``` ```

133
api.go
View File

@ -2,6 +2,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"log"
"net/http" "net/http"
"strconv" "strconv"
@ -22,9 +23,34 @@ func NewAPI() *API {
} }
} }
//NewGame takes a POST and creates a new game //NewGame takes a POST and creates a new game or returns an open one
func (a *API) NewGame(res http.ResponseWriter, req *http.Request) { func (a *API) NewGame(res http.ResponseWriter, req *http.Request) {
for i, g := range a.games {
if !g.redPlayer.Ready {
log.Println("red player somehow not ready")
g.redPlayer.Ready = true
if g.bluePlayer.Ready {
g.simulator.Setup()
initDummy(g.simulator)
log.Println("dummy game started")
}
respondWithJSON(res, http.StatusOK, newGameResp{i, "red"})
return
}
if !g.bluePlayer.Ready {
g.bluePlayer.Ready = true
if g.redPlayer.Ready {
g.simulator.Setup()
initDummy(g.simulator)
log.Println("dummy game started")
}
respondWithJSON(res, http.StatusOK, newGameResp{i, "blue"})
return
}
}
log.Printf("creating new game %v", a.nextInt)
a.games[a.nextInt] = NewSession() a.games[a.nextInt] = NewSession()
a.games[a.nextInt].redPlayer.Ready = true
respondWithJSON(res, http.StatusOK, newGameResp{a.nextInt, "red"}) respondWithJSON(res, http.StatusOK, newGameResp{a.nextInt, "red"})
a.nextInt = a.nextInt + 1 a.nextInt = a.nextInt + 1
} }
@ -40,7 +66,8 @@ func (a *API) GetGame(res http.ResponseWriter, req *http.Request) {
var gr gameReq var gr gameReq
decoder := json.NewDecoder(req.Body) decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(&gr); err != nil { if err := decoder.Decode(&gr); err != nil {
respondWithError(res, http.StatusBadRequest, "Invalid resquest payload") log.Println(err)
respondWithError(res, http.StatusBadRequest, "Invalid request payload")
return return
} }
defer req.Body.Close() defer req.Body.Close()
@ -48,12 +75,21 @@ func (a *API) GetGame(res http.ResponseWriter, req *http.Request) {
respondWithError(res, http.StatusBadRequest, "Bad player ID") respondWithError(res, http.StatusBadRequest, "Bad player ID")
return return
} }
var p *Player
s, isset := a.games[id] s, isset := a.games[id]
if !isset { if !isset {
respondWithError(res, http.StatusBadRequest, "No such game") respondWithError(res, http.StatusBadRequest, "No such game")
return return
} }
respondWithJSON(res, http.StatusOK, gameResp{s.simulator}) if gr.PlayerID == "red" {
p = s.redPlayer
} else {
p = s.bluePlayer
}
log.Println("sending game state")
respondWithJSON(res, http.StatusOK, gameResp{s.getBoard(p)})
return return
} }
@ -81,5 +117,96 @@ func (a *API) GetGameStatus(res http.ResponseWriter, req *http.Request) {
respondWithError(res, http.StatusBadRequest, "No such game") respondWithError(res, http.StatusBadRequest, "No such game")
return return
} }
log.Println("sending game status")
respondWithJSON(res, http.StatusOK, gameStatusResp{s.simulator.State, s.moveNum}) respondWithJSON(res, http.StatusOK, gameStatusResp{s.simulator.State, s.moveNum})
} }
//PostMove attempts to make a game move
func (a *API) PostMove(res http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
id, err := strconv.Atoi(vars["id"])
if err != nil {
respondWithError(res, http.StatusBadRequest, "Invalid game ID")
return
}
var gr gameMovePostReq
decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(&gr); err != nil {
respondWithError(res, http.StatusBadRequest, "Invalid resquest payload")
return
}
defer req.Body.Close()
if gr.PlayerID != "red" && gr.PlayerID != "blue" {
respondWithError(res, http.StatusBadRequest, "Bad player ID")
return
}
var p *Player
s, isset := a.games[id]
if !isset {
respondWithError(res, http.StatusBadRequest, "No such game")
return
}
if gr.PlayerID == "red" {
p = s.redPlayer
} else {
p = s.bluePlayer
}
parsed, err := s.tryMove(p, gr.Move)
if err != nil {
respondWithError(res, http.StatusBadRequest, err.Error())
}
result, err := s.mutate(parsed)
if err != nil {
respondWithError(res, http.StatusBadRequest, err.Error())
}
if result == "" {
respondWithJSON(res, http.StatusOK, gameMovePostRes{true, false, "", err})
}
respondWithJSON(res, http.StatusOK, gameMovePostRes{true, true, result, err})
}
//GetMove returns the move made at turn X
func (a *API) GetMove(res http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
id, err := strconv.Atoi(vars["id"])
if err != nil {
respondWithError(res, http.StatusBadRequest, "Invalid game ID")
return
}
move, err := strconv.Atoi(vars["movenum"])
if err != nil {
respondWithError(res, http.StatusBadRequest, "Invalid move number")
return
}
var gr gameMoveReq
decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(&gr); err != nil {
respondWithError(res, http.StatusBadRequest, "Invalid resquest payload")
return
}
defer req.Body.Close()
if gr.PlayerID != "red" && gr.PlayerID != "blue" {
respondWithError(res, http.StatusBadRequest, "Bad player ID")
return
}
var p *Player
s, isset := a.games[id]
if !isset {
respondWithError(res, http.StatusBadRequest, "No such game")
return
}
if gr.PlayerID == "red" {
p = s.redPlayer
} else {
p = s.bluePlayer
}
moveRes, err := s.getMove(p, move)
if err != nil {
respondWithError(res, http.StatusBadRequest, "No such move")
return
}
respondWithJSON(res, http.StatusOK, gameMoveRes{moveRes})
}

View File

@ -1,6 +1,6 @@
package main package main
import "git.saintnet.tech/freego" import "git.saintnet.tech/stryan/freego"
//type newGameReq struct{} //type newGameReq struct{}
@ -14,7 +14,7 @@ type gameReq struct {
} }
type gameResp struct { type gameResp struct {
GameBoard *freego.Game `json:"board"` GameBoard [8][8]*ViewTile `json:"board"`
} }
type gameStatusReq struct { type gameStatusReq struct {
@ -25,3 +25,23 @@ type gameStatusResp struct {
GameStatus freego.GameState `json:"game_status"` GameStatus freego.GameState `json:"game_status"`
Move int `json:"move"` Move int `json:"move"`
} }
type gameMovePostReq struct {
PlayerID string `json:"player_id"`
Move string `json:"move"`
}
type gameMovePostRes struct {
Valid bool `json:"valid"`
Result bool `json:"result"`
Parsed string `json:"parsed"`
Error error `json:"error"`
}
type gameMoveReq struct {
PlayerID string `json:"player_id"`
}
type gameMoveRes struct {
Move string `json:"move"`
}

4
go.mod
View File

@ -1,10 +1,8 @@
module git.saintnet.tech/freego_api module git.saintnet.tech/freego_api
replace git.saintnet.tech/freego => ../freego
go 1.17 go 1.17
require ( require (
git.saintnet.tech/freego v0.0.0-00010101000000-000000000000 // indirect git.saintnet.tech/stryan/freego v0.0.0-20220307210942-38a41634101a // indirect
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.0 // indirect
) )

12
go.sum
View File

@ -1,2 +1,14 @@
git.saintnet.tech/stryan/freego v0.0.0-20220307180035-64bab97c38d6 h1:0GzkfU8R4Rj7SVxTpkOf9oZN4YpW2xe/IjYbNP5Zxjs=
git.saintnet.tech/stryan/freego v0.0.0-20220307180035-64bab97c38d6/go.mod h1:NXXisQVSPklkvs2Qg6Iv3LqXNaJwwEtho/2WmzhvAZc=
git.saintnet.tech/stryan/freego v0.0.0-20220307194514-b5fc90339454 h1:KK8YuhJYMjbmyREbCDX+DshFaarrzxhjRTyPDIDhzDs=
git.saintnet.tech/stryan/freego v0.0.0-20220307194514-b5fc90339454/go.mod h1:NXXisQVSPklkvs2Qg6Iv3LqXNaJwwEtho/2WmzhvAZc=
git.saintnet.tech/stryan/freego v0.0.0-20220307202314-dd8dce886942 h1:fgVoPchS1ETLuPKMtu4pO6zU5JRqXp+O4iy49uIaxhE=
git.saintnet.tech/stryan/freego v0.0.0-20220307202314-dd8dce886942/go.mod h1:NXXisQVSPklkvs2Qg6Iv3LqXNaJwwEtho/2WmzhvAZc=
git.saintnet.tech/stryan/freego v0.0.0-20220307205125-093ef4caf221 h1:OfBW5x9SAjpk+dGRbdmw0qmh8Ru3eJl6wjDTD4QwxAU=
git.saintnet.tech/stryan/freego v0.0.0-20220307205125-093ef4caf221/go.mod h1:NXXisQVSPklkvs2Qg6Iv3LqXNaJwwEtho/2WmzhvAZc=
git.saintnet.tech/stryan/freego v0.0.0-20220307205900-a33244481774 h1:V5V4/SEX3HjeilyO7gi9fp7DpevkDIrKPukdgLOMEXw=
git.saintnet.tech/stryan/freego v0.0.0-20220307205900-a33244481774/go.mod h1:NXXisQVSPklkvs2Qg6Iv3LqXNaJwwEtho/2WmzhvAZc=
git.saintnet.tech/stryan/freego v0.0.0-20220307210942-38a41634101a h1:y/UwFZN0yZS0OYcdXnjvbdiwY4ZKIi8+K8x2xRwJ1L8=
git.saintnet.tech/stryan/freego v0.0.0-20220307210942-38a41634101a/go.mod h1:NXXisQVSPklkvs2Qg6Iv3LqXNaJwwEtho/2WmzhvAZc=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=

View File

@ -14,8 +14,8 @@ func main() {
router.HandleFunc("/game", api.NewGame).Methods("POST") router.HandleFunc("/game", api.NewGame).Methods("POST")
router.HandleFunc("/game/{id}", api.GetGame).Methods("GET") router.HandleFunc("/game/{id}", api.GetGame).Methods("GET")
router.HandleFunc("/game/{id}/status", api.GetGameStatus).Methods("GET") router.HandleFunc("/game/{id}/status", api.GetGameStatus).Methods("GET")
router.HandleFunc("/game/{id}/move", nil).Methods("POST") router.HandleFunc("/game/{id}/move", api.PostMove).Methods("POST")
router.HandleFunc("/game/{id}/move/{movenum}", nil).Methods("GET") router.HandleFunc("/game/{id}/move/{movenum}", api.GetMove).Methods("GET")
log.Fatal(http.ListenAndServe(":1379", router)) log.Fatal(http.ListenAndServe(":1379", router))
} }

View File

@ -1,21 +1,116 @@
package main package main
import "git.saintnet.tech/freego" import (
"errors"
"fmt"
"log"
"git.saintnet.tech/stryan/freego"
)
//Session represents an active game //Session represents an active game
type Session struct { type Session struct {
simulator *freego.Game simulator *freego.Game
redReady bool redPlayer *Player
blueReady bool bluePlayer *Player
moveNum int moveNum int
moveList []freego.ParsedCommand
}
//Player is a player in a match
type Player struct {
Ready bool
Team freego.Colour
}
//ID returns player ID
func (p *Player) ID() int {
panic("not implemented") // TODO: Implement
}
//Colour returns player team
func (p *Player) Colour() freego.Colour {
return p.Team
} }
//NewSession creates a new game session //NewSession creates a new game session
func NewSession() *Session { func NewSession() *Session {
sim := freego.NewGame()
return &Session{ return &Session{
simulator: freego.NewGame(), simulator: sim,
redReady: false, redPlayer: &Player{false, freego.Red},
blueReady: false, bluePlayer: &Player{false, freego.Blue},
moveNum: 0, moveNum: 1,
moveList: make([]freego.ParsedCommand, 20),
} }
} }
func (s *Session) tryMove(player *Player, move string) (*freego.ParsedCommand, error) {
raw, err := freego.NewRawCommand(move)
if err != nil {
return nil, err
}
p, err := s.simulator.Parse(player, raw)
if err != nil {
return nil, err
}
return p, nil
}
func (s *Session) mutate(p *freego.ParsedCommand) (string, error) {
success, err := s.simulator.Mutate(p)
if err != nil {
return "", err
}
if success {
s.moveList[s.moveNum] = *p
s.moveNum++
return fmt.Sprintf("%v %v", s.moveNum-1, p.String()), nil
}
return "", nil
}
func (s *Session) getMove(p *Player, num int) (string, error) {
if num <= 0 || num >= s.moveNum {
log.Printf("tried to get move number %v when move is %v", num, s.moveNum)
return "", errors.New("invalid move number")
}
return fmt.Sprintf("%v %v", num, s.moveList[num].String()), nil
}
func (s *Session) getBoard(p *Player) [8][8]*ViewTile {
var res [8][8]*ViewTile
for i := 0; i < 8; i++ {
for j := 0; j < 8; j++ {
cur := NewViewTile()
terrain, err := s.simulator.Board.IsTerrain(i, j)
if err != nil {
panic(err)
}
if terrain {
cur.Terrain = true
} else {
piece, err := s.simulator.Board.GetPiece(i, j)
if err != nil {
panic(err)
}
if piece != nil {
if piece.Hidden {
cur.Hidden = true
if piece.Owner == p.Colour() {
cur.Piece = piece.Rank.String()
} else {
cur.Piece = "Unknown"
}
} else {
cur.Piece = piece.Rank.String()
}
} else {
cur.Empty = true
}
}
res[i][j] = cur
}
}
return res
}

50
util.go
View File

@ -2,7 +2,11 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt"
"net/http" "net/http"
"git.saintnet.tech/stryan/freego"
) )
func respondWithError(res http.ResponseWriter, code int, message string) { func respondWithError(res http.ResponseWriter, code int, message string) {
@ -16,3 +20,49 @@ func respondWithJSON(res http.ResponseWriter, code int, payload interface{}) {
res.WriteHeader(code) res.WriteHeader(code)
res.Write(response) res.Write(response)
} }
//TODO remove this when you can actually setup a game
func initDummy(g *freego.Game) {
//Setup terrain
terrain := []struct {
x, y, t int
}{
{1, 1, 1},
{2, 2, 1},
}
for _, tt := range terrain {
res, err := g.Board.AddTerrain(tt.x, tt.y, tt.t)
if err != nil {
panic(err)
}
if !res {
panic(errors.New("Error creating terrain"))
}
}
pieces := []struct {
x, y int
p *freego.Piece
}{
{0, 0, freego.NewPiece(freego.Flag, freego.Blue)},
{3, 0, freego.NewPiece(freego.Spy, freego.Blue)},
{2, 0, freego.NewPiece(freego.Captain, freego.Blue)},
{3, 1, freego.NewPiece(freego.Marshal, freego.Blue)},
{0, 1, freego.NewPiece(freego.Bomb, freego.Blue)},
{1, 6, freego.NewPiece(freego.Flag, freego.Red)},
{3, 6, freego.NewPiece(freego.Spy, freego.Red)},
{2, 7, freego.NewPiece(freego.Captain, freego.Red)},
{0, 6, freego.NewPiece(freego.Marshal, freego.Red)},
{0, 7, freego.NewPiece(freego.Bomb, freego.Red)},
}
for _, tt := range pieces {
res, err := g.SetupPiece(tt.x, tt.y, tt.p)
if err != nil {
panic(fmt.Errorf("Piece %v,%v:%v", tt.x, tt.y, err))
}
if !res {
panic(errors.New("error placing dummy piece"))
}
}
g.Start()
}

14
view_tile.go Normal file
View File

@ -0,0 +1,14 @@
package main
//ViewTile is a json friendly version of a tile
type ViewTile struct {
Piece string `json:"piece"`
Terrain bool `json:"terrain"`
Hidden bool `json:"hidden"`
Empty bool `json:"empty"`
}
//NewViewTile creates a new ViewTile
func NewViewTile() *ViewTile {
return &ViewTile{}
}