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/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{ Channel: "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 } } } }