Skip to content

Commit

Permalink
move story tellers to the same cottage in the night
Browse files Browse the repository at this point in the history
  • Loading branch information
zku committed Jun 22, 2024
1 parent 869cddb commit e3fd064
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 21 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ require (
require (
github.com/gorilla/websocket v1.4.2 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/sys v0.15.0 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
Expand Down
59 changes: 47 additions & 12 deletions mover/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"time"

"github.com/bwmarrin/discordgo"
"golang.org/x/exp/slices"
)

// Bot is a BotC multi-bot voice channel mover.
Expand Down Expand Up @@ -212,8 +213,9 @@ func (b *Bot) prepareNightMoves(ctx context.Context, s discordSession, i *discor
}

// Anyone who isn't already in a private night time cottage needs to move.
var storyTellerCottageID string
fullCottageIDs := make(map[string]bool)
var userNeedsMove []string
var userNeedsMove []*discordgo.Member
for _, member := range vs.members {
userVoiceState := vs.userToVoiceState[member.User.ID]
if userVoiceState != nil && userVoiceState.ChannelID != "" {
Expand All @@ -222,9 +224,12 @@ func (b *Bot) prepareNightMoves(ctx context.Context, s discordSession, i *discor
// This only happens if a new player joins during the night phase.
if nightCottageChannelIDs[userVoiceState.ChannelID] {
fullCottageIDs[userVoiceState.ChannelID] = true
if storyTellerCottageID == "" && member.User.ID == i.Member.User.ID {
storyTellerCottageID = userVoiceState.ChannelID
}
} else {
// Otherwise, they need to move.
userNeedsMove = append(userNeedsMove, member.User.ID)
userNeedsMove = append(userNeedsMove, member)
}
}
}
Expand All @@ -233,15 +238,40 @@ func (b *Bot) prepareNightMoves(ctx context.Context, s discordSession, i *discor
return fmt.Errorf("not enough cottages available, need %d user movements but only have %d empty cottages", len(userNeedsMove), len(nightCottageChannelIDs)-len(fullCottageIDs))
}

// Find the story teller role ID.
allRoles, err := s.GuildRoles(i.GuildID, discordgo.WithContext(ctx))
if err != nil {
return fmt.Errorf("cannot fetch guild roles: %w", err)
}
var storyTellerRoleID string
for _, role := range allRoles {
if role.Name == b.cfg.StoryTellerRole {
storyTellerRoleID = role.ID
break
}
}
if storyTellerRoleID == "" {
return fmt.Errorf("cannot determine story teller role ID for name %s", b.cfg.StoryTellerRole)
}

// Build the movement plan.
plan := make(map[string]string)
for _, user := range userNeedsMove {
for _, member := range userNeedsMove {
isStoryTeller := slices.Contains(member.Roles, storyTellerRoleID)
if isStoryTeller && storyTellerCottageID != "" {
// Move story tellers into the same cottage at night.
plan[member.User.ID] = storyTellerCottageID
continue
}
for _, cottage := range vs.cottages {
if fullCottageIDs[cottage.ID] {
continue // This cottage is already full.
}
plan[user] = cottage.ID
plan[member.User.ID] = cottage.ID
fullCottageIDs[cottage.ID] = true
if isStoryTeller {
storyTellerCottageID = cottage.ID
}
break
}
}
Expand Down Expand Up @@ -314,17 +344,22 @@ func (b *Bot) checkUserIsStoryTeller(ctx context.Context, s discordSession, guil
if err != nil {
return fmt.Errorf("cannot fetch guild roles: %w", err)
}
roleIDToName := make(map[string]string)

var storyTellerRoleID string
for _, role := range allRoles {
roleIDToName[role.ID] = role.Name
if role.Name == b.cfg.StoryTellerRole {
storyTellerRoleID = role.ID
break
}
}

// User must be a story teller.
for _, roleID := range member.Roles {
if b.cfg.StoryTellerRole == roleIDToName[roleID] {
// Found it.
return nil
}
if storyTellerRoleID == "" {
return fmt.Errorf("cannot find story teller role %s among %#v", b.cfg.StoryTellerRole, allRoles)
}

if slices.Contains(member.Roles, storyTellerRoleID) {
// User is a story teller.
return nil
}

return fmt.Errorf("user %v (%v) is not a story teller", member.User.Username, member.DisplayName())
Expand Down
43 changes: 34 additions & 9 deletions mover/bot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func (f *fakeDiscordSession) StateGuild(guildID string) (*discordgo.Guild, error
{UserID: "user2", ChannelID: "inn"},
{UserID: "user3", ChannelID: "barber"},
{UserID: "storyteller", ChannelID: "barber"},
{UserID: "storyteller2", ChannelID: "library"},
},
}, nil
}
Expand All @@ -56,10 +57,11 @@ func (f *fakeDiscordSession) GuildMembers(guildID string, after string, limit in
}

return []*discordgo.Member{
{User: &discordgo.User{ID: "user1"}},
{User: &discordgo.User{ID: "user2"}},
{User: &discordgo.User{ID: "user3"}},
{User: &discordgo.User{ID: "storyteller"}},
{User: &discordgo.User{ID: "user1"}, Roles: []string{"role1"}},
{User: &discordgo.User{ID: "user2"}, Roles: []string{"role1"}},
{User: &discordgo.User{ID: "user3"}, Roles: []string{"role1"}},
{User: &discordgo.User{ID: "storyteller"}, Roles: []string{"role1", "storyteller"}},
{User: &discordgo.User{ID: "storyteller2"}, Roles: []string{"role1", "storyteller"}},
}, nil
}

Expand Down Expand Up @@ -162,9 +164,10 @@ func TestPrepareDayMoves(t *testing.T) {
}

want := map[string]string{
"user2": "townsquare",
"user3": "townsquare",
"storyteller": "townsquare",
"user2": "townsquare",
"user3": "townsquare",
"storyteller": "townsquare",
"storyteller2": "townsquare",
}

select {
Expand Down Expand Up @@ -210,14 +213,36 @@ func TestPrepareNightMoves(t *testing.T) {

select {
case plan := <-b.ch:
if len(plan.moves) != 4 {
t.Fatalf("Expected 4 movements, got %#v", plan.moves)
t.Logf("Movement plan: %#v", plan.moves)
if len(plan.moves) != 5 {
t.Fatalf("Expected 5 movements, got %#v", plan.moves)
}

var storyTellerCottageID string
fullCottageIDs := make(map[string]bool)
for user, channel := range plan.moves {
if !strings.HasPrefix(channel, "cottage") {
t.Fatalf("Expected all players to move to cottages, received move %s -> %s instead", user, channel)
}
if strings.HasPrefix(user, "storyteller") {
if storyTellerCottageID == "" {
storyTellerCottageID = channel
} else {
// Check that all STs are in the same cottage. The only time this would not happen is if
// multiple STs are already in different cottages during the night when a new night move
// is initiated.
if channel != storyTellerCottageID {
t.Fatalf("Expected story teller %s to be in the story teller cottage %s, but instead is moved to %s", user, storyTellerCottageID, channel)
}
}
} else {
if fullCottageIDs[channel] {
t.Fatalf("Attempted to move 2 non-story tellers to the same cottage %s", channel)
}
fullCottageIDs[channel] = true
}
}

default:
t.Fatal("Expected to receive plan, got nothing.")
}
Expand Down

0 comments on commit e3fd064

Please sign in to comment.