Skip to content

Commit 6d264cd

Browse files
committedApr 10, 2021
Refactor game package, implement guessing and clue giving
At this point, the game is mostly implemented, minus a few missing websockets things we need to send down, and some documentation, and the whole frontend.
1 parent 36d0e18 commit 6d264cd

File tree

11 files changed

+391
-95
lines changed

11 files changed

+391
-95
lines changed
 

‎cmd/codenames-local/main.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,7 @@ func main() {
8787
cards[i] = c
8888
}
8989
b := &codenames.Board{Cards: cards}
90-
g, err := game.New(b, &game.Config{
91-
Starter: teamMap[*starter],
90+
g, err := game.New(b, teamMap[*starter], &game.Config{
9291
RedSpymaster: rsm,
9392
BlueSpymaster: bsm,
9493
RedOperative: rop,

‎cmd/codenames-local/run.sh

-6
This file was deleted.

‎cmd/codenames-local/run_ai.sh

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/bin/bash
2+
go run github.com/bcspragu/Codenames/cmd/codenames-local \
3+
--model_file="../../data/everything.bin" \
4+
--words="$(go run github.com/bcspragu/Codenames/cmd/boardgen-cli)" \
5+
--use_ai=true

‎cmd/codenames-local/run_local.sh

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
go run github.com/bcspragu/Codenames/cmd/codenames-local \
2+
--words="$(go run github.com/bcspragu/Codenames/cmd/boardgen-cli)" \
3+
--use_ai=false

‎codenames/db.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,11 @@ type Game struct {
8585
}
8686

8787
type GameState struct {
88-
ActiveTeam Team `json:"active_team"`
89-
ActiveRole Role `json:"active_role"`
90-
Board *Board `json:"board"`
88+
ActiveTeam Team `json:"active_team"`
89+
ActiveRole Role `json:"active_role"`
90+
Board *Board `json:"board"`
91+
NumGuessesLeft int `json:"num_guesses_left"`
92+
StartingTeam Team `json:"starting_team"`
9193
}
9294

9395
type PlayerRole struct {

‎consensus/consensus.go

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package consensus
2+
3+
import (
4+
"sync"
5+
6+
"github.com/bcspragu/Codenames/codenames"
7+
)
8+
9+
func New() *Guesser {
10+
return &Guesser{}
11+
}
12+
13+
type Vote struct {
14+
UserID codenames.UserID
15+
Word string
16+
}
17+
18+
type Guesser struct {
19+
mu sync.Mutex
20+
guesses map[codenames.GameID][]*Vote
21+
}
22+
23+
func (g *Guesser) RecordVote(gID codenames.GameID, uID codenames.UserID, word string) {
24+
g.mu.Lock()
25+
defer g.mu.Unlock()
26+
27+
for _, vote := range g.guesses[gID] {
28+
if vote.UserID == uID {
29+
// Update an existing player's vote.
30+
vote.Word = word
31+
return
32+
}
33+
}
34+
35+
// If we're here, this is a new vote.
36+
g.guesses[gID] = append(g.guesses[gID], &Vote{
37+
UserID: uID,
38+
Word: word,
39+
})
40+
}
41+
42+
func (g *Guesser) ReachedConsensus(gID codenames.GameID, totalVoters int) (string, bool) {
43+
g.mu.Lock()
44+
defer g.mu.Unlock()
45+
46+
votes := make(map[string]int)
47+
for _, vote := range g.guesses[gID] {
48+
votes[vote.Word]++
49+
}
50+
51+
// We require a strict majority, meaning > 50%. E.g.
52+
// totalVoters == 2, majority == 2
53+
// totalVoters == 3, majority == 2
54+
// totalVoters == 4, majority == 3
55+
// totalVoters == 5, majority == 3
56+
// totalVoters == 6, majority == 4
57+
majority := totalVoters/2 + 1
58+
for word, cnt := range votes {
59+
if cnt >= majority {
60+
return word, true
61+
}
62+
}
63+
64+
return "", false
65+
}
66+
67+
func (g *Guesser) Clear(gID codenames.GameID) {
68+
g.mu.Lock()
69+
defer g.mu.Unlock()
70+
71+
delete(g.guesses, gID)
72+
}

‎game/game.go

+148-67
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,41 @@
11
package game
22

33
import (
4+
"errors"
45
"fmt"
5-
"log"
66
"strings"
77

88
"github.com/bcspragu/Codenames/codenames"
99
)
1010

11-
// Game represents a game of codenames.
11+
// Game represents a game of codenames. It supports two modes of operation:
12+
// - Play() mode: Plays the whole game out at once. It expects that all of the
13+
// roles in the config have been configured, and everything set up.
14+
// - Move() mode: Plays out a single move, through the Move() function. Will
15+
// reject actions that are requested out of turn, or where callers aren't
16+
// configured for it. This means a *Game can be partially constructed with only
17+
// the information necessary for a given move.
1218
type Game struct {
13-
groundTruth *codenames.Board
14-
cfg *Config
15-
activeTeam codenames.Team
19+
state *codenames.GameState
20+
cfg *Config
1621
}
1722

1823
// Config holds configuration options for a game of Codenames.
1924
type Config struct {
20-
// Starter is the team that goes first.
21-
Starter codenames.Team
22-
2325
RedSpymaster codenames.Spymaster
2426
BlueSpymaster codenames.Spymaster
2527

2628
RedOperative codenames.Operative
2729
BlueOperative codenames.Operative
2830
}
2931

32+
func NewForMove(state *codenames.GameState) *Game {
33+
return &Game{state: state}
34+
}
35+
3036
// New validates and initializes a game of Codenames.
31-
func New(b *codenames.Board, cfg *Config) (*Game, error) {
32-
if err := validateBoard(b, cfg.Starter); err != nil {
37+
func New(b *codenames.Board, startingTeam codenames.Team, cfg *Config) (*Game, error) {
38+
if err := validateBoard(b, startingTeam); err != nil {
3339
return nil, fmt.Errorf("invalid board given: %v", err)
3440
}
3541

@@ -47,9 +53,13 @@ func New(b *codenames.Board, cfg *Config) (*Game, error) {
4753
}
4854

4955
return &Game{
50-
groundTruth: b,
51-
cfg: cfg,
52-
activeTeam: cfg.Starter,
56+
state: &codenames.GameState{
57+
StartingTeam: startingTeam,
58+
ActiveTeam: startingTeam,
59+
ActiveRole: codenames.SpymasterRole,
60+
Board: b,
61+
},
62+
cfg: cfg,
5363
}, nil
5464
}
5565

@@ -93,102 +103,172 @@ type Outcome struct {
93103
// team, if anyone hit the assassin, etc.
94104
}
95105

106+
type Action string
107+
108+
const (
109+
ActionGiveClue = Action("GIVE_CLUE")
110+
ActionGuess = Action("GUESS")
111+
)
112+
113+
type Move struct {
114+
Team codenames.Team
115+
116+
Action Action
117+
// Only populated for Action == ActionGiveClue
118+
GiveClue *codenames.Clue
119+
// Only populated for Action == ActionGuess
120+
Guess string
121+
}
122+
123+
func (g *Game) Move(mv *Move) (*codenames.GameState, codenames.GameStatus, error) {
124+
switch mv.Action {
125+
case ActionGiveClue:
126+
if g.state.ActiveRole != codenames.SpymasterRole {
127+
return nil, "", fmt.Errorf("can't give a clue when %q %q should be acting", g.state.ActiveTeam, g.state.ActiveRole)
128+
}
129+
if mv.GiveClue == nil {
130+
return nil, "", errors.New("no clue was given")
131+
}
132+
g.handleGiveClue(mv.GiveClue)
133+
case ActionGuess:
134+
if g.state.ActiveRole != codenames.OperativeRole {
135+
return nil, "", fmt.Errorf("can't guess when %q %q should be acting", g.state.ActiveTeam, g.state.ActiveRole)
136+
}
137+
if mv.Guess == "" {
138+
// This is passing.
139+
g.endTurn()
140+
} else {
141+
if err := g.handleGuess(mv.Guess); err != nil {
142+
return nil, "", fmt.Errorf("handleGuess(%q): %w", mv.Guess, err)
143+
}
144+
}
145+
default:
146+
return nil, "", fmt.Errorf("unknown action %q", mv.Action)
147+
}
148+
149+
state := codenames.Pending
150+
if over, _ := g.gameOver(); over {
151+
state = codenames.Finished
152+
}
153+
154+
return g.state, state, nil
155+
}
156+
157+
func (g *Game) handleGiveClue(clue *codenames.Clue) {
158+
numGuesses := clue.Count
159+
if numGuesses == 0 {
160+
numGuesses = -1
161+
}
162+
163+
g.state.NumGuessesLeft = numGuesses
164+
g.state.ActiveRole = codenames.OperativeRole
165+
}
166+
167+
func (g *Game) handleGuess(guess string) error {
168+
g.state.NumGuessesLeft--
169+
170+
c, err := g.reveal(guess)
171+
if err != nil {
172+
return fmt.Errorf("reveal(%q) on %q: %v", guess, g.state.ActiveTeam, err)
173+
}
174+
175+
// Check if their guess ended the game.
176+
if over, _ := g.gameOver(); over {
177+
return nil
178+
}
179+
180+
if !g.canKeepGuessing(c) {
181+
g.endTurn()
182+
}
183+
184+
return nil
185+
}
186+
187+
func (g *Game) endTurn() {
188+
curTeam := g.state.ActiveTeam
189+
if curTeam == codenames.BlueTeam {
190+
curTeam = codenames.RedTeam
191+
} else {
192+
curTeam = codenames.BlueTeam
193+
}
194+
g.state.NumGuessesLeft = 0
195+
g.state.ActiveTeam = curTeam
196+
g.state.ActiveRole = codenames.SpymasterRole
197+
}
198+
96199
func (g *Game) Play() (*Outcome, error) {
97200
for {
98201
// Let's play a round.
99202
sm, op := g.cfg.RedSpymaster, g.cfg.RedOperative
100-
if g.activeTeam == codenames.BlueTeam {
203+
if g.state.ActiveTeam == codenames.BlueTeam {
101204
sm, op = g.cfg.BlueSpymaster, g.cfg.BlueOperative
102205
}
103206

104-
clue, err := sm.GiveClue(codenames.CloneBoard(g.groundTruth))
207+
clue, err := sm.GiveClue(codenames.CloneBoard(g.state.Board))
105208
if err != nil {
106-
return nil, fmt.Errorf("GiveClue on %q: %v", g.activeTeam, err)
209+
return nil, fmt.Errorf("GiveClue on %q: %w", g.state.ActiveTeam, err)
107210
}
108-
numGuesses := clue.Count
109-
if numGuesses == 0 {
110-
numGuesses = -1
211+
if _, _, err := g.Move(&Move{
212+
Action: ActionGiveClue,
213+
Team: g.state.ActiveTeam,
214+
GiveClue: clue,
215+
}); err != nil {
216+
return nil, fmt.Errorf("error giving clue: %w", err)
111217
}
112218

113-
for {
114-
log.Println(numGuesses)
115-
guess, err := op.Guess(codenames.Revealed(g.groundTruth), clue)
116-
if err != nil {
117-
return nil, fmt.Errorf("Guess on %q: %v", g.activeTeam, err)
118-
}
119-
numGuesses--
120-
121-
// TODO: If their guess is totally invalid, give them some sort of
122-
// recovery mechanism to try again?
123-
// Note from the future: For now, we assume clients send only valid
124-
// guesses.
125-
c, err := g.reveal(guess)
219+
for g.state.ActiveRole == codenames.OperativeRole {
220+
guess, err := op.Guess(codenames.Revealed(g.state.Board), clue)
126221
if err != nil {
127-
return nil, fmt.Errorf("reveal(%q) on %q: %v", guess, g.activeTeam, err)
128-
}
129-
130-
// Check if their guess ended the game.
131-
over, winner := g.gameOver()
132-
if over {
133-
return &Outcome{Winner: winner}, nil
222+
return nil, fmt.Errorf("Guess on %q: %v", g.state.ActiveTeam, err)
134223
}
135-
log.Printf("Guess %s was a %s", guess, c.Agent)
136-
137-
if g.canKeepGuessing(numGuesses, c) {
138-
continue
224+
if _, _, err = g.Move(&Move{
225+
Action: ActionGuess,
226+
Team: g.state.ActiveTeam,
227+
Guess: guess,
228+
}); err != nil {
229+
return nil, fmt.Errorf("Guess on %q: %v", g.state.ActiveTeam, err)
139230
}
140-
if numGuesses == 0 {
141-
log.Println("Out of guesses")
142-
}
143-
144-
break
145-
}
146-
147-
if g.activeTeam == codenames.BlueTeam {
148-
g.activeTeam = codenames.RedTeam
149-
} else {
150-
g.activeTeam = codenames.BlueTeam
151231
}
152232
}
153233
}
154234

155235
func (g *Game) reveal(word string) (codenames.Card, error) {
156-
for i, card := range g.groundTruth.Cards {
236+
for i, card := range g.state.Board.Cards {
157237
if strings.ToLower(card.Codename) != strings.ToLower(word) {
158238
continue
159239
}
160240

161-
if g.groundTruth.Cards[i].Revealed {
241+
if g.state.Board.Cards[i].Revealed {
162242
return codenames.Card{}, fmt.Errorf("%q has already been guessed", word)
163243
}
164244

165245
// If the card hasn't been reveal, reveal it.
166-
g.groundTruth.Cards[i].Revealed = true
246+
g.state.Board.Cards[i].Revealed = true
167247
return card, nil
168248
}
169249
return codenames.Card{}, fmt.Errorf("no card found for guess %q", word)
170250
}
171251

172-
func (g *Game) canKeepGuessing(numGuesses int, card codenames.Card) bool {
252+
func (g *Game) canKeepGuessing(card codenames.Card) bool {
173253
targetAgent := codenames.RedAgent
174-
if g.activeTeam == codenames.BlueTeam {
254+
if g.state.ActiveTeam == codenames.BlueTeam {
175255
targetAgent = codenames.BlueAgent
176256
}
177257

178258
// They can keep guessing if the card was for their team and they have
179259
// guesses left.
180-
return card.Agent == targetAgent && numGuesses != 0
260+
return card.Agent == targetAgent && g.state.NumGuessesLeft != 0
181261
}
182262

183263
func (g *Game) gameOver() (bool, codenames.Team) {
184264
got := make(map[codenames.Agent]int)
185-
for i, cn := range g.groundTruth.Cards {
186-
if g.groundTruth.Cards[i].Revealed {
265+
for i, cn := range g.state.Board.Cards {
266+
if g.state.Board.Cards[i].Revealed {
187267
got[cn.Agent]++
188268
}
189269
}
190270

191-
for ag, wc := range want(g.cfg.Starter) {
271+
for ag, wc := range want(g.state.StartingTeam) {
192272
if gc := got[ag]; gc == wc {
193273
switch ag {
194274
case codenames.RedAgent:
@@ -198,12 +278,13 @@ func (g *Game) gameOver() (bool, codenames.Team) {
198278
// If we've revealed all the blue cards, the blue team has won.
199279
return true, codenames.BlueTeam
200280
case codenames.Assassin:
201-
// If we've revealed the assassin, the not-active team wins.
202-
switch g.activeTeam {
281+
// If we've revealed the assassin, the not-active team wins. Though by
282+
// the time we get here, we've already switched active teams.
283+
switch g.state.ActiveTeam {
203284
case codenames.BlueTeam:
204-
return true, codenames.RedTeam
205-
case codenames.RedTeam:
206285
return true, codenames.BlueTeam
286+
case codenames.RedTeam:
287+
return true, codenames.RedTeam
207288
}
208289
}
209290
}

‎go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/gorilla/websocket v1.4.1
1212
github.com/mattn/go-sqlite3 v1.14.6
1313
github.com/namsral/flag v1.7.4-pre
14+
github.com/olekukonko/tablewriter v0.0.5 // indirect
1415
github.com/ziutek/blas v0.0.0-20190227122918-da4ca23e90bb // indirect
1516
golang.org/x/net v0.0.0-20210326220855-61e056675ecf
1617
google.golang.org/api v0.43.0

‎go.sum

+4
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,14 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
138138
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
139139
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
140140
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
141+
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
142+
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
141143
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
142144
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
143145
github.com/namsral/flag v1.7.4-pre h1:b2ScHhoCUkbsq0d2C15Mv+VU8bl8hAXV8arnWiOHNZs=
144146
github.com/namsral/flag v1.7.4-pre/go.mod h1:OXldTctbM6SWH1K899kPZcf65KxJiD7MsceFUpB5yDo=
147+
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
148+
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
145149
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
146150
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
147151
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=

‎io/io.go

+32-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io"
77

88
"github.com/bcspragu/Codenames/codenames"
9+
"github.com/olekukonko/tablewriter"
910
)
1011

1112
// Spymaster asks the user on the terminal to enter a clue. It assumes they
@@ -19,7 +20,8 @@ type Spymaster struct {
1920
Team codenames.Team
2021
}
2122

22-
func (s *Spymaster) GiveClue(_ *codenames.Board) (*codenames.Clue, error) {
23+
func (s *Spymaster) GiveClue(b *codenames.Board) (*codenames.Clue, error) {
24+
s.printBoard(b)
2325
fmt.Fprintf(s.Out, "%s Spymaster, enter a clue [ex. 'Muffins 3']: ", s.Team)
2426
sc := bufio.NewScanner(s.In)
2527
if !sc.Scan() {
@@ -28,6 +30,35 @@ func (s *Spymaster) GiveClue(_ *codenames.Board) (*codenames.Clue, error) {
2830
return codenames.ParseClue(sc.Text())
2931
}
3032

33+
func (s *Spymaster) printBoard(b *codenames.Board) {
34+
table := tablewriter.NewWriter(s.Out)
35+
36+
for i := 0; i < 5; i++ {
37+
var row []string
38+
var colors []tablewriter.Colors
39+
for j := 0; j < 5; j++ {
40+
card := b.Cards[i*5+j]
41+
var c tablewriter.Colors
42+
switch card.Agent {
43+
case codenames.BlueAgent:
44+
c = append(c, tablewriter.FgBlueColor)
45+
case codenames.RedAgent:
46+
c = append(c, tablewriter.FgHiRedColor)
47+
case codenames.Assassin:
48+
c = append(c, tablewriter.BgHiRedColor)
49+
}
50+
if card.Revealed {
51+
c = append(c, tablewriter.UnderlineSingle)
52+
}
53+
colors = append(colors, c)
54+
row = append(row, card.Codename)
55+
}
56+
table.Rich(row, colors)
57+
}
58+
59+
table.Render()
60+
}
61+
3162
// Operative asks the user on the terminal to enter a guess.
3263
type Operative struct {
3364
// in is a reader where the user's guess is read from.

‎web/web.go

+120-16
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313

1414
"github.com/bcspragu/Codenames/boardgen"
1515
"github.com/bcspragu/Codenames/codenames"
16+
"github.com/bcspragu/Codenames/consensus"
17+
"github.com/bcspragu/Codenames/game"
1618
"github.com/bcspragu/Codenames/hub"
1719
"github.com/gorilla/mux"
1820
"github.com/gorilla/securecookie"
@@ -24,12 +26,13 @@ const (
2426
)
2527

2628
type Srv struct {
27-
sc *securecookie.SecureCookie
28-
hub *hub.Hub
29-
mux *mux.Router
30-
db codenames.DB
31-
r *rand.Rand
32-
ws *websocket.Upgrader
29+
sc *securecookie.SecureCookie
30+
hub *hub.Hub
31+
mux *mux.Router
32+
db codenames.DB
33+
r *rand.Rand
34+
ws *websocket.Upgrader
35+
consensus *consensus.Guesser
3336
}
3437

3538
// New returns an initialized server.
@@ -40,11 +43,12 @@ func New(db codenames.DB, r *rand.Rand) (*Srv, error) {
4043
}
4144

4245
s := &Srv{
43-
sc: sc,
44-
hub: hub.New(),
45-
db: db,
46-
r: r,
47-
ws: &websocket.Upgrader{}, // use default options, for now
46+
sc: sc,
47+
hub: hub.New(),
48+
db: db,
49+
r: r,
50+
ws: &websocket.Upgrader{}, // use default options, for now
51+
consensus: consensus.New(),
4852
}
4953

5054
s.mux = s.initMux()
@@ -151,9 +155,10 @@ func (s *Srv) serveCreateGame(w http.ResponseWriter, r *http.Request) {
151155
id, err := s.db.NewGame(&codenames.Game{
152156
CreatedBy: u.ID,
153157
State: &codenames.GameState{
154-
ActiveTeam: ar,
155-
ActiveRole: codenames.SpymasterRole,
156-
Board: boardgen.New(ar, s.r),
158+
StartingTeam: ar,
159+
ActiveTeam: ar,
160+
ActiveRole: codenames.SpymasterRole,
161+
Board: boardgen.New(ar, s.r),
157162
},
158163
})
159164
if err != nil {
@@ -360,12 +365,111 @@ func (s *Srv) serveStartGame(w http.ResponseWriter, r *http.Request, u *codename
360365
}{true})
361366
}
362367

363-
func (s *Srv) serveClue(w http.ResponseWriter, r *http.Request, u *codenames.User, game *codenames.Game, userPR *codenames.PlayerRole, prs []*codenames.PlayerRole) {
368+
func (s *Srv) serveClue(w http.ResponseWriter, r *http.Request, u *codenames.User, g *codenames.Game, userPR *codenames.PlayerRole, prs []*codenames.PlayerRole) {
369+
if g.Status != codenames.Playing {
370+
http.Error(w, "can't give clues to a not-playing game", http.StatusBadRequest)
371+
return
372+
}
373+
374+
var req struct {
375+
Word string `json:"word"`
376+
Count int `json:"count"`
377+
}
378+
379+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
380+
http.Error(w, err.Error(), http.StatusBadRequest)
381+
return
382+
}
383+
384+
// We don't need to check if the status changed/game is over, because giving
385+
// a clue will never end the game.
386+
newState, _, err := game.NewForMove(g.State).Move(&game.Move{
387+
Action: game.ActionGiveClue,
388+
Team: userPR.Team,
389+
GiveClue: &codenames.Clue{
390+
Word: req.Word,
391+
Count: req.Count,
392+
},
393+
})
394+
if err != nil {
395+
// We assume the error is the result of a bad request.
396+
http.Error(w, fmt.Sprintf("failed to make move : %v", err), http.StatusBadRequest)
397+
return
398+
}
364399

400+
// Update the state in the database.
401+
if err := s.db.UpdateState(g.ID, newState); err != nil {
402+
http.Error(w, fmt.Sprintf("failed to update game state: %v", err), http.StatusInternalServerError)
403+
return
404+
}
365405
}
366406

367-
func (s *Srv) serveGuess(w http.ResponseWriter, r *http.Request, u *codenames.User, game *codenames.Game, userPR *codenames.PlayerRole, prs []*codenames.PlayerRole) {
407+
func (s *Srv) serveGuess(w http.ResponseWriter, r *http.Request, u *codenames.User, g *codenames.Game, userPR *codenames.PlayerRole, prs []*codenames.PlayerRole) {
408+
if g.Status != codenames.Playing {
409+
http.Error(w, "can't guess in a not-playing game", http.StatusBadRequest)
410+
return
411+
}
412+
413+
var req struct {
414+
Guess string `json:"word"`
415+
Confirmed bool `json:"confirmed"`
416+
}
417+
418+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
419+
http.Error(w, err.Error(), http.StatusBadRequest)
420+
return
421+
}
422+
423+
// TODO(bcspragu): Let other players know what their vote was.
424+
425+
if !req.Confirmed {
426+
// If it's not confirmed (e.g. it's just tentative), so we shouldn't count
427+
// the votes.
428+
return
429+
}
430+
431+
s.consensus.RecordVote(g.ID, u.ID, req.Guess)
432+
433+
guess, hasConsensus := s.consensus.ReachedConsensus(g.ID, countVoters(prs, g.State.ActiveTeam))
434+
if !hasConsensus {
435+
return
436+
}
437+
438+
newState, newStatus, err := game.NewForMove(g.State).Move(&game.Move{
439+
Action: game.ActionGuess,
440+
Team: userPR.Team,
441+
Guess: guess,
442+
})
443+
if err != nil {
444+
// We assume the error is the result of a bad request.
445+
http.Error(w, fmt.Sprintf("failed to make move : %v", err), http.StatusBadRequest)
446+
return
447+
}
448+
449+
// They've made the guess, clear out the consensus for the next time.
450+
s.consensus.Clear(g.ID)
368451

452+
// Update the state in the database.
453+
if err := s.db.UpdateState(g.ID, newState); err != nil {
454+
http.Error(w, fmt.Sprintf("failed to update game state: %v", err), http.StatusInternalServerError)
455+
return
456+
}
457+
458+
// The game is over, we should let folks know.
459+
if newStatus == codenames.Finished {
460+
// TODO(bcspragu): Let folks know.
461+
return
462+
}
463+
}
464+
465+
func countVoters(prs []*codenames.PlayerRole, team codenames.Team) int {
466+
cnt := 0
467+
for _, pr := range prs {
468+
if pr.Role == codenames.OperativeRole && pr.Team == team {
469+
cnt++
470+
}
471+
}
472+
return cnt
369473
}
370474

371475
func (s *Srv) serveData(w http.ResponseWriter, r *http.Request, u *codenames.User, game *codenames.Game, userPR *codenames.PlayerRole, prs []*codenames.PlayerRole) {

0 commit comments

Comments
 (0)
Please sign in to comment.