codies/internal/game/room.go

364 lines
5.8 KiB
Go

package game
import (
"github.com/gofrs/uuid"
"github.com/zikaeroh/codies/internal/words"
"github.com/zikaeroh/codies/internal/words/static"
)
type PlayerID = uuid.UUID
type WordList struct {
Name string
Custom bool
List words.List
Enabled bool
}
func defaultWords() []*WordList {
return []*WordList{
{
Name: "Base",
List: static.Default,
Enabled: true,
},
{
Name: "Duet",
List: static.Duet,
},
{
Name: "Undercover",
List: static.Undercover,
},
}
}
type Room struct {
rand Rand
// Configuration for the next new game.
Rows, Cols int
Version int
Board *Board
Turn Team
Winner *Team
Players map[PlayerID]*Player
Teams [][]PlayerID // To preserve the ordering of teams.
WordLists []*WordList
}
func NewRoom(rand Rand) *Room {
if rand == nil {
rand = globalRand{}
}
return &Room{
rand: rand,
Rows: 5,
Cols: 5,
Players: make(map[PlayerID]*Player),
Teams: make([][]PlayerID, 2), // TODO: support more than 2 teams
WordLists: defaultWords(),
}
}
type Player struct {
ID PlayerID
Nickname string
Team Team
Spymaster bool
}
func (r *Room) AddPlayer(id PlayerID, nickname string) {
if p, ok := r.Players[id]; ok {
if p.Nickname == nickname {
return
}
p.Nickname = nickname
r.Version++
return
}
team := r.smallestTeam()
p := &Player{
ID: id,
Nickname: nickname,
Team: team,
}
r.Players[id] = p
r.Teams[team] = append(r.Teams[team], id)
r.Version++
}
func (r *Room) smallestTeam() Team {
min := Team(0)
minLen := len(r.Teams[0])
for tInt, team := range r.Teams {
if len(team) < minLen {
min = Team(tInt)
minLen = len(team)
}
}
return min
}
func (r *Room) words() (list words.List) {
for _, w := range r.WordLists {
if w.Enabled {
list = list.Concat(w.List)
}
}
return list
}
func (r *Room) NewGame() {
words := r.words()
if r.Rows*r.Cols > words.Len() {
panic("not enough words")
}
r.Version++
r.Winner = nil
r.Turn = Team(r.rand.Intn(len(r.Teams)))
r.Board = newBoard(r.Rows, r.Cols, words, r.Turn, len(r.Teams), r.rand)
}
func (r *Room) EndTurn(id PlayerID) {
if r.Winner != nil {
return
}
p := r.Players[id]
if p == nil {
return
}
if p.Team != r.Turn || p.Spymaster {
return
}
r.ForceEndTurn()
}
func (r *Room) nextTeam() Team {
return r.Turn.next(len(r.Teams))
}
func (r *Room) nextTurn() {
r.Turn = r.nextTeam()
}
func (r *Room) ForceEndTurn() {
r.Version++
r.nextTurn()
}
func (r *Room) RemovePlayer(id PlayerID) {
p := r.Players[id]
if p == nil {
return
}
r.Version++
delete(r.Players, id)
r.Teams[p.Team] = removePlayer(r.Teams[p.Team], id)
}
func (r *Room) Reveal(id PlayerID, row, col int) {
if r.Winner != nil {
return
}
p := r.Players[id]
if p == nil {
return
}
if p.Spymaster || p.Team != r.Turn {
return
}
tile := r.Board.Get(row, col)
if tile == nil {
return
}
if tile.Revealed {
return
}
tile.Revealed = true
switch {
case tile.Neutral:
r.nextTurn()
case tile.Bomb:
// TODO: Who wins when there's more than one team?
// Maybe eliminate the team who clicked?
winner := r.nextTeam()
r.Winner = &winner
default:
r.Board.WordCounts[tile.Team]--
if r.Board.WordCounts[tile.Team] == 0 {
winner := tile.Team
r.Winner = &winner
} else if tile.Team != p.Team {
r.nextTurn()
}
}
r.Version++
}
func (r *Room) ChangeRole(id PlayerID, spymaster bool) {
if r.Winner != nil {
return
}
p := r.Players[id]
if p == nil {
return
}
if p.Spymaster == spymaster {
return
}
p.Spymaster = spymaster
r.Version++
}
func (r *Room) ChangeTeam(id PlayerID, team Team) {
if team < 0 || int(team) >= len(r.Teams) {
return
}
p := r.Players[id]
if p == nil {
return
}
if p.Team == team {
return
}
r.Teams[p.Team] = removePlayer(r.Teams[p.Team], id)
r.Teams[team] = append(r.Teams[team], id)
p.Team = team
r.Version++
}
func removePlayer(team []PlayerID, remove PlayerID) []PlayerID {
newTeam := make([]PlayerID, 0, len(team)-1)
for _, id := range team {
if id != remove {
newTeam = append(newTeam, id)
}
}
return newTeam
}
func (r *Room) RandomizeTeams() {
players := make([]PlayerID, 0, len(r.Players))
for id := range r.Players {
players = append(players, id)
}
r.rand.Shuffle(len(players), func(i, j int) {
players[i], players[j] = players[j], players[i]
})
numTeams := len(r.Teams)
newTeams := make([][]PlayerID, numTeams)
for i := range newTeams {
newTeams[i] = make([]PlayerID, 0, len(players)/numTeams)
}
for i, id := range players {
team := i % numTeams
newTeams[team] = append(newTeams[team], id)
}
r.rand.Shuffle(numTeams, func(i, j int) {
newTeams[i], newTeams[j] = newTeams[j], newTeams[i]
})
for team, players := range newTeams {
for _, id := range players {
r.Players[id].Team = Team(team)
}
}
r.Teams = newTeams
r.Version++
}
func (r *Room) ChangePack(num int, enable bool) {
if num < 0 || num >= len(r.WordLists) {
return
}
pack := r.WordLists[num]
if pack.Enabled == enable {
return
}
if !enable {
total := 0
for _, p := range r.WordLists {
if p.Enabled {
total++
}
}
if total < 2 {
return
}
}
pack.Enabled = enable
r.Version++
}
func (r *Room) AddPack(name string, wds []string) {
if len(r.WordLists) >= 10 {
return
}
list := &WordList{
Name: name,
Custom: true,
List: words.NewList(wds),
}
r.WordLists = append(r.WordLists, list)
r.Version++
}
func (r *Room) RemovePack(num int) {
if num < 0 || num >= len(r.WordLists) {
return
}
if pack := r.WordLists[num]; !pack.Custom || pack.Enabled {
return
}
// https://github.com/golang/go/wiki/SliceTricks
lists := r.WordLists
copy(lists[num:], lists[num+1:])
lists[len(lists)-1] = nil
lists = lists[:len(lists)-1]
r.WordLists = lists
r.Version++
}