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.Version++
128 r.Winner = nil
129 r.Turn = Team(r.rand.Intn(len(r.Teams)))
130 r.Board = newBoard(r.Rows, r.Cols, words, r.Turn, len(r.Teams), r.rand)
131}
132
133func (r *Room) EndTurn(id PlayerID) {
134 if r.Winner != nil {
135 return
136 }
137
138 p := r.Players[id]
139 if p == nil {
140 return
141 }
142
143 if p.Team != r.Turn || p.Spymaster {
144 return
145 }
146
147 r.ForceEndTurn()
148}
149
150func (r *Room) nextTeam() Team {
151 return r.Turn.next(len(r.Teams))
152}
153
154func (r *Room) nextTurn() {
155 r.Turn = r.nextTeam()
156}
157
158func (r *Room) ForceEndTurn() {
159 r.Version++
160 r.nextTurn()
161}
162
163func (r *Room) RemovePlayer(id PlayerID) {
164 p := r.Players[id]
165 if p == nil {
166 return
167 }
168
169 r.Version++
170 delete(r.Players, id)
171
172 r.Teams[p.Team] = removePlayer(r.Teams[p.Team], id)
173}
174
175func (r *Room) Reveal(id PlayerID, row, col int) {
176 if r.Winner != nil {
177 return
178 }
179
180 p := r.Players[id]
181 if p == nil {
182 return
183 }
184
185 if p.Spymaster || p.Team != r.Turn {
186 return
187 }
188
189 tile := r.Board.Get(row, col)
190 if tile == nil {
191 return
192 }
193
194 if tile.Revealed {
195 return
196 }
197
198 tile.Revealed = true
199
200 switch {
201 case tile.Neutral:
202 r.nextTurn()
203 case tile.Bomb:
204 // TODO: Who wins when there's more than one team?
205 // Maybe eliminate the team who clicked?
206 winner := r.nextTeam()
207 r.Winner = &winner
208 default:
209 r.Board.WordCounts[tile.Team]--
210 if r.Board.WordCounts[tile.Team] == 0 {
211 winner := tile.Team
212 r.Winner = &winner
213 } else if tile.Team != p.Team {
214 r.nextTurn()
215 }
216 }
217
218 r.Version++
219}
220
221func (r *Room) ChangeRole(id PlayerID, spymaster bool) {
222 if r.Winner != nil {
223 return
224 }
225
226 p := r.Players[id]
227 if p == nil {
228 return
229 }
230
231 if p.Spymaster == spymaster {
232 return
233 }
234
235 p.Spymaster = spymaster
236 r.Version++
237}
238
239func (r *Room) ChangeTeam(id PlayerID, team Team) {
240 if team < 0 || int(team) >= len(r.Teams) {
241 return
242 }
243
244 p := r.Players[id]
245 if p == nil {
246 return
247 }
248
249 if p.Team == team {
250 return
251 }
252
253 r.Teams[p.Team] = removePlayer(r.Teams[p.Team], id)
254 r.Teams[team] = append(r.Teams[team], id)
255 p.Team = team
256 r.Version++
257}
258
259func removePlayer(team []PlayerID, remove PlayerID) []PlayerID {
260 newTeam := make([]PlayerID, 0, len(team)-1)
261 for _, id := range team {
262 if id != remove {
263 newTeam = append(newTeam, id)
264 }
265 }
266 return newTeam
267}
268
269func (r *Room) RandomizeTeams() {
270 players := make([]PlayerID, 0, len(r.Players))
271 for id := range r.Players {
272 players = append(players, id)
273 }
274
275 r.rand.Shuffle(len(players), func(i, j int) {
276 players[i], players[j] = players[j], players[i]
277 })
278
279 numTeams := len(r.Teams)
280 newTeams := make([][]PlayerID, numTeams)
281 for i := range newTeams {
282 newTeams[i] = make([]PlayerID, 0, len(players)/numTeams)
283 }
284
285 for i, id := range players {
286 team := i % numTeams
287 newTeams[team] = append(newTeams[team], id)
288 }
289
290 r.rand.Shuffle(numTeams, func(i, j int) {
291 newTeams[i], newTeams[j] = newTeams[j], newTeams[i]
292 })
293
294 for team, players := range newTeams {
295 for _, id := range players {
296 r.Players[id].Team = Team(team)
297 }
298 }
299
300 r.Teams = newTeams
301 r.Version++
302}
303
304func (r *Room) ChangePack(num int, enable bool) {
305 if num < 0 || num >= len(r.WordLists) {
306 return
307 }
308
309 pack := r.WordLists[num]
310
311 if pack.Enabled == enable {
312 return
313 }
314
315 if !enable {
316 total := 0
317 for _, p := range r.WordLists {
318 if p.Enabled {
319 total++
320 }
321 }
322
323 if total < 2 {
324 return
325 }
326 }
327
328 pack.Enabled = enable
329 r.Version++
330}
331
332func (r *Room) AddPack(name string, wds []string) {
333 if len(r.WordLists) >= 10 {
334 return
335 }
336
337 list := &WordList{
338 Name: name,
339 Custom: true,
340 List: words.NewList(wds),
341 }
342 r.WordLists = append(r.WordLists, list)
343 r.Version++
344}
345
346func (r *Room) RemovePack(num int) {
347 if num < 0 || num >= len(r.WordLists) {
348 return
349 }
350
351 if pack := r.WordLists[num]; !pack.Custom || pack.Enabled {
352 return
353 }
354
355 // https://github.com/golang/go/wiki/SliceTricks
356 lists := r.WordLists
357 copy(lists[num:], lists[num+1:])
358 lists[len(lists)-1] = nil
359 lists = lists[:len(lists)-1]
360 r.WordLists = lists
361
362 r.Version++
363}