diff --git a/.drone.yml b/.drone.yml index 5935376..1d1718a 100644 --- a/.drone.yml +++ b/.drone.yml @@ -7,14 +7,14 @@ steps: commands: - cd ./draftbot - GOPROXY=https://modules.fromouter.space go mod download - - CGO_ENABLED=0 go test . + - CGO_ENABLED=0 go test ./... - name: build_draftbot image: golang commands: - cd ./draftbot - GOPROXY=https://modules.fromouter.space go mod download - - CGO_ENABLED=0 go install . + - CGO_ENABLED=0 go install ./... volumes: - name: gopath path: /go diff --git a/draftbot/bot/bot_test.go b/draftbot/bot/bot_test.go new file mode 100644 index 0000000..016c097 --- /dev/null +++ b/draftbot/bot/bot_test.go @@ -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") + } + } + } +} diff --git a/draftbot/draftbot.go b/draftbot/draftbot.go index 783b9ae..52f6378 100644 --- a/draftbot/draftbot.go +++ b/draftbot/draftbot.go @@ -29,6 +29,7 @@ func NewDraftBot(botAPI BotInterface, name string) *DraftBot { return &DraftBot{ API: botAPI, Name: name, + Rooms: make(map[string]roomInfo), Sessions: make(map[string]*session), } } @@ -37,7 +38,7 @@ func NewDraftBot(botAPI BotInterface, name string) *DraftBot { func (d *DraftBot) OnMessage(msg room.ServerMessage) { switch msg.Type { case room.MsgMessage: - if *logAll { + if logAll { logger.Log("event", "message", "roomid", msg.RoomID, "from", msg.Message.From, @@ -49,7 +50,7 @@ func (d *DraftBot) OnMessage(msg room.ServerMessage) { d.handleMessage(msg.RoomID, *msg.Message) } case room.MsgEvent: - if *logAll { + if logAll { logger.Log("event", "event", "roomid", msg.RoomID, "content", msg.Event.Message) diff --git a/draftbot/draftbot.messages.go b/draftbot/draftbot.messages.go index 541ccae..9319441 100644 --- a/draftbot/draftbot.messages.go +++ b/draftbot/draftbot.messages.go @@ -103,11 +103,7 @@ func (d *DraftBot) cmdPickCard(roomid string, msg room.Message) { func (d *DraftBot) cmdCreateSession(roomid string, msg room.Message) { // Get session options from data - type sessionOptions struct { - players int - options draftOptions - } - var opt sessionOptions + var opt SessionOptions err := mapstructure.Decode(msg.Data, &opt) if err != nil { d.sendMessage(roomid, room.Message{ @@ -118,7 +114,7 @@ func (d *DraftBot) cmdCreateSession(roomid string, msg room.Message) { return } - sess, err := newSession(opt.players, opt.options) + sess, err := newSession(opt.Players, opt.Options) if err != nil { d.sendMessage(roomid, room.Message{ To: msg.From, @@ -140,7 +136,7 @@ func (d *DraftBot) cmdCreateSession(roomid string, msg 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), + Message: fmt.Sprintf("Created a new draft session for %d players, type: %s", opt.Players, opt.Options.Type), }) } diff --git a/draftbot/draftbot_test.go b/draftbot/draftbot_test.go index 6c104d8..871cd94 100644 --- a/draftbot/draftbot_test.go +++ b/draftbot/draftbot_test.go @@ -2,12 +2,19 @@ package main_test import ( "testing" + "time" + + "git.fromouter.space/mcg/draft/mlp" "git.fromouter.space/mcg/cardgage/client/bot" + lobby "git.fromouter.space/mcg/cardgage/lobby/proto" room "git.fromouter.space/mcg/cardgage/room/api" draft "git.fromouter.space/mcg/mlp-server-tools/draftbot" ) +const TestBotName = "test-bot" +const TestRoomName = "test-room" + type MockServer struct { in chan room.ServerMessage out chan room.BotMessage @@ -35,8 +42,163 @@ func (m *MockServer) Bind(fn bot.MessageHandler) { func TestDraftSession(t *testing.T) { mock := makeMockServer() - draftbot := draft.NewDraftBot(mock, "bot") + draftbot := draft.NewDraftBot(mock, TestBotName) go mock.Bind(draftbot.OnMessage) - //TODO sample session + // 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", draft.SessionOptions{ + Players: 8, // Two players, six bots + Options: draft.DraftOptions{ + Type: draft.DraftSet, + Positioning: draft.PosEven, + Set: mlp.SetAbsoluteDiscord, + PackCount: 4, + }, + }) + + mock.expect(t, "session-open", 5) + + // Join session as owner + mock.message("test-owner", "join", nil) + mock.expect(t, "player-joined-session", 5) + + // .. and as second player + mock.message("test-guest", "join", nil) + mock.expect(t, "player-joined-session", 5) + + // 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(t, 5, "session-start", "draft-order") + + //TODO make players pick cards etc + + // 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() + draftbot := draft.NewDraftBot(mock, TestBotName) + go mock.Bind(draftbot.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", draft.SessionOptions{ + Players: 8, // Two players, six bots + Options: draft.DraftOptions{ + Type: draft.DraftSet, + Positioning: draft.PosEven, + Set: mlp.SetAbsoluteDiscord, + PackCount: 4, + }, + }) + mock.expect(t, "must-be-owner", 5) + + //TODO: + // Try to start session when session doesn't exist + // Try to create session twice + // Try to start session with no players + // Try to start session as not the owner + // Try to make too many players join a session +} + +func (m *MockServer) expect(t *testing.T, typ string, timeout int) { + 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 + t.Fatalf("Expected message \"%s\" but got \"%s\"", typ, msg.Message.Type) + } + return + case <-time.After(time.Duration(timeout) * time.Second): + t.Fatalf("Expected message \"%s\" but found nothing (timeout after %d seconds)!", typ, timeout) + } + } +} + +func (m *MockServer) multiexpect(t *testing.T, timeout int, types ...string) { + for { + if len(types) < 1 { + return + } + select { + case msg := <-m.out: + // Skip all actions + if msg.Type != room.MsgMessage { + continue + } + + // 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 + t.Fatalf("Expected one of %s but got \"%s\"", types, msg.Message.Type) + } + return + case <-time.After(time.Duration(timeout) * time.Second): + t.Fatalf("Expected one of %s but found nothing (timeout after %d seconds)!", types, timeout) + } + } +} + +func (m *MockServer) message(from string, typ string, data interface{}) { + m.in <- room.ServerMessage{ + RoomID: TestRoomName, + Type: room.MsgMessage, + Message: &room.Message{ + From: from, + To: TestBotName, + Type: typ, + Data: data, + }, + } } diff --git a/draftbot/go.sum b/draftbot/go.sum index 2d60590..4d1cbb1 100644 --- a/draftbot/go.sum +++ b/draftbot/go.sum @@ -32,6 +32,7 @@ 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= @@ -105,6 +106,7 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 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= @@ -144,6 +146,7 @@ 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= diff --git a/draftbot/main.go b/draftbot/main.go index 76de1d2..080496f 100644 --- a/draftbot/main.go +++ b/draftbot/main.go @@ -17,14 +17,15 @@ import ( ) var logger log.Logger -var logAll *bool +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)") - logAll = flag.Bool("debug.log", false, "Log a lot of stuff") + 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) diff --git a/draftbot/session.go b/draftbot/session.go index 6f86304..51841ea 100644 --- a/draftbot/session.go +++ b/draftbot/session.go @@ -20,7 +20,7 @@ var ( ) type session struct { - Options draftOptions + Options DraftOptions Players map[string]*draft.Player Bots []*bot.Bot Pod *draft.Pod @@ -35,27 +35,34 @@ type session struct { // Types of drafts const ( - draftBlock = "block" - draftSet = "set" - draftCube = "cube" - draftI8PCube = "i8pcube" + 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 + PosRandom = "random" // Place players randomly + PosEven = "even" // Place players spaced as evenly as possible ) -type draftOptions struct { +// 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 string `json:"block,omitempty"` + Block mlp.BlockID `json:"block,omitempty"` // Set draft properties - Set string `json:"set,omitempty"` + Set mlp.SetID `json:"set,omitempty"` // Cube draft properties CubeURL string `json:"cube_url,omitempty"` @@ -69,17 +76,17 @@ type draftOptions struct { PackCount int `json:"pack_count,omitempty"` // Set and Cube } -func (do draftOptions) getProvider() (draft.PackProvider, error) { +func (do DraftOptions) getProvider() (draft.PackProvider, error) { switch do.Type { - case draftBlock: - return mlp.BlockPacks(mlp.BlockID(do.Block)) - case draftSet: - set, err := mlp.LoadSetHTTP(mlp.SetID(do.Set)) + 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: + case DraftCube: cards, err := loadCube(do.CubeURL) if err != nil { return nil, err @@ -89,7 +96,7 @@ func (do draftOptions) getProvider() (draft.PackProvider, error) { PackSize: do.PackSize, } return draft.PacksFromSet(do.PackCount, cube), nil - case draftI8PCube: + case DraftI8PCube: cube, err := loadI8PCube(do.CubeURL) if err != nil { return nil, err @@ -99,7 +106,7 @@ func (do draftOptions) getProvider() (draft.PackProvider, error) { return nil, errors.New("unknown draft type") } -func newSession(playerCount int, opt draftOptions) (*session, error) { +func newSession(playerCount int, opt DraftOptions) (*session, error) { // Get pack provider for given options provider, err := opt.getProvider() if err != nil { @@ -109,6 +116,7 @@ func newSession(playerCount int, opt draftOptions) (*session, error) { 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 @@ -130,7 +138,7 @@ func (s *session) Start() error { playerSpot := make(map[int]string) switch s.Options.Positioning { - case posRandom: + case PosRandom: // Assign a random number to each player for pname := range s.Players { var pos int @@ -143,7 +151,7 @@ func (s *session) Start() error { } playerSpot[pos] = pname } - case posEven: + case PosEven: // Space players evenly playerRatio := float64(spots) / float64(players) i := 0