aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2025-09-08 20:42:26 +0200
committerFlorian Fischer <florian.fischer@muhq.space>2025-09-08 20:42:26 +0200
commit0cbb13f420840d49f1a2f12f97c779b263102337 (patch)
tree0d81d94ae92e8dc002b269f4ac69d3dbcaf7f595
parent492417cf3a8109a2f063dcffe4017dfda477fef3 (diff)
downloadmuhqs-game-0cbb13f420840d49f1a2f12f97c779b263102337.tar.gz
muhqs-game-0cbb13f420840d49f1a2f12f97c779b263102337.zip
implement spell target parsing and magic/no!
-rw-r--r--README.md1
-rw-r--r--data/cards/magic/no!.yml4
-rw-r--r--go/game/cardImplementations.go12
-rw-r--r--go/game/cardImplementations_test.go19
-rw-r--r--go/game/targets.go74
-rw-r--r--go/game/targets_test.go41
-rw-r--r--latex/cards/de/magic/no!.tex2
-rw-r--r--latex/cards/en/magic/no!.tex2
8 files changed, 142 insertions, 13 deletions
diff --git a/README.md b/README.md
index 4c0292c1..07fd006a 100644
--- a/README.md
+++ b/README.md
@@ -121,6 +121,7 @@ Things that should be done eventually:
- [ ] implement log with hoverable components
- [ ] support big card grids (for sealed)
- [ ] implement declared action targeting
+ - [ ] implement action targeting
- [ ] Game Logic
- [ ] implement constrained X-Cost
- [ ] implement HandCardSelection with condition
diff --git a/data/cards/magic/no!.yml b/data/cards/magic/no!.yml
index 632ce001..77bdf9f3 100644
--- a/data/cards/magic/no!.yml
+++ b/data/cards/magic/no!.yml
@@ -5,5 +5,5 @@ type: spell
buy: 5
play: 2
effect:
- en: Counter one spell
- de: Neutraliziere einen Zauberspruch
+ en: Counter target declared spell
+ de: Neutraliziere einen angekündigten Zauber deiner Wahl
diff --git a/go/game/cardImplementations.go b/go/game/cardImplementations.go
index 0af99570..886bf3bb 100644
--- a/go/game/cardImplementations.go
+++ b/go/game/cardImplementations.go
@@ -324,6 +324,17 @@ func (*moreImpl) onPlay(a *PlayAction) {
a.Controller().DrawN(2)
}
+type noImpl struct{ cardImplementationBase }
+
+func (*noImpl) playTargets() TargetDesc {
+ return newTargetDesc("declared spell")
+}
+
+func (*noImpl) onPlay(a *PlayAction) {
+ s := a.Controller().gameState
+ s.counterAction(a.Target().Selection()[0].(Action))
+}
+
type pierceImpl struct{ cardImplementationBase }
func (*pierceImpl) playTargets() TargetDesc {
@@ -911,6 +922,7 @@ func init() {
"magic/heal!": &healImpl{},
"magic/mine!": &mineImpl{},
"magic/more!": &moreImpl{},
+ "magic/no!": &noImpl{},
"magic/pierce!": &pierceImpl{},
"magic/ritual!": &ritualImpl{},
"magic/rush!": &rushImpl{},
diff --git a/go/game/cardImplementations_test.go b/go/game/cardImplementations_test.go
index 8e298491..9b356c5b 100644
--- a/go/game/cardImplementations_test.go
+++ b/go/game/cardImplementations_test.go
@@ -496,3 +496,22 @@ func TestApproachSupremacy(t *testing.T) {
t.Fatal("player did not win")
}
}
+
+func TestNo(t *testing.T) {
+ s, _, p, o := newMockState()
+ p.Resource = 20
+ o.Resource = 6
+ approach := NewPlayAction(p, NewCard("exp1/approach_supremacy!"))
+
+ no := NewPlayAction(o, NewCard("magic/no!"))
+
+ s.declareAction(approach)
+ no.Target().AddSelection(approach)
+
+ s.declareAction(no)
+ s.stack.pop() // Resolve no!
+
+ if !s.stack.IsEmpty() {
+ t.Fatal("Approach not countered")
+ }
+}
diff --git a/go/game/targets.go b/go/game/targets.go
index 56ec9b30..92fc6104 100644
--- a/go/game/targets.go
+++ b/go/game/targets.go
@@ -397,10 +397,10 @@ func singleTargetConstraint(desc string, s *LocalState, action Action) TargetCon
constraints = append(constraints, parseTileTargetConstraint(desc, s, action)...)
} else if strings.Contains(desc, "card") {
constraints = append(constraints, parseCardTargetConstraint(desc, s, action)...)
- // } else if strings.Contains("action") {
+ // } else if strings.Contains(desc, "action") {
// constraints = append(constraints, parseActionTargetConstraint(desc, s, action)...)
- // } else if strings.Contains("spell") {
- // constraints = append(constraints, parseSpellTargetConstraint(desc, s, action)...)
+ } else if strings.Contains(desc, "spell") {
+ constraints = append(constraints, parseSpellTargetConstraint(desc, s, action)...)
}
return conjunction(constraints...)
@@ -512,7 +512,7 @@ func typeTargetConstraint[T any](typeDesc string) TargetConstraintFunc {
}
}
-func cardTypeTargetConstraint(f func(CardType) bool) TargetConstraintFunc {
+func permanentCardTypeConstraint(f func(CardType) bool) TargetConstraintFunc {
return func(t any) (err error) {
p, ok := t.(Permanent)
if !ok {
@@ -530,12 +530,31 @@ func cardTypeTargetConstraint(f func(CardType) bool) TargetConstraintFunc {
}
}
+func cardTypeConstraint(cardType CardType) TargetConstraintFunc {
+ return func(t any) (err error) {
+ c, ok := t.(*Card)
+ if !ok {
+ err = fmt.Errorf("unexpected target type %T not *Card", t)
+ return
+ }
+
+ if c.Type != cardType {
+ err = fmt.Errorf("unexpected card type: %v not %v", c.Type, cardType)
+ return
+ }
+
+ return
+ }
+}
+
var (
- permanentTargetConstraint TargetConstraintFunc = typeTargetConstraint[Permanent]("Permanent")
- unitTargetConstraint TargetConstraintFunc = typeTargetConstraint[*Unit]("*Unit")
- tileTargetConstraint TargetConstraintFunc = typeTargetConstraint[*Tile]("*Tile")
- artifactTargetConstraint TargetConstraintFunc = cardTypeTargetConstraint(CardType.IsArtifact)
- cardTargetConstraint TargetConstraintFunc = typeTargetConstraint[*Card]("*Card")
+ permanentTargetConstraint TargetConstraintFunc = typeTargetConstraint[Permanent]("Permanent")
+ unitTargetConstraint TargetConstraintFunc = typeTargetConstraint[*Unit]("*Unit")
+ tileTargetConstraint TargetConstraintFunc = typeTargetConstraint[*Tile]("*Tile")
+ artifactTargetConstraint TargetConstraintFunc = permanentCardTypeConstraint(CardType.IsArtifact)
+ cardTargetConstraint TargetConstraintFunc = typeTargetConstraint[*Card]("*Card")
+ playActionTargetConstraint TargetConstraintFunc = typeTargetConstraint[*PlayAction]("*PlayAction")
+ spellTargetConstraint TargetConstraintFunc = cardTypeConstraint(CardTypes.Spell)
)
var ErrTargetHasShroud = errors.New("target has shroud")
@@ -856,6 +875,43 @@ func parseCardTargetConstraint(desc string, s *LocalState, action Action) []Targ
return constraints
}
+func playedCardTypeConstraint(cardType CardType) TargetConstraintFunc {
+ return func(t any) error {
+ pa := t.(*PlayAction)
+ if pa.Card.Type != cardType {
+ return fmt.Errorf("played cardtype %s not %s", pa.Card.Type, cardType)
+ }
+ return nil
+ }
+}
+
+func parseSpellTargetConstraint(desc string, s *LocalState, action Action) []TargetConstraintFunc {
+ var constraints []TargetConstraintFunc
+
+ // The target has to be a declared play action
+ if strings.Contains(desc, "declared") {
+ constraints = []TargetConstraintFunc{
+ playActionTargetConstraint,
+ playedCardTypeConstraint(CardTypes.Spell),
+ func(t any) error {
+ pa := t.(*PlayAction)
+ if !utils.InterfaceSliceContains(utils.TypedSliceToInterfaceSlice(s.stack.Actions), pa) {
+ return fmt.Errorf("action %s not declared", pa)
+ }
+ return nil
+ },
+ }
+ // The target may be any spell card
+ } else {
+ constraints = []TargetConstraintFunc{
+ cardTargetConstraint,
+ spellTargetConstraint,
+ }
+ }
+
+ return constraints
+}
+
func (t *Target) candidates() []any {
c := []any{}
if strings.Contains(t.desc, "unit") ||
diff --git a/go/game/targets_test.go b/go/game/targets_test.go
index 07df4b60..134eef5f 100644
--- a/go/game/targets_test.go
+++ b/go/game/targets_test.go
@@ -257,3 +257,44 @@ func TestUnitConstraint(t *testing.T) {
t.Fatal("'units you control' does not contain own archer")
}
}
+
+func TestSpellConstraint(t *testing.T) {
+ s, _, p, _ := newMockState()
+
+ spellInDiscard := NewCard("magic/more!")
+ p.DiscardPile.AddCard(spellInDiscard)
+ p.Resource = 3
+ declaredSpell := NewPlayAction(p, NewCard("magic/ritual!"))
+ s.declareAction(declaredSpell)
+ unit := NewPlayAction(p, NewCard("base/archer"))
+
+ desc := newTargetDesc("spell")
+ target := newTarget(s, desc, NewPassPriority(p))
+
+ if err := target.AddSelection(declaredSpell); err == nil {
+ t.Fatal("could target declared spell")
+ }
+
+ if err := target.AddSelection(spellInDiscard); err != nil {
+ t.Fatal("failed to target spell:", err)
+ }
+
+ if err := target.AddSelection(unit); err == nil {
+ t.Fatal("could target unit play action:", err)
+ }
+
+ desc = newTargetDesc("declared spell")
+ target = newTarget(s, desc, NewPassPriority(p))
+
+ if err := target.AddSelection(declaredSpell); err != nil {
+ t.Fatal("failed to target spell:", err)
+ }
+
+ if err := target.AddSelection(spellInDiscard); err == nil {
+ t.Fatal("spell in discard pile is not declared:", err)
+ }
+
+ if err := target.AddSelection(unit); err == nil {
+ t.Fatal("could target unit play action:", err)
+ }
+}
diff --git a/latex/cards/de/magic/no!.tex b/latex/cards/de/magic/no!.tex
index 0f5fc486..5ad634dd 100644
--- a/latex/cards/de/magic/no!.tex
+++ b/latex/cards/de/magic/no!.tex
@@ -6,7 +6,7 @@
\begin{tikzpicture}
\cardtypeSpell{Nein!}
\cardbuycost{5}
-\cardcontent{Neutraliziere einen Zauberspruch}
+\cardcontent{Neutraliziere einen angekündigten Zauber deiner Wahl}
\cardplaycost{2}
\cardmodule{magic}
\cardborder
diff --git a/latex/cards/en/magic/no!.tex b/latex/cards/en/magic/no!.tex
index ccb7b8ca..a456dc3c 100644
--- a/latex/cards/en/magic/no!.tex
+++ b/latex/cards/en/magic/no!.tex
@@ -6,7 +6,7 @@
\begin{tikzpicture}
\cardtypeSpell{No!}
\cardbuycost{5}
-\cardcontent{Counter one spell}
+\cardcontent{Counter target declared spell}
\cardplaycost{2}
\cardmodule{magic}
\cardborder