package game import ( "errors" "fmt" "strings" "sync" "muhq.space/muhqs-game/go/log" ) // A Draft coordinating drafting among multiple players and bots. // TODO: support P variable in draft description. type Draft struct { players []*Player cardsPerPack int packSize int packsPerPlayer int sets []SetIdentifier } var ( ErrMissingDraftSets = errors.New("missing draft sets") ErrInvalidDraft = errors.New("invalid draft") ) type ErrParseDraftDesc struct { err error } func (err ErrParseDraftDesc) Error() string { return err.err.Error() } func NewDraftFromDesc(players []*Player, desc string) (*Draft, error) { if !strings.ContainsRune(desc, ':') { return nil, ErrMissingDraftSets } var setList string s := strings.Split(desc, ":") desc, setList = s[0], s[1] if sets, err := SetListToSets(setList); err != nil { return nil, err } else { return NewDraft(players, desc, sets) } } func NewDraft(players []*Player, desc string, sets []SetIdentifier) (*Draft, error) { d := &Draft{players: players, sets: sets} err := d.parseDesc(desc) log.Debug("created new draft", "desc", d) return d, err } func (d *Draft) parseDesc(desc string) error { // strings.ReplaceAll(desc, "P", fmt.Sprintf("%d", len(d.players))) _, err := fmt.Sscanf(desc, "%dx[%d;%d]", &d.packsPerPlayer, &d.cardsPerPack, &d.packSize) if err != nil { return ErrParseDraftDesc{err} } return nil } func (d *Draft) Valid() bool { required := d.cardsPerPack * len(d.players) return d.packSize > required } func (d *Draft) PackSize() int { return d.packSize } func (d *Draft) PacksPerPlayer() int { return d.packsPerPlayer } func (d *Draft) CardsPerPack() int { return d.cardsPerPack } func (d *Draft) PlayerNames() []string { n := make([]string, 0, len(d.players)) for _, p := range d.players { n = append(n, p.Name) } return n } func (d *Draft) Desc() string { return fmt.Sprintf("%dx[%d;%d]", d.packsPerPlayer, d.cardsPerPack, d.packSize) } func (d *Draft) Sets() []SetIdentifier { return d.sets } // PreparePacks returns enough random packs suitable to run the draft. func (d *Draft) PreparePacks() []PileOfCards { pool := NewPileOfCards() requiredCards := d.packsPerPlayer * d.packSize for pool.Size() < requiredCards { for _, set := range d.sets { setDeck := NewDeck() for _, c := range NewDeckFromCardPaths(set.CardPaths()).Cards() { if c.IsBuyable() { setDeck.AddCard(c) } } setDeck.MoveInto(pool) } } nPlayers := len(d.players) packs := make([]PileOfCards, 0, nPlayers*d.packsPerPlayer) for range nPlayers * d.packsPerPlayer { pack := NewRandomPackFromPool(pool, d.packSize) packs = append(packs, pack) } return packs } // DealRound concurrently prompts the players for the current pick. // The pack `i` and the pick `j` determine who is prompted with a specific pack. func (d *Draft) DealRound(i, j int, packs []PileOfCards) { nPlayers := len(d.players) var wg sync.WaitGroup wg.Add(nPlayers) for k, player := range d.players { go func() { pack := packs[i*nPlayers+(k+j)%nPlayers] pickPrompt := newDraftPickPrompt(pack) log.Debug("prompt for pick ", "player", player.Name, "pack", i*nPlayers+(k+j)%nPlayers) pick, err := prompt(player.Ctrl, pickPrompt) // select a random card on error if err != nil { pick = newRandomDraftPick(player, pickPrompt) } pick.resolve(player.gameState) wg.Done() }() } wg.Wait() } // Run executes the draft. // This method only returns after the draft has finished. func (d *Draft) Run() { packs := d.PreparePacks() nPlayers := len(d.players) log.Debug("run draft", "packs", d.packsPerPlayer, "cards", d.cardsPerPack, "players", len(d.players)) for i := range d.packsPerPlayer { for j := range d.cardsPerPack * nPlayers { d.DealRound(i, j, packs) } } } func (d *Draft) AddPlayer(p *Player) { if p.Ctrl == nil { log.Fatal("added player without control") } d.players = append(d.players, p) } // AddPlayerReplacingAI adds a player to the draft replacing the first encountered AI. func (d *Draft) AddPlayerReplacingAI(new *Player) { if new.Ctrl == nil { log.Fatal("added player without control") } // try to replace AI for i, p := range d.players { if _, ok := p.Ctrl.(*randomDraftAiCtrl); ok { d.players[i] = new return } } d.players = append(d.players, new) } func (d *Draft) Players() []*Player { return d.players } type randomDraftAiCtrl struct { ai *Player pack PileOfCards } func (ctrl *randomDraftAiCtrl) Player() *Player { return ctrl.ai } func (ctrl *randomDraftAiCtrl) Close() { } func (ctrl *randomDraftAiCtrl) SendNotification(n PlayerNotification) error { if n.Valid() && n.Notification == DraftPickPrompt { ctrl.pack = n.Context.(PileOfCards) } return nil } func (ctrl *randomDraftAiCtrl) RecvNotification() (n PlayerNotification, err error) { return } func (ctrl *randomDraftAiCtrl) SendAction(Action) error { return nil } func (ctrl *randomDraftAiCtrl) RecvAction() (Action, error) { return NewDraftPick(ctrl.ai, ctrl.pack, ctrl.pack.RandomCard()), nil } func (d *Draft) AddRandomAi() { p := &Player{Id: -1, Name: "randomDraftAI", Deck: NewDeck()} p.Ctrl = &randomDraftAiCtrl{ai: p} d.players = append(d.players, p) }