diff --git a/README.md b/README.md index b12fe22..4725d8c 100644 --- a/README.md +++ b/README.md @@ -11,4 +11,6 @@ Currently stores no state and has no user authentication, so using it should jus POST /game GET /game/{id} GET /game/{id}/status + POST /game/{id}/move + GET /game/{id}/move/{movenum} ``` diff --git a/api.go b/api.go index 7a55036..9eee459 100644 --- a/api.go +++ b/api.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "log" "net/http" "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) { + 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].redPlayer.Ready = true respondWithJSON(res, http.StatusOK, newGameResp{a.nextInt, "red"}) a.nextInt = a.nextInt + 1 } @@ -40,7 +66,8 @@ func (a *API) GetGame(res http.ResponseWriter, req *http.Request) { var gr gameReq decoder := json.NewDecoder(req.Body) 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 } 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") return } + var p *Player + s, isset := a.games[id] if !isset { respondWithError(res, http.StatusBadRequest, "No such game") 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 } @@ -81,5 +117,96 @@ func (a *API) GetGameStatus(res http.ResponseWriter, req *http.Request) { respondWithError(res, http.StatusBadRequest, "No such game") return } + log.Println("sending game status") 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}) +} diff --git a/api_types.go b/api_types.go index 5def71c..ab159c5 100644 --- a/api_types.go +++ b/api_types.go @@ -1,6 +1,6 @@ package main -import "git.saintnet.tech/freego" +import "git.saintnet.tech/stryan/freego" //type newGameReq struct{} @@ -14,7 +14,7 @@ type gameReq struct { } type gameResp struct { - GameBoard *freego.Game `json:"board"` + GameBoard [8][8]*ViewTile `json:"board"` } type gameStatusReq struct { @@ -25,3 +25,23 @@ type gameStatusResp struct { GameStatus freego.GameState `json:"game_status"` 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"` +} diff --git a/go.mod b/go.mod index 7a4fdfe..21b461a 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,8 @@ module git.saintnet.tech/freego_api -replace git.saintnet.tech/freego => ../freego - go 1.17 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 ) diff --git a/go.sum b/go.sum index 5350288..62dddc6 100644 --- a/go.sum +++ b/go.sum @@ -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/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= diff --git a/main.go b/main.go index eff0b12..ea50141 100644 --- a/main.go +++ b/main.go @@ -14,8 +14,8 @@ func main() { router.HandleFunc("/game", api.NewGame).Methods("POST") router.HandleFunc("/game/{id}", api.GetGame).Methods("GET") router.HandleFunc("/game/{id}/status", api.GetGameStatus).Methods("GET") - router.HandleFunc("/game/{id}/move", nil).Methods("POST") - router.HandleFunc("/game/{id}/move/{movenum}", nil).Methods("GET") + router.HandleFunc("/game/{id}/move", api.PostMove).Methods("POST") + router.HandleFunc("/game/{id}/move/{movenum}", api.GetMove).Methods("GET") log.Fatal(http.ListenAndServe(":1379", router)) } diff --git a/session.go b/session.go index e156e71..5474124 100644 --- a/session.go +++ b/session.go @@ -1,21 +1,116 @@ package main -import "git.saintnet.tech/freego" +import ( + "errors" + "fmt" + "log" + + "git.saintnet.tech/stryan/freego" +) //Session represents an active game type Session struct { - simulator *freego.Game - redReady bool - blueReady bool - moveNum int + simulator *freego.Game + redPlayer *Player + bluePlayer *Player + 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 func NewSession() *Session { + sim := freego.NewGame() return &Session{ - simulator: freego.NewGame(), - redReady: false, - blueReady: false, - moveNum: 0, + simulator: sim, + redPlayer: &Player{false, freego.Red}, + bluePlayer: &Player{false, freego.Blue}, + 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 +} diff --git a/util.go b/util.go index 1528156..78a0023 100644 --- a/util.go +++ b/util.go @@ -2,7 +2,11 @@ package main import ( "encoding/json" + "errors" + "fmt" "net/http" + + "git.saintnet.tech/stryan/freego" ) 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.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() +} diff --git a/view_tile.go b/view_tile.go new file mode 100644 index 0000000..a038653 --- /dev/null +++ b/view_tile.go @@ -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{} +}