package game import ( "fmt" "image/color" "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, color color.Color) *Player AddNewAiPlayer(name string, color color.Color) *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 } 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) AddNewPlayer(name string, deck *Deck, color color.Color) *Player { p := NewPlayer(len(s.players)+1, name, deck, s, color) s.players = append(s.players, p) return s.players[len(s.players)-1] } func (s *LocalState) AddNewAiPlayer(name string, color color.Color) *Player { switch name { case "kraken": return s.AddKraken(color) 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 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) } winners := []*Player{} for len(winners) == 0 { 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 p.upkeep() s.phaseChange() s.activePhase = Phases.ActionPhase p.actionPhase() s.phaseChange() s.activePhase = Phases.BuyPhase p.buyPhase() s.phaseChange() s.activePhase = Phases.DiscardStep p.discardStep() s.endOfTurn() winners = s._map.WinCondition(s) } } 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() { for _, p := range s.permanents { p.Card().Impl.stateBasedActions(s, p) if p.IsDestroyed() { s.destroyPermanent(p) } } s.handleTriggers() } 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(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 *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 { 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 := 0; i < nPlayers; i++ { p := s.players[(i+s.activePlayerId-1)%nPlayers] actions := playerActions[p] pActions := make([]Action, 0, len(actions)) p.Ctrl.SendNotification(newDeclareTriggeredActionsPrompt(actions)) switch a := p.Ctrl.RecvAction().(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 { nPlayers := len(s.players) i := 0 if skipFirst { i++ } for ; i < nPlayers; i++ { p := s.players[(i+s.activePlayerId-1)%nPlayers] s.stateBasedActions() 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 *LocalState) phaseChange() { skipFirst := true for !s.allPassing(skipFirst) { s.stack.resolve() skipFirst = false } } 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 } 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] } } } 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) 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()) s.events = append(s.events, Event{eventType: EventTypes.Destruction, affected: []any{p}}) } func (s *LocalState) fight(p1, p2 Permanent) { p1.fight(p2) p2.fight(p1) } 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.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 *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() { // 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() }