diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7fed7b8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "draftbot"] + path = draftbot + url = git@git.fromouter.space:mcg/draftbot.git diff --git a/draftbot b/draftbot new file mode 160000 index 0000000..6bce437 --- /dev/null +++ b/draftbot @@ -0,0 +1 @@ +Subproject commit 6bce4375dacfe773b0eea30d4be1dbe96f5548cd diff --git a/draftbot/bot/bot.go b/draftbot/bot/bot.go deleted file mode 100644 index a64f6b8..0000000 --- a/draftbot/bot/bot.go +++ /dev/null @@ -1,37 +0,0 @@ -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]) -} diff --git a/draftbot/bot/bot_test.go b/draftbot/bot/bot_test.go deleted file mode 100644 index 016c097..0000000 --- a/draftbot/bot/bot_test.go +++ /dev/null @@ -1,66 +0,0 @@ -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") - } - } - } -} diff --git a/draftbot/cmd/cgdraftbot/main.go b/draftbot/cmd/cgdraftbot/main.go deleted file mode 100644 index f0279dc..0000000 --- a/draftbot/cmd/cgdraftbot/main.go +++ /dev/null @@ -1,97 +0,0 @@ -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") -} diff --git a/draftbot/cube.go b/draftbot/cube.go deleted file mode 100644 index d08793b..0000000 --- a/draftbot/cube.go +++ /dev/null @@ -1,81 +0,0 @@ -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() -} diff --git a/draftbot/draftbot.go b/draftbot/draftbot.go deleted file mode 100644 index 5084f51..0000000 --- a/draftbot/draftbot.go +++ /dev/null @@ -1,152 +0,0 @@ -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) - } -} diff --git a/draftbot/draftbot.messages.go b/draftbot/draftbot.messages.go deleted file mode 100644 index 6575468..0000000 --- a/draftbot/draftbot.messages.go +++ /dev/null @@ -1,190 +0,0 @@ -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) - } -} diff --git a/draftbot/draftbot_test.go b/draftbot/draftbot_test.go deleted file mode 100644 index 3962e80..0000000 --- a/draftbot/draftbot_test.go +++ /dev/null @@ -1,433 +0,0 @@ -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()) -} diff --git a/draftbot/go.mod b/draftbot/go.mod deleted file mode 100644 index dc4b043..0000000 --- a/draftbot/go.mod +++ /dev/null @@ -1,11 +0,0 @@ -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 -) diff --git a/draftbot/go.sum b/draftbot/go.sum deleted file mode 100644 index c4de40c..0000000 --- a/draftbot/go.sum +++ /dev/null @@ -1,202 +0,0 @@ -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= diff --git a/draftbot/session.go b/draftbot/session.go deleted file mode 100644 index 2b3dc94..0000000 --- a/draftbot/session.go +++ /dev/null @@ -1,281 +0,0 @@ -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 - } - } - } -} diff --git a/draftbot/testdata/cube.go b/draftbot/testdata/cube.go deleted file mode 100644 index 011a660..0000000 --- a/draftbot/testdata/cube.go +++ /dev/null @@ -1,130 +0,0 @@ -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" - ] - } -}`