draftbot/session.go

282 lines
6.4 KiB
Go

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
}
}
}
}