aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2025-08-15 20:52:55 +0200
committerFlorian Fischer <florian.fischer@muhq.space>2025-08-20 15:57:38 +0200
commit488f6377e6f6fd3099bb927a986d548fbc1eb63f (patch)
tree6cf6a3daed56b279a1f708cc03e551a387810640
parent139ad01740eba2ff1e0a9d46593d2278b32310ce (diff)
downloadmuhqs-game-488f6377e6f6fd3099bb927a986d548fbc1eb63f.tar.gz
muhqs-game-488f6377e6f6fd3099bb927a986d548fbc1eb63f.zip
implement mace and add damage dealt events/triggers
-rw-r--r--go/game/cardImplementations.go56
-rw-r--r--go/game/cardImplementations_test.go42
-rw-r--r--go/game/permanent.go26
-rw-r--r--go/game/playerControl_test.go5
-rw-r--r--go/game/state.go52
-rw-r--r--go/game/trigger.go91
-rw-r--r--go/game/unit.go2
7 files changed, 195 insertions, 79 deletions
diff --git a/go/game/cardImplementations.go b/go/game/cardImplementations.go
index 2a627a94..dcd11808 100644
--- a/go/game/cardImplementations.go
+++ b/go/game/cardImplementations.go
@@ -189,7 +189,7 @@ func (*pikemanImpl) onETB(s *LocalState, p Permanent) {
type spearImpl struct{ cardImplementationBase }
func (*spearImpl) onPile(p Permanent) {
- // FIXME: add
+ // FIXME: support multiple flexAttacks
p.(*Unit).Attack.flexAttack = pikeAttack()
}
@@ -198,6 +198,40 @@ func (*spearImpl) onUnpile(p Permanent) {
p.(*Unit).Attack.flexAttack = nil
}
+type maceImpl struct {
+ cardImplementationBase
+ triggers map[Permanent]Trigger
+}
+
+func (i *maceImpl) onPile(p Permanent) {
+ s := p.Controller().gameState
+ t := newDamageDealtByTrigger(p, false, func(s *LocalState, e Event) ActionResolveFunc {
+ return func(*LocalState) {
+ for _, a := range e.affected {
+ if u, ok := a.(*Unit); ok {
+ u.adjustMarks(UnitStates.Paralysis, 1)
+ }
+ }
+ }
+ }, "afflict paralysis on damage")
+ s.addTrigger(t)
+ // Remember trigger
+ i.triggers[p] = t
+}
+
+func (i *maceImpl) onUnpile(p Permanent) {
+ s := p.Controller().gameState
+ s.removeTrigger(i.triggers[p])
+}
+
+type poisonDaggerImpl struct{ cardImplementationBase }
+
+func (*poisonDaggerImpl) onPile(p Permanent) {
+}
+
+func (*poisonDaggerImpl) onUnpile(p Permanent) {
+}
+
// ====== Magic Set ======
type attackImpl struct{ cardImplementationBase }
@@ -511,10 +545,12 @@ func (*drownedSailorImpl) onDrop(sailor Permanent) {
type flyingDutchmenImpl struct{ cardImplementationBase }
func (*flyingDutchmenImpl) onETB(s *LocalState, p Permanent) {
- s.addTrigger(newEnemyDeathTrigger(p, func(s *LocalState) {
- sailor := NewCard("kraken/drowned_sailor")
- if p.Tile().IsAvailableForCard(sailor) {
- addPermanentToPile(p, NewUnit(sailor, nil, p.Controller()))
+ s.addTrigger(newEnemyDeathTrigger(p, func(*LocalState, Event) ActionResolveFunc {
+ return func(s *LocalState) {
+ sailor := NewCard("kraken/drowned_sailor")
+ if p.Tile().IsAvailableForCard(sailor) {
+ addPermanentToPile(p, NewUnit(sailor, nil, p.Controller()))
+ }
}
}, p.Card().getEffects()[1]))
}
@@ -685,8 +721,10 @@ func (*illusionistImpl) fullActions(u *Unit) []*FullAction {
type illusionImpl struct{ cardImplementationBase }
func (*illusionImpl) onETB(s *LocalState, p Permanent) {
- s.addTrigger(newTargetedTrigger(p, true, func(s *LocalState) {
- s.destroyPermanent(p)
+ s.addTrigger(newTargetedTrigger(p, true, func(*LocalState, Event) ActionResolveFunc {
+ return func(s *LocalState) {
+ s.destroyPermanent(p)
+ }
}, p.Card().getEffects()[0]))
}
@@ -701,7 +739,9 @@ func init() {
"base/wormtongue": &wormtongueImpl{},
"base/pikeman": &pikemanImpl{},
- "equipment/spear": &spearImpl{},
+ "equipments/mace": &maceImpl{triggers: make(map[Permanent]Trigger)},
+ "equipments/poison_dagger": &poisonDaggerImpl{},
+ "equipments/spear": &spearImpl{},
"magic/attack!": &attackImpl{},
"magic/appear!": &appearImpl{},
diff --git a/go/game/cardImplementations_test.go b/go/game/cardImplementations_test.go
index 5160816e..14339df7 100644
--- a/go/game/cardImplementations_test.go
+++ b/go/game/cardImplementations_test.go
@@ -190,3 +190,45 @@ symbols:
// TODO: test return to owner
}
+
+func TestMace(t *testing.T) {
+ s := NewLocalState()
+ m := newEmpty2x2Map()
+ s.SetMap(m)
+
+ player := s.AddNewPlayer("p", NewDeck())
+ ctrl := newMockPlayerControl(player)
+ ctrl.actionToSend = NewPassPriority(player)
+ player.Ctrl = ctrl
+
+ opo := s.AddNewPlayer("o", NewDeck())
+ ctrl = newMockPlayerControl(opo)
+ ctrl.actionToSend = NewPassPriority(opo)
+ opo.Ctrl = ctrl
+
+ k := s.addNewUnit(NewCard("base/knight"), Position{0, 0}, player)
+ p := s.addNewUnit(NewCard("base/pioneer"), Position{0, 1}, opo)
+ mace := newEquipment(NewCard("equipments/mace"), k, player)
+ s.addPermanent(mace)
+
+ if len(s.triggers) == 0 {
+ t.Fatal("mace trigger not registered")
+ }
+
+ s.fight(k, p)
+ s.stateBasedActions()
+ if len(s.stack.Actions) == 0 {
+ t.Fatal("mace not triggered")
+ }
+ s.stack.resolve()
+
+ if p.Marks(UnitStates.Paralysis) != 1 {
+ t.Fatal("pioneer not paralysed")
+ }
+
+ s.destroyPermanent(mace)
+
+ if len(s.triggers) != 0 {
+ t.Fatal("mace trigger not removed")
+ }
+}
diff --git a/go/game/permanent.go b/go/game/permanent.go
index 4c73a26a..54404b77 100644
--- a/go/game/permanent.go
+++ b/go/game/permanent.go
@@ -135,7 +135,11 @@ func DealDamage(src, dest Permanent, damage int) {
reduction := max(armor-piercing, 0)
actualDamage = max(actualDamage-reduction, 0)
}
- dest.adjustDamage(actualDamage)
+ if actualDamage > 0 {
+ dest.adjustDamage(actualDamage)
+ src.Controller().gameState.fireEvent(
+ Event{eventType: damageDealt, affected: []any{dest}, sources: []any{src}})
+ }
}
func (p *permanentBase) adjustDamage(damage int) {
@@ -263,12 +267,7 @@ func movePermanent(p Permanent, t *Tile) {
log.Panicf("moving %v to not available tile %v", p, t)
}
- if p.ContainingPerm() != nil {
- removePermanentFromPile(p.ContainingPerm(), p)
- } else {
- leaveTile(p)
- }
-
+ leaveTileOrPile(p)
enterTileOrPile(p, t)
}
@@ -287,15 +286,10 @@ func enterTileOrPile(p Permanent, t *Tile) {
}
}
-func _leaveTile(p Permanent) {
- if p.Tile() != nil {
- p.Card().Impl.onLeaving(p.Tile())
- p.Tile().leaving(p)
- }
-}
-
-func leaveTile(p Permanent) {
- if p.Tile() != nil {
+func leaveTileOrPile(p Permanent) {
+ if cont := p.ContainingPerm(); cont != nil {
+ removePermanentFromPile(cont, p)
+ } else {
p.Card().Impl.onLeaving(p.Tile())
p.Tile().leaving(p)
p.SetTile(nil)
diff --git a/go/game/playerControl_test.go b/go/game/playerControl_test.go
index 0182e30b..2727df0b 100644
--- a/go/game/playerControl_test.go
+++ b/go/game/playerControl_test.go
@@ -7,9 +7,10 @@ import (
type MockPlayerControl struct {
p *Player
sentNotifications []PlayerNotification
+ actionToSend Action
}
-func newMockPlayerControl(p *Player) PlayerControl {
+func newMockPlayerControl(p *Player) *MockPlayerControl {
return &MockPlayerControl{p: p}
}
@@ -25,7 +26,7 @@ func (ctrl *MockPlayerControl) SendNotification(n PlayerNotification) error {
}
func (ctrl *MockPlayerControl) RecvAction() (Action, error) {
- return nil, nil
+ return ctrl.actionToSend, nil
}
func (ctrl *MockPlayerControl) SendAction(Action) error { return nil }
diff --git a/go/game/state.go b/go/game/state.go
index fd79b114..b58c0992 100644
--- a/go/game/state.go
+++ b/go/game/state.go
@@ -10,7 +10,6 @@ import (
"time"
"golang.org/x/exp/slices"
- "muhq.space/muhqs-game/go/utils"
)
type State interface {
@@ -217,45 +216,6 @@ func (s *LocalState) Loop() []*Player {
}
}
-func (s *LocalState) addTrigger(trigger Trigger) {
- s.triggers = append(s.triggers, trigger)
-}
-
-func (s *LocalState) removeTriggers(triggers []Trigger) {
- for _, t := range triggers {
- s.triggers = utils.RemoveFromUnorderedSlice(s.triggers, t)
- }
-}
-
-func (s *LocalState) handleTriggers() {
- var actions []*TriggeredAction
- var triggersToRemove []Trigger
-
- for _, e := range s.events {
- for _, t := range s.triggers {
- a, remove := t.trigger(s, e)
- if a != nil {
- actions = append(actions, a...)
- }
- if remove {
- triggersToRemove = append(triggersToRemove, t)
- }
- }
- }
-
- // Reset occured events
- s.events = nil
-
- s.removeTriggers(triggersToRemove)
-
- if len(actions) > 0 {
- orderedActions := s.orderTriggeredActions(actions)
- for _, a := range orderedActions {
- s.declareAction(a)
- }
- }
-}
-
func (s *LocalState) stateBasedActions() []*Player {
for _, p := range s.permanents {
p.Card().Impl.stateBasedActions(s, p)
@@ -510,7 +470,7 @@ func (s *LocalState) declareAction(a Action) {
t := a.Targets()
if t != nil {
// FIXME: seperate between targets choices implemented as target
- s.events = append(s.events, newTargetEvent(a, t))
+ s.fireEvent(newTargetEvent(a, t))
}
}
}
@@ -696,7 +656,7 @@ func removePtr[P *Unit](collection []P, ptr P) []P {
func (s *LocalState) destroyPermanent(p Permanent) {
t := TileOrContainingPermTile(p)
- _leaveTile(p)
+ leaveTileOrPile(p)
s.RemovePermanent(p)
if pile := p.Pile(); len(pile) > 0 {
@@ -708,7 +668,7 @@ func (s *LocalState) destroyPermanent(p Permanent) {
p.Owner().DiscardPile.AddCard(p.Card())
- s.events = append(s.events, Event{eventType: EventTypes.Destruction, affected: []any{p}})
+ s.fireEvent(Event{eventType: EventTypes.Destruction, affected: []any{p}})
}
// fight implements the effect of two permanents fighting.
@@ -734,8 +694,8 @@ func (s *LocalState) switchPermanents(p1 Permanent, p2 Permanent) {
t1 := TileOrContainingPermTile(p1)
t2 := TileOrContainingPermTile(p2)
- leaveTile(p1)
- leaveTile(p2)
+ leaveTileOrPile(p1)
+ leaveTileOrPile(p2)
enterTileOrPile(p1, t2)
enterTileOrPile(p2, t1)
@@ -903,7 +863,7 @@ func (s *LocalState) endOfTurn() []*Player {
}
s.eotEffects = nil
- s.events = append(s.events, newEotEvent())
+ s.fireEvent(newEotEvent())
return s.stateBasedActions()
}
diff --git a/go/game/trigger.go b/go/game/trigger.go
index 34caf9b1..b061ea78 100644
--- a/go/game/trigger.go
+++ b/go/game/trigger.go
@@ -5,6 +5,8 @@ import (
"log"
"golang.org/x/exp/slices"
+
+ "muhq.space/muhqs-game/go/utils"
)
type eventType int
@@ -16,6 +18,7 @@ const (
play
target
etb
+ damageDealt
)
var EventTypes = struct {
@@ -25,6 +28,7 @@ var EventTypes = struct {
Play eventType
Target eventType
Etb eventType
+ DamageDealt eventType
}{
Destruction: destruction,
Eot: eot,
@@ -32,6 +36,7 @@ var EventTypes = struct {
Play: play,
Target: target,
Etb: etb,
+ DamageDealt: damageDealt,
}
func (e eventType) String() string {
@@ -48,6 +53,8 @@ func (e eventType) String() string {
return "target"
case etb:
return "etb"
+ case damageDealt:
+ return "damageDealt"
}
log.Panicf("Unknown evenType: %d", e)
@@ -80,6 +87,11 @@ func newTargetEvent(source any, targets *Targets) Event {
return Event{eventType: target, sources: []any{source}, affected: affected}
}
+func (s *LocalState) fireEvent(e Event) {
+ log.Println("fire event:", e)
+ s.events = append(s.events, e)
+}
+
type Trigger interface {
Source() any
String() string
@@ -90,7 +102,7 @@ type Trigger interface {
type triggerBase struct {
source any
condition func(*LocalState, Event) (bool, bool)
- resolveFunc ActionResolveFunc
+ resolveFunc func(*LocalState, Event) ActionResolveFunc
costFunc ActionCostFunc
desc string
}
@@ -115,10 +127,10 @@ func (t *triggerBase) trigger(s *LocalState, e Event) ([]*TriggeredAction, bool)
return nil, remove
}
- return []*TriggeredAction{newTriggeredAction(e, t, t.resolveFunc, t.costFunc)}, remove
+ return []*TriggeredAction{newTriggeredAction(e, t, t.resolveFunc(s, e), t.costFunc)}, remove
}
-func newTargetedTrigger(p Permanent, singleshot bool, resolveFunc ActionResolveFunc, desc string) Trigger {
+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) {
c := event.eventType == EventTypes.Target &&
@@ -129,7 +141,7 @@ func newTargetedTrigger(p Permanent, singleshot bool, resolveFunc ActionResolveF
return &t
}
-func newDeathTrigger(p Permanent, singleshot bool, resolveFunc ActionResolveFunc, permCond func(Permanent) bool, desc string) Trigger {
+func newDeathTrigger(p Permanent, singleshot bool, resolveFunc func(*LocalState, Event) ActionResolveFunc, permCond func(Permanent) bool, desc string) Trigger {
t := triggerBase{source: p, resolveFunc: resolveFunc, desc: desc}
t.condition = func(_ *LocalState, event Event) (bool, bool) {
if event.eventType != EventTypes.Sacrifice && event.eventType != EventTypes.Destruction {
@@ -144,14 +156,81 @@ func newDeathTrigger(p Permanent, singleshot bool, resolveFunc ActionResolveFunc
return &t
}
-func newOwnDeathTrigger(p Permanent, resolveFunc ActionResolveFunc, desc string) Trigger {
+func newOwnDeathTrigger(p Permanent, resolveFunc func(*LocalState, Event) ActionResolveFunc, desc string) Trigger {
return newDeathTrigger(p, true, resolveFunc, func(destroyedPerm Permanent) bool {
return p == destroyedPerm
}, desc)
}
-func newEnemyDeathTrigger(p Permanent, resolveFunc ActionResolveFunc, desc string) Trigger {
+func newEnemyDeathTrigger(p Permanent, resolveFunc func(*LocalState, Event) ActionResolveFunc, desc string) Trigger {
return newDeathTrigger(p, true, resolveFunc, func(destroyedPerm Permanent) bool {
return p.Controller().IsEnemy(destroyedPerm.Controller())
}, desc)
}
+
+func newDamageDealtTrigger(source any, singleshot bool, cond func(Event) bool, resolveFunc func(*LocalState, Event) ActionResolveFunc, desc string) Trigger {
+ t := triggerBase{source: source, resolveFunc: resolveFunc, desc: desc}
+ t.condition = func(_ *LocalState, event Event) (bool, bool) {
+ if event.eventType != EventTypes.DamageDealt {
+ return false, false
+ }
+
+ return cond(event), singleshot
+ }
+ return &t
+}
+
+func newDamageDealtByTrigger(source any, singleshot bool, resolveFunc func(*LocalState, Event) ActionResolveFunc, desc string) Trigger {
+ return newDamageDealtTrigger(source, singleshot,
+ func(e Event) bool { return slices.Contains(e.sources, source) },
+ resolveFunc, desc)
+}
+
+func newDamageDealtToTrigger(p Permanent, singleshot bool, resolveFunc func(*LocalState, Event) ActionResolveFunc, desc string) Trigger {
+ return newDamageDealtTrigger(p, singleshot,
+ func(e Event) bool { return slices.Contains(e.affected, p.(any)) },
+ resolveFunc, desc)
+}
+
+func (s *LocalState) addTrigger(trigger Trigger) {
+ s.triggers = append(s.triggers, trigger)
+}
+
+func (s *LocalState) removeTriggers(triggers []Trigger) {
+ for _, t := range triggers {
+ s.triggers = utils.RemoveFromUnorderedSlice(s.triggers, t)
+ }
+}
+
+func (s *LocalState) removeTrigger(trigger Trigger) {
+ s.removeTriggers([]Trigger{trigger})
+}
+
+func (s *LocalState) handleTriggers() {
+ var actions []*TriggeredAction
+ var triggersToRemove []Trigger
+
+ for _, e := range s.events {
+ for _, t := range s.triggers {
+ a, remove := t.trigger(s, e)
+ if a != nil {
+ actions = append(actions, a...)
+ }
+ if remove {
+ triggersToRemove = append(triggersToRemove, t)
+ }
+ }
+ }
+
+ // Reset occured events
+ s.events = nil
+
+ s.removeTriggers(triggersToRemove)
+
+ if len(actions) > 0 {
+ orderedActions := s.orderTriggeredActions(actions)
+ for _, a := range orderedActions {
+ s.declareAction(a)
+ }
+ }
+}
diff --git a/go/game/unit.go b/go/game/unit.go
index 7032d464..8473b830 100644
--- a/go/game/unit.go
+++ b/go/game/unit.go
@@ -351,7 +351,7 @@ func (u *Unit) equip(e *Equipment) {
if e.containingPerm != nil {
removePermanentFromPile(e.containingPerm, e)
} else {
- leaveTile(e)
+ leaveTileOrPile(e)
}
addPermanentToPile(u, e)
}