1
1
package game
2
2
3
3
import (
4
+ "errors"
4
5
"fmt"
5
- "log"
6
6
"strings"
7
7
8
8
"github.com/bcspragu/Codenames/codenames"
9
9
)
10
10
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.
12
18
type Game struct {
13
- groundTruth * codenames.Board
14
- cfg * Config
15
- activeTeam codenames.Team
19
+ state * codenames.GameState
20
+ cfg * Config
16
21
}
17
22
18
23
// Config holds configuration options for a game of Codenames.
19
24
type Config struct {
20
- // Starter is the team that goes first.
21
- Starter codenames.Team
22
-
23
25
RedSpymaster codenames.Spymaster
24
26
BlueSpymaster codenames.Spymaster
25
27
26
28
RedOperative codenames.Operative
27
29
BlueOperative codenames.Operative
28
30
}
29
31
32
+ func NewForMove (state * codenames.GameState ) * Game {
33
+ return & Game {state : state }
34
+ }
35
+
30
36
// 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 {
33
39
return nil , fmt .Errorf ("invalid board given: %v" , err )
34
40
}
35
41
@@ -47,9 +53,13 @@ func New(b *codenames.Board, cfg *Config) (*Game, error) {
47
53
}
48
54
49
55
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 ,
53
63
}, nil
54
64
}
55
65
@@ -93,102 +103,172 @@ type Outcome struct {
93
103
// team, if anyone hit the assassin, etc.
94
104
}
95
105
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
+
96
199
func (g * Game ) Play () (* Outcome , error ) {
97
200
for {
98
201
// Let's play a round.
99
202
sm , op := g .cfg .RedSpymaster , g .cfg .RedOperative
100
- if g .activeTeam == codenames .BlueTeam {
203
+ if g .state . ActiveTeam == codenames .BlueTeam {
101
204
sm , op = g .cfg .BlueSpymaster , g .cfg .BlueOperative
102
205
}
103
206
104
- clue , err := sm .GiveClue (codenames .CloneBoard (g .groundTruth ))
207
+ clue , err := sm .GiveClue (codenames .CloneBoard (g .state . Board ))
105
208
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 )
107
210
}
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 )
111
217
}
112
218
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 )
126
221
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 )
134
223
}
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 )
139
230
}
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
151
231
}
152
232
}
153
233
}
154
234
155
235
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 {
157
237
if strings .ToLower (card .Codename ) != strings .ToLower (word ) {
158
238
continue
159
239
}
160
240
161
- if g .groundTruth .Cards [i ].Revealed {
241
+ if g .state . Board .Cards [i ].Revealed {
162
242
return codenames.Card {}, fmt .Errorf ("%q has already been guessed" , word )
163
243
}
164
244
165
245
// If the card hasn't been reveal, reveal it.
166
- g .groundTruth .Cards [i ].Revealed = true
246
+ g .state . Board .Cards [i ].Revealed = true
167
247
return card , nil
168
248
}
169
249
return codenames.Card {}, fmt .Errorf ("no card found for guess %q" , word )
170
250
}
171
251
172
- func (g * Game ) canKeepGuessing (numGuesses int , card codenames.Card ) bool {
252
+ func (g * Game ) canKeepGuessing (card codenames.Card ) bool {
173
253
targetAgent := codenames .RedAgent
174
- if g .activeTeam == codenames .BlueTeam {
254
+ if g .state . ActiveTeam == codenames .BlueTeam {
175
255
targetAgent = codenames .BlueAgent
176
256
}
177
257
178
258
// They can keep guessing if the card was for their team and they have
179
259
// guesses left.
180
- return card .Agent == targetAgent && numGuesses != 0
260
+ return card .Agent == targetAgent && g . state . NumGuessesLeft != 0
181
261
}
182
262
183
263
func (g * Game ) gameOver () (bool , codenames.Team ) {
184
264
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 {
187
267
got [cn .Agent ]++
188
268
}
189
269
}
190
270
191
- for ag , wc := range want (g .cfg . Starter ) {
271
+ for ag , wc := range want (g .state . StartingTeam ) {
192
272
if gc := got [ag ]; gc == wc {
193
273
switch ag {
194
274
case codenames .RedAgent :
@@ -198,12 +278,13 @@ func (g *Game) gameOver() (bool, codenames.Team) {
198
278
// If we've revealed all the blue cards, the blue team has won.
199
279
return true , codenames .BlueTeam
200
280
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 {
203
284
case codenames .BlueTeam :
204
- return true , codenames .RedTeam
205
- case codenames .RedTeam :
206
285
return true , codenames .BlueTeam
286
+ case codenames .RedTeam :
287
+ return true , codenames .RedTeam
207
288
}
208
289
}
209
290
}
0 commit comments