Telegram bot for drafting MLP:CCG (@mlpdraftbot)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

510 lines
13 KiB

package main
import (
"fmt"
"log"
"strconv"
"strings"
"git.fromouter.space/mcg/draft/mlp"
"git.fromouter.space/mcg/draft"
"git.fromouter.space/hamcha/tg"
lobby "git.fromouter.space/mcg/cardgage/lobby/proto"
room "git.fromouter.space/mcg/cardgage/room/api"
"git.fromouter.space/mcg/draftbot"
)
type draftUser struct {
ID int64
Session string
}
const (
positionEven = "Evenly spaced"
positionRandom = "At random"
)
const (
stepPlayerCount = iota
stepSpacing = iota
stepDraftType = iota
stepDraftInfo = iota
//stepPackCount = iota
//stepExtraPackCount = iota // Only for I8PCube
stepDone = iota
)
type draftSession struct {
Step int
Options draftbot.SessionOptions
// Session open message
SessionOpenMessage int64
// Pick message
CurrentPack int
PickDone []string
PickWaiting []string
PickMessage int64
PickNum int
}
type tgInterface struct {
client *tg.Telegram
bot *draftbot.DraftBot
usermap map[string]*draftUser
sessions map[int64]*draftSession
}
// Draft types
const (
DTSetDraft = "Set draft"
DTBlockDraft = "Block draft (BROKEN!)"
DTCubeDraft = "Cube draft"
DTI8PCube = "Seeded cube"
)
const (
blockPremiere = "Premiere"
blockOdyssey = "Odyssey"
blockDefenders = "Defenders"
)
func (tgi *tgInterface) Send(msg room.BotMessage) {
// Get room ID
chatid, err := strconv.ParseInt(msg.RoomID, 10, 64)
if err != nil {
log.Printf("[WARN] Trying to send message to weird place: \"%s\". Dropped.\n", msg.RoomID)
return
}
// For now, skip non-messages
if msg.Type != room.MsgMessage {
//TODO
return
}
// Are we sending a message to a player in particular?
if msg.Message.To != "" {
chatid = tgi.usermap[msg.Message.To].ID
}
switch msg.Message.Type {
case "draft-newpack":
tgi.sessions[chatid].CurrentPack++
tgi.sessions[chatid].PickNum = 0
tgi.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: chatid,
Text: msg.Message.Message,
})
tgi.sessions[chatid].PickMessage = 0
case "draft-newpick":
tgi.sessions[chatid].PickNum++
tgi.sessions[chatid].PickDone = []string{}
tgi.sessions[chatid].PickWaiting = playerNames(tgi.bot.Sessions[msg.RoomID].Players)
if tgi.sessions[chatid].PickMessage != 0 {
tgi.client.EditText(tg.ClientEditTextData{
ChatID: chatid,
MessageID: tgi.sessions[chatid].PickMessage,
Text: txtCurrentPick(tgi.sessions[chatid]),
})
} else {
msg, _ := tgi.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: chatid,
Text: txtCurrentPick(tgi.sessions[chatid]),
})
tgi.sessions[chatid].PickMessage = msg.MessageID
}
case "card-picked":
picked := msg.Message.Data.(struct {
Player string
})
player := "@" + picked.Player
tgi.sessions[chatid].PickDone = append(tgi.sessions[chatid].PickDone, player)
tgi.sessions[chatid].PickWaiting = removeItem(tgi.sessions[chatid].PickWaiting, player)
tgi.client.EditText(tg.ClientEditTextData{
ChatID: chatid,
MessageID: tgi.sessions[chatid].PickMessage,
Text: txtCurrentPick(tgi.sessions[chatid]),
})
case "draft-availablepicks":
cards := msg.Message.Data.(draft.Pack)
cardphotos := []tg.APIInputMediaPhoto{}
buttons := []tg.APIKeyboardButton{}
for _, card := range cards {
cardphotos = append(cardphotos, tg.APIInputMediaPhoto{
Type: "photo",
Media: fmt.Sprintf("http://ponyhead.com/img/cards/%s.jpg", card.ID),
})
buttons = append(buttons, tg.APIKeyboardButton{
Text: card.ID,
})
}
// If sending more than 10 cards, split in two messages
if len(cardphotos) > 10 {
half := len(cardphotos) / 2
tgi.client.SendAlbum(tg.ClientAlbumData{
ChatID: chatid,
Silent: true,
Media: cardphotos[:half],
})
tgi.client.SendAlbum(tg.ClientAlbumData{
ChatID: chatid,
Silent: true,
Media: cardphotos[half:],
})
} else {
tgi.client.SendAlbum(tg.ClientAlbumData{
ChatID: chatid,
Silent: true,
Media: cardphotos,
})
}
tgi.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: chatid,
Text: "Choose your pick:",
ReplyMarkup: &tg.APIReplyKeyboardMarkup{
Keyboard: rebalance(buttons),
OneTime: true,
Resize: true,
},
})
case "session-open":
opt := msg.Message.Data.(draftbot.SessionOptions)
tgi.sessions[chatid].Options = opt
msg, _ := tgi.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: chatid,
Text: txtDraftSessionCreated(opt,
tgi.bot.Sessions[msg.RoomID].Players),
})
tgi.sessions[chatid].SessionOpenMessage = msg.MessageID
case "player-joined-session":
tgi.client.EditText(tg.ClientEditTextData{
ChatID: chatid,
MessageID: tgi.sessions[chatid].SessionOpenMessage,
Text: txtDraftSessionCreated(tgi.sessions[chatid].Options,
tgi.bot.Sessions[msg.RoomID].Players),
})
case "draft-order":
players := msg.Message.Data.([]string)
tgi.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: chatid,
Text: txtDraftSessionOrder(players),
})
case "draft-picks":
picks := msg.Message.Data.([]string)
tgi.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: chatid,
Text: txtYourPicks(picks),
})
default:
tgi.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: chatid,
Text: fmt.Sprintf("[%s] %s", msg.Message.Type, msg.Message.Message),
})
}
}
func (tgi *tgInterface) OnMessage(msg tg.APIMessage) {
switch msg.Chat.Type {
case tg.ChatTypeGroup, tg.ChatTypeSupergroup:
if msg.Text != nil {
if isCommand(msg, "newdraft") {
tgi.NewDraft(msg)
return
}
if isCommand(msg, "join") {
tgi.JoinDraft(msg.Chat, msg.User)
return
}
if isCommand(msg, "startdraft") {
tgi.StartDraft(msg.Chat, msg.User)
return
}
if session, ok := tgi.sessions[msg.Chat.ChatID]; ok && session.Step != stepDone {
tgi.nextCreateDraftStep(msg)
return
}
}
case tg.ChatTypePrivate:
if user, ok := tgi.usermap[msg.User.Username]; ok && user.Session != "" {
if msg.Text != nil && (*msg.Text)[0] != '/' {
tgi.PickCard(msg.User, *msg.Text)
}
}
}
}
func (tgi *tgInterface) Bind(bot *draftbot.DraftBot) {
tgi.bot = bot
}
func (tgi *tgInterface) message(user tg.APIUser, session string, typ string, data interface{}) {
tgi.usermap[user.Username] = &draftUser{
ID: user.UserID,
Session: session,
}
tgi.bot.OnMessage(room.ServerMessage{
RoomID: session,
Type: room.MsgMessage,
Message: &room.Message{
From: user.Username,
To: botname,
Type: typ,
Data: data,
},
})
}
func (tgi *tgInterface) NewDraft(msg tg.APIMessage) {
chat := msg.Chat
author := msg.User
chatid := strconv.FormatInt(chat.ChatID, 10)
tgi.bot.OnMessage(room.ServerMessage{
RoomID: chatid,
Type: room.MsgEvent,
Event: &room.Event{
Type: room.EvtNewRoom,
Room: &lobby.Room{
Id: chatid,
Name: *chat.Title,
Creator: author.Username,
},
},
})
// Open interactive dialog
tgi.sessions[chat.ChatID] = &draftSession{
Step: stepPlayerCount,
Options: draftbot.SessionOptions{
Options: draftbot.DraftOptions{
// For now let's make these non-modifiable
PackCount: 4,
PackSize: 12,
MainCount: 4,
ProblemCount: 1,
},
},
}
tgi.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: msg.Chat.ChatID,
Text: "How many players will be partecipating in the draft?",
ReplyID: &msg.MessageID,
})
}
func (tgi *tgInterface) nextCreateDraftStep(msg tg.APIMessage) {
session, ok := tgi.sessions[msg.Chat.ChatID]
if !ok {
return
}
switch session.Step {
case stepPlayerCount:
n, err := strconv.Atoi(*msg.Text)
if err != nil {
tgi.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: msg.Chat.ChatID,
Text: "That's not a valid number!",
ReplyID: &msg.MessageID,
})
return
}
if n < 1 || n > 8 {
tgi.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: msg.Chat.ChatID,
Text: "Please choose a reasonable number. (1-8)",
ReplyID: &msg.MessageID,
})
return
}
session.Options.Players = n
tgi.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: msg.Chat.ChatID,
Text: "How should player be positioned in the pod (assuming bots)?",
ReplyID: &msg.MessageID,
ReplyMarkup: tg.APIReplyKeyboardMarkup{
Selective: true,
OneTime: true,
Keyboard: rebalance([]tg.APIKeyboardButton{
{Text: positionEven},
{Text: positionRandom},
}),
},
})
session.Step = stepSpacing
case stepSpacing:
switch *msg.Text {
case positionEven:
session.Options.Options.Positioning = draftbot.PosEven
case positionRandom:
session.Options.Options.Positioning = draftbot.PosRandom
default:
tgi.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: msg.Chat.ChatID,
Text: "Sorry, valid choices are: " + strings.Join([]string{positionEven, positionRandom}, ", "),
ReplyID: &msg.MessageID,
ReplyMarkup: tg.APIReplyKeyboardMarkup{
Selective: true,
OneTime: true,
Keyboard: rebalance([]tg.APIKeyboardButton{
{Text: positionEven},
{Text: positionRandom},
}),
},
})
return
}
tgi.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: msg.Chat.ChatID,
Text: "What type of draft do you want?",
ReplyID: &msg.MessageID,
ReplyMarkup: tg.APIReplyKeyboardMarkup{
Selective: true,
OneTime: true,
Keyboard: rebalance([]tg.APIKeyboardButton{
{Text: DTSetDraft},
{Text: DTBlockDraft},
{Text: DTCubeDraft},
{Text: DTI8PCube},
}),
},
})
session.Step = stepDraftType
case stepDraftType:
switch *msg.Text {
case DTSetDraft:
session.Options.Options.Type = draftbot.DraftSet
tgi.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: msg.Chat.ChatID,
Text: "What set do you want to draft?",
ReplyID: &msg.MessageID,
ReplyMarkup: tg.APIReplyKeyboardMarkup{
Selective: true,
OneTime: true,
Keyboard: rebalance([]tg.APIKeyboardButton{
{Text: string(mlp.SetPremiere)},
{Text: string(mlp.SetCanterlotNights)},
{Text: string(mlp.SetCrystalGames)},
{Text: string(mlp.SetAbsoluteDiscord)},
{Text: string(mlp.SetEquestrialOdysseys)},
{Text: string(mlp.SetHighMagic)},
{Text: string(mlp.SetMarksInTime)},
{Text: string(mlp.SetDefendersOfEquestria)},
{Text: string(mlp.SetSeaquestriaBeyond)},
{Text: string(mlp.SetFriendsForever)},
}),
},
})
session.Step = stepDraftInfo
case DTBlockDraft:
session.Options.Options.Type = draftbot.DraftBlock
tgi.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: msg.Chat.ChatID,
Text: "What block do you want to draft?",
ReplyID: &msg.MessageID,
ReplyMarkup: tg.APIReplyKeyboardMarkup{
Selective: true,
OneTime: true,
Keyboard: rebalance([]tg.APIKeyboardButton{
{Text: blockPremiere},
{Text: blockOdyssey},
{Text: blockDefenders},
}),
},
})
session.Step = stepDraftInfo
case DTCubeDraft:
session.Options.Options.Type = draftbot.DraftCube
tgi.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: msg.Chat.ChatID,
Text: "Write the URL of a file containing a line-separated list of cards in the cube",
ReplyID: &msg.MessageID,
})
session.Step = stepDraftInfo
case DTI8PCube:
session.Options.Options.Type = draftbot.DraftI8PCube
tgi.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: msg.Chat.ChatID,
Text: "Write the URL of a I8P-JSON cube",
ReplyID: &msg.MessageID,
})
session.Step = stepDraftInfo
default:
tgi.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: msg.Chat.ChatID,
Text: "Sorry, valid choices are: " + strings.Join([]string{DTSetDraft, DTBlockDraft, DTCubeDraft, DTI8PCube}, " "),
ReplyID: &msg.MessageID,
})
return
}
case stepDraftInfo:
switch session.Options.Options.Type {
case draftbot.DraftSet:
session.Options.Options.Set = mlp.SetID(*msg.Text)
case draftbot.DraftBlock:
session.Options.Options.Block = mlp.BlockID(*msg.Text)
case draftbot.DraftCube, draftbot.DraftI8PCube:
// Check for a valid URL
session.Options.Options.CubeURL = *msg.Text
}
chatid := strconv.FormatInt(msg.Chat.ChatID, 10)
tgi.message(msg.User, chatid, "create", session.Options)
session.Step = stepDone
}
}
func (tgi *tgInterface) OpenDraft(chat *tg.APIChat, author tg.APIUser, options draftbot.SessionOptions) {
chatid := strconv.FormatInt(chat.ChatID, 10)
tgi.message(author, chatid, "create", options)
}
func (tgi *tgInterface) JoinDraft(chat *tg.APIChat, usr tg.APIUser) {
chatid := strconv.FormatInt(chat.ChatID, 10)
tgi.message(usr, chatid, "join", nil)
}
func (tgi *tgInterface) StartDraft(chat *tg.APIChat, usr tg.APIUser) {
chatid := strconv.FormatInt(chat.ChatID, 10)
tgi.message(usr, chatid, "start", nil)
}
func (tgi *tgInterface) PickCard(usr tg.APIUser, cardid string) {
tgi.message(usr, tgi.usermap[usr.Username].Session, "pick", cardid)
}
func rebalance(buttons []tg.APIKeyboardButton) [][]tg.APIKeyboardButton {
out := [][]tg.APIKeyboardButton{}
currentRow := []tg.APIKeyboardButton{}
var perRow int
switch {
case len(buttons) < 5:
perRow = 2
case len(buttons) < 10:
perRow = 3
default:
perRow = 4
}
for i := range buttons {
currentRow = append(currentRow, buttons[i])
if i%perRow == (perRow - 1) {
out = append(out, currentRow)
currentRow = []tg.APIKeyboardButton{}
}
}
if len(currentRow) > 0 {
out = append(out, currentRow)
}
return out
}
func removeItem(lst []string, item string) []string {
for i := range lst {
if lst[i] == item {
return append(lst[:i], lst[i+1:]...)
}
}
return lst
}