package game import ( "fmt" "log" "math/rand" "strconv" "strings" "time" "golang.org/x/exp/slices" "muhq.space/muhqs-game/go/utils" ) type State struct { Stores []*Store Exile *PileOfCardsBase Map *Map Stack *Stack Players []*Player ActivePlayer *Player ActivePhase PhaseType Permanents []Permanent Units []*Unit Rand *rand.Rand eotEffects []effect outstandingEquipment []*Card events []Event triggers []Trigger } func NewState() *State { s := &State{} s.Stack = NewStack(s) s.Exile = NewPileOfCards() s.Rand = rand.New(rand.NewSource(time.Now().UnixNano())) return s } func (s *State) AddNewPlayer(name string, deck *Deck) { p := NewPlayer(len(s.Players)+1, name, deck, s) s.Players = append(s.Players, p) } func (s *State) IsActivePlayer(p *Player) bool { return s.ActivePlayer == p } func (s *State) Loop() []*Player { winners := []*Player{} 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 len(winners) == 0 { for _, p := range s.Players { s.ActivePlayer = p 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.DrawPhase p.Draw() s.ActivePhase = Phases.UpkeepPhase p.upkeep() s.phaseChange() s.ActivePhase = Phases.ActionPhase p.actionPhase() s.phaseChange() s.ActivePhase = Phases.BuyPhase p.buyPhase() s.phaseChange() s.ActivePhase = Phases.DiscardPhase p.discardPhase() s.endOfTurn() winners = s.Map.WinCondition(s) } } return winners } func (s *State) removeTriggers(triggers []Trigger) { for _, t := range triggers { s.triggers = utils.RemoveFromUnorderedSlice(s.triggers, t) } } func (s *State) 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 *State) stateBasedActions() { for _, p := range s.Permanents { p.Card().Impl.stateBasedActions(s, p) if p.IsDestroyed() { s.destroyPermanent(p) } } s.handleTriggers() } func (s *State) 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 *State) 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(s.Map), 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 *State) 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 *State) 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 *State) 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 *State) 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 *State) 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 *State) 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 *State) pushAction(a Action) { if !a.PayCosts(s) { log.Panicf("Cost for %s could not be paid", a) } s.Stack.push(a) } func (s *State) 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 !s.Stack.IsEmpty() { err = fmt.Errorf("Slow action can not be declared while other actions are on the stack") } } } if err == nil { if pa, ok := a.(*PlayAction); ok { pa.Source().(*Player).Hand.RemoveCard(pa.Card) } s.pushAction(a) } s.broadcastNotification(newDeclaredActionNotification(a, err)) } func (s *State) 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 := 0; i < nPlayers; i++ { p := s.Players[(i+s.ActivePlayer.Id-1)%nPlayers] actions := playerActions[p] p.Ctrl.SendNotification(newDeclareTriggeredActionsPrompt(actions)) switch a := p.Ctrl.RecvAction().(type) { case *DeclareTriggeredActionsAction: pActions := make([]Action, 0, len(a.actions)) for _, a := range a.actions { pActions = append(pActions, a) } orderedActions = append(orderedActions, pActions...) default: log.Panicf("Unexpected action type %T after DeclareTriggerPrompt", a) } } return orderedActions } func (s *State) allPassing(skipFirst bool) bool { nPlayers := len(s.Players) i := 0 if skipFirst { i++ } for ; i < nPlayers; i++ { p := s.Players[(i+s.ActivePlayer.Id-1)%nPlayers] p.Ctrl.SendNotification(newPriorityNotification()) a := p.Ctrl.RecvAction() switch a.(type) { case *PassPriority: log.Printf("%s passed %d/%d\n", p.Name, i+1, nPlayers) default: s.declareAction(a) return false } } return true } func (s *State) phaseChange() { skipFirst := true for !s.allPassing(skipFirst) { s.Stack.resolve() skipFirst = false } } func (s *State) broadcastNotification(n PlayerNotification) { for _, p := range s.Players { p.Ctrl.SendNotification(n) } } func (s *State) 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()) } } func (s *State) addNewUnit(card *Card, pos Position, owner *Player) *Unit { u := NewUnit(card, s.Map.TileAt(pos), owner) s.addPermanent(u) return u } func (s *State) addNewArtifact(card *Card, pos Position, owner *Player) *Artifact { a := NewArtifact(card, s.Map.TileAt(pos), owner) s.addPermanent(a) return a } func (s *State) addNewEquipment(card *Card, pos Position, owner *Player) *Equipment { e := newEquipment(card, s.Map.TileAt(pos), owner) s.addPermanent(e) return e } func (s *State) 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] } } } 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 *State) destroyPermanent(p Permanent) { t := TileOrContainingPermTile(p) _leaveTile(p) s.removePermanent(p) switch p := p.(type) { case *Unit: s.Units = removePtr(s.Units, 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()) } func (s *State) fight(p1, p2 Permanent) { p1.fight(p2) p2.fight(p1) } func (s *State) switchPermanents(p1 Permanent, p2 Permanent) { t1 := TileOrContainingPermTile(p1) t2 := TileOrContainingPermTile(p2) leaveTile(p1) leaveTile(p2) enterTileOrPile(p1, t2) enterTileOrPile(p2, t1) } func (s *State) FilterUnits(filter func(*Unit) bool) []*Unit { units := []*Unit{} for _, u := range s.Units { if filter(u) { units = append(units, u) } } return units } func (s *State) OwnUnits(player *Player) []*Unit { return s.FilterUnits(func(u *Unit) bool { return u.Controller() == player }) } func (s *State) EnemyUnits(player *Player) []*Unit { return s.FilterUnits(func(u *Unit) bool { return u.Controller().IsEnemy(player) }) } func (s *State) resolvePlay(p *Player, c *Card, targets *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(s, c, p, targets) default: log.Printf("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 *State) 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 *State) 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 *State) 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 *State) 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 *State) 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 *State) addEotEffect(e effect) { e.apply(s) s.eotEffects = append(s.eotEffects, e) } func (s *State) endOfTurn() { // 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()) s.stateBasedActions() }