diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2025-08-15 20:52:55 +0200 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-08-20 15:57:38 +0200 |
| commit | 488f6377e6f6fd3099bb927a986d548fbc1eb63f (patch) | |
| tree | 6cf6a3daed56b279a1f708cc03e551a387810640 | |
| parent | 139ad01740eba2ff1e0a9d46593d2278b32310ce (diff) | |
| download | muhqs-game-488f6377e6f6fd3099bb927a986d548fbc1eb63f.tar.gz muhqs-game-488f6377e6f6fd3099bb927a986d548fbc1eb63f.zip | |
implement mace and add damage dealt events/triggers
| -rw-r--r-- | go/game/cardImplementations.go | 56 | ||||
| -rw-r--r-- | go/game/cardImplementations_test.go | 42 | ||||
| -rw-r--r-- | go/game/permanent.go | 26 | ||||
| -rw-r--r-- | go/game/playerControl_test.go | 5 | ||||
| -rw-r--r-- | go/game/state.go | 52 | ||||
| -rw-r--r-- | go/game/trigger.go | 91 | ||||
| -rw-r--r-- | go/game/unit.go | 2 |
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) } |
