diff options
| -rw-r--r-- | go/game/cardImplementations.go | 54 | ||||
| -rw-r--r-- | go/game/cardImplementations_test.go | 47 | ||||
| -rw-r--r-- | go/game/events.go | 9 | ||||
| -rw-r--r-- | go/game/permanent.go | 5 | ||||
| -rw-r--r-- | go/game/player.go | 2 | ||||
| -rw-r--r-- | go/game/range.go | 6 | ||||
| -rw-r--r-- | go/game/state.go | 47 | ||||
| -rw-r--r-- | go/game/trigger.go | 16 |
8 files changed, 171 insertions, 15 deletions
diff --git a/go/game/cardImplementations.go b/go/game/cardImplementations.go index 886bf3bb..fa7259a6 100644 --- a/go/game/cardImplementations.go +++ b/go/game/cardImplementations.go @@ -247,6 +247,59 @@ func (*poisonDaggerImpl) onPile(p Permanent) { func (*poisonDaggerImpl) onUnpile(p Permanent) { } +type relicImpl struct{ cardImplementationBase } + +func (*relicImpl) onETB(s *LocalState, p Permanent) { + t := newUpkeepTrigger( + p, + func(*LocalState, Event) bool { return p.IsDestroyed() }, + func(*LocalState, Event) bool { + controlled := make(map[*Player]int) + for _, t := range AdjacentTiles(p) { + if t.Permanent == nil { + continue + } + if _, found := controlled[t.Permanent.Controller()]; found { + controlled[t.Permanent.Controller()] += 1 + } else { + controlled[t.Permanent.Controller()] = 1 + } + } + return len(controlled) > 0 + }, + func(*LocalState, Event) ActionResolveFunc { + return func(*LocalState) { + controlled := make(map[*Player]int) + for _, t := range AdjacentTiles(p) { + if t.Permanent == nil { + continue + } + if _, found := controlled[t.Permanent.Controller()]; found { + controlled[t.Permanent.Controller()] += 1 + } else { + controlled[t.Permanent.Controller()] = 1 + } + } + // Find maximum amount of units around relic + max := 0 + for _, c := range controlled { + if c > max { + max = c + } + } + // Each player controlling the most units gain 1 resource + for p, c := range controlled { + if c == max { + p.gainResource(1) + } + } + } + }, + "relic gain", + ) + s.addTrigger(t) +} + type spearImpl struct{ cardImplementationBase } func (*spearImpl) onPile(p Permanent) { @@ -914,6 +967,7 @@ func init() { "equipments/banner": &bannerImpl{aoe: bannerAoE}, "equipments/mace": &maceImpl{triggers: make(map[Permanent]Trigger)}, "equipments/poison_dagger": &poisonDaggerImpl{}, + "equipments/relic": &relicImpl{}, "equipments/spear": &spearImpl{}, "magic/appear!": &appearImpl{}, diff --git a/go/game/cardImplementations_test.go b/go/game/cardImplementations_test.go index 9b356c5b..3a6ea9e2 100644 --- a/go/game/cardImplementations_test.go +++ b/go/game/cardImplementations_test.go @@ -515,3 +515,50 @@ func TestNo(t *testing.T) { t.Fatal("Approach not countered") } } + +func TestRelic(t *testing.T) { + s, _, p, o := newMockState() + p.Resource = 0 + o.Resource = 0 + s.addNewArtifact(NewCard("equipments/relic"), Position{0, 0}, p) + + s.changePhase(upkeepPhase) + if !s.stack.IsEmpty() { + t.Fatal("Unexpected relic trigger") + } + + s.addNewUnit(NewCard("base/archer"), Position{0, 1}, p) + + s.changePhase(upkeepPhase) + if s.stack.IsEmpty() { + t.Fatal("Relic not triggered") + } + s.stack.pop() + if p.Resource != 1 { + t.Fatal("player did not gain 1 resource") + } + + s.addNewUnit(NewCard("base/archer"), Position{1, 1}, o) + s.changePhase(upkeepPhase) + if s.stack.IsEmpty() { + t.Fatal("Relic not triggered") + } + s.stack.pop() + if p.Resource != 2 && o.Resource != 1 { + t.Fatal("player and opponent did not gain 1 resource each") + } + + s.addNewUnit(NewCard("base/archer"), Position{1, 0}, o) + s.changePhase(upkeepPhase) + if s.stack.IsEmpty() { + t.Fatal("Relic not triggered") + } + s.stack.pop() + if p.Resource != 2 { + t.Fatal("player resource changed") + } + + if o.Resource != 2 { + t.Fatal("opponent did not gain 1 resource") + } +} diff --git a/go/game/events.go b/go/game/events.go index 4e2479a8..2e0b0f08 100644 --- a/go/game/events.go +++ b/go/game/events.go @@ -21,6 +21,7 @@ const ( declaredAction resolvedAction discard + phaseChange ) var EventTypes = struct { @@ -35,6 +36,7 @@ var EventTypes = struct { DeclaredAction eventType ResolvedAction eventType Discard eventType + PhaseChange eventType }{ Destruction: destruction, Eot: eot, @@ -47,6 +49,7 @@ var EventTypes = struct { DeclaredAction: declaredAction, ResolvedAction: resolvedAction, Discard: discard, + PhaseChange: phaseChange, } func (e eventType) String() string { @@ -73,6 +76,8 @@ func (e eventType) String() string { return "resolvedAction" case discard: return "discard" + case phaseChange: + return "phaseChange" } log.Panicf("Unknown eventType: %d", e) @@ -124,6 +129,10 @@ func newResolvedActionEvent(action Action, err error) Event { } } +func newPhaseChangeEvent() Event { + return Event{eventType: phaseChange} +} + func (s *LocalState) fireEvent(e Event) { log.Game(e) s.events = append(s.events, e) diff --git a/go/game/permanent.go b/go/game/permanent.go index 1f78cb87..9cf8754b 100644 --- a/go/game/permanent.go +++ b/go/game/permanent.go @@ -314,3 +314,8 @@ func TileOrContainingPermTile(p Permanent) *Tile { return tile } + +func AdjacentTiles(p Permanent) []*Tile { + m := p.Controller().gameState.Map() + return TilesInRange(m, p, 1) +} diff --git a/go/game/player.go b/go/game/player.go index 09488a32..47b2c474 100644 --- a/go/game/player.go +++ b/go/game/player.go @@ -116,7 +116,7 @@ func (p *Player) upkeep() []*Player { p.gainResource(p.ResourceGain()) s := p.gameState - // TODO: handle upkeep triggers + s.handleTriggers() // Skip upkeep prompt if player does not controll any units controllsUnits := false diff --git a/go/game/range.go b/go/game/range.go index 83003dfa..c91ad2a8 100644 --- a/go/game/range.go +++ b/go/game/range.go @@ -62,10 +62,16 @@ func tilesInRangeFromOrigin(m *Map, origin Position, r int, includeOrigin bool) return tiles } +// TilesInRangeFromOrigin returns all tiles in range r from a position. +// +// The position itself is excluded. func TilesInRangeFromOrigin(m *Map, origin Position, r int) []*Tile { return tilesInRangeFromOrigin(m, origin, r, false) } +// TilesInRange returns all tiles in range r from a Permanent. +// +// The permanent's position itself is excluded. func TilesInRange(m *Map, p Permanent, r int) []*Tile { return TilesInRangeFromOrigin(m, TileOrContainingPermTile(p).Position, r) } diff --git a/go/game/state.go b/go/game/state.go index a9c2f638..1ab3c95d 100644 --- a/go/game/state.go +++ b/go/game/state.go @@ -153,6 +153,12 @@ func (s *LocalState) IsActivePlayer(p *Player) bool { return s.activePlayerId == p.Id } +func (s *LocalState) changePhase(next PhaseType) []*Player { + s.activePhase = next + s.fireEvent(newPhaseChangeEvent()) + return s.stateBasedActions() +} + func (s *LocalState) Loop() []*Player { // Prepare the map if s._map.Prepare != nil { @@ -177,6 +183,7 @@ func (s *LocalState) Loop() []*Player { s.activePlayerId = p.Id p.Turn++ + // Shuffle discard pile into empty deck if p.Deck.IsEmpty() { e := newShuffleEvent(p, []PileOfCards{p.DiscardPile}) s.fireEvent(e) @@ -185,42 +192,50 @@ func (s *LocalState) Loop() []*Player { p.Deck.Shuffle(s.Rand) } - s.activePhase = Phases.DrawStep + // Switch to draw step without a priority round + if w := s.changePhase(Phases.DrawStep); len(w) > 0 { + return w + } p.Draw() - s.activePhase = Phases.UpkeepPhase + // Switch to upkeep without a priority round + if w := s.changePhase(Phases.UpkeepPhase); len(w) > 0 { + return w + } winners := p.upkeep() if len(winners) > 0 { return winners } - winners = s.phaseChange() + // Full phase change to the actions phase with a priority round + winners = s.phaseChange(Phases.ActionPhase) if len(winners) > 0 { return winners } - s.activePhase = Phases.ActionPhase winners = p.actionPhase() if len(winners) > 0 { return winners } - winners = s.phaseChange() + + // Full phase change to the buy phase with a priority round + winners = s.phaseChange(Phases.BuyPhase) if len(winners) > 0 { return winners } - s.activePhase = Phases.BuyPhase winners = p.buyPhase() if len(winners) > 0 { return winners } - winners = s.phaseChange() + + // Full phase change to the discard step with a priority round + winners = s.phaseChange(Phases.DiscardStep) if len(winners) > 0 { return winners } - - s.activePhase = Phases.DiscardStep p.discardStep() + // End the trun after discarding without a priority round winners = s.endOfTurn() if len(winners) > 0 { return winners @@ -499,12 +514,13 @@ func (s *LocalState) allPassing(skipFirst bool) (bool, []*Player) { return true, nil } -func (s *LocalState) phaseChange() []*Player { +func (s *LocalState) phaseChange(next PhaseType) (w []*Player) { skipFirst := true for { - passing, w := s.allPassing(skipFirst) + var passing bool + passing, w = s.allPassing(skipFirst) if len(w) > 0 { - return w + break } if passing { break @@ -512,12 +528,15 @@ func (s *LocalState) phaseChange() []*Player { w = s.stack.resolve() if len(w) > 0 { - return w + break } skipFirst = false } - return nil + + s.changePhase(next) + + return } func (s *LocalState) broadcastNotification(n PlayerNotification) { diff --git a/go/game/trigger.go b/go/game/trigger.go index 6ab42856..6401dee6 100644 --- a/go/game/trigger.go +++ b/go/game/trigger.go @@ -45,6 +45,22 @@ func (t *triggerBase) trigger(s *LocalState, e Event) ([]*TriggeredAction, bool) return []*TriggeredAction{newTriggeredAction(e, t, t.resolveFunc(s, e), t.costFunc)}, remove } +func newUpkeepTrigger( + src any, + remove func(*LocalState, Event) bool, + cond func(*LocalState, Event) bool, + resolveFunc func(*LocalState, Event) ActionResolveFunc, + desc string, +) Trigger { + t := triggerBase{source: src, resolveFunc: resolveFunc, desc: desc} + t.condition = func(s *LocalState, event Event) (bool, bool) { + triggered := event.eventType == EventTypes.PhaseChange && s.activePhase == Phases.UpkeepPhase && cond(s, event) + removed := remove(s, event) + return triggered, removed + } + return &t +} + func newTargetedTrigger(p Permanent, singleshot bool, resolveFunc func(*LocalState, Event) ActionResolveFunc, desc string) Trigger { t := triggerBase{source: p, resolveFunc: resolveFunc, desc: desc} t.condition = func(_ *LocalState, event Event) (bool, bool) { |
