aboutsummaryrefslogtreecommitdiff
path: root/go
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2025-04-29 15:05:12 -0500
committerFlorian Fischer <florian.fischer@muhq.space>2025-08-20 15:57:16 +0200
commit8ec9ff690082e9761e056bf5740daf6b80769f1f (patch)
tree02e2c5a4f2deb440f4b8fc79237742cddb8815f6 /go
parenta56641946ad4adcf2e59c08ca4befe70ca1e0e1b (diff)
downloadmuhqs-game-8ec9ff690082e9761e056bf5740daf6b80769f1f.tar.gz
muhqs-game-8ec9ff690082e9761e056bf5740daf6b80769f1f.zip
implement simple bot based draft
Diffstat (limited to 'go')
-rw-r--r--go/TODO1
-rw-r--r--go/client/draft.go199
-rw-r--r--go/client/startMenu.go11
-rw-r--r--go/game/action.go22
-rw-r--r--go/game/draft.go116
-rw-r--r--go/game/pileOfCards.go10
-rw-r--r--go/game/player.go23
-rw-r--r--go/game/playerControl.go7
8 files changed, 385 insertions, 4 deletions
diff --git a/go/TODO b/go/TODO
index 577121ca..4874a313 100644
--- a/go/TODO
+++ b/go/TODO
@@ -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