aboutsummaryrefslogtreecommitdiff
path: root/go/game
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/game
parenta56641946ad4adcf2e59c08ca4befe70ca1e0e1b (diff)
downloadmuhqs-game-8ec9ff690082e9761e056bf5740daf6b80769f1f.tar.gz
muhqs-game-8ec9ff690082e9761e056bf5740daf6b80769f1f.zip
implement simple bot based draft
Diffstat (limited to 'go/game')
-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
5 files changed, 175 insertions, 3 deletions
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