dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

werewolf.go (20548B)


      1 package interceptors
      2 
      3 import (
      4 	"bytes"
      5 	"context"
      6 	"dkforest/pkg/database"
      7 	dutils "dkforest/pkg/database/utils"
      8 	"dkforest/pkg/hashset"
      9 	"dkforest/pkg/utils"
     10 	"dkforest/pkg/web/handlers/interceptors/command"
     11 	"errors"
     12 	"fmt"
     13 	"github.com/sirupsen/logrus"
     14 	"html/template"
     15 	"math/rand"
     16 	"sort"
     17 	"strings"
     18 	"time"
     19 )
     20 
     21 var WWInstance *Werewolf
     22 
     23 const (
     24 	PreGameState = iota + 1
     25 	DayState
     26 	NightState
     27 	VoteState
     28 	EndGameState
     29 )
     30 
     31 const (
     32 	TownspeopleRole = "townspeople"
     33 	WerewolfRole    = "werewolf"
     34 	SeerRole        = "seer"
     35 	HealerRole      = "healer"
     36 )
     37 
     38 var ErrInvalidPlayerName = errors.New("unknown player name, please send a valid name")
     39 
     40 type Werewolf struct {
     41 	db               *database.DkfDB
     42 	ctx              context.Context
     43 	cancel           context.CancelFunc
     44 	readyCh          chan bool
     45 	narratorID       database.UserID
     46 	roomID           database.RoomID
     47 	werewolfGroupID  database.GroupID
     48 	spectatorGroupID database.GroupID
     49 	deadGroupID      database.GroupID
     50 	players          map[database.Username]*Player
     51 	playersAlive     map[database.Username]*Player
     52 	state            int64
     53 	werewolfSet      *hashset.HashSet[database.UserID]
     54 	spectatorSet     *hashset.HashSet[database.UserID]
     55 	townspersonSet   *hashset.HashSet[database.UserID]
     56 	healerID         *database.UserID
     57 	seerID           *database.UserID
     58 	werewolfCh       chan string
     59 	seerCh           chan string
     60 	healerCh         chan string
     61 	votesCh          chan string
     62 	voted            *hashset.HashSet[database.UserID] // Keep track of which user voted already
     63 }
     64 
     65 // Return either or not the userID is an active player (alive)
     66 func (b *Werewolf) isAlivePlayer(userID database.UserID) bool {
     67 	for _, player := range b.playersAlive {
     68 		if player.UserID == userID {
     69 			return true
     70 		}
     71 	}
     72 	return false
     73 }
     74 
     75 func (b *Werewolf) InterceptPreGameMsg(cmd *command.Command) {
     76 	if cmd.Message == "/players" {
     77 		b.Narrate("Registered players: "+b.alivePlayersStr(), nil, nil)
     78 		cmd.Err = command.ErrRedirect
     79 		return
     80 
     81 	} else if cmd.Message == "/join" {
     82 		if cmd.AuthUser.IsHellbanned {
     83 			cmd.Err = command.ErrRedirect
     84 			return
     85 		}
     86 		if _, found := b.players[cmd.AuthUser.Username]; found {
     87 			cmd.Err = command.ErrRedirect
     88 			return
     89 		}
     90 		player := &Player{
     91 			UserID:   cmd.AuthUser.ID,
     92 			Username: cmd.AuthUser.Username,
     93 		}
     94 		b.players[cmd.AuthUser.Username] = player
     95 		b.playersAlive[cmd.AuthUser.Username] = player
     96 		b.Narrate(cmd.AuthUser.Username.AtStr()+" joined the Game", nil, nil)
     97 		cmd.Err = command.ErrRedirect
     98 		return
     99 
    100 	} else if cmd.Message == "/spectate" {
    101 		b.spectatorSet.Insert(cmd.AuthUser.ID)
    102 		b.Narrate(cmd.AuthUser.Username.AtStr()+" spectate the Game", nil, nil)
    103 		cmd.Err = command.ErrRedirect
    104 		return
    105 
    106 	} else if cmd.Message == "/start" {
    107 		b.cancel()
    108 		time.Sleep(time.Second)
    109 		utils.SGo(func() {
    110 			b.StartGame(cmd.DB)
    111 		})
    112 		cmd.Err = command.ErrRedirect
    113 		return
    114 	}
    115 }
    116 
    117 func (b *Werewolf) InterceptNightMsg(cmd *command.Command) {
    118 	if cmd.GroupID != nil && *cmd.GroupID == b.werewolfGroupID {
    119 		select {
    120 		case b.werewolfCh <- cmd.Message:
    121 			cmd.Err = command.ErrRedirect
    122 		default:
    123 			cmd.Err = errors.New("narrator doesn't need your input")
    124 		}
    125 		return
    126 	} else if b.isForNarrator(cmd) && b.seerID != nil && cmd.AuthUser.ID == *b.seerID {
    127 		select {
    128 		case b.seerCh <- cmd.Message:
    129 			cmd.Err = command.ErrRedirect
    130 		default:
    131 			cmd.Err = errors.New("narrator doesn't need your input")
    132 		}
    133 		return
    134 	} else if b.isForNarrator(cmd) && b.healerID != nil && cmd.AuthUser.ID == *b.healerID {
    135 		select {
    136 		case b.healerCh <- cmd.Message:
    137 			cmd.Err = command.ErrRedirect
    138 		default:
    139 			cmd.Err = errors.New("narrator doesn't need your input")
    140 		}
    141 		return
    142 	}
    143 	cmd.Err = errors.New("chat disabled")
    144 	return
    145 }
    146 
    147 // Return either or not the message is a PM for the narrator
    148 func (b *Werewolf) isForNarrator(cmd *command.Command) bool {
    149 	return cmd.ToUser != nil && cmd.ToUser.ID == b.narratorID
    150 }
    151 
    152 func (b *Werewolf) InterceptVoteMsg(cmd *command.Command) {
    153 	if !b.isAlivePlayer(cmd.AuthUser.ID) || !b.isForNarrator(cmd) {
    154 		cmd.Err = errors.New("chat disabled")
    155 		return
    156 	}
    157 	if b.isForNarrator(cmd) {
    158 		if !b.voted.Contains(cmd.AuthUser.ID) {
    159 			name := cmd.Message
    160 			if b.isValidPlayerName(name) {
    161 				b.votesCh <- name
    162 			} else {
    163 				b.Narrate(ErrInvalidPlayerName.Error(), &cmd.AuthUser.ID, nil)
    164 			}
    165 		} else {
    166 			b.Narrate("You have already voted", &cmd.AuthUser.ID, nil)
    167 		}
    168 	}
    169 }
    170 
    171 var tuto = `Tutorial:
    172 "/join" to join the Game
    173 "/players" list the players that have joined the Game
    174 "/start" to start the Game
    175 "/stop" to stop the Game
    176 "/ready" will skip the 5min conversation
    177 "/tuto" will display this tutorial
    178 "/clear" will reset the room and display this tutorial
    179 
    180 Werewolf: To kill someone during the night, you have to reply in the "werewolf" group with the name of the person to kill (no @)
    181 Seer/Healer: You have reply to the narrator with the name (eg: "/pm 0 n0tr1v")
    182 Townspeople: To vote, you have to pm the narrator with a name (eg: "/pm 0 n0tr1v")`
    183 
    184 func (b *Werewolf) InterceptMsg(cmd *command.Command) {
    185 	if cmd.Room.ID != b.roomID {
    186 		return
    187 	}
    188 
    189 	SlashInterceptor{}.InterceptMsg(cmd)
    190 
    191 	// If the message is a PM not for the narrator, we reject it
    192 	if cmd.ToUser != nil && (cmd.ToUser.ID != b.narratorID && cmd.AuthUser.ID != b.narratorID) {
    193 		cmd.Err = errors.New("PM not allowed at this room")
    194 		return
    195 	}
    196 
    197 	// Spectator can chat all the time
    198 	if cmd.GroupID != nil && *cmd.GroupID == b.spectatorGroupID {
    199 		return
    200 	}
    201 
    202 	if cmd.AuthUser.IsModerator() && cmd.Message == "/stop" {
    203 		b.Narrate(fmt.Sprintf("@%s used /stop", cmd.AuthUser.Username), nil, nil)
    204 		b.cancel()
    205 		cmd.Err = command.ErrRedirect
    206 		return
    207 	} else if cmd.AuthUser.IsModerator() && cmd.Message == "/ready" {
    208 		b.Narrate(fmt.Sprintf("@%s used /ready", cmd.AuthUser.Username), nil, nil)
    209 		b.readyCh <- true
    210 		cmd.Err = command.ErrRedirect
    211 		return
    212 	} else if cmd.AuthUser.IsModerator() && cmd.Message == "/tuto" {
    213 		b.Narrate(tuto, nil, nil)
    214 		cmd.Err = command.ErrRedirect
    215 		return
    216 	} else if cmd.AuthUser.IsModerator() && cmd.Message == "/clear" {
    217 		_ = cmd.DB.DeleteChatRoomMessages(b.roomID)
    218 		b.Narrate(tuto, nil, nil)
    219 		cmd.Err = command.ErrRedirect
    220 		return
    221 	}
    222 
    223 	// Anyone can talk during these states
    224 	if b.state == PreGameState || b.state == EndGameState {
    225 		if b.state == PreGameState {
    226 			b.InterceptPreGameMsg(cmd)
    227 		}
    228 		return
    229 	}
    230 
    231 	// Otherwise, non-playing people cannot talk in public chat
    232 	if !b.isAlivePlayer(cmd.AuthUser.ID) {
    233 		cmd.Err = errors.New("public chat disabled")
    234 		return
    235 	}
    236 
    237 	switch b.state {
    238 	case DayState:
    239 	case VoteState:
    240 		b.InterceptVoteMsg(cmd)
    241 	case NightState:
    242 		b.InterceptNightMsg(cmd)
    243 	default:
    244 		cmd.Err = errors.New("public chat disabled")
    245 		return
    246 	}
    247 }
    248 
    249 // Wait until we receive the votes from all the players
    250 func (b *Werewolf) waitVotes() (votes []string) {
    251 	for len(votes) < len(b.playersAlive) {
    252 		var vote string
    253 		select {
    254 		case vote = <-b.votesCh:
    255 		case <-time.After(15 * time.Second):
    256 			b.Narrate(fmt.Sprintf("Waiting votes %d/%d", len(votes), len(b.playersAlive)), nil, nil)
    257 			continue
    258 		case <-b.ctx.Done():
    259 			return
    260 		}
    261 		votes = append(votes, vote)
    262 	}
    263 	return
    264 }
    265 
    266 func (b *Werewolf) waitNameFromWerewolf() (name string) {
    267 	for {
    268 		select {
    269 		case name = <-b.werewolfCh:
    270 		case <-time.After(15 * time.Second):
    271 			b.Narrate("Waiting reply from werewolf", nil, nil)
    272 			continue
    273 		case <-b.ctx.Done():
    274 			return
    275 		}
    276 		if b.isValidPlayerName(name) {
    277 			break
    278 		}
    279 		b.Narrate(ErrInvalidPlayerName.Error(), nil, &b.werewolfGroupID)
    280 	}
    281 	return name
    282 }
    283 
    284 func (b *Werewolf) waitNameFromSeer() (name string) {
    285 	for {
    286 		select {
    287 		case name = <-b.seerCh:
    288 		case <-time.After(15 * time.Second):
    289 			b.Narrate("Waiting reply from seer", nil, nil)
    290 			continue
    291 		case <-b.ctx.Done():
    292 			return
    293 		}
    294 		if b.isValidPlayerName(name) {
    295 			break
    296 		}
    297 		b.Narrate(ErrInvalidPlayerName.Error(), b.seerID, nil)
    298 	}
    299 	return name
    300 }
    301 
    302 func (b *Werewolf) waitNameFromHealer() (name string) {
    303 	for {
    304 		select {
    305 		case name = <-b.healerCh:
    306 		case <-time.After(15 * time.Second):
    307 			b.Narrate("Waiting reply from healer", nil, nil)
    308 			continue
    309 		case <-b.ctx.Done():
    310 			return
    311 		}
    312 		if b.isValidPlayerName(name) {
    313 			break
    314 		}
    315 		b.Narrate(ErrInvalidPlayerName.Error(), b.healerID, nil)
    316 	}
    317 	return name
    318 }
    319 
    320 // Return either a name is a valid alive player name or not
    321 func (b *Werewolf) isValidPlayerName(name string) bool {
    322 	name = strings.TrimSpace(name)
    323 	for _, player := range b.playersAlive {
    324 		if string(player.Username) == name {
    325 			return true
    326 		}
    327 	}
    328 	return false
    329 }
    330 
    331 // Narrate register a chat message on behalf of the narrator user
    332 func (b *Werewolf) Narrate(msg string, toUserID *database.UserID, groupID *database.GroupID) {
    333 	html, _, _ := dutils.ProcessRawMessage(b.db, msg, "", b.narratorID, b.roomID, nil, false, true, false)
    334 	b.NarrateRaw(html, toUserID, groupID)
    335 }
    336 
    337 func (b *Werewolf) NarrateRaw(msg string, toUserID *database.UserID, groupID *database.GroupID) {
    338 	_, _ = b.db.CreateOrEditMessage(nil, msg, msg, "", b.roomID, b.narratorID, toUserID, nil, groupID, false, false, false)
    339 }
    340 
    341 // Display roles assigned at beginning of the Game
    342 func (b *Werewolf) displayRoles() {
    343 	msg := "Roles were:\n"
    344 	for _, player := range b.players {
    345 		msg += player.Username.AtStr() + " : " + player.Role + "\n"
    346 	}
    347 	b.Narrate(msg, nil, nil)
    348 }
    349 
    350 func (b *Werewolf) StartGame(db *database.DkfDB) {
    351 	defer func() {
    352 		b.displayRoles()
    353 		b.reset()
    354 	}()
    355 	b.ctx, b.cancel = context.WithCancel(context.Background())
    356 	// Assign roles
    357 	playersArr := make([]*Player, 0)
    358 	for _, player := range b.playersAlive {
    359 		playersArr = append(playersArr, player)
    360 	}
    361 	rand.Shuffle(len(playersArr), func(i, j int) { playersArr[i], playersArr[j] = playersArr[j], playersArr[i] })
    362 	for idx, player := range playersArr {
    363 		if idx == 0 {
    364 			b.werewolfSet.Insert(player.UserID)
    365 			_, _ = db.AddUserToRoomGroup(b.roomID, b.werewolfGroupID, player.UserID)
    366 			player.Role = WerewolfRole
    367 			werewolfMsg := "During the day you seem to be a regular Townsperson.\n" +
    368 				"However, you’ve been kissed by the Night and transform into a Werewolf when the sun sets.\n" +
    369 				"Your new nature compels you to kill and eat a Townsperson every night."
    370 			b.Narrate(werewolfMsg, &player.UserID, nil)
    371 		} else if idx == 1 {
    372 			b.townspersonSet.Insert(player.UserID)
    373 			b.healerID = &player.UserID
    374 			player.Role = HealerRole
    375 			healerMsg := "You’re a Townsperson with the unique ability to save lives.\n" +
    376 				"During the night, you’ll get a chance to protect another Townsperson from death if they are attacked by the Werewolves.\n" +
    377 				"You can choose to protect yourself."
    378 			b.Narrate(healerMsg, &player.UserID, nil)
    379 		} else if idx == 2 {
    380 			b.townspersonSet.Insert(player.UserID)
    381 			b.seerID = &player.UserID
    382 			player.Role = SeerRole
    383 			seerMsg := "You’re a Townsperson with the unique ability to peer into a person’s soul and see their true nature.\n" +
    384 				"During the night, you’ll get a chance to see if another Townsperson is a Werewolf.\n" +
    385 				"However, use this information wisely because it can lead to you being targeted by the Werewolves the next night if they deduce your identity."
    386 			b.Narrate(seerMsg, &player.UserID, nil)
    387 		} else {
    388 			b.townspersonSet.Insert(player.UserID)
    389 			player.Role = TownspeopleRole
    390 			townspersonMsg := "You’re a regular member of the town.\n" +
    391 				"Perhaps you’re a baker, merchant, or soldier.\n" +
    392 				"Your job is to save the town by eliminating the Werewolves that have infiltrated your town and started feeding on your neighbors.\n" +
    393 				"Also, try to avoid getting killed yourself."
    394 			b.Narrate(townspersonMsg, &player.UserID, nil)
    395 		}
    396 	}
    397 	b.state = DayState
    398 	b.Narrate("players: "+b.alivePlayersStr(), nil, nil)
    399 	b.Narrate("Day 1: It is day time. players can now introduce themselves. (5min)", nil, nil)
    400 
    401 	select {
    402 	case <-time.After(5 * time.Minute):
    403 	case <-b.readyCh:
    404 	case <-b.ctx.Done():
    405 		b.Narrate("STOP SIGNAL - Game is being stopped", nil, nil)
    406 		return
    407 	}
    408 
    409 	for {
    410 		b.state = NightState
    411 		b.Narrate("Townspeople, go to sleep", nil, nil)
    412 		playerNameToKill := b.processWerewolf()
    413 		b.processSeer()
    414 		playerNameToSave := b.processHealer()
    415 
    416 		b.state = DayState
    417 		b.Narrate("Townspeople, wake up", nil, nil)
    418 		if playerNameToKill == playerNameToSave {
    419 			b.Narrate("Someone was attacked last night, but they survived", nil, nil)
    420 		} else {
    421 			b.Narrate("Everyone wakes up to see a trail of blood leading to the forest.\n"+
    422 				"There you find @"+playerNameToKill+"’s mangled remains by the Great Oak.\n"+
    423 				"Curiously, there are deep claw marks in the bark of the surrounding trees.\n"+
    424 				"It looks like @"+playerNameToKill+" put up a fight.", nil, nil)
    425 			b.kill(db, database.Username(playerNameToKill))
    426 		}
    427 
    428 		b.Narrate("players still alive: "+b.alivePlayersStr(), nil, nil)
    429 		if b.werewolfSet.Empty() {
    430 			b.Narrate("Townspeople win", nil, nil)
    431 			break
    432 		} else if b.townspersonSet.Len() <= 1 {
    433 			b.Narrate("Werewolf win", nil, nil)
    434 			break
    435 		}
    436 
    437 		b.Narrate("Townspeople now have 5min to discuss the events", nil, nil)
    438 
    439 		select {
    440 		case <-time.After(5 * time.Minute):
    441 		case <-b.readyCh:
    442 		case <-b.ctx.Done():
    443 			b.Narrate("STOP SIGNAL - Game is being stopped", nil, nil)
    444 			return
    445 		}
    446 
    447 		b.state = VoteState
    448 		b.voted = hashset.New[database.UserID]()
    449 		b.Narrate("It's now time to vote for execution. PM me the name you vote to execute or \"none\"", nil, nil)
    450 		killName := b.killVote()
    451 		if killName == "" {
    452 			b.Narrate("Townspeople do not want to execute anyone", nil, nil)
    453 		} else {
    454 			b.Narrate("Townspeople execute @"+killName, nil, nil)
    455 			b.kill(db, database.Username(killName))
    456 		}
    457 
    458 		b.Narrate("players still alive: "+b.alivePlayersStr(), nil, nil)
    459 
    460 		if b.werewolfSet.Empty() {
    461 			b.Narrate("Townspeople win", nil, nil)
    462 			break
    463 		} else if b.townspersonSet.Len() == 1 {
    464 			b.Narrate("Werewolf win", nil, nil)
    465 			break
    466 		}
    467 	}
    468 	b.state = EndGameState
    469 	b.Narrate("Game ended", nil, nil)
    470 }
    471 
    472 // Return the names of alive players. ie: "user1, user2, user3"
    473 func (b *Werewolf) alivePlayersStr() (out string) {
    474 	arr := make([]string, 0)
    475 	for _, player := range b.playersAlive {
    476 		arr = append(arr, player.Username.AtStr())
    477 	}
    478 	sort.Slice(arr, func(i, j int) bool { return arr[i] < arr[j] })
    479 	return strings.Join(arr, ", ")
    480 }
    481 
    482 // Kill a player
    483 func (b *Werewolf) kill(db *database.DkfDB, playerName database.Username) {
    484 	player, found := b.playersAlive[playerName]
    485 	if !found {
    486 		return
    487 	}
    488 	delete(b.playersAlive, playerName)
    489 	switch player.Role {
    490 	case WerewolfRole:
    491 		b.werewolfSet.Remove(player.UserID)
    492 		_ = db.RmUserFromRoomGroup(b.roomID, b.werewolfGroupID, player.UserID)
    493 	case TownspeopleRole:
    494 		b.townspersonSet.Remove(player.UserID)
    495 	case HealerRole:
    496 		b.townspersonSet.Remove(player.UserID)
    497 		b.healerID = nil
    498 	case SeerRole:
    499 		b.townspersonSet.Remove(player.UserID)
    500 		b.seerID = nil
    501 	}
    502 	_, _ = db.AddUserToRoomGroup(b.roomID, b.deadGroupID, player.UserID)
    503 }
    504 
    505 // Return the name of the player name that receive the most vote
    506 func (b *Werewolf) killVote() string {
    507 
    508 	// Send a PM to all players saying they have to vote for a name
    509 	for _, player := range b.playersAlive {
    510 		msg := "Who do you vote to kill? (name | none)"
    511 		msg += b.createKillVoteForm()
    512 		b.NarrateRaw(msg, &player.UserID, nil)
    513 	}
    514 
    515 	votes := b.waitVotes()
    516 	// Get the max voted name
    517 	maxName := "none"
    518 	maxCount := 0
    519 	voteMap := make(map[string]int) // keep track of how many votes for each values
    520 	for _, vote := range votes {
    521 		tmp := voteMap[vote]
    522 		tmp++
    523 		voteMap[vote] = tmp
    524 		if tmp > maxCount {
    525 			maxCount = tmp
    526 			maxName = vote
    527 		}
    528 	}
    529 	if maxName == "none" {
    530 		return ""
    531 	}
    532 	return maxName
    533 }
    534 
    535 func (b *Werewolf) getAlivePlayersArr(includeWerewolves bool) []database.Username {
    536 	arr := make([]database.Username, 0)
    537 	for _, player := range b.playersAlive {
    538 		if !includeWerewolves && b.werewolfSet.Contains(player.UserID) {
    539 			continue
    540 		}
    541 		arr = append(arr, player.Username)
    542 	}
    543 	sort.Slice(arr, func(i, j int) bool { return arr[i] < arr[j] })
    544 	return arr
    545 }
    546 
    547 func (b *Werewolf) createPickUserForm() string {
    548 	arr := b.getAlivePlayersArr(true)
    549 
    550 	htmlTmpl := `
    551 <form method="post" action="/api/v1/werewolf">
    552 	{{ range $idx, $p := .Arr }}
    553 		<input type="radio" ID="player{{ $idx }}" name="message" value="/pm 0 {{ $p }}" /><label for="player{{ $idx }}">{{ $p }}</label><br />
    554 	{{ end }}
    555 	<button type="submit" name="btn_submit">ok</button>
    556 </form>`
    557 	data := map[string]any{
    558 		"Arr": arr,
    559 	}
    560 	var buf bytes.Buffer
    561 	_ = utils.Must(template.New("").Parse(htmlTmpl)).Execute(&buf, data)
    562 	return buf.String()
    563 }
    564 
    565 func (b *Werewolf) createKillVoteForm() string {
    566 	arr := b.getAlivePlayersArr(true)
    567 
    568 	htmlTmpl := `
    569 <form method="post" action="/api/v1/werewolf">
    570 	{{ range $idx, $p := .Arr }}
    571 		<input type="radio" ID="player{{ $idx }}" name="message" value="/pm 0 {{ $p }}" /><label for="player{{ $idx }}">{{ $p }}</label><br />
    572 	{{ end }}
    573 	<input type="radio" ID="none" name="message" value="/pm 0 none" /><label for="none">none</label><br />
    574 	<button type="submit" name="btn_submit">ok</button>
    575 </form>`
    576 	data := map[string]any{
    577 		"Arr": arr,
    578 	}
    579 	var buf bytes.Buffer
    580 	_ = utils.Must(template.New("").Parse(htmlTmpl)).Execute(&buf, data)
    581 	return buf.String()
    582 }
    583 
    584 func (b *Werewolf) createWerewolfPickUserForm() string {
    585 	arr := b.getAlivePlayersArr(false)
    586 
    587 	htmlTmpl := `
    588 <form method="post" action="/api/v1/werewolf">
    589 	{{ range $idx, $p := .Arr }}
    590 		<input type="radio" ID="player{{ $idx }}" name="message" value="/g werewolf {{ $p }}" /><label for="player{{ $idx }}">{{ $p }}</label><br />
    591 	{{ end }}
    592 	<button type="submit" name="btn_submit">ok</button>
    593 </form>`
    594 	data := map[string]any{
    595 		"Arr": arr,
    596 	}
    597 	var buf bytes.Buffer
    598 	_ = utils.Must(template.New("").Parse(htmlTmpl)).Execute(&buf, data)
    599 	return buf.String()
    600 }
    601 
    602 func (b *Werewolf) processWerewolf() string {
    603 	b.UnlockGroup("werewolf")
    604 	msg := "Werewolf, who do you want to kill?"
    605 	msg += b.createWerewolfPickUserForm()
    606 	b.NarrateRaw(msg, nil, &b.werewolfGroupID)
    607 	name := b.waitNameFromWerewolf()
    608 	b.Narrate(name+" will be killed", nil, &b.werewolfGroupID)
    609 	b.LockGroup("werewolf")
    610 	return name
    611 }
    612 
    613 func (b *Werewolf) processSeer() {
    614 	if b.seerID == nil {
    615 		return
    616 	}
    617 	msg := "Seer, who do you want to identify?"
    618 	msg += b.createPickUserForm()
    619 	b.NarrateRaw(msg, b.seerID, nil)
    620 	name := b.waitNameFromSeer()
    621 	player := b.playersAlive[database.Username(name)]
    622 	b.Narrate(name+" is a "+player.Role, b.seerID, nil)
    623 }
    624 
    625 func (b *Werewolf) processHealer() string {
    626 	if b.healerID == nil {
    627 		return ""
    628 	}
    629 	msg := "Healer, who do you want to save?"
    630 	msg += b.createPickUserForm()
    631 	b.NarrateRaw(msg, b.healerID, nil)
    632 	name := b.waitNameFromHealer()
    633 	b.Narrate(name+" will survive the night", b.healerID, nil)
    634 	return name
    635 }
    636 
    637 func (b *Werewolf) LockGroups() {
    638 	b.LockGroup("werewolf")
    639 }
    640 
    641 func (b *Werewolf) LockGroup(groupName string) {
    642 	group, _ := b.db.GetRoomGroupByName(b.roomID, groupName)
    643 	group.Locked = true
    644 	group.DoSave(b.db)
    645 }
    646 
    647 func (b *Werewolf) UnlockGroup(groupName string) {
    648 	group, _ := b.db.GetRoomGroupByName(b.roomID, groupName)
    649 	group.Locked = false
    650 	group.DoSave(b.db)
    651 }
    652 
    653 type Player struct {
    654 	UserID   database.UserID
    655 	Username database.Username
    656 	Role     string
    657 }
    658 
    659 func (b *Werewolf) reset() {
    660 	b.ctx, b.cancel = context.WithCancel(context.Background())
    661 	b.state = PreGameState
    662 	b.players = make(map[database.Username]*Player)
    663 	b.playersAlive = make(map[database.Username]*Player)
    664 	b.werewolfSet = hashset.New[database.UserID]()
    665 	b.spectatorSet = hashset.New[database.UserID]()
    666 	b.townspersonSet = hashset.New[database.UserID]()
    667 	b.voted = hashset.New[database.UserID]()
    668 	b.werewolfCh = make(chan string)
    669 	b.seerCh = make(chan string)
    670 	b.healerCh = make(chan string)
    671 	b.votesCh = make(chan string)
    672 	b.readyCh = make(chan bool)
    673 	_ = b.db.ClearRoomGroup(b.roomID, b.werewolfGroupID)
    674 	_ = b.db.ClearRoomGroup(b.roomID, b.spectatorGroupID)
    675 	_ = b.db.ClearRoomGroup(b.roomID, b.deadGroupID)
    676 }
    677 
    678 func NewWerewolf(db *database.DkfDB) *Werewolf {
    679 	// Prepare room
    680 	room, err := db.GetChatRoomByName("werewolf")
    681 	if err != nil {
    682 		logrus.Error("#werewolf room not found")
    683 		return nil
    684 	}
    685 	zeroUser := dutils.GetZeroUser(db)
    686 	_ = db.DeleteChatRoomGroups(room.ID)
    687 	werewolfGroup, _ := db.CreateChatRoomGroup(room.ID, "werewolf", "#ffffff")
    688 	werewolfGroup.Locked = true
    689 	werewolfGroup.DoSave(db)
    690 	spectatorGroup, _ := db.CreateChatRoomGroup(room.ID, "spectator", "#ffffff")
    691 	deadGroup, _ := db.CreateChatRoomGroup(room.ID, "dead", "#ffffff")
    692 
    693 	b := new(Werewolf)
    694 	b.db = db
    695 	b.werewolfGroupID = werewolfGroup.ID
    696 	b.spectatorGroupID = spectatorGroup.ID
    697 	b.deadGroupID = deadGroup.ID
    698 	b.narratorID = zeroUser.ID
    699 	b.roomID = room.ID
    700 	b.reset()
    701 	return b
    702 }