package game import ( "errors" "fmt" "maps" "math/rand" "slices" "strconv" "strings" "time" "muhq.space/muhqs-game/go/log" ) type State interface { Players() []*Player PlayerByName(name string) *Player PlayerById(id int) *Player AddNewPlayer(name string, deck *Deck) *Player AddNewAiPlayer(name string) *Player ActivePlayer() *Player ActivePhase() PhaseType SetMap(m *Map) Map() *Map Stores() []*Store Stack() *Stack Exile() PileOfCards Permanents() []Permanent Units() []*Unit Loop() []*Player } type LocalState struct { exile *PileOfCardsBase _map *Map stack *Stack players []*Player activePlayerId int activePhase PhaseType permanents []Permanent units []*Unit Rand *rand.Rand eotEffects []effect outstandingEquipment []*Card events []Event triggers []Trigger } func (s *LocalState) Players() []*Player { return s.players } // PlayerById returns the player using its 1-based identifier. func (s *LocalState) PlayerById(id int) *Player { return s.players[id-1] } func (s *LocalState) PlayerByName(name string) *Player { for _, p := range s.players { if p.Name == name { return p } } return nil } func (s *LocalState) ActivePlayer() *Player { return s.players[s.activePlayerId-1] } func (s *LocalState) ActivePhase() PhaseType { return s.activePhase } func (s *LocalState) Map() *Map { return s._map } // Stores returns a slice of Stores. // Either all stores on the map or the stores of all players are returned. func (s *LocalState) Stores() []*Store { if s._map.HasStores() { return slices.Collect(maps.Values(s._map.Stores)) } stores := make([]*Store, 0, len(s.players)) for _, p := range s.players { stores = append(stores, p.Store) } return stores } func (s *LocalState) SetMap(m *Map) { s._map = m } func (s *LocalState) Exile() PileOfCards { return s.exile } func (s *LocalState) Stack() *Stack { return s.stack } func (s *LocalState) Permanents() []Permanent { return s.permanents } func (s *LocalState) Units() []*Unit { return s.units } func NewLocalState() *LocalState { s := &LocalState{} s.stack = NewStack(s) s.exile = NewPileOfCards() s.Rand = rand.New(rand.NewSource(time.Now().UnixNano())) return s } func (s *LocalState) addPlayer(p *Player) *Player { // Start with the first player if s.activePlayerId == 0 { s.activePlayerId = p.Id } s.players = append(s.players, p) return s.players[len(s.players)-1] } func (s *LocalState) AddNewPlayerDeckAndStore(name string, deck *Deck, store *Store) *Player { p := NewPlayerWithDeckAndStore(len(s.players)+1, name, deck, store, s) return s.addPlayer(p) } func (s *LocalState) AddNewPlayer(name string, deck *Deck) *Player { p := NewPlayer(len(s.players)+1, name, deck, s) return s.addPlayer(p) } func (s *LocalState) AddNewAiPlayer(name string) *Player { switch name { case "kraken": return s.AddKraken() default: log.Fatalf("Ai Player %s not implemented", name) } return nil } func (s *LocalState) IsActivePlayer(p *Player) bool { return s.activePlayerId == p.Id } func (s *LocalState) changePhase(next PhaseType) []*Player { ev := newPhaseChangeEvent(s.activePhase, next) s.activePhase = next s.fireEvent(ev) return s.stateBasedActions() } func (s *LocalState) Loop() []*Player { // Prepare the map if s._map.Prepare != nil { s._map.Prepare(s) } if s._map.HasStores() { poc := NewPileOfCards() for _, p := range s.players { if p.IsKraken() { continue } p.Store.MoveInto(poc) } s._map.distributeStoreCards(poc, s.Rand) } for { for _, p := range s.players { // TODO: remove conceded players from turn rotation s.activePlayerId = p.Id p.Turn++ // Shuffle discard pile into empty deck if p.Deck.IsEmpty() { e := newShuffleEvent(p, []PileOfCards{p.DiscardPile}) s.fireEvent(e) p.DiscardPile.MoveInto(p.Deck) p.Deck.Shuffle(s.Rand) } // Switch to draw step without a priority round if w := s.changePhase(Phases.DrawStep); len(w) > 0 { return w } p.Draw() // Switch to upkeep without a priority round if w := s.changePhase(Phases.UpkeepPhase); len(w) > 0 { return w } winners := p.upkeep() if len(winners) > 0 { return winners } // Full phase change to the actions phase with a priority round winners = s.phaseChange(Phases.ActionPhase) if len(winners) > 0 { return winners } winners = p.actionPhase() if len(winners) > 0 { return winners } // Full phase change to the buy phase with a priority round winners = s.phaseChange(Phases.BuyPhase) if len(winners) > 0 { return winners } winners = p.buyPhase() if len(winners) > 0 { return winners } // Full phase change to the discard step with a priority round winners = s.phaseChange(Phases.DiscardStep) if len(winners) > 0 { return winners } p.discardStep() // End the trun after discarding without a priority round winners = s.endOfTurn() if len(winners) > 0 { return winners } } } } func (s *LocalState) stateBasedActions() []*Player { for _, p := range s.permanents { p.Card().Impl.stateBasedActions(s, p) if p.IsDestroyed() { s.destroyPermanent(p) } } s.handleTriggers() w := s._map.WinCondition.check(s) return w } func (s *LocalState) isValidBuy(a *BuyAction) error { card := a.card() if s._map.HasStores() { var storeTiles []*Tile for pos, store := range s._map.Stores { if slices.Contains(store.cards, card) { storeTiles = append(storeTiles, s._map.TileAt(pos)) } } if storeTiles == nil { return fmt.Errorf("no store contains %s", card.Name) } controlsUnitOnStore := false for _, tile := range storeTiles { controlsUnitOnStore = controlsUnitOnStore || (tile.Permanent != nil && tile.Permanent.Card().Type == CardTypes.Unit && tile.Permanent.Controller() == a.player) } if !controlsUnitOnStore { return fmt.Errorf("%s' controls no unit on a store containing %s", a.player.Name, card.Name) } } else if !slices.Contains(a.player.Store.cards, card) { return fmt.Errorf("%s's store does not contain %s", a.player.Name, card.Name) } if !a.card().IsBuyable() { return fmt.Errorf("card %s is not buyable", card.Name) } return nil } func (s *LocalState) IsValidEquip(a *EquipAction) error { uTile := TileOrContainingPermTile(a.Source().(*Unit)) eTile := TileOrContainingPermTile(a.equipment) if !IsPositionInRange(uTile.Position, eTile.Position, 1) { return errors.New("can only equip adjacent equipments") } return nil } func (s *LocalState) IsValidArtifactSwitch(a *ArtifactSwitchAction) error { u := a.Source().(*Unit) uTile := TileOrContainingPermTile(u) artifact := a.Artifact aTile := TileOrContainingPermTile(artifact) if !IsPositionInRange(uTile.Position, aTile.Position, 1) { return errors.New("can only switch position with adjacent equipments") } solid, err := artifact.XEffect("solid") if err != nil && artifact.Controller() != u.Controller() && solid > 0 { return errors.New("can only switch with solid artifacts you control") } return nil } func (s *LocalState) IsValidArtifactMove(a *ArtifactMoveAction) error { u := a.Source().(*Unit) uTile := TileOrContainingPermTile(u) artifact := a.Artifact aTile := TileOrContainingPermTile(artifact) if !IsPositionInRange(uTile.Position, aTile.Position, 1) { return errors.New("can only switch position with adjacent equipments") } solid, err := artifact.XEffect("solid") if err != nil && artifact.Controller() != u.Controller() && solid > 0 { return errors.New("can only switch with solid artifacts you control") } return nil } func (s *LocalState) ValidateAction(a Action) (err error) { err = a.CheckTargets(s) if err != nil { return } switch a := a.(type) { case *EquipAction: err = s.IsValidEquip(a) case *ArtifactSwitchAction: err = s.IsValidArtifactSwitch(a) case *ArtifactMoveAction: err = s.IsValidArtifactMove(a) case *BuyAction: } return } func (s *LocalState) pushAction(a Action) { if !a.PayCosts(s) { log.Panicf("Cost for %s could not be paid", a) } s.stack.push(a) } type ErrDeclareAction struct { reason string } func (err ErrDeclareAction) Error() string { return err.reason } var ( ErrDeclareActionWrongPhase = ErrDeclareAction{"wrong phase"} ErrDeclareActionStackNotEmpty = ErrDeclareAction{"stack not empty"} ) func (s *LocalState) checkActionTiming(a Action) error { switch a.(type) { case *BuyAction: if s.activePhase != Phases.BuyPhase || s.activePlayerId != a.Controller().Id { return ErrDeclareActionWrongPhase } case *UpkeepAction: if s.activePhase != Phases.UpkeepPhase || s.activePlayerId != a.Controller().Id { return ErrDeclareActionWrongPhase } default: if a.Speed() == ActionSpeeds.Slow { if !s.stack.IsEmpty() { return ErrDeclareActionStackNotEmpty } else if s.activePlayerId != a.Controller().Id || s.activePhase != Phases.ActionPhase { return ErrDeclareActionWrongPhase } } } return nil } func (s *LocalState) declareAction(a Action) { var err error err = s.ValidateAction(a) if err == nil { err = s.checkActionTiming(a) } if err == nil { s.pushAction(a) s.fireEvent(newDeclaredActionEvent(a)) } log.Info("declare action", "action", a, "error", err) s.broadcastNotification(newDeclaredActionNotification(a, err)) if err == nil { t := a.Targets() if t != nil { // FIXME: seperate between targets choices implemented as target s.fireEvent(newTargetEvent(a, t)) } } } func (s *LocalState) counterAction(a Action) { s.stack.remove(a) } func (s *LocalState) orderTriggeredActions(triggeredActions []*TriggeredAction) []Action { // Sort triggers by their controller playerActions := make(map[*Player][]*TriggeredAction) for _, action := range triggeredActions { p := action.Controller() if actions, found := playerActions[p]; found { playerActions[p] = append(actions, action) } else { playerActions[p] = []*TriggeredAction{action} } } orderedActions := []Action{} nPlayers := len(s.players) for i := range nPlayers { p := s.players[(i+s.activePlayerId-1)%nPlayers] actions := playerActions[p] pActions := make([]Action, 0, len(actions)) p.Ctrl.SendNotification(newDeclareTriggeredActionsPrompt(actions)) _a, err := p.Ctrl.RecvAction() if err != nil { // FIXME } switch a := _a.(type) { case *ConcedeAction: a.player.concede() return nil case *DeclareTriggeredActionsAction: for _, a := range a.actions { // Use the order passed back by the user pActions = append(pActions, a) } orderedActions = append(orderedActions, pActions...) case nil, *PassPriority: // Support kraken AI always sending PassPriority and mockPlayerControl for _, a := range actions { // Simply use the order the actions were triggered pActions = append(pActions, a) } orderedActions = append(orderedActions, pActions...) default: log.Panicf("Unexpected action type %T after DeclareTriggerPrompt", a) } } return orderedActions } func (s *LocalState) allPassing(skipFirst bool) (bool, []*Player) { nPlayers := len(s.players) i := 0 if skipFirst { i++ } for ; i < nPlayers; i++ { w := s.stateBasedActions() if len(w) > 0 { return false, w } p := s.players[(i+s.activePlayerId-1)%nPlayers] if p.Conceded { continue } p.Ctrl.SendNotification(newPriorityNotification()) _a, err := p.Ctrl.RecvAction() if err != nil { // FIXME } switch a := _a.(type) { case *ConcedeAction: a.player.concede() case *PassPriority: log.Info("priority passed", "player", p.Name, "passed", fmt.Sprintf("%d/%d", i+1, nPlayers)) case nil: log.Fatal("received nil action from ", p.Name, " at ", s.activePhase) default: s.declareAction(a) return false, nil } } return true, nil } func (s *LocalState) phaseChange(next PhaseType) (w []*Player) { skipFirst := true for { var passing bool passing, w = s.allPassing(skipFirst) if len(w) > 0 { break } if passing { break } w = s.stack.resolve() if len(w) > 0 { break } skipFirst = false } s.changePhase(next) return } func (s *LocalState) broadcastNotification(n PlayerNotification) { for _, p := range s.players { p.Ctrl.SendNotification(n) } } func (s *LocalState) addPermanent(perm Permanent) { s.permanents = append(s.permanents, perm) switch p := perm.(type) { case *Unit: s.units = append(s.units, p) } if t := perm.Tile(); t != nil { enterTile(perm, t) } perm.Card().Impl.onETB(s, perm) } func (s *LocalState) addNewUnit(card *Card, pos Position, owner *Player) *Unit { u := NewUnit(card, s._map.TileAt(pos), owner) s.addPermanent(u) return u } func (s *LocalState) addNewArtifact(card *Card, pos Position, owner *Player) *Artifact { a := NewArtifact(card, s._map.TileAt(pos), owner) s.addPermanent(a) return a } func (s *LocalState) addNewEquipment(card *Card, pos Position, owner *Player) *Equipment { e := newEquipment(card, s._map.TileAt(pos), owner) s.addPermanent(e) return e } // MovePermanent moves a permanent without doing anything else. // This method does not perform any checks or trigger effects. // Its purpose is to modify the game state in situations where rules are not enforced (ui.FreeMapControl). func (s *LocalState) MovePermanent(p Permanent, t *Tile) { if cp := p.ContainingPerm(); cp != nil { removePermanentFromPile(cp, p) } else { p.Tile().Permanent = nil } if cp := t.Permanent; cp != nil { addPermanentToPile(cp, p) } else { t.Permanent = p p.SetTile(t) } } // RemovePermanent removes a Permanent from the game state. // This method does not perform checks or triggers effects. func (s *LocalState) RemovePermanent(p Permanent) { for i, perm := range s.permanents { if perm == p { s.permanents[i] = s.permanents[len(s.permanents)-1] s.permanents = s.permanents[:len(s.permanents)-1] } } switch p := p.(type) { case *Unit: s.units = removePtr(s.units, p) } } func removePtr[P *Unit](collection []P, ptr P) []P { for i, p := range collection { if p == ptr { collection[i] = collection[len(collection)-1] return collection[:len(collection)-1] } } return collection } func (s *LocalState) destroyPermanent(p Permanent) { t := TileOrContainingPermTile(p) leaveTileOrPile(p) // Reset tile to format pile strings dependent on the permanents tile p.SetTile(t) s.RemovePermanent(p) if pile := p.Pile(); len(pile) > 0 { dropAction := newPileDropAction(p, t, pile) _a, err := prompt(p.Controller().Ctrl, newTargetSelectionPrompt(dropAction, fmt.Sprintf("Drop %s's pile", p))) if err != nil { // FIXME } switch a := _a.(type) { case *ConcedeAction: a.player.concede() case *PileDropAction: s.declareAction(a) default: log.Error("received other action on pile drop prompt") } } p.Owner().DiscardPile.AddCard(p.Card()) s.fireEvent(Event{eventType: EventTypes.Destruction, affected: []any{p}}) } // fight implements the effect of two permanents fighting. func (s *LocalState) fight(p1, p2 Permanent) { p1.fight(p2) p2.fight(p1) } // attack implements the effects of a permanent p1 attacking another permanent p2. func (s *LocalState) attack(p1, p2 Permanent) { if p1 == p2 { log.Panic("A unit can not attack itself") } d := DistanceBetweenPermanents(p1, p2) if d > 1 && p2.RangeProtected() { log.Panic("Range protected unit can only be attacked in melee combat") } s.fight(p1, p2) } func (s *LocalState) switchPermanents(p1 Permanent, p2 Permanent) { t1 := TileOrContainingPermTile(p1) t2 := TileOrContainingPermTile(p2) leaveTileOrPile(p1) leaveTileOrPile(p2) enterTileOrPile(p1, t2) enterTileOrPile(p2, t1) } func (s *LocalState) FilterUnits(filter func(*Unit) bool) []*Unit { units := []*Unit{} for _, u := range s.units { if filter(u) { units = append(units, u) } } return units } func (s *LocalState) OwnUnits(player *Player) []*Unit { return s.FilterUnits(func(u *Unit) bool { return u.Controller() == player }) } func (s *LocalState) EnemyUnits(player *Player) []*Unit { return s.FilterUnits(func(u *Unit) bool { return u.Controller().IsEnemy(player) }) } func (s *LocalState) resolvePlay(a *PlayAction) { p := a.Controller() c := a.Card targets := a.targets switch c.Type { case CardTypes.Unit: tile := targets.ts[0].sel[0].(*Tile) u := s.addNewUnit(c, tile.Position, p) if len(s.outstandingEquipment) > 0 { for _, e := range s.outstandingEquipment { u.equip(newEquipment(e, nil, p)) } s.outstandingEquipment = nil } case CardTypes.Artifact: tile := targets.ts[0].sel[0].(*Tile) s.addNewArtifact(c, tile.Position, p) case CardTypes.Equipment: if len(targets.ts[0].sel) == 0 { s.outstandingEquipment = append(s.outstandingEquipment, c) } else { tile, _ := targets.ts[0].sel[0].(*Tile) s.addNewEquipment(c, tile.Position, p) } case CardTypes.Spell: c.Impl.onPlay(a) default: log.Panicf("Resolving unhandled card type: %s\n", c.Type.String()) } if c.Type == CardTypes.Spell { if !s.exile.Contains(c) { p.DiscardPile.AddCard(c) } } } func (s *LocalState) additionalSpawnsFor(p *Player, cType CardType) (spawns []*Tile) { for _, perm := range s.permanents { if perm.Controller() != p { continue } spawns = append(spawns, perm.Card().Impl.additionalSpawnsFor(perm, cType)...) } return } func (s *LocalState) SpawnTiles(p *Player) []*Tile { spawns := s._map.FilterTiles(func(t *Tile) bool { if t.Type != TileTypes.Spawn { return false } return strings.HasSuffix(t.Raw, strconv.Itoa(p.Id)) || t.Raw == "player spawn" }) return spawns } func (s *LocalState) WaterSpawnTiles(p *Player) []*Tile { if p.IsKraken() { return s.KrakenSpawnTiles() } docks := s._map.FilterTiles(func(t *Tile) bool { if t.Type != TileTypes.Docks { return false } return t.Raw == "docks" || strings.HasSuffix(t.Raw, strconv.Itoa(p.Id)) }) spawns := []*Tile{} for _, dock := range docks { for _, candidate := range TilesInRangeFromOrigin(s._map, dock.Position, 1) { if candidate.Water { spawns = append(spawns, candidate) } } } return spawns } func (s *LocalState) AvailableSpawnTiles(p *Player, c *Card) []*Tile { if !c.IsPermanent() { log.Panicf("AvailableSpawnTiles called for %s", c.Type.String()) } candidates := c.Impl.spawnTiles(s, p) if candidates == nil { // Default spawn tiles if c.hasPlacementConstrain("swimming") { candidates = s.WaterSpawnTiles(p) } else { candidates = s.SpawnTiles(p) } candidates = append(candidates, s.additionalSpawnsFor(p, c.Type)...) } tiles := []*Tile{} for _, candidate := range candidates { if candidate.IsAvailableForCard(c) { tiles = append(tiles, candidate) } } return tiles } func (s *LocalState) redistributeMapStoreCards() { poc := NewPileOfCards() for _, store := range s._map.Stores { store.MoveInto(poc) } s._map.distributeStoreCards(poc, s.Rand) for _, p := range s.players { p.clearKnownStore() } } func (s *LocalState) addEotEffect(e effect) { e.apply(s) s.eotEffects = append(s.eotEffects, e) } func (s *LocalState) endOfTurn() []*Player { // Move all outstanding equipment cards to the discard pile for _, e := range s.outstandingEquipment { s.ActivePlayer().DiscardPile.AddCard(e) } for _, e := range s.eotEffects { e.end(s) } s.eotEffects = nil s.fireEvent(newEotEvent()) return s.stateBasedActions() } // ResolveAction resolves an action. // Additionally it resets the targets of the action to make it reusable. func (s *LocalState) ResolveAction(a Action) { a.resolve(s) // Clear selections to make the action reusable if a.Targets() != nil { a.Targets().ClearSelections() } } func (s *LocalState) SetPhase(p PhaseType) { s.activePhase = p } func (s *LocalState) SetActivePlayer(p *Player) { s.activePlayerId = p.Id }