room.go

  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}