package game import ( "fmt" "log" "math/rand" "strconv" "strings" "time" "golang.org/x/exp/slices" ) type State struct { Decks []*Deck Stores []*Store Exile *PileOfCardsBase Map *Map Stack *Stack Hands []*Hand DiscardPiles []*DiscardPile Players []*Player ActivePlayer *Player ActivePhase PhaseType Permanents []Permanent Units []*Unit Artifacts []*Artifact Rand *rand.Rand } 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() winners = s.Map.WinCondition(s) } } return winners } func (s *State) IsValidPlay(a *PlayAction) error { if s.ActivePhase != Phases.ActionPhase { log.Fatalf("Play actions can only be declared during one's actions phase") } if a.Card.IsPermanent() { spawn := a.Targets().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 := a.Targets().sel[0].(*Tile) if !slices.Contains(u.MoveRangeTiles(s.Map), tile) { return fmt.Errorf("Unit %s at %s can not move to %s", u.card.Name, u.tile.Position.String(), tile.Position.String()) } if tile.Permanent != nil { return fmt.Errorf("Unit %s at %s can not move to occupied %s", u.card.Name, u.tile.Position.String(), tile.Position.String()) } 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.Targets().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 { if s.Map.HasStores() { var storeTiles []*Tile for pos, store := range s.Map.Stores { if slices.Contains(store.cards, a.card) { storeTiles = append(storeTiles, s.Map.TileAt(pos)) } } if storeTiles == nil { return fmt.Errorf("no store contains %s", a.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, a.card.Name) } } else if !slices.Contains(a.player.Store.cards, a.card) { return fmt.Errorf("%s's store does not contain %s", a.player.Name, a.card.Name) } 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 *BuyAction: } return } func (s *State) PushAction(a Action) { if !a.PayCosts(s) { log.Fatalf("Cost for %s could not be paid", a) } s.Stack.Push(a) } func (s *State) DeclareAction(a Action) { var err error = nil err = a.CheckTargets(s) if err != nil { switch a := a.(type) { case *BuyAction: if s.ActivePhase != Phases.BuyPhase { err = fmt.Errorf("Cards can only be bought during one's buy phase") } if !a.card.IsBuyable() { err = fmt.Errorf("Card %s is not buyable", a.card.Name) } 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 { s.PushAction(a) } s.broadcastNotification(NewDeclaredActionNotification(a, err)) } 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) case *Artifact: s.Artifacts = append(s.Artifacts, p) } } func (s *State) AddNewUnit(card *Card, pos Position, owner *Player) { s.AddPermanent(NewUnit(card, s.Map.TileAt(pos), owner)) } func (s *State) AddNewArtifact(card *Card, pos Position, owner *Player) { s.AddPermanent(NewArtifact(card, s.Map.TileAt(pos), owner)) } func (s *State) AddNewPermFromPath(cardPath string, pos Position, owner *Player) { card := NewCard(cardPath) switch card.Type { case CardTypes.Unit: s.AddNewUnit(card, pos, owner) case CardTypes.Artifact: s.AddNewArtifact(card, pos, owner) } } 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 | *Artifact](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) { s.removePermanent(p) switch p := p.(type) { case *Unit: s.Units = removePtr(s.Units, p) case *Artifact: s.Artifacts = removePtr(s.Artifacts, p) } p.Owner().DiscardPile.AddCard(p.Card()) } func (s *State) Fight(p1, p2 Permanent) { p1.Fight(p2) p2.Fight(p1) if p1.IsDestroyed() { s.DestroyPermanent(p1) } if p2.IsDestroyed() { s.DestroyPermanent(p2) } } func (s *State) MoveUnit(u *Unit, t *Tile) { u.move(s, t) } 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.sel[0].(*Tile) s.AddNewUnit(c, tile.Position, p) case CardTypes.Artifact: tile := targets.sel[0].(*Tile) s.AddNewArtifact(c, tile.Position, p) default: log.Printf("Resolving unhandled card type: %s\n", c.Type.String()) } if c.IsPermanent() { } else { p.DiscardPile.AddCard(c) } } 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.Fatalf("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) } } tiles := []*Tile{} for _, candidate := range candidates { if candidate.IsAvailableForCard(c) { tiles = append(tiles, candidate) } } return tiles }