aboutsummaryrefslogtreecommitdiff
path: root/go/game
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2025-09-08 15:38:07 +0200
committerFlorian Fischer <florian.fischer@muhq.space>2025-09-08 15:41:45 +0200
commita3ca136cd776ff0089dc33e401d1fc474dc17138 (patch)
tree1968a8ff318c7f021e2cc94df5a057eeee39b099 /go/game
parentee605fa7fa48803756b0c3216afc4bd692ec144a (diff)
downloadmuhqs-game-a3ca136cd776ff0089dc33e401d1fc474dc17138.tar.gz
muhqs-game-a3ca136cd776ff0089dc33e401d1fc474dc17138.zip
support explicitly winning and implement Approach Supremacy!
Diffstat (limited to 'go/game')
-rw-r--r--go/game/action.go45
-rw-r--r--go/game/card.go2
-rw-r--r--go/game/cardImplementations.go28
-rw-r--r--go/game/cardImplementations_test.go27
-rw-r--r--go/game/cardParsing.go8
-rw-r--r--go/game/kraken.go2
-rw-r--r--go/game/player.go5
-rw-r--r--go/game/stack.go10
-rw-r--r--go/game/state.go4
-rw-r--r--go/game/targets.go2
-rw-r--r--go/game/winCondition.go35
-rw-r--r--go/game/winCondition_test.go39
12 files changed, 187 insertions, 20 deletions
diff --git a/go/game/action.go b/go/game/action.go
index ab38f238..b9201d4a 100644
--- a/go/game/action.go
+++ b/go/game/action.go
@@ -316,18 +316,29 @@ var (
equipmentPlayActionTarget = TargetDesc{"available spawn tile", "?"}
)
-func NewPlayActionCostFunc(player *Player, cost int, card *Card) ActionCostFunc {
+func NewPlayActionCostFunc(a *PlayAction, cost int) ActionCostFunc {
+ player := a.source.(*Player)
+ s := player.gameState
+ card := a.Card
+
return func(*LocalState) bool {
if player.Resource < cost {
return false
}
+ if additionalCosts := getCardImplementation(card).additionalPlayCosts(a); additionalCosts != nil {
+ if !additionalCosts(s) {
+ return false
+ }
+ }
player.Resource -= cost
player.Hand.RemoveCard(card)
return true
}
}
-func NewPlayAction(p *Player, c *Card, args ...any) *PlayAction {
+// _newPlayAction returns a new PlayAction controlled by the player for a certain card.
+// It is important that the returned PlayAction has no cost function yet.
+func _newPlayAction(p *Player, c *Card) *PlayAction {
a := &PlayAction{
ActionBase{
source: p,
@@ -336,17 +347,7 @@ func NewPlayAction(p *Player, c *Card, args ...any) *PlayAction {
-1,
}
- if len(args) > 0 {
- if costFunc, ok := args[0].(ActionCostFunc); ok {
- a.costFunc = costFunc
- }
- }
-
s := p.gameState
- if a.costFunc == nil {
- a.costFunc = NewPlayActionCostFunc(p, c.PlayCosts.Costs(s), c)
- }
-
if c.IsPermanent() {
if c.Type == CardTypes.Equipment {
a.targets = newTargets(newTarget(s, equipmentPlayActionTarget, a))
@@ -366,8 +367,26 @@ func NewPlayAction(p *Player, c *Card, args ...any) *PlayAction {
return a
}
+func newPlayActionWithCostFunc(p *Player, c *Card, costFunc ActionCostFunc) *PlayAction {
+ a := _newPlayAction(p, c)
+ a.costFunc = costFunc
+ return a
+}
+
+func NewPlayAction(p *Player, c *Card) *PlayAction {
+ a := _newPlayAction(p, c)
+
+ s := p.gameState
+ if a.costFunc == nil {
+ a.costFunc = NewPlayActionCostFunc(a, c.PlayCosts.Costs(s))
+ }
+
+ return a
+}
+
func NewPlayActionVariadicCosts(p *Player, c *Card, variadicCosts int) *PlayAction {
- a := NewPlayAction(p, c, NewPlayActionCostFunc(p, c.PlayCosts.Costs(p.gameState, variadicCosts), c))
+ a := _newPlayAction(p, c)
+ a.costFunc = NewPlayActionCostFunc(a, c.PlayCosts.Costs(p.gameState, variadicCosts))
a.ChoosenVariadicCost = variadicCosts
return a
}
diff --git a/go/game/card.go b/go/game/card.go
index bdacd51b..d608973d 100644
--- a/go/game/card.go
+++ b/go/game/card.go
@@ -114,6 +114,7 @@ type cardImplementation interface {
stateBasedActions(*LocalState, Permanent)
+ additionalPlayCosts(*PlayAction) ActionCostFunc
additionalSpawnsFor(Permanent, CardType) []*Tile
onEntering(*Tile)
@@ -140,6 +141,7 @@ func (*cardImplementationBase) playTargets() TargetDesc { return INVALID_TARGET_
func (*cardImplementationBase) stateBasedActions(*LocalState, Permanent) {}
+func (*cardImplementationBase) additionalPlayCosts(*PlayAction) ActionCostFunc { return nil }
func (*cardImplementationBase) additionalSpawnsFor(Permanent, CardType) []*Tile { return nil }
func (*cardImplementationBase) onEntering(*Tile) {}
diff --git a/go/game/cardImplementations.go b/go/game/cardImplementations.go
index 0780d6f9..0af99570 100644
--- a/go/game/cardImplementations.go
+++ b/go/game/cardImplementations.go
@@ -741,6 +741,33 @@ func (*unholyCannonballImpl) onPlay(a *PlayAction) {
// ====== Exp1 Set ======
+type approachSupremacyImpl struct {
+ cardImplementationBase
+ cast map[*Player]int
+}
+
+// Use an additionalPlayCost function to track the times the spell was cast.
+// Since no real additional costs exists the function always returns true.
+func (impl *approachSupremacyImpl) additionalPlayCosts(a *PlayAction) ActionCostFunc {
+ return func(*LocalState) bool {
+ p := a.Controller()
+ if _, found := impl.cast[p]; !found {
+ impl.cast[p] = 1
+ } else {
+ impl.cast[p] += 1
+ }
+ return true
+ }
+}
+
+// Win the game if the spell resolves and was cast three or more times
+func (impl *approachSupremacyImpl) onPlay(a *PlayAction) {
+ p := a.Controller()
+ if impl.cast[p] >= 3 {
+ p.win()
+ }
+}
+
type backupImpl struct{ cardImplementationBase }
func (*backupImpl) onPlay(a *PlayAction) {
@@ -911,6 +938,7 @@ func init() {
"kraken/tides_change!": &tidesChangeImpl{},
"kraken/unholy_cannonball": &unholyCannonballImpl{},
+ "exp1/approach_supremacy!": &approachSupremacyImpl{cast: make(map[*Player]int)},
"exp1/backup!": &backupImpl{},
"exp1/illusion": &illusionImpl{},
"exp1/illusionist": &illusionistImpl{},
diff --git a/go/game/cardImplementations_test.go b/go/game/cardImplementations_test.go
index 8ab87ecd..8e298491 100644
--- a/go/game/cardImplementations_test.go
+++ b/go/game/cardImplementations_test.go
@@ -469,3 +469,30 @@ func TestPierce(t *testing.T) {
t.Fatal("Archer did not take 2 damage")
}
}
+
+func TestApproachSupremacy(t *testing.T) {
+ s, _, p, _ := newMockState()
+ p.Resource = 60
+ pa := NewPlayAction(p, NewCard("exp1/approach_supremacy!"))
+
+ // Play the first time
+ s.declareAction(pa)
+ s.stack.pop() // resolve once
+ if p.Won {
+ t.Fatal("player already won")
+ }
+
+ // Play the second time
+ s.declareAction(pa)
+ s.counterAction(pa) // do not resolve it
+ if p.Won {
+ t.Fatal("player already won")
+ }
+
+ // Play the third time
+ s.declareAction(pa)
+ s.stack.pop() // resolve it
+ if !p.Won {
+ t.Fatal("player did not win")
+ }
+}
diff --git a/go/game/cardParsing.go b/go/game/cardParsing.go
index b214f7b7..2d2feeb3 100644
--- a/go/game/cardParsing.go
+++ b/go/game/cardParsing.go
@@ -18,6 +18,7 @@ type dynamicCardImplementation struct {
_stateBasedActions func(*LocalState, Permanent)
+ _additionalPlayCosts func(*PlayAction) ActionCostFunc
_additionalSpawnsFor func(Permanent, CardType) []*Tile
_onEntering func(*Tile)
@@ -61,6 +62,13 @@ func (impl *dynamicCardImplementation) stateBasedActions(s *LocalState, p Perman
}
}
+func (impl *dynamicCardImplementation) additionalPlayCosts(a *PlayAction) ActionCostFunc {
+ if impl._additionalPlayCosts != nil {
+ return impl._additionalPlayCosts(a)
+ }
+ return nil
+}
+
func (impl *dynamicCardImplementation) additionalSpawnsFor(p Permanent, ct CardType) []*Tile {
if impl._additionalSpawnsFor != nil {
return impl._additionalSpawnsFor(p, ct)
diff --git a/go/game/kraken.go b/go/game/kraken.go
index 945eef6e..b9da3bcc 100644
--- a/go/game/kraken.go
+++ b/go/game/kraken.go
@@ -133,7 +133,7 @@ func (ctrl *KrakenControl) krakenTurn() {
return true
}
- a := NewPlayAction(kraken, c, costFunc)
+ a := newPlayActionWithCostFunc(kraken, c, costFunc)
err := selectRandomTargets(s.Rand, a.Targets())
if err != nil {
log.Info("Diacard because no target for", "card", c.Name)
diff --git a/go/game/player.go b/go/game/player.go
index b3cdfe0f..09488a32 100644
--- a/go/game/player.go
+++ b/go/game/player.go
@@ -16,6 +16,7 @@ type Player struct {
Resource int
DrawPerTurn int
Conceded bool
+ Won bool
Hand *Hand
DiscardPile *DiscardPile
Deck *Deck
@@ -323,3 +324,7 @@ func (p *Player) concede() {
p.Conceded = true
p.gameState.broadcastNotification(newConcededNotification(p))
}
+
+func (p *Player) win() {
+ p.Won = true
+}
diff --git a/go/game/stack.go b/go/game/stack.go
index 4d2a056e..bb93f40c 100644
--- a/go/game/stack.go
+++ b/go/game/stack.go
@@ -21,6 +21,16 @@ func (s *Stack) push(a Action) {
s.Actions = append(s.Actions, a)
}
+func (s *Stack) remove(a Action) {
+ for i, o := range s.Actions {
+ if o != a {
+ continue
+ }
+ s.Actions = append(s.Actions[:i], s.Actions[i+1:]...)
+ return
+ }
+}
+
func (s *Stack) pop() {
l := len(s.Actions)
if l == 0 {
diff --git a/go/game/state.go b/go/game/state.go
index 9baaea5f..a9c2f638 100644
--- a/go/game/state.go
+++ b/go/game/state.go
@@ -410,6 +410,10 @@ func (s *LocalState) declareAction(a Action) {
}
}
+func (s *LocalState) counterAction(a Action) {
+ s.stack.remove(a)
+}
+
func (s *LocalState) orderTriggeredActions(triggeredActions []*TriggeredAction) []Action {
// Sort triggers by their controller
playerActions := make(map[*Player][]*TriggeredAction)
diff --git a/go/game/targets.go b/go/game/targets.go
index 9bfd1378..56ec9b30 100644
--- a/go/game/targets.go
+++ b/go/game/targets.go
@@ -478,6 +478,8 @@ func attackableTargetConstraint(action Action) TargetConstraintFunc {
func posFromTileOrPermanent(tileOrPermanent any) Position {
switch obj := tileOrPermanent.(type) {
+ case Position:
+ return obj
case *Tile:
return obj.Position
case Permanent:
diff --git a/go/game/winCondition.go b/go/game/winCondition.go
index 67c0190e..0a97330c 100644
--- a/go/game/winCondition.go
+++ b/go/game/winCondition.go
@@ -43,13 +43,18 @@ func BossGame(name string) *winCondition {
return false
})
+ var winners []*Player
if bossFound {
+ if winners = explicitWinners(s); len(winners) > 0 {
+ return winners
+ }
+
return singleConcessionWinner(s)
}
- winners := make([]*Player, 0, len(s.players))
+ // No Boss was found
for _, p := range s.players {
- if p.Name != name && !p.Conceded {
+ if p.Name != name && !p.Conceded && !slices.Contains(winners, p) {
winners = append(winners, p)
}
}
@@ -72,7 +77,12 @@ var KingGame = &winCondition{
foundKings[u.owner] = struct{}{}
}
+ var winners []*Player
if len(foundKings) == len(s.Players()) {
+ if winners = explicitWinners(s); len(winners) > 0 {
+ return winners
+ }
+
return singleConcessionWinner(s)
}
@@ -93,9 +103,8 @@ var KingGame = &winCondition{
return s.Players()
}
- winners := []*Player{}
for _, p := range s.Players() {
- if !slices.Contains(loosers, p) {
+ if !slices.Contains(loosers, p) && !slices.Contains(winners, p) {
winners = append(winners, p)
}
}
@@ -107,9 +116,9 @@ var KingGame = &winCondition{
// DeathMatch returns the players without enemy units.
var DeathMatch = &winCondition{
condition: func(s *LocalState) []*Player {
- winners := []*Player{}
+ winners := explicitWinners(s)
for _, p := range s.players {
- if len(s.EnemyUnits(p)) == 0 {
+ if len(s.EnemyUnits(p)) == 0 && !slices.Contains(winners, p) {
winners = append(winners, p)
}
}
@@ -121,6 +130,7 @@ var DeathMatch = &winCondition{
desc: "Destroy all enemy units",
}
+// singleConcessionWinner returns the player left over after the rest conceded.
func singleConcessionWinner(s *LocalState) []*Player {
var winner *Player
for _, p := range s.players {
@@ -136,3 +146,16 @@ func singleConcessionWinner(s *LocalState) []*Player {
}
return []*Player{winner}
}
+
+// explicitWinners returns the players that explicitly won the game.
+// Winning the game is sometimes possible without fulfilling a maps win condition,
+// but by achieving some external condition (like Approach Supremacy!).
+func explicitWinners(s *LocalState) []*Player {
+ winners := []*Player{}
+ for _, p := range s.players {
+ if p.Won {
+ winners = append(winners, p)
+ }
+ }
+ return winners
+}
diff --git a/go/game/winCondition_test.go b/go/game/winCondition_test.go
index 0f380e92..8ed06dff 100644
--- a/go/game/winCondition_test.go
+++ b/go/game/winCondition_test.go
@@ -80,3 +80,42 @@ func TestKrakenGame(t *testing.T) {
t.Fatal("p is not the winner", w)
}
}
+
+func TestExplicitWinning(t *testing.T) {
+ s, _, p, o := newMockState()
+ s.addNewUnit(NewCard("misc/king"), Position{0, 0}, p)
+ s.addNewUnit(NewCard("misc/king"), Position{1, 1}, o)
+
+ p.win()
+ s._map.WinCondition = KingGame
+ w := s._map.WinCondition.check(s)
+ if len(w) != 1 {
+ t.Fatal("winner not declared", w)
+ }
+ if w[0] != p {
+ t.Fatal("p is not the winner", w)
+ }
+
+ s._map.WinCondition = DeathMatch
+ w = s._map.WinCondition.check(s)
+ if len(w) != 1 {
+ t.Fatal("winner not declared", w)
+ }
+ if w[0] != p {
+ t.Fatal("p is not the winner", w)
+ }
+
+ s, _, p, o = newMockState()
+ o.Name = KRAKEN_NAME
+ s.addNewUnit(NewCard("kraken/the_kraken"), Position{1, 1}, o)
+ p.win()
+
+ s._map.WinCondition = BossGame(KRAKEN_NAME)
+ w = s._map.WinCondition.check(s)
+ if len(w) != 1 {
+ t.Fatal("winner not declared", w)
+ }
+ if w[0] != p {
+ t.Fatal("p is not the winner", w)
+ }
+}