diff options
Diffstat (limited to 'go/game/state.go')
| -rw-r--r-- | go/game/state.go | 453 |
1 files changed, 453 insertions, 0 deletions
diff --git a/go/game/state.go b/go/game/state.go new file mode 100644 index 00000000..1d4b644f --- /dev/null +++ b/go/game/state.go @@ -0,0 +1,453 @@ +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 unit.Attack == INVALID_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.Range) { + return fmt.Errorf("Attack target on %s is not in attack range %d of %s on %s", + target.Tile().Position.String(), unit.Attack.Range, 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 +} |
