1package game
2
3import (
4 "github.com/gofrs/uuid"
5 "github.com/zikaeroh/codies/internal/words"
6 "github.com/zikaeroh/codies/internal/words/static"
7)
8
9type PlayerID = uuid.UUID
10
11type WordList struct {
12 Name string
13 Custom bool
14 List words.List
15
16 Enabled bool
17}
18
19func defaultWords() []*WordList {
20 return []*WordList{
21 {
22 Name: "Base",
23 List: static.Default,
24 Enabled: true,
25 },
26 {
27 Name: "Duet",
28 List: static.Duet,
29 },
30 {
31 Name: "Undercover",
32 List: static.Undercover,
33 },
34 }
35}
36
37type Room struct {
38 rand Rand
39
40 // Configuration for the next new game.
41 Rows, Cols int
42
43 Version int
44 Board *Board
45 Turn Team
46 Winner *Team
47 Players map[PlayerID]*Player
48 Teams [][]PlayerID // To preserve the ordering of teams.
49 WordLists []*WordList
50}
51
52func NewRoom(rand Rand) *Room {
53 if rand == nil {
54 rand = globalRand{}
55 }
56
57 return &Room{
58 rand: rand,
59 Rows: 5,
60 Cols: 5,
61 Players: make(map[PlayerID]*Player),
62 Teams: make([][]PlayerID, 2), // TODO: support more than 2 teams
63 WordLists: defaultWords(),
64 }
65}
66
67type Player struct {
68 ID PlayerID
69 Nickname string
70 Team Team
71 Spymaster bool
72}
73
74func (r *Room) AddPlayer(id PlayerID, nickname string) {
75 if p, ok := r.Players[id]; ok {
76 if p.Nickname == nickname {
77 return
78 }
79
80 p.Nickname = nickname
81 r.Version++
82 return
83 }
84
85 team := r.smallestTeam()
86 p := &Player{
87 ID: id,
88 Nickname: nickname,
89 Team: team,
90 }
91
92 r.Players[id] = p
93 r.Teams[team] = append(r.Teams[team], id)
94 r.Version++
95}
96
97func (r *Room) smallestTeam() Team {
98 min := Team(0)
99 minLen := len(r.Teams[0])
100
101 for tInt, team := range r.Teams {
102 if len(team) < minLen {
103 min = Team(tInt)
104 minLen = len(team)
105 }
106 }
107
108 return min
109}
110
111func (r *Room) words() (list words.List) {
112 for _, w := range r.WordLists {
113 if w.Enabled {
114 list = list.Concat(w.List)
115 }
116 }
117 return list
118}
119
120func (r *Room) NewGame() {
121 words := r.words()
122
123 if r.Rows*r.Cols > words.Len() {
124 panic("not enough words")
125 }
126
127 r.Winner = nil
128 r.Turn = Team(r.rand.Intn(len(r.Teams)))
129 r.Board = newBoard(r.Rows, r.Cols, words, r.Turn, len(r.Teams), r.rand)
130
131 for _, p := range r.Players {
132 p.Spymaster = false
133 }
134
135 r.Version++
136}
137
138func (r *Room) EndTurn(id PlayerID) {
139 if r.Winner != nil {
140 return
141 }
142
143 p := r.Players[id]
144 if p == nil {
145 return
146 }
147
148 if p.Team != r.Turn || p.Spymaster {
149 return
150 }
151
152 r.ForceEndTurn()
153}
154
155func (r *Room) nextTeam() Team {
156 return r.Turn.next(len(r.Teams))
157}
158
159func (r *Room) nextTurn() {
160 r.Turn = r.nextTeam()
161}
162
163func (r *Room) ForceEndTurn() {
164 r.Version++
165 r.nextTurn()
166}
167
168func (r *Room) RemovePlayer(id PlayerID) {
169 p := r.Players[id]
170 if p == nil {
171 return
172 }
173
174 r.Version++
175 delete(r.Players, id)
176
177 r.Teams[p.Team] = removePlayer(r.Teams[p.Team], id)
178}
179
180func (r *Room) Reveal(id PlayerID, row, col int) {
181 if r.Winner != nil {
182 return
183 }
184
185 p := r.Players[id]
186 if p == nil {
187 return
188 }
189
190 if p.Spymaster || p.Team != r.Turn {
191 return
192 }
193
194 tile := r.Board.Get(row, col)
195 if tile == nil {
196 return
197 }
198
199 if tile.Revealed {
200 return
201 }
202
203 tile.Revealed = true
204
205 switch {
206 case tile.Neutral:
207 r.nextTurn()
208 case tile.Bomb:
209 // TODO: Who wins when there's more than one team?
210 // Maybe eliminate the team who clicked?
211 winner := r.nextTeam()
212 r.Winner = &winner
213 default:
214 r.Board.WordCounts[tile.Team]--
215 if r.Board.WordCounts[tile.Team] == 0 {
216 winner := tile.Team
217 r.Winner = &winner
218 } else if tile.Team != p.Team {
219 r.nextTurn()
220 }
221 }
222
223 r.Version++
224}
225
226func (r *Room) ChangeRole(id PlayerID, spymaster bool) {
227 if r.Winner != nil {
228 return
229 }
230
231 p := r.Players[id]
232 if p == nil {
233 return
234 }
235
236 if p.Spymaster == spymaster {
237 return
238 }
239
240 p.Spymaster = spymaster
241 r.Version++
242}
243
244func (r *Room) ChangeTeam(id PlayerID, team Team) {
245 if team < 0 || int(team) >= len(r.Teams) {
246 return
247 }
248
249 p := r.Players[id]
250 if p == nil {
251 return
252 }
253
254 if p.Team == team {
255 return
256 }
257
258 r.Teams[p.Team] = removePlayer(r.Teams[p.Team], id)
259 r.Teams[team] = append(r.Teams[team], id)
260 p.Team = team
261 r.Version++
262}
263
264func removePlayer(team []PlayerID, remove PlayerID) []PlayerID {
265 newTeam := make([]PlayerID, 0, len(team)-1)
266 for _, id := range team {
267 if id != remove {
268 newTeam = append(newTeam, id)
269 }
270 }
271 return newTeam
272}
273
274func (r *Room) RandomizeTeams() {
275 players := make([]PlayerID, 0, len(r.Players))
276 for id := range r.Players {
277 players = append(players, id)
278 }
279
280 r.rand.Shuffle(len(players), func(i, j int) {
281 players[i], players[j] = players[j], players[i]
282 })
283
284 numTeams := len(r.Teams)
285 newTeams := make([][]PlayerID, numTeams)
286 for i := range newTeams {
287 newTeams[i] = make([]PlayerID, 0, len(players)/numTeams)
288 }
289
290 for i, id := range players {
291 team := i % numTeams
292 newTeams[team] = append(newTeams[team], id)
293 }
294
295 r.rand.Shuffle(numTeams, func(i, j int) {
296 newTeams[i], newTeams[j] = newTeams[j], newTeams[i]
297 })
298
299 for team, players := range newTeams {
300 for _, id := range players {
301 r.Players[id].Team = Team(team)
302 }
303 }
304
305 r.Teams = newTeams
306 r.Version++
307}
308
309func (r *Room) ChangePack(num int, enable bool) {
310 if num < 0 || num >= len(r.WordLists) {
311 return
312 }
313
314 pack := r.WordLists[num]
315
316 if pack.Enabled == enable {
317 return
318 }
319
320 if !enable {
321 total := 0
322 for _, p := range r.WordLists {
323 if p.Enabled {
324 total++
325 }
326 }
327
328 if total < 2 {
329 return
330 }
331 }
332
333 pack.Enabled = enable
334 r.Version++
335}
336
337func (r *Room) AddPack(name string, wds []string) {
338 if len(r.WordLists) >= 10 {
339 return
340 }
341
342 list := &WordList{
343 Name: name,
344 Custom: true,
345 List: words.NewList(wds),
346 }
347 r.WordLists = append(r.WordLists, list)
348 r.Version++
349}
350
351func (r *Room) RemovePack(num int) {
352 if num < 0 || num >= len(r.WordLists) {
353 return
354 }
355
356 if pack := r.WordLists[num]; !pack.Custom || pack.Enabled {
357 return
358 }
359
360 // https://github.com/golang/go/wiki/SliceTricks
361 lists := r.WordLists
362 copy(lists[num:], lists[num+1:])
363 lists[len(lists)-1] = nil
364 lists = lists[:len(lists)-1]
365 r.WordLists = lists
366
367 r.Version++
368}