package game import ( "fmt" "log" "math/rand" "strconv" "strings" "time" "golang.org/x/exp/slices" "muhq.space/muhqs-game/go/utils" ) 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 { stores []*Store 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 } func (s *LocalState) Stores() []*Store { return s.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) 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 { s.activePlayerId = p.Id p.Turn++ if p.Deck.IsEmpty() { log.Printf("Shuffle %s pile into the deck\n", p.Name) p.DiscardPile.MoveInto(p.Deck) p.Deck.Shuffle(s.Rand) } s.activePhase = Phases.DrawStep p.Draw() s.activePhase = Phases.UpkeepPhase winners := p.upkeep() if len(winners) > 0 { return winners } winners = s.phaseChange() if len(winners) > 0 { return winners } s.activePhase = Phases.ActionPhase winners = p.actionPhase() if len(winners) > 0 { return winners } winners = s.phaseChange() if len(winners) > 0 { return winners } s.activePhase = Phases.BuyPhase winners = p.buyPhase() if len(winners) > 0 { return winners } winners = s.phaseChange() if len(winners) > 0 { return winners } s.activePhase = Phases.DiscardStep p.discardStep() winners = s.endOfTurn() if len(winners) > 0 { return winners } } } } func (s *LocalState) addTrigger(trigger Trigger) { s.triggers = append(s.triggers, trigger) } func (s *LocalState) removeTriggers(triggers []Trigger) { for _, t := range triggers { s.triggers = utils.RemoveFromUnorderedSlice(s.triggers, t) } } func (s *LocalState) handleTriggers() { var actions []*TriggeredAction var triggersToRemove []Trigger for _, e := range s.events { for _, t := range s.triggers { a, remove := t.trigger(s, e) if a != nil { actions = append(actions, a...) } if remove { triggersToRemove = append(triggersToRemove, t) } } } // Reset occured events s.events = nil s.removeTriggers(triggersToRemove) if len(actions) > 0 { orderedActions := s.orderTriggeredActions(actions) for _, a := range orderedActions { s.declareAction(a) } } } 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(s) return w } func (s *LocalState) IsValidPlay(a *PlayAction) error { if s.activePhase != Phases.ActionPhase { return fmt.Errorf("Play actions can only be declared during one's actions phase") } if a.Card.IsPermanent() { // Equipments may be played not on a spawn tile but equiped to the next played unit if a.Card.Type == CardTypes.Equipment && len(a.Target().sel) == 0 { return nil } spawn := a.Target().sel[0].(*Tile) if !slices.Contains(s.AvailableSpawnTiles(a.Source().(*Player), a.Card), spawn) { return fmt.Errorf("Spawn %v is not a valid spawn tile for %s", spawn.Position.String(), a.Card.Name) } } return nil } func (s *LocalState) IsValidMove(a *MoveAction) error { if s.activePhase != Phases.ActionPhase { return fmt.Errorf("Move actions can only be declared during one's actions phase") } u := a.Source().(*Unit) tile := relaxedTileTarget(a, a.Target().sel[0]) if !slices.Contains(u.MoveRangeTiles(), tile) { return fmt.Errorf("Unit %s@%v can not move to %s", u.card.Name, u.tile.Position, tile.Position.String()) } if !u.IsAvailableTile(tile) { return fmt.Errorf("Unit %s@%v can not move to not available tile %v", u.card.Name, u.tile.Position, tile) } return nil } func (s *LocalState) IsValidAttack(a *AttackAction) error { if s.activePhase != Phases.ActionPhase { return fmt.Errorf("Attack actions can only be declared during one's actions phase") } unit := a.Source().(*Unit) if INVALID_ATTACK(unit.Attack) { return fmt.Errorf("%s can not attack", unit.card.Name) } target := a.Target().sel[0].(Permanent) if !IsPositionInRange(unit.tile.Position, target.Tile().Position, unit.Attack.MaxRange()) { return fmt.Errorf("Attack target on %s is not in attack range %d of %s on %s", target.Tile().Position.String(), unit.Attack.MaxRange(), unit.card.Name, unit.tile.Position.String()) } return nil } 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 { if s.activePhase != Phases.ActionPhase { return fmt.Errorf("Play actions can only be declared during one's actions phase") } uTile := TileOrContainingPermTile(a.Source().(*Unit)) eTile := TileOrContainingPermTile(a.equipment) if !IsPositionInRange(uTile.Position, eTile.Position, 1) { return fmt.Errorf("Can only equip adjacent equipments") } return nil } func (s *LocalState) IsValidArtifactSwitch(a *ArtifactSwitchAction) error { if s.activePhase != Phases.ActionPhase { return fmt.Errorf("Play actions can only be declared during one's actions phase") } u := a.Source().(*Unit) uTile := TileOrContainingPermTile(u) artifact := a.Artifact aTile := TileOrContainingPermTile(artifact) if !IsPositionInRange(uTile.Position, aTile.Position, 1) { return fmt.Errorf("Can only switch position with adjacent equipments") } solid, err := artifact.XEffect("solid") if err != nil && artifact.Controller() != u.Controller() && solid > 0 { return fmt.Errorf("Can only switch with solid artifacts you control") } return nil } func (s *LocalState) IsValidArtifactMove(a *ArtifactMoveAction) error { if s.activePhase != Phases.ActionPhase { return fmt.Errorf("Play actions can only be declared during one's actions phase") } u := a.Source().(*Unit) uTile := TileOrContainingPermTile(u) artifact := a.Artifact aTile := TileOrContainingPermTile(artifact) if !IsPositionInRange(uTile.Position, aTile.Position, 1) { return fmt.Errorf("Can only switch position with adjacent equipments") } solid, err := artifact.XEffect("solid") if err != nil && artifact.Controller() != u.Controller() && solid > 0 { return fmt.Errorf("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 *PlayAction: err = s.IsValidPlay(a) case *MoveAction: err = s.IsValidMove(a) case *AttackAction: err = s.IsValidAttack(a) 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) } func (s *LocalState) declareAction(a Action) { var err error err = a.CheckTargets(s) if err == nil { switch a.(type) { case *BuyAction: if s.activePhase != Phases.BuyPhase { err = fmt.Errorf("Cards can only be bought during one's buy phase") } case *FreeAction: default: if a.Speed() == ActionSpeeds.Slow && !s.stack.IsEmpty() { err = fmt.Errorf("Slow action can not be declared while other actions are on the stack") } } } if err == nil { s.pushAction(a) } s.broadcastNotification(newDeclaredActionNotification(a, err)) if err == nil { t := a.Targets() if t != nil { // FIXME: seperate between targets choices implemented as target s.events = append(s.events, newTargetEvent(a, t)) } } } func (s *LocalState) orderTriggeredActions(triggeredActions []*TriggeredAction) []Action { 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 *DeclareTriggeredActionsAction: for _, a := range a.actions { // Use the order passed back by the user pActions = append(pActions, a) } orderedActions = append(orderedActions, pActions...) case *PassPriority: // Support kraken AI always sending PassPriority 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++ { p := s.players[(i+s.activePlayerId-1)%nPlayers] w := s.stateBasedActions() if len(w) > 0 { return false, w } p.Ctrl.SendNotification(newPriorityNotification()) _a, err := p.Ctrl.RecvAction() if err != nil { // FIXME } switch a := _a.(type) { case *PassPriority: log.Printf("%s passed %d/%d\n", p.Name, 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() []*Player { skipFirst := true for { passing, w := s.allPassing(skipFirst) if len(w) > 0 { return w } if passing { break } w = s.stack.resolve() if len(w) > 0 { return w } skipFirst = false } return nil } 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 perm.Tile() != nil { enterTile(perm, perm.Tile()) } 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) _leaveTile(p) s.RemovePermanent(p) if pile := p.Pile(); len(pile) > 0 { dropAction := newPileDropAction(p, t, pile) prompt(p.Controller().Ctrl, newTargetSelectionPrompt(dropAction, fmt.Sprintf("Drop %v's pile", p))) s.declareAction(dropAction) } p.Owner().DiscardPile.AddCard(p.Card()) s.events = append(s.events, 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) leaveTile(p1) leaveTile(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.events = append(s.events, newEotEvent()) return s.stateBasedActions() } // ResolveAction resolves an action without using the stack. func (s *LocalState) ResolveAction(a Action) { a.resolve(s) // Clear selections to make the acrion reusable a.Targets().ClearSelections() } func (s *LocalState) SetPhase(p PhaseType) { s.activePhase = p } func (s *LocalState) SetActivePlayer(p *Player) { s.activePlayerId = p.Id }