aboutsummaryrefslogtreecommitdiff
path: root/go/game/state.go
diff options
context:
space:
mode:
Diffstat (limited to 'go/game/state.go')
-rw-r--r--go/game/state.go453
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
+}