Skip to content

Commit

Permalink
update paths
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisfregly committed Apr 4, 2024
1 parent 4900068 commit c93c18b
Show file tree
Hide file tree
Showing 11 changed files with 62 additions and 100 deletions.
47 changes: 17 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,23 @@ There are three main processes in this system.
## Flows

### Game Creation
- Send `POST https://<host>/create` with some `qqn` such as `[key "tictactoe"][id "example"][teams "red, blue"]`.
- Send `POST https://<host>/game` with some `qqn` such as `[key "tictactoe"][id "example"][teams "red, blue"]`.
- `controller` processes the request and, if valid, creates K8s ConfigMap, Pod, Service, and Ingress resources.
- Game can now be accessed at `https://<host>/tictactoe/example`.
- Game can now be accessed at `https://<host>/game/tictactoe/example`.

### Game Connection
- Join a game by connection to `wss://<host>/tictactoe/example/connect` with websockets.
- Join a game by connection to `wss://<host>/game/tictactoe/example` with websockets.
- Connection should be open to a `server` instance and relevant game messages should be recieved.

### Game Cleanup
- `watcher` will kick off every `X` timeperiod.
- Job requests data from all live games by calling `GET https://<host>/<key>/<id>/active` for each game.
- Job requests data from all live games by calling `GET https://<host>/game/<key>/<id>/activity` for each game.
- If there are no connected players and no recent updates then all K8s related resources are deleted.

## REST API

<details>
<summary><code>POST</code> <code><b>/create</b></code> <code>(create a game)</code></summary>
<summary><code>POST</code> <code><b>/game</b></code> <code>(create a game)</code></summary>

##### Parameters

Expand All @@ -77,12 +77,12 @@ There are three main processes in this system.
##### Example cURL

> ```javascript
> curl -X POST -H "Content-Type: application/qgn" --data @post.qgn https://api.quibbble.com/create
> curl -X POST -H "Content-Type: application/qgn" --data @post.qgn https://api.quibbble.com/game
> ```
</details>
<details>
<summary><code>DELETE</code> <code><b>/delete?key={key}&id={id}</b></code> <code>(delete a game)</code></summary>
<summary><code>DELETE</code> <code><b>/game?key={key}&id={id}</b></code> <code>(delete a game)</code></summary>
##### Parameters
Expand All @@ -103,12 +103,12 @@ There are three main processes in this system.
##### Example cURL
> ```javascript
> curl -X DELETE https://api.quibbble.com/delete?key={key}&id={id}
> curl -X DELETE https://api.quibbble.com/game?key={key}&id={id}
> ```
</details>
<details>
<summary><code>WEBSOCKET</code> <code><b>/{key}/{id}/connect</b></code> <code>(connect to a game)</code></summary>
<summary><code>WEBSOCKET</code> <code><b>/game/{key}/{id}</b></code> <code>(connect to a game)</code></summary>
##### Parameters
Expand All @@ -125,12 +125,12 @@ There are three main processes in this system.
##### Example wscat
> ```javascript
> wscat -c wss://api.quibbble.com/{key}/{id}/connect
> wscat -c wss://api.quibbble.com/game/{key}/{id}
> ```
</details>
<details>
<summary><code>GET</code> <code><b>/{key}/{id}/snapshot?format={format}</b></code> <code>(get game snapshot)</code></summary>
<summary><code>GET</code> <code><b>/game/{key}/{id}/snapshot?format={format}</b></code> <code>(get game snapshot)</code></summary>
##### Parameters
Expand All @@ -153,12 +153,12 @@ There are three main processes in this system.
##### Example cURL
> ```javascript
> curl -X GET https://api.quibbble.com/{key}/{id}/snapshot?format=json
> curl -X GET https://api.quibbble.com/game/{key}/{id}/snapshot?format=json
> ```
</details>
<details>
<summary><code>GET</code> <code><b>/stats</b></code> <code>(get all game stats)</code></summary>
<summary><code>GET</code> <code><b>/game/activity</b></code> <code>(get all games activity)</code></summary>
##### Parameters
Expand All @@ -169,18 +169,18 @@ There are three main processes in this system.
> | http code | content-type | response |
> |---------------|-----------------------------------------|---------------------------------------------------------------------|
> | `200` | `application/json` | Stats for all games |
> | `200` | `application/json` | Activity for all games |
> | `500` | `text/plain;charset=UTF-8` | `Internal Server Error` |
##### Example cURL
> ```javascript
> curl -X GET https://api.quibbble.com/stats
> curl -X GET https://api.quibbble.com/game/activity
> ```
</details>
<details>
<summary><code>GET</code> <code><b>/{key}/{id}/active</b></code> <code>(get game activity)</code></summary>
<summary><code>GET</code> <code><b>/game/{key}/{id}/activity</b></code> <code>(get game activity)</code></summary>
##### Parameters
Expand All @@ -200,7 +200,7 @@ There are three main processes in this system.
##### Example cURL
> ```javascript
> curl -X GET https://api.quibbble.com/{key}/{id}/active
> curl -X GET https://api.quibbble.com/game/{key}/{id}/activity
> ```
</details>
Expand All @@ -209,19 +209,6 @@ There are three main processes in this system.
### Sendable Messages
<details>
<summary><code><b>join</b></code> <code>(join a team)</code></summary>
##### Message
```json
{
"type": "join",
"details": "$TEAM"
}
```
</details>
<details>
<summary><code><b>action</b></code> <code>(perform a game action)</code></summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type Stats struct {
type Activity struct {
LiveGameCount map[string]int `json:"live_game_count"`
LivePlayerCount map[string]int `json:"live_player_count"`
}

func (c *Controller) stats() (*Stats, error) {
stats := Stats{
func (c *Controller) activity() (*Activity, error) {
stats := Activity{
LiveGameCount: make(map[string]int),
LivePlayerCount: make(map[string]int),
}
Expand Down Expand Up @@ -53,7 +53,7 @@ func (c *Controller) stats() (*Stats, error) {
if err != nil {
return nil, err
}
var active qs.Active
var active qs.Activity
if err := json.Unmarshal(body, &active); err != nil {
return nil, err
}
Expand Down
8 changes: 4 additions & 4 deletions internal/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ func NewController(config *GameServerConfig, clientset *kubernetes.Clientset, st
config: config,
allowOrigins: allowOrigins,
}
c.mux.HandleFunc("/create", c.createHandler)
c.mux.HandleFunc("/delete", c.deleteHandler)
c.mux.HandleFunc("/stats", c.statsHandler)
c.mux.HandleFunc("/health", healthHandler)
c.mux.HandleFunc("POST /game", c.createHandler)
c.mux.HandleFunc("DELETE /game", c.deleteHandler)
c.mux.HandleFunc("GET /game/activity", c.activityHandler)
c.mux.HandleFunc("GET /health", healthHandler)
return c
}

Expand Down
10 changes: 5 additions & 5 deletions internal/controller/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ func (c *Controller) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (c *Controller) createHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
panic(err)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
snapshot, err := qgn.Parse(string(body))
if err != nil {
w.Write([]byte(err.Error()))
w.WriteHeader(http.StatusBadRequest)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}

Expand Down Expand Up @@ -81,8 +81,8 @@ func (c *Controller) deleteHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(http.StatusText(http.StatusOK)))
}

func (c *Controller) statsHandler(w http.ResponseWriter, r *http.Request) {
stats, err := c.stats()
func (c *Controller) activityHandler(w http.ResponseWriter, r *http.Request) {
stats, err := c.activity()
if err != nil {
log.Println(err.Error())
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
Expand Down
4 changes: 2 additions & 2 deletions internal/server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,13 @@ func (gs *GameServer) snapshotHandler(w http.ResponseWriter, r *http.Request) {
}
}

type Active struct {
type Activity struct {
PlayerCount int `json:"player_count"`
LastUpdated time.Time `json:"last_updated"`
}

func (gs *GameServer) activeHandler(w http.ResponseWriter, r *http.Request) {
raw, _ := json.Marshal(Active{
raw, _ := json.Marshal(Activity{
PlayerCount: len(gs.connected),
LastUpdated: gs.lastUpdated,
})
Expand Down
22 changes: 8 additions & 14 deletions internal/server/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ type Message struct {

func (gs *GameServer) sendSnapshotMessage(player *Player) {
snapshot, _ := gs.game.GetSnapshotJSON()
if player.team != nil {
snapshot, _ = gs.GetSnapshotJSON(*player.team)
if team := gs.team(player.uid); team != nil {
snapshot, _ = gs.GetSnapshotJSON(*team)
}
payload, _ := json.Marshal(Message{
Type: SnapshotMessage,
Expand All @@ -53,22 +53,18 @@ func (gs *GameServer) sendSnapshotMessages() {
}

func (gs *GameServer) sendConnectionMessages() {
teams := make(map[string]*string)
usernames := make(map[string]string)
for player := range gs.connected {
teams[player.uid] = player.team
usernames[player.uid] = player.username
}
for p := range gs.connected {
payload, _ := json.Marshal(Message{
Type: ConnectionMessage,
Details: struct {
UID string `json:"uid"`
Teams map[string]*string `json:"teams"`
Usernames map[string]string `json:"usernames"`
Players map[string][]string `json:"players"`
Usernames map[string]string `json:"usernames"`
}{
UID: p.uid,
Teams: teams,
Players: gs.players,
Usernames: usernames,
},
})
Expand All @@ -80,14 +76,12 @@ func (gs *GameServer) sendChatMessages(player *Player, message string) {
payload, _ := json.Marshal(Message{
Type: ChatMessage,
Details: struct {
UID string `json:"uid"`
Username string `json:"username"`
Team *string `json:"team"`
Message string `json:"message"`
UID string `json:"uid"`
Username string `json:"username"`
Message string `json:"message"`
}{
UID: player.uid,
Username: player.username,
Team: player.team,
Message: message,
},
})
Expand Down
4 changes: 0 additions & 4 deletions internal/server/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ type Player struct {
// username is the name displayed to others.
username string

// team represents the team the player joined.
team *string

// messageCh provides a channel the game server use to
// send messages to the player.
messageCh chan []byte
Expand All @@ -59,7 +56,6 @@ func NewPlayer(uid, username string, conn *websocket.Conn, actionCh chan *Action
return &Player{
uid: uid,
username: username,
team: nil,
messageCh: make(chan []byte, playerMessageBuffer),
actionCh: actionCh,
conn: conn,
Expand Down
46 changes: 15 additions & 31 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ func NewGameServer(game qg.Game, id, typ string, players map[string][]string, co
completeFn: completeFn,
}
go gs.Start()
gs.mux.Handle("/connect", authenticate(http.HandlerFunc(gs.connectHandler)))
gs.mux.HandleFunc("/snapshot", gs.snapshotHandler)
gs.mux.HandleFunc("/active", gs.activeHandler)
gs.mux.HandleFunc("/health", healthHandler)
// these will be prefixed by /game/{key}/{id} when being called through nginx
gs.mux.Handle("GET /", authenticate(http.HandlerFunc(gs.connectHandler)))
gs.mux.HandleFunc("GET /snapshot", gs.snapshotHandler)
gs.mux.HandleFunc("GET /activity", gs.activeHandler)
gs.mux.HandleFunc("GET /health", healthHandler)
return gs
}

Expand All @@ -77,9 +78,6 @@ func (gs *GameServer) Start() {
select {
case p := <-gs.joinCh:
gs.connected[p] = struct{}{}
if gs.typ == qgn.AIType || gs.typ == qgn.MultiplayerType {
p.team = team(gs.players, p.uid)
}
gs.sendConnectionMessages()
gs.sendSnapshotMessage(p)
case p := <-gs.leaveCh:
Expand All @@ -88,25 +86,6 @@ func (gs *GameServer) Start() {
gs.sendConnectionMessages()
case a := <-gs.actionCh:
switch a.Type {
case Join:
if gs.typ == qgn.AIType || gs.typ == qgn.MultiplayerType {
gs.sendErrorMessage(a.Player, fmt.Errorf("join action disabled for this game type"))
continue
}
snapshot, err := gs.game.GetSnapshotJSON()
if err != nil {
gs.sendErrorMessage(a.Player, err)
continue
}
team, ok := a.Details.(string)
if !ok || !slices.Contains(snapshot.Teams, team) {
gs.sendMessage(a.Player, ErrInvalidActionMessage)
continue
}
a.Player.team = &team
gs.sendConnectionMessages()
gs.sendSnapshotMessage(a.Player)
continue
case Chat:
message, ok := a.Details.(string)
if !ok {
Expand All @@ -116,18 +95,19 @@ func (gs *GameServer) Start() {
gs.sendChatMessages(a.Player, message)
continue
default:
if a.Player.team == nil {
team := gs.team(a.uid)
if team == nil {
gs.sendErrorMessage(a.Player, fmt.Errorf("not part of a team"))
continue
}
a.Action.Team = *a.Player.team
a.Action.Team = *team
if err := gs.game.Do(a.Action); err != nil {
gs.sendErrorMessage(a.Player, err)
continue
}
gs.sendSnapshotMessages()

if snapshot, err := gs.game.GetSnapshotJSON(); err == nil && len(snapshot.Winners) > 0 {
if snapshot, err := gs.GetSnapshotJSON(); err == nil && len(snapshot.Winners) > 0 {
gs.completeFn(gs.game)
}
}
Expand Down Expand Up @@ -161,13 +141,17 @@ func (gs *GameServer) GetSnapshotQGN() (*qgn.Snapshot, error) {
return snapshot, nil
}

func team(players map[string][]string, uid string) *string {
func (gs *GameServer) team(uid string) *string {
var team *string
for t, players := range players {
for t, players := range gs.players {
if slices.Contains(players, uid) {
team = &t
break
}
}
if team != nil && gs.typ == qgn.LocalType {
snapshot, _ := gs.GetSnapshotJSON()
team = &snapshot.Turn
}
return team
}
Loading

0 comments on commit c93c18b

Please sign in to comment.