Skip to content

Commit 36d0e18

Browse files
committed
Some refactoring, some better auth, some card hiding
1 parent 9291b96 commit 36d0e18

File tree

4 files changed

+153
-97
lines changed

4 files changed

+153
-97
lines changed

codenames/codenames.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,10 @@ func Targets(cards []Card, agent Agent) []Card {
142142
// board where the card Agent is only populated for revealed cards.
143143
func Revealed(b *Board) *Board {
144144
out := make([]Card, len(b.Cards))
145+
copy(out, b.Cards)
145146
for i, card := range b.Cards {
146-
out[i].Revealed = card.Revealed
147-
out[i].Codename = card.Codename
148-
out[i].RevealedBy = card.RevealedBy
149-
if card.Revealed {
150-
out[i].Agent = card.Agent
147+
if !card.Revealed {
148+
out[i].Agent = UnknownAgent
151149
}
152150
}
153151
return &Board{Cards: out}

codenames/db.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ func (p PlayerID) String() string {
3030
return string(p.PlayerType) + ":" + p.ID
3131
}
3232

33+
func (p PlayerID) IsUser(uID UserID) bool {
34+
return p.PlayerType == PlayerTypeHuman && p.ID == string(uID)
35+
}
36+
3337
type UserID string
3438
type GameID string
3539

game/game.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ func (g *Game) Play() (*Outcome, error) {
120120

121121
// TODO: If their guess is totally invalid, give them some sort of
122122
// recovery mechanism to try again?
123+
// Note from the future: For now, we assume clients send only valid
124+
// guesses.
123125
c, err := g.reveal(guess)
124126
if err != nil {
125127
return nil, fmt.Errorf("reveal(%q) on %q: %v", guess, g.activeTeam, err)
@@ -152,14 +154,17 @@ func (g *Game) Play() (*Outcome, error) {
152154

153155
func (g *Game) reveal(word string) (codenames.Card, error) {
154156
for i, card := range g.groundTruth.Cards {
155-
if strings.ToLower(card.Codename) == strings.ToLower(word) {
156-
// If the card hasn't been reveal, reveal it.
157-
if !g.groundTruth.Cards[i].Revealed {
158-
g.groundTruth.Cards[i].Revealed = true
159-
return card, nil
160-
}
157+
if strings.ToLower(card.Codename) != strings.ToLower(word) {
158+
continue
159+
}
160+
161+
if g.groundTruth.Cards[i].Revealed {
161162
return codenames.Card{}, fmt.Errorf("%q has already been guessed", word)
162163
}
164+
165+
// If the card hasn't been reveal, reveal it.
166+
g.groundTruth.Cards[i].Revealed = true
167+
return card, nil
163168
}
164169
return codenames.Card{}, fmt.Errorf("no card found for guess %q", word)
165170
}

web/web.go

Lines changed: 135 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ import (
1919
"github.com/gorilla/websocket"
2020
)
2121

22+
const (
23+
maxOperativesPerTeam = 10
24+
)
25+
2226
type Srv struct {
2327
sc *securecookie.SecureCookie
2428
hub *hub.Hub
@@ -59,18 +63,18 @@ func (s *Srv) initMux() *mux.Router {
5963
// Pending games.
6064
m.HandleFunc("/api/games", s.servePendingGames).Methods("GET")
6165
// Get game.
62-
m.HandleFunc("/api/game/{id}", s.serveGame).Methods("GET")
66+
m.HandleFunc("/api/game/{id}", s.requireGameAuth(s.serveGame)).Methods("GET")
6367
// Join game.
6468
m.HandleFunc("/api/game/{id}/join", s.serveJoinGame).Methods("POST")
6569
// Start game.
66-
m.HandleFunc("/api/game/{id}/start", s.serveStartGame).Methods("POST")
70+
m.HandleFunc("/api/game/{id}/start", s.requireGameAuth(s.serveStartGame)).Methods("POST")
6771
// Serve a clue to a game.
68-
m.HandleFunc("/api/game/{id}/clue", s.serveClue).Methods("POST")
72+
m.HandleFunc("/api/game/{id}/clue", s.requireGameAuth(s.serveClue)).Methods("POST")
6973
// Serve a card guess to a game.
70-
m.HandleFunc("/api/game/{id}/guess", s.serveGuess).Methods("POST")
74+
m.HandleFunc("/api/game/{id}/guess", s.requireGameAuth(s.serveGuess)).Methods("POST")
7175

7276
// WebSocket handler for games.
73-
m.HandleFunc("/api/game/{id}/ws", s.serveData).Methods("GET")
77+
m.HandleFunc("/api/game/{id}/ws", s.requireGameAuth(s.serveData)).Methods("GET")
7478

7579
return m
7680
}
@@ -172,19 +176,9 @@ func (s *Srv) servePendingGames(w http.ResponseWriter, r *http.Request) {
172176
jsonResp(w, gIDs)
173177
}
174178

175-
func (s *Srv) serveGame(w http.ResponseWriter, r *http.Request) {
176-
vars := mux.Vars(r)
177-
id, ok := vars["id"]
178-
if !ok {
179-
http.Error(w, "no game ID provided", http.StatusBadRequest)
180-
return
181-
}
182-
gID := codenames.GameID(id)
183-
184-
game, err := s.db.Game(gID)
185-
if err != nil {
186-
http.Error(w, "failed to load game", http.StatusInternalServerError)
187-
return
179+
func (s *Srv) serveGame(w http.ResponseWriter, r *http.Request, u *codenames.User, game *codenames.Game, userPR *codenames.PlayerRole, prs []*codenames.PlayerRole) {
180+
if userPR.Role != codenames.SpymasterRole {
181+
game.State.Board = codenames.Revealed(game.State.Board)
188182
}
189183

190184
jsonResp(w, game)
@@ -248,22 +242,37 @@ func (s *Srv) serveJoinGame(w http.ResponseWriter, r *http.Request) {
248242
http.Error(w, err.Error(), http.StatusInternalServerError)
249243
return
250244
}
251-
spymasters := make(map[codenames.Team]bool)
245+
roleCount := make(map[codenames.Role]map[codenames.Team]int)
252246
for _, pr := range prs {
253-
if pr.Role != codenames.SpymasterRole {
254-
continue
247+
rc, ok := roleCount[pr.Role]
248+
if !ok {
249+
rc = make(map[codenames.Team]int)
255250
}
256-
if spymasters[pr.Team] {
251+
if pr.PlayerID.IsUser(u.ID) {
252+
errMsg := fmt.Sprintf("can't join game as %q %q, already joined as %q %q", desiredTeam, desiredRole, pr.Team, pr.Role)
253+
http.Error(w, errMsg, http.StatusBadRequest)
254+
return
255+
}
256+
if pr.Role == codenames.SpymasterRole && rc[pr.Team] > 1 {
257257
http.Error(w, fmt.Sprintf("multiple players set as %q spymaster", pr.Team), http.StatusInternalServerError)
258258
return
259259
}
260-
spymasters[pr.Team] = true
260+
if pr.Role == codenames.OperativeRole && rc[pr.Team] > maxOperativesPerTeam {
261+
http.Error(w, fmt.Sprintf("too many players set as %q operatives", pr.Team), http.StatusInternalServerError)
262+
return
263+
}
264+
rc[pr.Team]++
265+
roleCount[pr.Role] = rc
261266
}
262267

263-
if desiredRole == codenames.SpymasterRole && spymasters[desiredTeam] {
268+
if desiredRole == codenames.SpymasterRole && roleCount[codenames.SpymasterRole][desiredTeam] > 0 {
264269
http.Error(w, fmt.Sprintf("team %q already has a spymaster", desiredTeam), http.StatusBadRequest)
265270
return
266271
}
272+
if desiredRole == codenames.OperativeRole && roleCount[codenames.OperativeRole][desiredTeam] >= maxOperativesPerTeam {
273+
http.Error(w, fmt.Sprintf("team %q already has max operatives", desiredTeam), http.StatusBadRequest)
274+
return
275+
}
267276

268277
if err := s.db.JoinGame(gID, &codenames.PlayerRole{
269278
PlayerID: codenames.PlayerID{
@@ -282,32 +291,7 @@ func (s *Srv) serveJoinGame(w http.ResponseWriter, r *http.Request) {
282291
}{true})
283292
}
284293

285-
func (s *Srv) serveStartGame(w http.ResponseWriter, r *http.Request) {
286-
vars := mux.Vars(r)
287-
id, ok := vars["id"]
288-
if !ok {
289-
http.Error(w, "no game ID provided", http.StatusBadRequest)
290-
return
291-
}
292-
gID := codenames.GameID(id)
293-
294-
u, err := s.loadUser(r)
295-
if err != nil {
296-
http.Error(w, err.Error(), http.StatusInternalServerError)
297-
return
298-
}
299-
300-
if u == nil {
301-
http.Error(w, "Not logged in", http.StatusUnauthorized)
302-
return
303-
}
304-
305-
game, err := s.db.Game(gID)
306-
if err != nil {
307-
http.Error(w, "failed to load game", http.StatusInternalServerError)
308-
return
309-
}
310-
294+
func (s *Srv) serveStartGame(w http.ResponseWriter, r *http.Request, u *codenames.User, game *codenames.Game, userPR *codenames.PlayerRole, prs []*codenames.PlayerRole) {
311295
if game.CreatedBy != u.ID {
312296
http.Error(w, "only the game creator can start the game", http.StatusForbidden)
313297
return
@@ -318,73 +302,80 @@ func (s *Srv) serveStartGame(w http.ResponseWriter, r *http.Request) {
318302
return
319303
}
320304

321-
prs, err := s.db.PlayersInGame(gID)
322-
if err != nil {
323-
http.Error(w, err.Error(), http.StatusInternalServerError)
324-
return
325-
}
326-
327305
if err := codenames.AllRolesFilled(prs); err != nil {
328306
http.Error(w, fmt.Sprintf("can't start game yet: %v", err), http.StatusBadRequest)
329307
return
330308
}
331309

332310
// If we're here, all the right roles are filled, the game is pending, and
333311
// the caller is the one who created the game, let's start it.
334-
if err := s.db.StartGame(gID); err != nil {
312+
if err := s.db.StartGame(game.ID); err != nil {
335313
http.Error(w, err.Error(), http.StatusInternalServerError)
336314
return
337315
}
338316
game.Status = codenames.Playing
339317

340-
if err := s.hub.ToGame(gID, &GameStart{
341-
Game: game,
342-
Players: prs,
343-
}); err != nil {
344-
http.Error(w, fmt.Sprintf("failed to send game start: %v", err), http.StatusInternalServerError)
345-
return
318+
// First, send the full board to the spymasters.
319+
for _, pr := range prs {
320+
if pr.Role != codenames.SpymasterRole {
321+
continue
322+
}
323+
if pr.PlayerID.PlayerType != codenames.PlayerTypeHuman {
324+
continue
325+
}
326+
327+
uID := codenames.UserID(pr.PlayerID.ID)
328+
if err := s.hub.ToUser(game.ID, uID, &GameStart{
329+
Game: game,
330+
Players: prs,
331+
}); err != nil {
332+
http.Error(w, fmt.Sprintf("failed to send game start: %v", err), http.StatusInternalServerError)
333+
return
334+
}
335+
}
336+
337+
// Now, clear out the card agent colorings and send that board to everyone
338+
// else.
339+
game.State.Board = codenames.Revealed(game.State.Board)
340+
for _, pr := range prs {
341+
if pr.Role != codenames.OperativeRole {
342+
continue
343+
}
344+
if pr.PlayerID.PlayerType != codenames.PlayerTypeHuman {
345+
continue
346+
}
347+
348+
uID := codenames.UserID(pr.PlayerID.ID)
349+
if err := s.hub.ToUser(game.ID, uID, &GameStart{
350+
Game: game,
351+
Players: prs,
352+
}); err != nil {
353+
http.Error(w, fmt.Sprintf("failed to send game start: %v", err), http.StatusInternalServerError)
354+
return
355+
}
346356
}
347357

348358
jsonResp(w, struct {
349359
Success bool `json:"success"`
350360
}{true})
351361
}
352362

353-
func (s *Srv) serveClue(w http.ResponseWriter, r *http.Request) {
363+
func (s *Srv) serveClue(w http.ResponseWriter, r *http.Request, u *codenames.User, game *codenames.Game, userPR *codenames.PlayerRole, prs []*codenames.PlayerRole) {
354364

355365
}
356366

357-
func (s *Srv) serveGuess(w http.ResponseWriter, r *http.Request) {
367+
func (s *Srv) serveGuess(w http.ResponseWriter, r *http.Request, u *codenames.User, game *codenames.Game, userPR *codenames.PlayerRole, prs []*codenames.PlayerRole) {
358368

359369
}
360370

361-
func (s *Srv) serveData(w http.ResponseWriter, r *http.Request) {
362-
vars := mux.Vars(r)
363-
id, ok := vars["id"]
364-
if !ok {
365-
http.Error(w, "no game ID provided", http.StatusBadRequest)
366-
return
367-
}
368-
gID := codenames.GameID(id)
369-
370-
u, err := s.loadUser(r)
371-
if err != nil {
372-
http.Error(w, err.Error(), http.StatusInternalServerError)
373-
return
374-
}
375-
376-
if u == nil {
377-
http.Error(w, "Not logged in", http.StatusUnauthorized)
378-
return
379-
}
380-
371+
func (s *Srv) serveData(w http.ResponseWriter, r *http.Request, u *codenames.User, game *codenames.Game, userPR *codenames.PlayerRole, prs []*codenames.PlayerRole) {
381372
conn, err := s.ws.Upgrade(w, r, nil)
382373
if err != nil {
383374
http.Error(w, err.Error(), http.StatusInternalServerError)
384375
return
385376
}
386377

387-
s.hub.Register(conn, gID, u.ID)
378+
s.hub.Register(conn, game.ID, u.ID)
388379
}
389380

390381
type jsBoard struct {
@@ -478,3 +469,61 @@ func (s *Srv) loadUser(r *http.Request) (*codenames.User, error) {
478469

479470
return u, nil
480471
}
472+
473+
type gameHandler = func(w http.ResponseWriter, r *http.Request, u *codenames.User, game *codenames.Game, userPR *codenames.PlayerRole, prs []*codenames.PlayerRole)
474+
475+
func (s *Srv) requireGameAuth(handler gameHandler) http.HandlerFunc {
476+
return func(w http.ResponseWriter, r *http.Request) {
477+
vars := mux.Vars(r)
478+
id, ok := vars["id"]
479+
if !ok {
480+
http.Error(w, "no game ID provided", http.StatusBadRequest)
481+
return
482+
}
483+
gID := codenames.GameID(id)
484+
485+
u, err := s.loadUser(r)
486+
if err != nil {
487+
http.Error(w, err.Error(), http.StatusInternalServerError)
488+
return
489+
}
490+
491+
if u == nil {
492+
http.Error(w, "Not logged in", http.StatusUnauthorized)
493+
return
494+
}
495+
496+
game, err := s.db.Game(gID)
497+
if err != nil {
498+
http.Error(w, "failed to load game", http.StatusInternalServerError)
499+
return
500+
}
501+
502+
prs, err := s.db.PlayersInGame(gID)
503+
if err != nil {
504+
http.Error(w, err.Error(), http.StatusInternalServerError)
505+
return
506+
}
507+
508+
userPR, ok := findRole(u.ID, prs)
509+
if !ok {
510+
http.Error(w, "You're not in this game", http.StatusForbidden)
511+
return
512+
}
513+
514+
handler(w, r, u, game, userPR, prs)
515+
}
516+
}
517+
518+
func findRole(uID codenames.UserID, prs []*codenames.PlayerRole) (*codenames.PlayerRole, bool) {
519+
for _, pr := range prs {
520+
if pr.PlayerID.PlayerType != codenames.PlayerTypeHuman {
521+
continue
522+
}
523+
524+
if pr.PlayerID.ID == string(uID) {
525+
return pr, true
526+
}
527+
}
528+
return nil, false
529+
}

0 commit comments

Comments
 (0)