Skip to content

Commit 709fc58

Browse files
committed
added player lists for secret santa
1 parent ec9dc87 commit 709fc58

File tree

6 files changed

+201
-0
lines changed

6 files changed

+201
-0
lines changed

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@ cake4everybot
1717

1818
# configuration files for tokens, access keys and other connection data
1919
*env.yaml
20+
21+
# runtime config files (as described in config.yml)
22+
modules/secretsanta/players.json
23+
twitch/prizes.json
24+
twitch/times.json

config.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ event:
3434
adventcalendar:
3535
images: data/images/adventcalendar
3636

37+
secretsanta:
38+
# the filepath for the players
39+
players: modules/secretsanta/players.json
40+
3741
twitch_giveaway:
3842
# The amount of points a single giveaway ticket costs.
3943
ticket_cost: 1000

data/lang/de.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ discord.command:
131131

132132
msg.setup.no_reactions: Diese Nachricht hat keine Reaktionen. Nur Leute, die mit %s reagiert haben, werden eingeschlossen.
133133
msg.setup.not_enough_reactions: Nicht genug Reaktionen um zu starten. Es werden mindestens %d Reaktionen benötigt.
134+
msg.setup.users: Teilnehmer
134135

135136
module:
136137
adventcalendar:

data/lang/en.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ discord.command:
131131

132132
msg.setup.no_reactions: This message doesn't have any vote reactions. Only members who reated with %s are included.
133133
msg.setup.not_enough_reactions: Not enough votes to start a game. At least %d votes are required.
134+
msg.setup.users: Members
134135

135136
module:
136137
adventcalendar:

modules/secretsanta/handlerMessageSetup.go

+38
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package secretsanta
33
import (
44
"cake4everybot/data/lang"
55
"cake4everybot/util"
6+
"fmt"
67

8+
"github.com/bwmarrin/discordgo"
79
)
810

911
func (cmd MsgCmd) handler() {
@@ -43,4 +45,40 @@ func (cmd MsgCmd) handler() {
4345
cmd.ReplyHiddenf(lang.GetDefault(tp+"msg.setup.not_enough_reactions"), 2)
4446
return
4547
}
48+
49+
e := &discordgo.MessageEmbed{
50+
Title: lang.GetDefault(tp + "title"),
51+
Color: 0x690042,
52+
}
53+
54+
var (
55+
names string
56+
players []*player = make([]*player, 0, len(users))
57+
)
58+
for _, u := range users {
59+
member, err := cmd.Session.GuildMember(cmd.Interaction.GuildID, u.ID)
60+
if member == nil {
61+
log.Printf("WARN: Could not get member '%s' from guild '%s': %v", u.ID, cmd.Interaction.GuildID, err)
62+
continue
63+
}
64+
players = append(players, &player{Member: member})
65+
names += fmt.Sprintf("%s\n", member.Mention())
66+
}
67+
if len(players) < 2 {
68+
cmd.ReplyHiddenf(lang.GetDefault(tp+"msg.setup.not_enough_reactions"), 2)
69+
return
70+
}
71+
util.AddEmbedField(e, lang.GetDefault(tp+"msg.setup.users"), names, false)
72+
73+
players = DerangementMatch(players)
74+
75+
err = cmd.setPlayers(players)
76+
if err != nil {
77+
log.Printf("Error on set players: %v\n", err)
78+
cmd.ReplyError()
79+
return
80+
}
81+
82+
util.SetEmbedFooter(cmd.Session, tp+"display", e)
83+
cmd.ReplyHiddenEmbed(e)
4684
}

modules/secretsanta/secretsantabase.go

+152
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@ package secretsanta
22

33
import (
44
"cake4everybot/util"
5+
"encoding/json"
6+
"fmt"
57
logger "log"
8+
"os"
69

710
"github.com/bwmarrin/discordgo"
11+
"github.com/spf13/viper"
12+
"golang.org/x/exp/rand"
813
)
914

1015
const (
@@ -20,3 +25,150 @@ type secretSantaBase struct {
2025
member *discordgo.Member
2126
user *discordgo.User
2227
}
28+
29+
// getPlayers returns the list of players for the current guild. If it is the first time, it loads
30+
// the players from the file or creates an empty file.
31+
func (ssb secretSantaBase) getPlayers() ([]*player, error) {
32+
if allPlayers != nil {
33+
return allPlayers[ssb.Interaction.GuildID], nil
34+
}
35+
36+
log.Println("First time getting players. Loading from file...")
37+
playersPath := viper.GetString("event.secretsanta.players")
38+
playersData, err := os.ReadFile(playersPath)
39+
if err != nil {
40+
if !os.IsNotExist(err) {
41+
return nil, fmt.Errorf("read players file: %v", err)
42+
}
43+
allPlayers = make(AllPlayers)
44+
playersData, err = json.Marshal(allPlayers)
45+
if err != nil {
46+
return nil, fmt.Errorf("marshal players file: %v", err)
47+
}
48+
err = os.WriteFile(playersPath, playersData, 0644)
49+
if err != nil {
50+
return nil, fmt.Errorf("write players file: %v", err)
51+
}
52+
log.Printf("Created players file: %s\n", playersPath)
53+
return []*player{}, nil
54+
}
55+
allPlayersUnresolved := AllPlayersUnresolved{}
56+
err = json.Unmarshal(playersData, &allPlayersUnresolved)
57+
if err != nil {
58+
return nil, fmt.Errorf("unmarshal players file: %v", err)
59+
}
60+
err = allPlayersUnresolved.Resolve(ssb.Session)
61+
if err != nil {
62+
return nil, fmt.Errorf("resolve players file: %v", err)
63+
}
64+
log.Printf("Got %d guilds from file", len(allPlayers))
65+
66+
return allPlayers[ssb.Interaction.GuildID], nil
67+
}
68+
69+
// setPlayers sets the players for the current guild.
70+
func (ssb secretSantaBase) setPlayers(players []*player) (err error) {
71+
if _, err = ssb.getPlayers(); err != nil {
72+
return err
73+
}
74+
75+
allPlayers[ssb.Interaction.GuildID] = players
76+
playersData, err := json.Marshal(allPlayers)
77+
if err != nil {
78+
return fmt.Errorf("marshal players file: %v", err)
79+
}
80+
err = os.WriteFile(viper.GetString("event.secretsanta.players"), playersData, 0644)
81+
if err != nil {
82+
return fmt.Errorf("write players file: %v", err)
83+
}
84+
return nil
85+
}
86+
87+
// player is a player in the secret santa game
88+
type player struct {
89+
*discordgo.Member
90+
91+
// Match is the matched player
92+
Match *player
93+
// Address is the address of the player
94+
Address string
95+
}
96+
97+
type playerUnresolved struct {
98+
ID string `json:"id"`
99+
MatchID string `json:"match"`
100+
Address string `json:"address"`
101+
}
102+
103+
// AllPlayers is a map from guild ID to a list of players
104+
type AllPlayers map[string][]*player
105+
106+
// allPlayers is the current state of all players.
107+
// See [AllPlayers]
108+
var allPlayers AllPlayers
109+
110+
// MarshalJSON implements json.Marshaler
111+
func (allPlayers AllPlayers) MarshalJSON() ([]byte, error) {
112+
m := make(AllPlayersUnresolved)
113+
for guildID, players := range allPlayers {
114+
for _, player := range players {
115+
m[guildID] = append(m[guildID], &playerUnresolved{
116+
ID: player.User.ID,
117+
MatchID: player.Match.User.ID,
118+
Address: player.Address,
119+
})
120+
}
121+
122+
}
123+
return json.Marshal(m)
124+
}
125+
126+
// AllPlayersUnresolved is a map from guild ID to a list of unresolved players.
127+
// Unresolved players have no member but only an ID
128+
type AllPlayersUnresolved map[string][]*playerUnresolved
129+
130+
// Resolve resolves allPlayersUnresolved into allPlayers
131+
func (allPlayersUnresolved AllPlayersUnresolved) Resolve(s *discordgo.Session) (err error) {
132+
allPlayers = make(AllPlayers)
133+
for guildID, unresolvedPlayers := range allPlayersUnresolved {
134+
resolvedPlayers := map[string]*player{}
135+
for _, up := range unresolvedPlayers {
136+
member, err := s.GuildMember(guildID, up.ID)
137+
if err != nil {
138+
return fmt.Errorf("failed to get guild member %s/%s: %v", guildID, up.ID, err)
139+
}
140+
resolvedPlayers[up.ID] = &player{
141+
Member: member,
142+
Match: resolvedPlayers[up.MatchID],
143+
Address: up.Address,
144+
}
145+
}
146+
for _, rp := range resolvedPlayers {
147+
if rp.Match != nil {
148+
continue
149+
}
150+
rp.Match = resolvedPlayers[rp.User.ID]
151+
152+
allPlayers[guildID] = append(allPlayers[guildID], rp)
153+
}
154+
}
155+
return nil
156+
}
157+
158+
// DerangementMatch matches the players in a way that no one gets matched to themselves.
159+
func DerangementMatch(players []*player) []*player {
160+
n := len(players)
161+
players2 := make([]*player, n)
162+
copy(players2, players)
163+
164+
for i := 0; i < n-1; i++ {
165+
j := i + rand.Intn(n-i-1) + 1
166+
players2[i], players2[j] = players2[j], players2[i]
167+
}
168+
169+
for i, item := range players {
170+
item.Match = players2[i]
171+
}
172+
173+
return players
174+
}

0 commit comments

Comments
 (0)