diff options
| -rw-r--r-- | go/game/action.go | 45 | ||||
| -rw-r--r-- | go/game/card.go | 2 | ||||
| -rw-r--r-- | go/game/cardImplementations.go | 28 | ||||
| -rw-r--r-- | go/game/cardImplementations_test.go | 27 | ||||
| -rw-r--r-- | go/game/cardParsing.go | 8 | ||||
| -rw-r--r-- | go/game/kraken.go | 2 | ||||
| -rw-r--r-- | go/game/player.go | 5 | ||||
| -rw-r--r-- | go/game/stack.go | 10 | ||||
| -rw-r--r-- | go/game/state.go | 4 | ||||
| -rw-r--r-- | go/game/targets.go | 2 | ||||
| -rw-r--r-- | go/game/winCondition.go | 35 | ||||
| -rw-r--r-- | go/game/winCondition_test.go | 39 |
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) + } +} |
