Move bot to its own repo

This commit is contained in:
Hamcha 2019-08-26 11:20:00 +02:00
commit 6bce4375da
Signed by: hamcha
GPG key ID: 44AD3571EB09A39E
11 changed files with 1680 additions and 0 deletions

37
bot/bot.go Normal file
View file

@ -0,0 +1,37 @@
package bot
import (
"math/rand"
"git.fromouter.space/mcg/draft"
)
// Bot implements a bot for filling spots in draft pods
type Bot struct {
player *draft.Player
// Currently unused, the bot just picks at random.
// In the future, these fields will be used for having the bot try to assemble
// a sort-of playable deck. The actual deck doesn't matter, just the fact
// that their picks make some sort of sense.
color1 string
color2 string
entryCount int
friendCount int
otherCount int
}
// MakeBot returns a bot for a given pod spot
func MakeBot(player *draft.Player) *Bot {
bot := &Bot{
player: player,
}
return bot
}
// PickNext makes the bot pick a card from his pack
func (b *Bot) PickNext() {
// For now, just pick a card at random
cardid := rand.Intn(len(b.player.CurrentPack))
b.player.Pick(b.player.CurrentPack[cardid])
}

66
bot/bot_test.go Normal file
View file

@ -0,0 +1,66 @@
package bot_test
import (
"testing"
"git.fromouter.space/mcg/mlp-server-tools/draftbot/bot"
"git.fromouter.space/mcg/draft"
)
const PACKSIZE = 5
// Test set that can be used by tests that don't need special features (like alternates)
var testSet = &draft.GenericSet{
Cards: []draft.Card{{ID: "a"}, {ID: "b"}, {ID: "c"}},
PackSize: PACKSIZE,
}
func TestPick(t *testing.T) {
const PacksPerPlayer = 3
const PlayersPerPod = 5
// Get provider for test set
testProvider := draft.PacksFromSet(PacksPerPlayer, testSet)
// Create pod
pod := draft.MakePod(PlayersPerPod, testProvider)
// Create a bot for each player in the pod
var bots []*bot.Bot
for _, player := range pod.Players {
bots = append(bots, bot.MakeBot(player))
}
// Simulate a round of drafting
// Repeat until all packs are gone
// Channels are tested when they should trigger
for packnum := 0; packnum < PacksPerPlayer; packnum++ {
// Open new packs!
err := pod.OpenPacks()
if err != nil {
t.Fatalf("Got an error while opening packs #%d: %s", packnum, err.Error())
}
for picknum := 0; picknum < PACKSIZE; picknum++ {
for _, bot := range bots {
bot.PickNext()
}
// Make sure either ReadyNextPick or ReadyNextPack triggers
select {
case <-pod.ReadyNextPick:
// Pass packs around
err := pod.NextPacks()
if err != nil {
t.Fatalf("Got an error while passing packs: %s", err.Error())
}
case <-pod.ReadyNextPack:
break
default:
t.Fatal("Either ReadyNextPick/ReadyNextPack should trigger but neither has")
}
}
}
}

97
cmd/cgdraftbot/main.go Normal file
View file

@ -0,0 +1,97 @@
package main // import "git.fromouter.space/mcg/mlp-server-tools/draftbot"
import (
"errors"
"flag"
"fmt"
"math/rand"
"os"
"os/signal"
"strings"
"syscall"
"time"
"git.fromouter.space/Artificiale/moa/sd"
botapi "git.fromouter.space/mcg/cardgage/client/bot"
"git.fromouter.space/mcg/draft/mlp"
bot "git.fromouter.space/mcg/mlp-server-tools/draftbot"
"github.com/go-kit/kit/log"
)
var logger log.Logger
var logAll bool
func main() {
consulAddr := flag.String("consul.addr", "consul:8500", "Consul address")
botName := flag.String("bot.name", "draftbot", "Bot name")
gameFilter := flag.String("filter.game", "mlpccg-mcg", "What game to filter for (separated by comma)")
tagFilter := flag.String("filter.tag", "draft", "What tags to filter for (separated by comma)")
flag.BoolVar(&logAll, "debug.log", false, "Log a lot of stuff")
flag.Parse()
logger = log.NewLogfmtLogger(os.Stderr)
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
logger = log.With(logger, "caller", log.DefaultCaller)
// Register with consul
registrar := sd.Register(*consulAddr, sd.Options{
Name: "draftbot",
Tags: []string{
"bot", // I am a room bot
},
}, logger)
defer registrar.Deregister()
// Seed RNG
rand.Seed(time.Now().UnixNano())
// Initialize consul client for service discovery
sd.InitClient(*consulAddr)
errs := make(chan error)
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
errs <- fmt.Errorf("%s", <-c)
}()
go func() {
games := strings.Split(*gameFilter, ",")
tags := strings.Split(*tagFilter, ",")
errs <- runBot(*botName, games, tags)
}()
logger.Log("exit", <-errs)
}
func runBot(name string, games, tags []string) error {
// Search for roomsvc
roomsvc, err := sd.GetOne("roomsvc")
if err != nil {
return err
}
// Load all sets into memory
err = mlp.LoadAllSets()
if err != nil {
return err
}
addr := fmt.Sprintf("ws://%s:%d/bot", roomsvc.Service.Address, roomsvc.Service.Port)
wsbot, err := botapi.NewBot(addr, botapi.Params{
Name: name,
MsgBufferSize: 10,
Logger: logger,
GameIDs: games,
Tags: tags,
})
if err != nil {
return err
}
draftbot := bot.NewDraftBot(wsbot, name)
wsbot.Listen(draftbot.OnMessage)
return errors.New("eof")
}

81
cube.go Normal file
View file

@ -0,0 +1,81 @@
package draftbot
import (
"encoding/json"
"io/ioutil"
"net/http"
"strings"
"git.fromouter.space/mcg/draft"
"git.fromouter.space/mcg/draft/mlp"
)
func loadCube(cubeURL string) ([]draft.Card, error) {
// Fetch document
resp, err := http.Get(cubeURL)
if err != nil {
return nil, err
}
respBytes, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, err
}
// Each line is a card ID
cardids := strings.Split(string(respBytes), "\n")
cards := make([]draft.Card, 0)
// Convert to draft.Card
for _, cardid := range cardids {
cardid := strings.TrimSpace(cardid)
if len(cardid) < 1 {
// Skip empty lines
continue
}
cards = append(cards, draft.Card{ID: cardid})
}
return cards, nil
}
// I8PCubeConfig is an external JSON doc with the I8PCube pack seeding schema and card list
type I8PCubeConfig struct {
Schema mlp.I8PSchema
ProblemPackSize int
Cards map[mlp.I8PType][]string
}
// ToCube creates a I8PCube from the config
func (cfg *I8PCubeConfig) ToCube() (*mlp.I8PCube, error) {
// Load cards from given list
var err error
pool := make(mlp.I8PPool)
for typ, cardids := range cfg.Cards {
pool[typ], err = mlp.LoadCardList(cardids, true)
if err != nil {
return nil, err
}
}
// Make cube and return it
return mlp.MakeI8PCube(pool, cfg.Schema, cfg.ProblemPackSize), nil
}
func loadI8PCube(cubeURL string) (*mlp.I8PCube, error) {
// Fetch document
resp, err := http.Get(cubeURL)
if err != nil {
return nil, err
}
// Deserialize response to JSON object
var cubeconf I8PCubeConfig
err = json.NewDecoder(resp.Body).Decode(&cubeconf)
resp.Body.Close()
if err != nil {
return nil, err
}
// Create cube from config
return cubeconf.ToCube()
}

152
draftbot.go Normal file
View file

@ -0,0 +1,152 @@
package draftbot
import (
room "git.fromouter.space/mcg/cardgage/room/api"
"github.com/go-kit/kit/log"
)
// DraftBot is the functional part of draftbot
type DraftBot struct {
API BotInterface
Logger log.Logger
Name string
Rooms map[string]roomInfo
Sessions map[string]*session
}
type roomInfo struct {
Name string
Owner string
}
// BotInterface is the interface needed by draftbot for working in cardgage
// This exists so that draftbot can be attached to a mocked API for testing
type BotInterface interface {
Send(room.BotMessage)
}
// NewDraftBot creates a new draft bot instance with the given name
// and communication interface
func NewDraftBot(botAPI BotInterface, name string) *DraftBot {
return &DraftBot{
API: botAPI,
Name: name,
Rooms: make(map[string]roomInfo),
Sessions: make(map[string]*session),
}
}
// OnMessage is the function to be called when messages are received
func (d *DraftBot) OnMessage(msg room.ServerMessage) {
switch msg.Type {
case room.MsgMessage:
if d.Logger != nil {
d.Logger.Log("event", "message",
"roomid", msg.RoomID,
"from", msg.Message.From,
"to", msg.Message.To,
"content", msg.Message.Message)
}
// Only consider messages that speak directly to me
if msg.Message.To == d.Name {
d.handleMessage(msg.RoomID, *msg.Message)
}
case room.MsgEvent:
if d.Logger != nil {
d.Logger.Log("event", "event",
"roomid", msg.RoomID,
"content", msg.Event.Message)
}
d.handleEvent(msg.RoomID, *msg.Event)
}
}
func (d *DraftBot) sendMessage(roomid string, msg room.Message) {
d.API.Send(room.BotMessage{
RoomID: roomid,
Type: room.MsgMessage,
Message: &msg,
})
}
func (d *DraftBot) handleMessage(roomid string, msg room.Message) {
// Get session in room
session, ok := d.Sessions[roomid]
if !ok {
// Room does not have a currently running session, ignore unless it's the owner asking for specific stuff
d.commands(commandMap{
// Owner wants to create a session
"create": d.cmdfnOnlyOwner(d.cmdCreateSession),
})(roomid, msg)
return
}
// Check if player is in the draft
_, ok = session.Players[msg.From]
if !ok {
// Player not in draft, are they asking to join?
d.commands(commandMap{
// Player wants to join the session
"join": d.cmdJoinSession,
// Owner wants to start the session (but not partecipate)
"start": d.cmdfnOnlyOwner(d.cmdStartSession),
})(roomid, msg)
return
}
// Players in the draft session
d.commands(commandMap{
// Player has picked a card
"pick": d.cmdPickCard,
// Owner wants to start the session
"start": d.cmdfnOnlyOwner(d.cmdStartSession),
})(roomid, msg)
}
func (d *DraftBot) handleEvent(roomid string, evt room.Event) {
switch evt.Type {
// Got added to a new room
case room.EvtNewRoom:
// Add room to the list
d.Rooms[evt.Room.Id] = roomInfo{
Name: evt.Room.Name,
Owner: evt.Room.Creator,
}
// Someone left
case room.EvtLeft:
// Check if room has a running session
sess, ok := d.Sessions[roomid]
if !ok {
break
}
// Check if player is in that session
player, ok := sess.Players[evt.PlayerName]
if !ok {
break
}
// Replace player with bot
//TODO
_ = player
// A room got closed
case room.EvtRoomClosed:
// Check if there's a session there
session, ok := d.Sessions[roomid]
if ok {
// Close session
session.exit <- true
// Remove session from list
delete(d.Sessions, roomid)
}
// Remove room from list
delete(d.Rooms, roomid)
}
}
func (d *DraftBot) handleSessionMessages(roomid string, s *session) {
for msg := range s.messages {
d.sendMessage(roomid, msg)
}
}

190
draftbot.messages.go Normal file
View file

@ -0,0 +1,190 @@
package draftbot
import (
"fmt"
"github.com/mitchellh/mapstructure"
room "git.fromouter.space/mcg/cardgage/room/api"
"git.fromouter.space/mcg/draft"
)
type commandHandler func(roomid string, msg room.Message)
type commandMap map[string]commandHandler
func (d *DraftBot) commands(commands commandMap) commandHandler {
return func(roomid string, msg room.Message) {
action, ok := commands[msg.Type]
if !ok {
cmdlist := []string{}
for cmd := range commands {
cmdlist = append(cmdlist, cmd)
}
d.sendMessage(roomid, room.Message{
To: msg.From,
Type: "command-unavailable",
Message: fmt.Sprintf("Available commands (at this state) are: %s", cmdlist),
})
return
}
action(roomid, msg)
}
}
func (d *DraftBot) cmdJoinSession(roomid string, msg room.Message) {
// Get session
session, _ := d.Sessions[roomid]
// Players can only join if session didn't start yet
if session.started {
d.sendMessage(roomid, room.Message{
To: msg.From,
Type: "session-already-started",
Message: "You can't join a running session",
})
return
}
// Check if there are still open slots
if len(session.Players)+1 > len(session.Pod.Players) {
d.sendMessage(roomid, room.Message{
To: msg.From,
Type: "session-full",
Message: "There aren't any spots left",
})
return
}
// Add player to the list
session.Players[msg.From] = nil
d.sendMessage(roomid, room.Message{
Channel: "draft",
Type: "player-joined-session",
Data: msg.From,
Message: fmt.Sprintf("%s joined the draft session (%d players, %d missing)", msg.From, len(session.Players), len(session.Pod.Players)-len(session.Players)),
})
}
func (d *DraftBot) cmdPickCard(roomid string, msg room.Message) {
// Get session
session, _ := d.Sessions[roomid]
// Get player
player, _ := session.Players[msg.From]
// Get picked card
picked := msg.Data.(string)
// Try to pick on player struct
err := player.Pick(draft.Card{ID: picked})
if err != nil {
if err == draft.ErrNotInPack {
d.sendMessage(roomid, room.Message{
To: msg.From,
Type: "invalid-pick",
})
} else {
// Technically not needed, as Pick can only throw ErrNotInPack right now
}
return
}
d.sendMessage(roomid, room.Message{
Channel: "draft",
Type: "card-picked",
Data: struct {
Player string
}{
msg.From,
},
Message: fmt.Sprintf("%s picked a card from his pack", msg.From),
})
}
func (d *DraftBot) cmdCreateSession(roomid string, msg room.Message) {
// Get session options from data
var opt SessionOptions
err := mapstructure.Decode(msg.Data, &opt)
if err != nil {
d.sendMessage(roomid, room.Message{
To: msg.From,
Type: "invalid-data",
Message: "Error parsing session options: " + err.Error(),
})
return
}
sess, err := newSession(opt.Players, opt.Options)
if err != nil {
d.sendMessage(roomid, room.Message{
To: msg.From,
Type: "session-create-error",
Data: err.Error(),
Message: "Error creating session: " + err.Error(),
})
return
}
// All ok, assign session
d.Sessions[roomid] = sess
// Start handling messages for the session
go d.handleSessionMessages(roomid, sess)
// Tell everyone about the new session
d.sendMessage(roomid, room.Message{
Channel: "draft",
Type: "session-open",
Data: opt,
Message: fmt.Sprintf("Created a new draft session for %d players, type: %s", opt.Players, opt.Options.Type),
})
}
func (d *DraftBot) cmdStartSession(roomid string, msg room.Message) {
// Get session
session, _ := d.Sessions[roomid]
// Try starting the session
err := session.Start()
if err != nil {
d.sendMessage(roomid, room.Message{
To: msg.From,
Type: "session-start-error",
Data: err.Error(),
Message: "Could not start session: " + err.Error(),
})
return
}
// Tell everyone about the new session
d.sendMessage(roomid, room.Message{
Channel: "draft",
Type: "session-start",
Message: "Session started, get drafting!",
})
}
func (d *DraftBot) cmdfnOnlyOwner(wrapped commandHandler) commandHandler {
return func(roomid string, msg room.Message) {
// Get room the message was sent from
roomData, ok := d.Rooms[roomid]
if !ok {
// Message from a room we're not in?
// Ignore it for now
return
}
// Make sure the message is coming from the room owner
if msg.From != roomData.Owner {
d.sendMessage(roomid, room.Message{
To: msg.From,
Type: "must-be-owner",
Message: "Sorry, only the room's owner can tell me to do that",
})
return
}
// Check done, call wrapped function
wrapped(roomid, msg)
}
}

433
draftbot_test.go Normal file
View file

@ -0,0 +1,433 @@
package draftbot_test
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"git.fromouter.space/mcg/draft"
"git.fromouter.space/mcg/draft/mlp"
mlptestdata "git.fromouter.space/mcg/draft/mlp/testdata"
draftbot "git.fromouter.space/mcg/mlp-server-tools/draftbot"
testdata "git.fromouter.space/mcg/mlp-server-tools/draftbot/testdata"
"git.fromouter.space/mcg/cardgage/client/bot"
lobby "git.fromouter.space/mcg/cardgage/lobby/proto"
room "git.fromouter.space/mcg/cardgage/room/api"
)
const TestBotName = "test-bot"
const TestRoomName = "test-room"
type MockServer struct {
in chan room.ServerMessage
out chan room.BotMessage
t *testing.T
timeout time.Duration
roomName string
}
func makeMockServer(t *testing.T, timeout int) *MockServer {
srv := &MockServer{
in: make(chan room.ServerMessage, 99),
out: make(chan room.BotMessage, 99),
t: t,
timeout: time.Duration(timeout) * time.Second,
roomName: TestRoomName,
}
return srv
}
func (m *MockServer) Send(msg room.BotMessage) {
m.out <- msg
}
func (m *MockServer) Bind(fn bot.MessageHandler) {
for msg := range m.in {
fn(msg)
}
}
func TestDraftSession(t *testing.T) {
mock := makeMockServer(t, 5)
drafter := draftbot.NewDraftBot(mock, TestBotName)
go mock.Bind(drafter.OnMessage)
// Create a new room
mock.in <- room.ServerMessage{
RoomID: TestRoomName,
Type: room.MsgEvent,
Event: &room.Event{
Type: room.EvtNewRoom,
Room: &lobby.Room{
Id: TestRoomName,
Name: "Test draft room",
Creator: "test-owner",
},
},
}
// Create new session with a fake message from owner
mock.message("test-owner", "create", draftbot.SessionOptions{
Players: 8, // Two players, six bots
Options: draftbot.DraftOptions{
Type: draftbot.DraftSet,
Positioning: draftbot.PosEven,
Set: mlp.SetAbsoluteDiscord,
PackCount: 4,
},
})
mock.expect("session-open")
// Join session as owner
mock.message("test-owner", "join", nil)
mock.expect("player-joined-session")
// .. and as second player
mock.message("test-guest", "join", nil)
mock.expect("player-joined-session")
// Try to start the session
mock.message("test-owner", "start", nil)
// These two can happen in any order, so they get their own special check
mock.multiexpect("session-start", "draft-order")
// Pick card for each player
for packi := 0; packi < 4; packi++ {
mock.expect("draft-newpack")
for cardi := 0; cardi < 12; cardi++ {
mock.expect("draft-newpick")
// Get packs
msg1 := mock.expect("draft-availablepicks")
pack1 := msg1.Data.(draft.Pack)
msg2 := mock.expect("draft-availablepicks")
pack2 := msg2.Data.(draft.Pack)
// Pick first card in each pack
mock.message(msg1.To, "pick", pack1[0].ID)
mock.message(msg2.To, "pick", pack2[0].ID)
// Intercept picked events
mock.multiexpect("card-picked", "card-picked")
}
}
mock.expect("draft-finish")
// Close the room
mock.in <- room.ServerMessage{
RoomID: TestRoomName,
Type: room.MsgEvent,
Event: &room.Event{
Type: room.EvtRoomClosed,
},
}
}
func TestDraftSessionButEverythingGoesWrong(t *testing.T) {
mock := makeMockServer(t, 5)
drafter := draftbot.NewDraftBot(mock, TestBotName)
go mock.Bind(drafter.OnMessage)
// Create a new room
mock.in <- room.ServerMessage{
Type: room.MsgEvent,
Event: &room.Event{
Type: room.EvtNewRoom,
Room: &lobby.Room{
Id: TestRoomName,
Name: "Test draft room",
Creator: "test-owner",
},
},
}
// Try creating a new session as NOT the owner
mock.message("test-guest", "create", draftbot.SessionOptions{
Players: 8,
Options: draftbot.DraftOptions{
Type: draftbot.DraftSet,
Positioning: draftbot.PosEven,
Set: mlp.SetAbsoluteDiscord,
PackCount: 4,
},
})
mock.expect("must-be-owner")
// Try creating a session with an invalid type
mock.message("test-owner", "create", draftbot.SessionOptions{
Players: 8,
Options: draftbot.DraftOptions{
Type: "lolwhat",
},
})
mock.expect("session-create-error")
// Try creating a session with invalid data
mock.message("test-owner", "create", 42)
mock.expect("invalid-data")
// Try starting a session that doesn't exist
mock.message("test-owner", "start", nil)
mock.expect("command-unavailable")
// Try creating the session twice
mock.message("test-owner", "create", draftbot.SessionOptions{
Players: 2,
Options: draftbot.DraftOptions{
Type: draftbot.DraftSet,
Positioning: draftbot.PosEven,
Set: mlp.SetAbsoluteDiscord,
PackCount: 4,
},
})
mock.expect("session-open")
mock.message("test-owner", "create", draftbot.SessionOptions{
Players: 2,
Options: draftbot.DraftOptions{
Type: draftbot.DraftSet,
Positioning: draftbot.PosEven,
Set: mlp.SetAbsoluteDiscord,
PackCount: 4,
},
})
mock.expect("command-unavailable")
// Try to start session when no-one has joined
mock.message("test-owner", "start", nil)
mock.expect("session-start-error")
// Try to make too many players join
mock.message("a", "join", nil)
mock.expect("player-joined-session")
mock.message("b", "join", nil)
mock.expect("player-joined-session")
mock.message("c", "join", nil)
mock.expect("session-full")
// Try to make someone join a session that already started
mock.message("test-owner", "start", nil)
mock.multiexpect("session-start", "draft-order", "draft-newpack", "draft-newpick", "draft-availablepicks", "draft-availablepicks")
mock.message("c", "join", nil)
mock.expect("session-already-started")
//TODO More picking, etc shenanigans
}
func TestDraftTypes(t *testing.T) {
// Load all sets into memory
err := mlp.LoadAllSets()
if err != nil {
t.Fatalf("Could not load MLP sets needed for some drafts: %s", err.Error())
}
mock := makeMockServer(t, 5)
drafter := draftbot.NewDraftBot(mock, TestBotName)
go mock.Bind(drafter.OnMessage)
makeSession := func(roomName string, options draftbot.DraftOptions) {
// Create a new room
mock.in <- room.ServerMessage{
RoomID: roomName,
Type: room.MsgEvent,
Event: &room.Event{
Type: room.EvtNewRoom,
Room: &lobby.Room{
Id: roomName,
Name: "Test draft room",
Creator: "test-owner",
},
},
}
mock.roomName = roomName
// Create new session
mock.message("test-owner", "create", draftbot.SessionOptions{
Players: 8, // Two players, six bots
Options: options,
})
mock.expect("session-open")
}
// Make Set draft session
makeSession("set", draftbot.DraftOptions{
Type: draftbot.DraftSet,
Positioning: draftbot.PosEven,
Set: mlp.SetAbsoluteDiscord,
PackCount: 4,
})
// Make Block draft session
makeSession("block", draftbot.DraftOptions{
Type: draftbot.DraftBlock,
Positioning: draftbot.PosRandom,
Block: mlp.BlockDefenders,
})
// Make a plain Cube draft session
makeSession("cube", draftbot.DraftOptions{
Type: draftbot.DraftCube,
Positioning: draftbot.PosEven,
CubeURL: mlp.HTTPSource + "cube",
PackSize: 4,
})
// Make a I8PCube draft session
makeSession("i8pcube", draftbot.DraftOptions{
Type: draftbot.DraftI8PCube,
Positioning: draftbot.PosEven,
CubeURL: mlp.HTTPSource + "i8pcube",
MainCount: 1,
ProblemCount: 1,
})
}
func TestDraftPositioning(t *testing.T) {
mock := makeMockServer(t, 5)
drafter := draftbot.NewDraftBot(mock, TestBotName)
go mock.Bind(drafter.OnMessage)
makeSession := func(roomName string, pos string) {
// Create a new room
mock.in <- room.ServerMessage{
RoomID: roomName,
Type: room.MsgEvent,
Event: &room.Event{
Type: room.EvtNewRoom,
Room: &lobby.Room{
Id: roomName,
Name: "Test draft room",
Creator: "test-owner",
},
},
}
mock.roomName = roomName
// Create new session
mock.message("test-owner", "create", draftbot.SessionOptions{
Players: 8, // Two players, six bots
Options: draftbot.DraftOptions{
Type: draftbot.DraftSet,
Positioning: pos,
Set: mlp.SetAbsoluteDiscord,
PackCount: 4,
},
})
mock.expect("session-open")
// Make two random players join
mock.message("a", "join", nil)
mock.expect("player-joined-session")
mock.message("b", "join", nil)
mock.expect("player-joined-session")
// Start the session
mock.message("test-owner", "start", nil)
mock.multiexpect("session-start", "draft-order", "draft-newpack", "draft-newpick", "draft-availablepicks", "draft-availablepicks")
}
makeSession("even", draftbot.PosEven)
makeSession("random", draftbot.PosRandom)
}
func (m *MockServer) expect(typ string) *room.Message {
for {
select {
case msg := <-m.out:
// Skip all actions
if msg.Type != room.MsgMessage {
continue
}
// Check expected type
if msg.Message.Type != typ {
// Oh noes
m.t.Fatalf("Expected message \"%s\" but got \"%s\" (%v)", typ, msg.Message.Type, msg.Message.Data)
}
return msg.Message
case <-time.After(m.timeout):
m.t.Fatalf("Expected message \"%s\" but found nothing (timeout)!", typ)
return nil
}
}
}
func (m *MockServer) multiexpect(types ...string) {
for {
if len(types) < 1 {
return
}
select {
case msg := <-m.out:
// Skip all actions
if msg.Type != room.MsgMessage {
continue
}
m.t.Logf("-> [%s] %s", msg.Message.Type, msg.Message.Message)
// Check expected type
found := false
for i, typ := range types {
if typ == msg.Message.Type {
found = true
types[i] = types[len(types)-1]
types = types[:len(types)-1]
break
}
}
if !found {
// Oh noes
m.t.Fatalf("Expected one of %s but got \"%s\"", types, msg.Message.Type)
}
case <-time.After(m.timeout):
m.t.Fatalf("Expected one of %s but found nothing (timeout)!", types)
}
}
}
func (m *MockServer) message(from string, typ string, data interface{}) {
m.t.Logf("<- <%s> %s (%v)", from, typ, data)
m.in <- room.ServerMessage{
RoomID: m.roomName,
Type: room.MsgMessage,
Message: &room.Message{
From: from,
To: TestBotName,
Type: typ,
Data: data,
},
}
}
func TestMain(m *testing.M) {
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/nopenope.json":
// 404
http.Error(res, "Not found", http.StatusNotFound)
case "/broken.json":
// Broken response
fmt.Fprintf(res, "{{{{")
case "/pr.json":
fmt.Fprintf(res, mlptestdata.SetPremiere)
case "/cube":
fmt.Fprintf(res, testdata.TestGenericCubeFile)
case "/i8pcube":
fmt.Fprintf(res, testdata.TestI8PCubeFile)
default:
fmt.Fprintf(res, mlptestdata.SetFriendForever)
}
}))
mlp.HTTPSource = testServer.URL + "/"
defer testServer.Close()
os.Exit(m.Run())
}

11
go.mod Normal file
View file

@ -0,0 +1,11 @@
module git.fromouter.space/mcg/mlp-server-tools/draftbot
go 1.12
require (
git.fromouter.space/Artificiale/moa v0.0.1-p2
git.fromouter.space/mcg/cardgage v0.0.4
git.fromouter.space/mcg/draft v0.0.7
github.com/go-kit/kit v0.8.0
github.com/mitchellh/mapstructure v1.1.2
)

202
go.sum Normal file
View file

@ -0,0 +1,202 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.fromouter.space/Artificiale/moa v0.0.1-p2 h1:KhoRQeYCFIpHZEucrXz142O5zfSsyExyhuPSazCrt6I=
git.fromouter.space/Artificiale/moa v0.0.1-p2/go.mod h1:dHYul6vVMwDCzre18AFs6NmI22yeI7AE0iQC1jFEQi0=
git.fromouter.space/mcg/cardgage v0.0.4 h1:LHMUeNMh0QiMkM3TgsLe9l5sDmanQrej6UiWSVTb67c=
git.fromouter.space/mcg/cardgage v0.0.4/go.mod h1:vCmJ9HRdRGSWg2YQW9oNG7geYACdgWYmzL+zZdrsYhQ=
git.fromouter.space/mcg/draft v0.0.7 h1:kTcvGSs8MjrGjajKrUi0jj5uuCN6BF7x4OCvoUxQkGg=
git.fromouter.space/mcg/draft v0.0.7/go.mod h1:QQmDm9FgAZL3b2/pIDd4Eo608SxMiCQQe5vIybe/CDY=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM=
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/cenkalti/backoff v2.0.0+incompatible h1:5IIPUHhlnUZbcHQsQou5k1Tn58nJkeJL9U+ig5CHJbY=
github.com/cenkalti/backoff v2.0.0+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4=
github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I=
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/consul/api v1.1.0 h1:BNQPM9ytxj6jbjjdRPioQ94T6YXriSopn0i8COv6SRA=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1 h1:LnuDWGNsoajlhGyHJvuWW6FVqRl8JOTPqS6CPTsYjhY=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.1.0 h1:vN9wG1D6KG6YHRTWr8512cxGOVgTMEfgEdSj/hr8MPc=
github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/memberlist v0.1.4 h1:gkyML/r71w3FL8gUi74Vk76avkj/9lYAY9lvg0OcoGs=
github.com/hashicorp/memberlist v0.1.4/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/serf v0.8.3 h1:MWYcmct5EtKz0efYooPcL0yNkem+7kWxqXDi/UIh+8k=
github.com/hashicorp/serf v0.8.3/go.mod h1:UpNcs7fFbpKIyZaUuSW6EPiH+eZC7OuyFD+wc1oal+k=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.13 h1:x7DQtkU0cedzeS8TD36tT/w1Hm4rDtfCaYYAHE7TTBI=
github.com/miekg/dns v1.1.13/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2BA=
github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg4X946/Y5Zwg=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180828065106-d99a578cf41b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190531132440-69e3a3a65b5b h1:cuzzKXORsbIjh3IeOgOj0x2QA08ntIxmwi1mkRC3qoc=
golang.org/x/sys v0.0.0-20190531132440-69e3a3a65b5b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fatih/pool.v2 v2.0.0 h1:xIFeWtxifuQJGk/IEPKsTduEKcKvPmhoiVDGpC40nKg=
gopkg.in/fatih/pool.v2 v2.0.0/go.mod h1:8xVGeu1/2jr2wm5V9SPuMht2H5AEmf5aFMGSQixtjTY=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1 h1:cBTUuUFTllRYtInK6FtBRa+tOBlZqpeDTnZF54xjGbg=
gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1/go.mod h1:x+1XKi70FH0kHCpvPQ78hGBCCxoNdE7sP+kEFdKgN6A=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

281
session.go Normal file
View file

@ -0,0 +1,281 @@
package draftbot
import (
"errors"
"fmt"
"math"
"math/rand"
room "git.fromouter.space/mcg/cardgage/room/api"
"git.fromouter.space/mcg/draft"
"git.fromouter.space/mcg/draft/mlp"
"git.fromouter.space/mcg/mlp-server-tools/draftbot/bot"
)
// Session-related errors
var (
ErrTooManyPlayers = errors.New("too many players")
ErrNotEnoughPlayers = errors.New("not enough players")
)
type session struct {
Options DraftOptions
Players map[string]*draft.Player
Bots []*bot.Bot
Pod *draft.Pod
// State management variables
started bool // Has the draft started already?
// Channels for communication while the session is running
messages chan room.Message
exit chan bool
}
// Types of drafts
const (
DraftBlock = "block"
DraftSet = "set"
DraftCube = "cube"
DraftI8PCube = "i8pcube"
)
// Ways in which players can be positioned along the draft pod
const (
PosRandom = "random" // Place players randomly
PosEven = "even" // Place players spaced as evenly as possible
)
// SessionOptions is the data contained in a create session request
type SessionOptions struct {
Players int `json:"players"`
Options DraftOptions `json:"options"`
}
// DraftOptions are the options needed for a draft session
type DraftOptions struct {
Type string `json:"type"`
Positioning string `json:"positioning"`
// Block draft properties
Block mlp.BlockID `json:"block,omitempty"`
// Set draft properties
Set mlp.SetID `json:"set,omitempty"`
// Cube draft properties
CubeURL string `json:"cube_url,omitempty"`
PackSize int `json:"pack_size,omitempty"`
// I8PCube properties
MainCount int `json:"main_count,omitempty"`
ProblemCount int `json:"problem_count,omitempty"`
// Shared
PackCount int `json:"pack_count,omitempty"` // Set and Cube
}
func (do DraftOptions) getProvider() (draft.PackProvider, error) {
switch do.Type {
case DraftBlock:
return mlp.BlockPacks(do.Block)
case DraftSet:
set, err := mlp.LoadSetHTTP(do.Set)
if err != nil {
return nil, err
}
return draft.PacksFromSet(do.PackCount, set), nil
case DraftCube:
cards, err := loadCube(do.CubeURL)
if err != nil {
return nil, err
}
cube := &draft.GenericCube{
Cards: cards,
PackSize: do.PackSize,
}
return draft.PacksFromSet(do.PackCount, cube), nil
case DraftI8PCube:
cube, err := loadI8PCube(do.CubeURL)
if err != nil {
return nil, err
}
return cube.PackProvider(do.MainCount, do.ProblemCount), nil
}
return nil, errors.New("unknown draft type")
}
func newSession(playerCount int, opt DraftOptions) (*session, error) {
// Get pack provider for given options
provider, err := opt.getProvider()
if err != nil {
return nil, err
}
return &session{
Options: opt,
Pod: draft.MakePod(playerCount, provider),
Players: make(map[string]*draft.Player),
messages: make(chan room.Message),
exit: make(chan bool),
}, nil
}
func (s *session) Start() error {
// Figure out how many players there are vs spots to be filled
spots := len(s.Pod.Players)
players := len(s.Players)
if players > spots {
return ErrTooManyPlayers
}
if players < 1 {
return ErrNotEnoughPlayers
}
// Assign players to their spot on the drafting pod
playerSpot := make(map[int]string)
switch s.Options.Positioning {
case PosRandom:
// Assign a random number to each player
for pname := range s.Players {
var pos int
for {
pos = rand.Intn(spots)
// Make sure chosen number wasn't already picked
if _, ok := playerSpot[pos]; !ok {
break
}
}
playerSpot[pos] = pname
}
case PosEven:
// Space players evenly
playerRatio := float64(spots) / float64(players)
i := 0
for name := range s.Players {
pos := int(math.Floor(playerRatio * float64(i)))
playerSpot[pos] = name
i++
}
}
// Prepare order to be broadcasted after all spots have been assigned
order := make([]string, len(s.Pod.Players))
// Assign player instances and make bots where needed
for i := range s.Pod.Players {
if name, ok := playerSpot[i]; ok {
s.Players[name] = s.Pod.Players[i]
order[i] = "player:" + name
} else {
s.Bots = append(s.Bots, bot.MakeBot(s.Pod.Players[i]))
order[i] = "bot"
}
}
// Notify players of the order
s.messages <- room.Message{
Channel: "draft",
Type: "draft-order",
Data: order,
}
// Start handling packs
s.started = true
go s.handlePicks()
return nil
}
func (s *session) handlePicks() {
// Pack loop, this `for` handles an entire draft session
totalPacks := len(s.Pod.Players[0].Packs)
currentPack := 0
for {
err := s.Pod.OpenPacks()
if err != nil {
if err == draft.ErrNoPacksLeft {
// Notify players that the draft is over
s.messages <- room.Message{
Channel: "draft",
Type: "draft-finish",
Message: "No more packs, the draft is over!",
}
// Send each player their deck
for pname, pdata := range s.Players {
s.messages <- room.Message{
To: pname,
Type: "draft-picks",
Data: draft.Pack(pdata.Picks).IDs(),
Message: fmt.Sprintf("Your picks are: %s", pdata.Picks),
}
}
return
}
// Something is wrong!
//TODO
return
}
currentPack++
s.messages <- room.Message{
Channel: "draft",
Type: "draft-newpack",
Data: []int{currentPack, totalPacks},
Message: fmt.Sprintf("Opening pack %d (of %d)", currentPack, totalPacks),
}
// Pick loop, this `for` handles exactly one round of packs
for {
s.messages <- room.Message{
To: "draft",
Type: "draft-newpick",
Message: fmt.Sprintf("New pick for pack #%d", currentPack),
}
// Make bots pick their cards
for _, bot := range s.Bots {
bot.PickNext()
}
// Tell every players their new cards
for pname, player := range s.Players {
s.messages <- room.Message{
To: pname,
Type: "draft-availablepicks",
Data: player.CurrentPack,
Message: fmt.Sprintf("You got these cards: %s", player.CurrentPack),
}
}
nextPack := false
select {
case <-s.Pod.ReadyNextPack:
// Break of pick loop, get next packs
nextPack = true
case <-s.Pod.ReadyNextPick:
// Pass packs around
err := s.Pod.NextPacks()
if err != nil {
if err == draft.ErrNoPendingPack {
// No more picks to do for this round of packs, go to next
break
} else {
// Something is wrong!
//TODO
return
}
}
case <-s.exit:
// Room closed, exit early
close(s.messages)
close(s.exit)
return
}
if nextPack {
break
}
}
}
}

130
testdata/cube.go vendored Normal file
View file

@ -0,0 +1,130 @@
package testdata
// TestGenericCubeFile is an example file for a generic cube draft
const TestGenericCubeFile = `pr1
pr2
pr3
pr4
pr5
pr6
pr7
pr8
pr9
pr10
pr11
pr12
pr13
pr14
pr15
pr16
pr17
pr18
pr19
pr20
pr21
pr22
pr23
pr24
pr25
pr26
pr27
pr28
pr29
pr30
`
// TestI8PCubeFile is an example file for a I8Pages' style cube draft
const TestI8PCubeFile = `{
"Schema": [
{ "Amount": 1, "Type": "blue" },
{ "Amount": 1, "Type": "orange" },
{ "Amount": 1, "Type": "pink" },
{ "Amount": 1, "Type": "purple" },
{ "Amount": 1, "Type": "white" },
{ "Amount": 1, "Type": "yellow" },
{ "Amount": 1, "Type": "none" },
{ "Amount": 2, "Type": "multi" },
{ "Amount": 2, "Type": "entry" },
{ "Amount": 1, "Type": "all" }
],
"Cards": {
"blue": [
"pr54",
"pr54",
"pr54"
],
"orange": [
"pr54",
"pr54",
"pr54"
],
"pink": [
"pr54",
"pr54",
"pr54"
],
"purple": [
"pr54",
"pr54",
"pr54"
],
"white": [
"pr54",
"pr54",
"pr54"
],
"yellow": [
"pr54",
"pr54",
"pr54"
],
"none": [
"pr54",
"pr54",
"pr54"
],
"multi": [
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54"
],
"entry": [
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54"
],
"problem": [
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54",
"pr54"
]
}
}`