diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2025-04-29 15:05:12 -0500 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-08-20 15:57:16 +0200 |
| commit | 8ec9ff690082e9761e056bf5740daf6b80769f1f (patch) | |
| tree | 02e2c5a4f2deb440f4b8fc79237742cddb8815f6 /go | |
| parent | a56641946ad4adcf2e59c08ca4befe70ca1e0e1b (diff) | |
| download | muhqs-game-8ec9ff690082e9761e056bf5740daf6b80769f1f.tar.gz muhqs-game-8ec9ff690082e9761e056bf5740daf6b80769f1f.zip | |
implement simple bot based draft
Diffstat (limited to 'go')
| -rw-r--r-- | go/TODO | 1 | ||||
| -rw-r--r-- | go/client/draft.go | 199 | ||||
| -rw-r--r-- | go/client/startMenu.go | 11 | ||||
| -rw-r--r-- | go/game/action.go | 22 | ||||
| -rw-r--r-- | go/game/draft.go | 116 | ||||
| -rw-r--r-- | go/game/pileOfCards.go | 10 | ||||
| -rw-r--r-- | go/game/player.go | 23 | ||||
| -rw-r--r-- | go/game/playerControl.go | 7 |
8 files changed, 385 insertions, 4 deletions
@@ -5,7 +5,6 @@ * implement spell target parsing * implement triggers * implement target selection for triggers -* implement draft * implement game log * implement LongTap as hover equivalent * finish AIs diff --git a/go/client/draft.go b/go/client/draft.go new file mode 100644 index 00000000..3950094f --- /dev/null +++ b/go/client/draft.go @@ -0,0 +1,199 @@ +package main + +import ( + "log" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" + + "muhq.space/muhqs-game/go/game" + "muhq.space/muhqs-game/go/ui" +) + +type Draft struct { + ui.Collection + app *app + player *game.Player + ctrl *game.ChanPlayerControl + draft *game.Draft + + setInput *ui.TextInput + descInput *ui.TextInput + deckList *ui.PocList +} + +func NewDraft(width, height int, app *app) *Draft { + d := &Draft{ + app: app, + Collection: ui.Collection{Width: width, Height: height}, + } + + d.AddWidget(ui.NewFixedTextBox( + (d.Width-START_BUTTON_WIDTH)/2-START_BUTTON_WIDTH, + (d.Height-START_BUTTON_HEIGHT)/2-3*START_BUTTON_HEIGHT, + START_BUTTON_WIDTH, + START_BUTTON_HEIGHT, + "sets:", + ).Centering(true)) + + d.setInput = ui.NewTextInput( + (d.Width-START_BUTTON_WIDTH)/2, + (d.Height-START_BUTTON_HEIGHT)/2-3*START_BUTTON_HEIGHT, + START_BUTTON_WIDTH, + START_BUTTON_HEIGHT, + "base,magic,equipments", + ) + d.AddWidget(d.setInput) + + d.AddWidget(ui.NewFixedTextBox( + (d.Width-START_BUTTON_WIDTH)/2-START_BUTTON_WIDTH, + (d.Height-START_BUTTON_HEIGHT)/2-2*START_BUTTON_HEIGHT, + START_BUTTON_WIDTH, + START_BUTTON_HEIGHT, + "draft:", + ).Centering(true)) + + d.descInput = ui.NewTextInput( + (d.Width-START_BUTTON_WIDTH)/2, + (d.Height-START_BUTTON_HEIGHT)/2-2*START_BUTTON_HEIGHT, + START_BUTTON_WIDTH, + START_BUTTON_HEIGHT, + "3x[2;8]", + ) + d.AddWidget(d.descInput) + + d.AddWidget(ui.NewFixedTextBox( + (d.Width-START_BUTTON_WIDTH)/2-START_BUTTON_WIDTH, + (d.Height-START_BUTTON_HEIGHT)/2-START_BUTTON_HEIGHT, + START_BUTTON_WIDTH, + START_BUTTON_HEIGHT, + "AI:", + ).Centering(true)) + + aiNInput := ui.NewNumberChoice( + (d.Width-START_BUTTON_WIDTH)/2, + (d.Height-START_BUTTON_HEIGHT)/2-START_BUTTON_HEIGHT, + 1, + nil, + ) + d.AddWidget(aiNInput) + + d.AddWidget(ui.NewSimpleButton( + (d.Width-START_BUTTON_WIDTH)/2, + (d.Height-START_BUTTON_HEIGHT)/2, + START_BUTTON_WIDTH, + START_BUTTON_HEIGHT, + "Start", + func(*ui.SimpleButton) { + d.startDraft(d.descInput.TextOrLabel(), d.setInput.TextOrLabel(), aiNInput.GetChoosen()) + })) + + startMenu := d.app.activities[len(d.app.activities)-1].(*StartMenu) + d.player = game.NewDraftPlayer(startMenu.playerName) + d.ctrl = game.NewChanPlayerControl(d.player) + d.player.Ctrl = d.ctrl + + return d +} + +func (d *Draft) Update() error { + if n := d.ctrl.RecvNotification(); n != nil { + log.Println("Received draft pick: %v\n", n) + d.presentPack(n) + } + + if err := ui.TouchManager.Update(); err != nil { + return err + } + + if err := d.Collection.Update(); err != nil { + return err + } + + if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { + d.app.popActivity() + } + + return nil +} + +func (d *Draft) presentPack(n *game.PlayerNotification) { + pack := n.Context.(game.PileOfCards) + grid := ui.NewCardGrid(0, 0, d.Width-SEALED_DECK_STRIP_WIDTH, d.Height, 0.5, pack).Columns(3) + selectCard := func(x, y int) { + _card := grid.FindObjectAt(x, y) + if _card == nil { + return + } + + card := _card.(*game.Card) + d.RemoveWidget(grid) + d.ctrl.SendAction(game.NewDraftPick(d.player, pack, card)) + } + grid.RegisterHandler("click", selectCard) + d.AddWidget(grid) +} + +func (d *Draft) Layout(width, height int) (int, int) { + return d.Width, d.Height +} + +func (d *Draft) startDraft(desc string, setList string, aiN int) { + + log.Printf("starting %s draft from '%s'\n", desc, setList) + + var sets []game.SetIdentifier + var err error + if sets, err = game.SetListToSets(setList); err != nil { + d.setInput.Bg(ui.WarningBg) + d.setInput.SetInput("") + return + } + + d.draft = game.NewDraft([]*game.Player{d.player}, desc, sets) + + log.Println(aiN) + for _ = range aiN { + log.Println("add ai") + d.draft.AddRandomAi() + } + + if !d.draft.Valid() { + d.descInput.Bg(ui.WarningBg) + d.descInput.SetInput("") + return + } + d.Clear() + + d.deckList = ui.NewPocList(d.Width-SEALED_DECK_STRIP_WIDTH/2, d.Height-START_BUTTON_HEIGHT-20, d.player.Deck) + d.deckList.Bg(ui.Gray) + { + cardsInDeck := 0 + d.deckList.RegisterHandler("update", func() error { + if n := d.player.Deck.Size(); n > cardsInDeck { + d.deckList.ForceRedraw() + cardsInDeck = n + } + return nil + }) + } + d.AddWidget(d.deckList) + + d.AddWidget(ui.NewSimpleButton( + d.Width-SEALED_DECK_STRIP_WIDTH/2-START_BUTTON_WIDTH/2, + d.Height-START_BUTTON_HEIGHT, + START_BUTTON_WIDTH, + START_BUTTON_HEIGHT, + "Done", + func(*ui.SimpleButton) { + d.done() + })) + + go d.draft.Run() +} + +func (d *Draft) done() { + startMenu := d.app.activities[len(d.app.activities)-2].(*StartMenu) + startMenu.deckInput.SetInput(d.player.Deck.ToList()) + d.app.popActivity() +} diff --git a/go/client/startMenu.go b/go/client/startMenu.go index 04b1ed31..10fa8e1f 100644 --- a/go/client/startMenu.go +++ b/go/client/startMenu.go @@ -110,6 +110,17 @@ func (m *StartMenu) build() { deckInput.Y+deckInput.Height, START_BUTTON_WIDTH, START_BUTTON_HEIGHT, + "Draft", + func(*ui.SimpleButton) { + draft := NewDraft(m.Width, m.Height, m.app) + m.app.pushActivity(draft) + })) + + m.AddWidget(ui.NewSimpleButton( + (m.Width-START_BUTTON_WIDTH)/2, + deckInput.Y+deckInput.Height+2*START_BUTTON_HEIGHT, + START_BUTTON_WIDTH, + START_BUTTON_HEIGHT, "Start", func(*ui.SimpleButton) { m.playerName = playerInput.Text() diff --git a/go/game/action.go b/go/game/action.go index 715f39a3..0743c264 100644 --- a/go/game/action.go +++ b/go/game/action.go @@ -84,6 +84,28 @@ func newHandCardSelection(p *Player, min, max int) Action { return a } +type DraftPick struct { + player *Player + pick *Card + pack PileOfCards +} + +func (pick *DraftPick) Source() interface{} { return pick.player } +func (pick *DraftPick) Controller() *Player { return pick.player } +func (pick *DraftPick) Speed() ActionSpeed { return fast } +func (pick *DraftPick) GameState() *LocalState { return pick.player.gameState } +func (pick *DraftPick) Targets() *Targets { return nil } +func (pick *DraftPick) Target() *Target { return nil } +func (*DraftPick) NeedsActionCostChoice() bool { return false } +func (pick *DraftPick) CheckTargets(s *LocalState) error { return nil } +func (*DraftPick) PayCosts(*LocalState) bool { return true } +func (pick *DraftPick) resolve(*LocalState) { pick.pack.MoveCard(pick.pick, pick.player.Deck) } +func (*DraftPick) String() string { return "draft pick" } + +func NewDraftPick(p *Player, pack PileOfCards, card *Card) Action { + return &DraftPick{player: p, pack: pack, pick: card} +} + type ActionBase struct { source interface{} Card *Card diff --git a/go/game/draft.go b/go/game/draft.go new file mode 100644 index 00000000..f1ab541b --- /dev/null +++ b/go/game/draft.go @@ -0,0 +1,116 @@ +package game + +import ( + "fmt" + "log" + "math/rand" + "strings" +) + +type Draft struct { + players []*Player + packs []PileOfCards + cardsPerPack int + packSize int + packsPerPlayer int + sets []SetIdentifier +} + +func NewDraftFromDesc(players []*Player, desc string) *Draft { + var setList string + s := strings.Split(desc, ":") + desc, setList = s[0], s[1] + if sets, err := SetListToSets(setList); err != nil { + log.Fatal(err) + } else { + + return NewDraft(players, desc, sets) + } + // not reachable + return nil +} + +func NewDraft(players []*Player, desc string, sets []SetIdentifier) *Draft { + d := &Draft{players: players, sets: sets} + d.parseDesc(desc) + log.Printf("created new draft %s\n", d) + return d +} + +func (d *Draft) parseDesc(desc string) { + fmt.Sscanf(desc, "%dx[%d;%d]", &d.packsPerPlayer, &d.cardsPerPack, &d.packSize) +} + +func (d *Draft) Valid() bool { + if d.packSize < d.cardsPerPack*len(d.players) { + return false + } + return true +} + +// run the draft +// Rhis method only returns after the draft has finished. +func (d *Draft) Run() { + pool := NewPileOfCards() + // TODO: increase pool to have enough cards for the draft + 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 len(d.players) * d.packsPerPlayer { + pack := NewRandomPackFromPool(pool, d.packSize) + packs = append(packs, pack) + } + + log.Printf("%d %d %d\n", d.packsPerPlayer, d.cardsPerPack, len(d.players)) + for i := range d.packsPerPlayer { + for j := range d.cardsPerPack * len(d.players) { + for k, player := range d.players { + pack := packs[i*nPlayers+(k+j)%nPlayers] + pickPrompt := newDraftPickPrompt(pack) + log.Printf("prompt %s for a pick from pack %d\n", player.Name, i*nPlayers+(k+j)%nPlayers) + pick := prompt(player.Ctrl, pickPrompt) + pick.resolve(player.gameState) + } + } + } +} + +type randomDraftAiCtrl struct { + ai *Player + pack PileOfCards +} + +func (ctrl *randomDraftAiCtrl) Player() *Player { + return ctrl.ai +} + +func (ctrl *randomDraftAiCtrl) SendNotification(n PlayerNotification) { + ctrl.pack = n.Context.(PileOfCards) +} + +func (ctrl *randomDraftAiCtrl) RecvAction() Action { + availCards := ctrl.pack.Cards() + var pick int + if n := len(availCards); n > 1 { + pick = rand.Intn(n - 1) + } else { + pick = 0 + } + return NewDraftPick(ctrl.ai, ctrl.pack, availCards[pick]) +} + +func (d *Draft) AddRandomAi() { + p := &Player{Id: -1, Name: "randomDraftAI", Deck: NewDeck()} + p.Ctrl = &randomDraftAiCtrl{ai: p} + d.players = append(d.players, p) +} diff --git a/go/game/pileOfCards.go b/go/game/pileOfCards.go index 5edae617..0ad0f157 100644 --- a/go/game/pileOfCards.go +++ b/go/game/pileOfCards.go @@ -2,6 +2,7 @@ package game import ( "fmt" + "math/rand" "golang.org/x/exp/slices" ) @@ -96,3 +97,12 @@ func (poc *PileOfCardsBase) ToList() (list string) { return list[:len(list)-1] } + +func NewRandomPackFromPool(pool PileOfCards, packSize int) PileOfCards { + pack := NewPileOfCards() + for _ = range packSize { + card := pool.Cards()[rand.Intn(len(pool.Cards())-1)] + pool.MoveCard(card, pack) + } + return pack +} diff --git a/go/game/player.go b/go/game/player.go index 845340a3..c05f7fe5 100644 --- a/go/game/player.go +++ b/go/game/player.go @@ -28,8 +28,7 @@ type Player struct { } // Creates a new player -// Must be called after a map was loaded to find -// the players Store. +// It initializes the players starting deck from the map's deck list if available. func NewPlayer(id int, name string, deck *Deck, gameState *LocalState, color color.Color) *Player { store := NewStore() deck.MoveInto(store) @@ -42,13 +41,31 @@ func NewPlayer(id int, name string, deck *Deck, gameState *LocalState, color col DrawPerTurn: MAX_DRAW, Hand: NewHand(), DiscardPile: NewDiscardPile(), - Deck: NewDeckFromDeckList(gameState.Map().StartDeckList), Store: store, gameState: gameState, Color: color, Ctrl: nil, knownStores: make(map[Position]bool), } + + // Prepopulate the Deck + if gameState != nil { + if m := gameState.Map(); m != nil { + p.Deck = NewDeckFromDeckList(m.StartDeckList) + } + } else { + p.Deck = NewDeck() + } + + return &p +} + +func NewDraftPlayer(name string) *Player { + p := Player{ + Name: name, + Deck: NewDeck(), + } + return &p } diff --git a/go/game/playerControl.go b/go/game/playerControl.go index 94c8be7e..bad1a13a 100644 --- a/go/game/playerControl.go +++ b/go/game/playerControl.go @@ -13,6 +13,7 @@ const ( PriorityNotification TargetSelectionPrompt DeclareTriggeredActionsPrompt + DraftPickPrompt ) func (n PlayerNotificationType) String() string { @@ -27,6 +28,8 @@ func (n PlayerNotificationType) String() string { return "TargetSelectionPrompt" case DeclareTriggeredActionsPrompt: return "DeclareTriggeredActionsPrompt" + case DraftPickPrompt: + return "DraftPickPrompt" default: log.Panicf("Unhandled notification %d", n) return "" @@ -93,6 +96,10 @@ func newDeclareTriggeredActionsPrompt(triggeredActions []*TriggeredAction) Playe return PlayerNotification{DeclareTriggeredActionsPrompt, triggeredActions, nil} } +func newDraftPickPrompt(pack PileOfCards) PlayerNotification { + return PlayerNotification{DraftPickPrompt, pack, nil} +} + type PlayerControl interface { Player() *Player RecvAction() Action |
