aboutsummaryrefslogtreecommitdiff
path: root/go/game/state.go
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2024-12-27 12:40:19 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2025-01-27 16:43:58 +0100
commit4e2ab55d345fd5e5c5d2d5bc56948611b15564d5 (patch)
treec6998ac54687c442ad0baa541edd0fbf917b1308 /go/game/state.go
parent5aa8e7ddbc41aed55e7918e6afaec0b5c07fa510 (diff)
downloadmuhqs-game-4e2ab55d345fd5e5c5d2d5bc56948611b15564d5.tar.gz
muhqs-game-4e2ab55d345fd5e5c5d2d5bc56948611b15564d5.zip
introduce game.State interface abstraction
This allows to not use the internal game state directly from client code.
Diffstat (limited to 'go/game/state.go')
-rw-r--r--go/game/state.go296
1 files changed, 189 insertions, 107 deletions
diff --git a/go/game/state.go b/go/game/state.go
index 078815da..cf7b7111 100644
--- a/go/game/state.go
+++ b/go/game/state.go
@@ -2,6 +2,7 @@ package game
import (
"fmt"
+ "image/color"
"log"
"math/rand"
"strconv"
@@ -12,16 +13,34 @@ import (
"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
+type State interface {
+ Players() []*Player
+ PlayerByName(name string) *Player
+ PlayerById(id int) *Player
+ AddNewPlayer(name string, deck *Deck, color color.Color)
+ AddNewAiPlayer(name string, color color.Color)
+ 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
@@ -29,41 +48,104 @@ type State struct {
triggers []Trigger
}
-func NewState() *State {
- s := &State{}
- s.Stack = NewStack(s)
- s.Exile = NewPileOfCards()
+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]
+}
+
+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 *State) AddNewPlayer(name string, deck *Deck) {
- p := NewPlayer(len(s.Players)+1, name, deck, s)
- s.Players = append(s.Players, p)
+func (s *LocalState) AddNewPlayer(name string, deck *Deck, color color.Color) {
+ p := NewPlayer(len(s.players)+1, name, deck, s, color)
+ s.players = append(s.players, p)
+}
+
+func (s *LocalState) AddNewAiPlayer(name string, color color.Color) {
+ switch name {
+ case "kraken":
+ s.AddKraken(color)
+ default:
+ log.Fatalf("Ai Player %s not implemented", name)
+ }
}
-func (s *State) IsActivePlayer(p *Player) bool {
- return s.ActivePlayer == p
+func (s *LocalState) IsActivePlayer(p *Player) bool {
+ return s.activePlayerId == p.Id
}
-func (s *State) Loop() []*Player {
+func (s *LocalState) Loop() []*Player {
winners := []*Player{}
- if s.Map.HasStores() {
+ if s._map.HasStores() {
poc := NewPileOfCards()
- for _, p := range s.Players {
+ for _, p := range s.players {
if p.IsKraken() {
continue
}
p.Store.MoveInto(poc)
}
- s.Map.distributeStoreCards(poc, s.Rand)
+ s._map.distributeStoreCards(poc, s.Rand)
}
for len(winners) == 0 {
- for _, p := range s.Players {
- s.ActivePlayer = p
+ for _, p := range s.players {
+ s.activePlayerId = p.Id
p.Turn++
if p.Deck.IsEmpty() {
@@ -72,44 +154,44 @@ func (s *State) Loop() []*Player {
p.Deck.Shuffle(s.Rand)
}
- s.ActivePhase = Phases.DrawPhase
+ s.activePhase = Phases.DrawPhase
p.Draw()
- s.ActivePhase = Phases.UpkeepPhase
+ s.activePhase = Phases.UpkeepPhase
p.upkeep()
s.phaseChange()
- s.ActivePhase = Phases.ActionPhase
+ s.activePhase = Phases.ActionPhase
p.actionPhase()
s.phaseChange()
- s.ActivePhase = Phases.BuyPhase
+ s.activePhase = Phases.BuyPhase
p.buyPhase()
s.phaseChange()
- s.ActivePhase = Phases.DiscardPhase
+ s.activePhase = Phases.DiscardPhase
p.discardPhase()
s.endOfTurn()
- winners = s.Map.WinCondition(s)
+ winners = s._map.WinCondition(s)
}
}
return winners
}
-func (s *State) addTrigger(trigger Trigger) {
+func (s *LocalState) addTrigger(trigger Trigger) {
s.triggers = append(s.triggers, trigger)
}
-func (s *State) removeTriggers(triggers []Trigger) {
+func (s *LocalState) removeTriggers(triggers []Trigger) {
for _, t := range triggers {
s.triggers = utils.RemoveFromUnorderedSlice(s.triggers, t)
}
}
-func (s *State) handleTriggers() {
+func (s *LocalState) handleTriggers() {
var actions []*TriggeredAction
var triggersToRemove []Trigger
@@ -138,8 +220,8 @@ func (s *State) handleTriggers() {
}
}
-func (s *State) stateBasedActions() {
- for _, p := range s.Permanents {
+func (s *LocalState) stateBasedActions() {
+ for _, p := range s.permanents {
p.Card().Impl.stateBasedActions(s, p)
if p.IsDestroyed() {
@@ -150,8 +232,8 @@ func (s *State) stateBasedActions() {
s.handleTriggers()
}
-func (s *State) IsValidPlay(a *PlayAction) error {
- if s.ActivePhase != Phases.ActionPhase {
+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")
}
@@ -171,15 +253,15 @@ func (s *State) IsValidPlay(a *PlayAction) error {
return nil
}
-func (s *State) IsValidMove(a *MoveAction) error {
- if s.ActivePhase != Phases.ActionPhase {
+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) {
+ 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())
}
@@ -192,8 +274,8 @@ func (s *State) IsValidMove(a *MoveAction) error {
return nil
}
-func (s *State) IsValidAttack(a *AttackAction) error {
- if s.ActivePhase != Phases.ActionPhase {
+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")
}
@@ -212,13 +294,13 @@ func (s *State) IsValidAttack(a *AttackAction) error {
return nil
}
-func (s *State) isValidBuy(a *BuyAction) error {
+func (s *LocalState) isValidBuy(a *BuyAction) error {
card := a.card()
- if s.Map.HasStores() {
+ if s._map.HasStores() {
var storeTiles []*Tile
- for pos, store := range s.Map.Stores {
+ for pos, store := range s._map.Stores {
if slices.Contains(store.cards, card) {
- storeTiles = append(storeTiles, s.Map.TileAt(pos))
+ storeTiles = append(storeTiles, s._map.TileAt(pos))
}
}
@@ -248,8 +330,8 @@ func (s *State) isValidBuy(a *BuyAction) error {
return nil
}
-func (s *State) IsValidEquip(a *EquipAction) error {
- if s.ActivePhase != Phases.ActionPhase {
+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")
}
@@ -262,8 +344,8 @@ func (s *State) IsValidEquip(a *EquipAction) error {
return nil
}
-func (s *State) IsValidArtifactSwitch(a *ArtifactSwitchAction) error {
- if s.ActivePhase != Phases.ActionPhase {
+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")
}
@@ -283,8 +365,8 @@ func (s *State) IsValidArtifactSwitch(a *ArtifactSwitchAction) error {
return nil
}
-func (s *State) IsValidArtifactMove(a *ArtifactMoveAction) error {
- if s.ActivePhase != Phases.ActionPhase {
+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")
}
@@ -304,7 +386,7 @@ func (s *State) IsValidArtifactMove(a *ArtifactMoveAction) error {
return nil
}
-func (s *State) ValidateAction(a Action) (err error) {
+func (s *LocalState) ValidateAction(a Action) (err error) {
err = a.CheckTargets(s)
if err != nil {
return
@@ -329,15 +411,15 @@ func (s *State) ValidateAction(a Action) (err error) {
return
}
-func (s *State) pushAction(a Action) {
+func (s *LocalState) pushAction(a Action) {
if !a.PayCosts(s) {
log.Panicf("Cost for %s could not be paid", a)
}
- s.Stack.push(a)
+ s.stack.push(a)
}
-func (s *State) declareAction(a Action) {
+func (s *LocalState) declareAction(a Action) {
var err error
err = a.CheckTargets(s)
@@ -345,12 +427,12 @@ func (s *State) declareAction(a Action) {
if err != nil {
switch a.(type) {
case *BuyAction:
- if s.ActivePhase != Phases.BuyPhase {
+ 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() {
+ if a.Speed() == ActionSpeeds.Slow && !s.stack.IsEmpty() {
err = fmt.Errorf("Slow action can not be declared while other actions are on the stack")
}
}
@@ -370,7 +452,7 @@ func (s *State) declareAction(a Action) {
}
}
-func (s *State) orderTriggeredActions(triggeredActions []*TriggeredAction) []Action {
+func (s *LocalState) orderTriggeredActions(triggeredActions []*TriggeredAction) []Action {
playerActions := make(map[*Player][]*TriggeredAction)
for _, action := range triggeredActions {
p := action.Controller()
@@ -382,9 +464,9 @@ func (s *State) orderTriggeredActions(triggeredActions []*TriggeredAction) []Act
}
orderedActions := []Action{}
- nPlayers := len(s.Players)
+ nPlayers := len(s.players)
for i := 0; i < nPlayers; i++ {
- p := s.Players[(i+s.ActivePlayer.Id-1)%nPlayers]
+ p := s.players[(i+s.activePlayerId-1)%nPlayers]
actions := playerActions[p]
pActions := make([]Action, 0, len(actions))
@@ -409,14 +491,14 @@ func (s *State) orderTriggeredActions(triggeredActions []*TriggeredAction) []Act
return orderedActions
}
-func (s *State) allPassing(skipFirst bool) bool {
- nPlayers := len(s.Players)
+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.ActivePlayer.Id-1)%nPlayers]
+ p := s.players[(i+s.activePlayerId-1)%nPlayers]
s.stateBasedActions()
p.Ctrl.SendNotification(newPriorityNotification())
a := p.Ctrl.RecvAction()
@@ -431,25 +513,25 @@ func (s *State) allPassing(skipFirst bool) bool {
return true
}
-func (s *State) phaseChange() {
+func (s *LocalState) phaseChange() {
skipFirst := true
for !s.allPassing(skipFirst) {
- s.Stack.resolve()
+ s.stack.resolve()
skipFirst = false
}
}
-func (s *State) broadcastNotification(n PlayerNotification) {
- for _, p := range s.Players {
+func (s *LocalState) broadcastNotification(n PlayerNotification) {
+ for _, p := range s.players {
p.Ctrl.SendNotification(n)
}
}
-func (s *State) addPermanent(perm Permanent) {
- s.Permanents = append(s.Permanents, perm)
+func (s *LocalState) addPermanent(perm Permanent) {
+ s.permanents = append(s.permanents, perm)
switch p := perm.(type) {
case *Unit:
- s.Units = append(s.Units, p)
+ s.units = append(s.units, p)
}
if perm.Tile() != nil {
@@ -459,29 +541,29 @@ func (s *State) addPermanent(perm Permanent) {
perm.Card().Impl.onETB(s, perm)
}
-func (s *State) addNewUnit(card *Card, pos Position, owner *Player) *Unit {
- u := NewUnit(card, s.Map.TileAt(pos), owner)
+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 *State) addNewArtifact(card *Card, pos Position, owner *Player) *Artifact {
- a := NewArtifact(card, s.Map.TileAt(pos), owner)
+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 *State) addNewEquipment(card *Card, pos Position, owner *Player) *Equipment {
- e := newEquipment(card, s.Map.TileAt(pos), owner)
+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 *State) removePermanent(p Permanent) {
- for i, perm := range s.Permanents {
+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]
+ s.permanents[i] = s.permanents[len(s.permanents)-1]
+ s.permanents = s.permanents[:len(s.permanents)-1]
}
}
}
@@ -496,7 +578,7 @@ func removePtr[P *Unit](collection []P, ptr P) []P {
return collection
}
-func (s *State) destroyPermanent(p Permanent) {
+func (s *LocalState) destroyPermanent(p Permanent) {
t := TileOrContainingPermTile(p)
_leaveTile(p)
@@ -504,7 +586,7 @@ func (s *State) destroyPermanent(p Permanent) {
switch p := p.(type) {
case *Unit:
- s.Units = removePtr(s.Units, p)
+ s.units = removePtr(s.units, p)
}
if pile := p.Pile(); len(pile) > 0 {
@@ -519,12 +601,12 @@ func (s *State) destroyPermanent(p Permanent) {
s.events = append(s.events, Event{eventType: EventTypes.Destruction, affected: []interface{}{p}})
}
-func (s *State) fight(p1, p2 Permanent) {
+func (s *LocalState) fight(p1, p2 Permanent) {
p1.fight(p2)
p2.fight(p1)
}
-func (s *State) switchPermanents(p1 Permanent, p2 Permanent) {
+func (s *LocalState) switchPermanents(p1 Permanent, p2 Permanent) {
t1 := TileOrContainingPermTile(p1)
t2 := TileOrContainingPermTile(p2)
@@ -535,9 +617,9 @@ func (s *State) switchPermanents(p1 Permanent, p2 Permanent) {
enterTileOrPile(p2, t1)
}
-func (s *State) FilterUnits(filter func(*Unit) bool) []*Unit {
+func (s *LocalState) FilterUnits(filter func(*Unit) bool) []*Unit {
units := []*Unit{}
- for _, u := range s.Units {
+ for _, u := range s.units {
if filter(u) {
units = append(units, u)
}
@@ -545,19 +627,19 @@ func (s *State) FilterUnits(filter func(*Unit) bool) []*Unit {
return units
}
-func (s *State) OwnUnits(player *Player) []*Unit {
+func (s *LocalState) OwnUnits(player *Player) []*Unit {
return s.FilterUnits(func(u *Unit) bool {
return u.Controller() == player
})
}
-func (s *State) EnemyUnits(player *Player) []*Unit {
+func (s *LocalState) EnemyUnits(player *Player) []*Unit {
return s.FilterUnits(func(u *Unit) bool {
return u.Controller().IsEnemy(player)
})
}
-func (s *State) resolvePlay(a *PlayAction) {
+func (s *LocalState) resolvePlay(a *PlayAction) {
p := a.Controller()
c := a.Card
targets := a.targets
@@ -588,14 +670,14 @@ func (s *State) resolvePlay(a *PlayAction) {
}
if c.Type == CardTypes.Spell {
- if !s.Exile.Contains(c) {
+ if !s.exile.Contains(c) {
p.DiscardPile.AddCard(c)
}
}
}
-func (s *State) additionalSpawnsFor(p *Player, cType CardType) (spawns []*Tile) {
- for _, perm := range s.Permanents {
+func (s *LocalState) additionalSpawnsFor(p *Player, cType CardType) (spawns []*Tile) {
+ for _, perm := range s.permanents {
if perm.Controller() != p {
continue
}
@@ -604,8 +686,8 @@ func (s *State) additionalSpawnsFor(p *Player, cType CardType) (spawns []*Tile)
return
}
-func (s *State) SpawnTiles(p *Player) []*Tile {
- spawns := s.Map.FilterTiles(func(t *Tile) bool {
+func (s *LocalState) SpawnTiles(p *Player) []*Tile {
+ spawns := s._map.FilterTiles(func(t *Tile) bool {
if t.Type != TileTypes.Spawn {
return false
}
@@ -616,12 +698,12 @@ func (s *State) SpawnTiles(p *Player) []*Tile {
return spawns
}
-func (s *State) WaterSpawnTiles(p *Player) []*Tile {
+func (s *LocalState) WaterSpawnTiles(p *Player) []*Tile {
if p.IsKraken() {
return s.KrakenSpawnTiles()
}
- docks := s.Map.FilterTiles(func(t *Tile) bool {
+ docks := s._map.FilterTiles(func(t *Tile) bool {
if t.Type != TileTypes.Docks {
return false
}
@@ -631,7 +713,7 @@ func (s *State) WaterSpawnTiles(p *Player) []*Tile {
spawns := []*Tile{}
for _, dock := range docks {
- for _, candidate := range TilesInRangeFromOrigin(s.Map, dock.Position, 1) {
+ for _, candidate := range TilesInRangeFromOrigin(s._map, dock.Position, 1) {
if candidate.Water {
spawns = append(spawns, candidate)
}
@@ -641,7 +723,7 @@ func (s *State) WaterSpawnTiles(p *Player) []*Tile {
return spawns
}
-func (s *State) AvailableSpawnTiles(p *Player, c *Card) []*Tile {
+func (s *LocalState) AvailableSpawnTiles(p *Player, c *Card) []*Tile {
if !c.IsPermanent() {
log.Panicf("AvailableSpawnTiles called for %s", c.Type.String())
}
@@ -669,27 +751,27 @@ func (s *State) AvailableSpawnTiles(p *Player, c *Card) []*Tile {
return tiles
}
-func (s *State) redistributeMapStoreCards() {
+func (s *LocalState) redistributeMapStoreCards() {
poc := NewPileOfCards()
- for _, store := range s.Map.Stores {
+ for _, store := range s._map.Stores {
store.MoveInto(poc)
}
- s.Map.distributeStoreCards(poc, s.Rand)
+ s._map.distributeStoreCards(poc, s.Rand)
- for _, p := range s.Players {
+ for _, p := range s.players {
p.clearKnownStore()
}
}
-func (s *State) addEotEffect(e effect) {
+func (s *LocalState) addEotEffect(e effect) {
e.apply(s)
s.eotEffects = append(s.eotEffects, e)
}
-func (s *State) endOfTurn() {
+func (s *LocalState) endOfTurn() {
// Move all outstanding equipment cards to the discard pile
for _, e := range s.outstandingEquipment {
- s.ActivePlayer.DiscardPile.AddCard(e)
+ s.ActivePlayer().DiscardPile.AddCard(e)
}
for _, e := range s.eotEffects {