package game import ( "muhq.space/muhqs-game/go/log" "muhq.space/muhqs-game/go/utils" ) const ( MAX_DRAW int = 3 ) type Player struct { Id int Name string Turn int Resource int DrawPerTurn int Conceded bool Won bool Hand *Hand DiscardPile *DiscardPile Deck *Deck Store *Store gameState *LocalState Ctrl PlayerControl knownStores map[Position]bool } // NewPlayerWithStore creates a new player. func NewPlayerWithDeckAndStore(id int, name string, deck *Deck, store *Store, gameState *LocalState) *Player { p := Player{ Id: id, Name: name, Turn: 0, Resource: 0, DrawPerTurn: MAX_DRAW, Hand: NewHand(), DiscardPile: NewDiscardPile(), Deck: deck, Store: store, gameState: gameState, Ctrl: nil, knownStores: make(map[Position]bool), } return &p } // NewPlayer creates a new player moving its deck to the 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) *Player { store := NewStore() deck.MoveInto(store) p := NewPlayerWithDeckAndStore(id, name, nil, store, gameState) // Prepopulate the Deck if gameState != nil { if m := gameState.Map(); m != nil { p.Deck = NewDeckFromDeckList(m.StartDeckList) } } if p.Deck == nil { p.Deck = NewDeck() } return p } // NewDraftPlayer returns a player struct only initialized with a name and an empty deck. func NewDraftPlayer(name string) *Player { p := Player{ Name: name, Deck: NewDeck(), } return &p } func (p *Player) ResourceGain() int { if p.Name == "The kraken" { return p.gameState.Map().ResourceGain * (len(p.gameState.Players()) - 1) } else { return p.gameState.Map().ResourceGain } } func (p *Player) UpkeepGain() int { return p.ResourceGain() - p.UpkeepCost() } func (p *Player) UpkeepCost() int { cost := 0 for _, u := range p.gameState.units { if u.Controller() != p { continue } cost += u.UpkeepCost() } return cost } func (p *Player) gainResource(gain int) { p.Resource += gain } func (p *Player) reduceResource(amount int) { p.Resource -= amount if p.Resource < 0 { p.Resource = 0 } } func (p *Player) upkeep() []*Player { p.gainResource(p.ResourceGain()) s := p.gameState s.handleTriggers() // Skip upkeep prompt if player does not controll any units controllsUnits := false for _, u := range s.units { if u.Controller() == p { controllsUnits = true break } } if !controllsUnits { return nil } a, err := prompt(p.Ctrl, newUpkeepPrompt(p)) if err != nil { // FIXME: handle error } if _, ok := a.(*PassPriority); ok { a = newUpkeepAction(p) } if _, ok := a.(*ConcedeAction); ok { p.concede() return s._map.WinCondition.check(s) } s.declareAction(a) return s.stack.resolve() } func (p *Player) actionPhase() []*Player { s := p.gameState for { a, err := promptAction(p.Ctrl) if err != nil { // FIXME } if _, ok := a.(*PassPriority); ok { return nil } if _, ok := a.(*ConcedeAction); ok { p.concede() return s._map.WinCondition.check(s) } if a == nil { log.Fatal("Received nil action from ", p.Name) } s.declareAction(a) w := s.stack.resolve() if len(w) > 0 { return w } } } func (p *Player) buyPhase() []*Player { s := p.gameState if p.Store.Size() == 0 && s.Map().HasStores() && len(p.knownStores) == 0 { return nil } a, err := promptBuy(p.Ctrl) if err != nil { // FIXME } if _, ok := a.(*PassPriority); ok { return nil } if _, ok := a.(*ConcedeAction); ok { p.concede() return s._map.WinCondition.check(s) } if a.Targets().HasSelections() { s.declareAction(a) return s.stack.resolve() } return nil } func (p *Player) _discardCard(c *Card) { p.Hand.MoveCard(c, p.DiscardPile) } func (p *Player) discardCard(c *Card) { p._discardCard(c) p.gameState.fireEvent(Event{discard, []any{p}, []any{c}}) } func (p *Player) discardCards(cards []*Card) { for _, c := range cards { p._discardCard(c) } p.gameState.fireEvent(Event{discard, []any{p}, utils.TypedSliceToInterfaceSlice(cards)}) } func (p *Player) discardHand() { p.discardCards(p.Hand.Cards()) } func (p *Player) discardStep() { if p.Hand.Size() == 0 { return } cards := p.PromptHandCardSelection(0, p.Hand.Size()) p.discardCards(cards) } func (p *Player) Prompt(n PlayerNotification) Action { p.Ctrl.SendNotification(n) _a, err := p.Ctrl.RecvAction() if err != nil { // FIXME } switch a := _a.(type) { case *PassPriority: return nil case *ConcedeAction: p.concede() return nil default: return a } } func PromptSelection[T any](p *Player, n PlayerNotification) []T { _a := p.Prompt(n) if _a == nil { return nil } switch a := _a.(type) { case *TargetSelection: err := a.CheckTargets(p.gameState) if err != nil { log.Panicf("Invalid selection: %v", err) } return utils.InterfaceSliceToTypedSlice[T](a.Target().sel) default: log.Panicf("Unexpected response type %T for selection", a) } return nil } func (p *Player) PromptHandCardSelection(min, max int) []*Card { return PromptSelection[*Card](p, newHandCardSelectionPrompt(p, min, max)) } func (p *Player) PromptAlliedUnitSelection(min, max int) []*Unit { return PromptSelection[*Unit](p, newAlliedUnitSelectionPrompt(p, min, max)) } func (p *Player) PromptTilesSelection(c TargetConstraintFunc, min, max int) []*Tile { return PromptSelection[*Tile](p, newTileSelectionPrompt(p, c, min, max)) } func (p *Player) Draw() { toDraw := p.DrawPerTurn - p.Hand.Size() if toDraw < 1 { return } p.DrawN(toDraw) } func (p *Player) DrawN(n int) { drawnCards := p.Deck.Draw(n) p.Hand.AddCards(drawnCards) } func (p *Player) IsEnemy(other *Player) bool { // TODO: support coop / teams return p != other } func (p *Player) AvailableStores() (stores []*Store) { m := p.gameState.Map() for pos, store := range m.Stores { t := m.TileAt(pos) if t.Permanent != nil && t.Permanent.Controller() == p { stores = append(stores, store) } } return } func (p *Player) KnowsStore(pos Position) bool { return p.knownStores[pos] } func (p *Player) addKnownStore(pos Position) { p.knownStores[pos] = true } func (p *Player) clearKnownStore() { p.knownStores = make(map[Position]bool) } func (p *Player) concede() { p.Conceded = true p.gameState.broadcastNotification(newConcededNotification(p)) } func (p *Player) win() { p.Won = true }