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