aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-02-07 00:32:39 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2025-08-20 15:50:08 +0200
commit0b12a11962ae0c94945a2b599d0b28f24c6428cf (patch)
tree640aac52133738ced7cdd17168292c5fec482559
parent4db8db0e847552077be9ebd9177ddc573c2b51d4 (diff)
downloadmuhqs-game-0b12a11962ae0c94945a2b599d0b28f24c6428cf.tar.gz
muhqs-game-0b12a11962ae0c94945a2b599d0b28f24c6428cf.zip
intermediate commit
adding area effects and hooks for permanents entering and leaving tiles
-rw-r--r--go/TODO10
-rw-r--r--go/game/ai.go1
-rw-r--r--go/game/areaEffect.go27
-rw-r--r--go/game/areaEffect_test.go69
-rw-r--r--go/game/artifact.go4
-rw-r--r--go/game/card.go23
-rw-r--r--go/game/cardImplementations.go232
-rw-r--r--go/game/cardParsing.go56
-rw-r--r--go/game/equipment.go1
-rw-r--r--go/game/map.go4
-rw-r--r--go/game/marks.go68
-rw-r--r--go/game/permanent.go105
-rw-r--r--go/game/player.go6
-rw-r--r--go/game/state.go28
-rw-r--r--go/game/tile.go38
-rw-r--r--go/game/unit.go62
-rw-r--r--go/game/unit_test.go6
17 files changed, 571 insertions, 169 deletions
diff --git a/go/TODO b/go/TODO
new file mode 100644
index 00000000..23274884
--- /dev/null
+++ b/go/TODO
@@ -0,0 +1,10 @@
+* implement upkeep action
+* implement target selection for triggers
+* implement Pile UI hint
+* implement spell target parsing / selection
+* implement triggers
+* implement draft
+* implement game log
+* finish AIs
+ * shy ai
+ * target-oriented
diff --git a/go/game/ai.go b/go/game/ai.go
index 181f6dc1..daeeff4f 100644
--- a/go/game/ai.go
+++ b/go/game/ai.go
@@ -317,6 +317,7 @@ func WanderingAI(ai *UnitAI, x int) {
break
}
}
+
// 2.
if ai.u.AvailMoveActions > 0 {
moveToRandomTile(ai)
diff --git a/go/game/areaEffect.go b/go/game/areaEffect.go
new file mode 100644
index 00000000..8991bc3c
--- /dev/null
+++ b/go/game/areaEffect.go
@@ -0,0 +1,27 @@
+package game
+
+type areaEffect interface {
+ onEntering(Permanent)
+ onLeaving(Permanent)
+}
+
+type dynamicAreaEffect struct {
+ _onEntering func(Permanent)
+ _onLeaving func(Permanent)
+}
+
+func newDynamicAreaEffect(enter func(Permanent), leave func(Permanent)) *dynamicAreaEffect {
+ return &dynamicAreaEffect{enter, leave}
+}
+
+func (e *dynamicAreaEffect) onEntering(p Permanent) {
+ if e._onEntering != nil {
+ e._onEntering(p)
+ }
+}
+
+func (e *dynamicAreaEffect) onLeaving(p Permanent) {
+ if e._onLeaving != nil {
+ e._onLeaving(p)
+ }
+}
diff --git a/go/game/areaEffect_test.go b/go/game/areaEffect_test.go
new file mode 100644
index 00000000..4f55bffd
--- /dev/null
+++ b/go/game/areaEffect_test.go
@@ -0,0 +1,69 @@
+package game
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestEnteringLeaving(t *testing.T) {
+ mapDef := `map: |1-
+ HH
+symbols:
+ H: house
+`
+ s := NewState()
+ r := strings.NewReader(mapDef)
+ s.Map, _ = readMap(r)
+
+ s.AddNewPlayer("player", NewDeck())
+ p := s.Players[0]
+
+ t0_0 := s.Map.TileAt(Position{0, 0})
+ entered0_0, left0_0 := false, false
+ t0_0.effects = append(t0_0.effects, newDynamicAreaEffect(
+ func(Permanent) { entered0_0 = true },
+ func(Permanent) { left0_0 = true },
+ ))
+
+ t0_1 := s.Map.TileAt(Position{1, 0})
+ entered0_1, left0_1 := false, false
+ t0_1.effects = append(t0_1.effects, newDynamicAreaEffect(
+ func(Permanent) { entered0_1 = true },
+ func(Permanent) { left0_1 = true },
+ ))
+
+ palisade := NewUnit(NewCard("base/archer"), t0_0, p)
+ s.AddPermanent(palisade)
+ if !entered0_0 || palisade.Tile() != t0_0 || t0_0.Permanent != palisade {
+ t.Fatalf("palisade did not enter tile %v", t0_0)
+ }
+
+ s.DestroyPermanent(palisade)
+ if !left0_0 || palisade.Tile() != nil || t0_0.Permanent != nil {
+ t.Fatalf("destroyed palisade did not leave tile %v", t0_0)
+ }
+
+ entered0_0, left0_0 = false, false
+ archer := NewUnit(NewCard("base/archer"), t0_0, p)
+ s.AddPermanent(archer)
+ if !entered0_0 || archer.Tile() != t0_0 || t0_0.Permanent != archer {
+ t.Fatalf("archer did not enter tile %v", t0_0)
+ }
+
+ archer.move(s, t0_1)
+ if !left0_0 || t0_0.Permanent != nil {
+ t.Fatalf("moved archer did not leave tile %v", t0_0)
+ }
+
+ if !entered0_1 || archer.Tile() != t0_1 || t0_1.Permanent != archer {
+ t.Fatalf("moved archer did not leave tile %v", t0_1)
+ }
+
+ s.DestroyPermanent(archer)
+ if !left0_1 || archer.Tile() != nil || t0_1.Permanent != nil {
+ t.Fatalf("destroyed archer did not leave tile %v", t0_1)
+ }
+
+ sword := newEquipmentFromPath("base/sword", t, p)
+ s.AddPermanent(sword)
+}
diff --git a/go/game/artifact.go b/go/game/artifact.go
index 916917d8..50fa2653 100644
--- a/go/game/artifact.go
+++ b/go/game/artifact.go
@@ -19,10 +19,6 @@ func NewArtifactFromPath(cardPath string, tile *Tile, owner *Player) *Artifact {
func NewArtifact(card *Card, tile *Tile, owner *Player) *Artifact {
a := &Artifact{NewPermanentBase(card, tile, owner), 0}
- if tile != nil {
- tile.Permanent = a
- }
-
effects := card.getEffects()
for _, e := range effects {
if strings.HasPrefix(e, "Solid") {
diff --git a/go/game/card.go b/go/game/card.go
index b1d913a8..c24e7585 100644
--- a/go/game/card.go
+++ b/go/game/card.go
@@ -76,10 +76,14 @@ type cardImplementation interface {
fullActions(*Unit) []*FullAction
freeActions(Permanent) []*FreeAction
+ stateBasedActions(*State, Permanent)
+
+ onEntering(*Tile)
+ onLeaving(*Tile)
onPlay(*State, *Player, *Targets)
- onPile(Permanent)
- onUnpile(Permanent)
- onDrop(Permanent)
+ onPile(containing Permanent)
+ onUnpile(containing Permanent)
+ onDrop(dropped Permanent)
}
// Default implementation of the cardImplementation interface.
@@ -91,10 +95,15 @@ type cardImplementationBase struct{}
func (*cardImplementationBase) spawnTiles(*State, *Player) []*Tile { return nil }
func (*cardImplementationBase) fullActions(*Unit) []*FullAction { return nil }
func (*cardImplementationBase) freeActions(Permanent) []*FreeAction { return nil }
-func (*cardImplementationBase) onPlay(*State, *Player, *Targets) {}
-func (*cardImplementationBase) onPile(Permanent) {}
-func (*cardImplementationBase) onUnpile(Permanent) {}
-func (*cardImplementationBase) onDrop(Permanent) {}
+
+func (*cardImplementationBase) stateBasedActions(*State, Permanent) {}
+
+func (*cardImplementationBase) onEntering(*Tile) {}
+func (*cardImplementationBase) onLeaving(*Tile) {}
+func (*cardImplementationBase) onPlay(*State, *Player, *Targets) {}
+func (*cardImplementationBase) onPile(Permanent) {}
+func (*cardImplementationBase) onUnpile(Permanent) {}
+func (*cardImplementationBase) onDrop(Permanent) {}
// Map of all card implementations
var cardImplementations map[string]cardImplementation
diff --git a/go/game/cardImplementations.go b/go/game/cardImplementations.go
index 271c88ea..cb32ab2c 100644
--- a/go/game/cardImplementations.go
+++ b/go/game/cardImplementations.go
@@ -1,14 +1,12 @@
package game
-type farmerImplementation struct {
- cardImplementationBase
-}
+import (
+ "log"
+)
-type advisorImplementation struct {
- cardImplementationBase
-}
+type advisorImpl struct{ cardImplementationBase }
-func (impl *advisorImplementation) fullActions(u *Unit) []*FullAction {
+func (impl *advisorImpl) fullActions(u *Unit) []*FullAction {
resolvePrototype := func(a Action) ActionResolveFunc {
u := a.Source().(*Unit)
return func(s *State) {
@@ -23,61 +21,229 @@ func (impl *advisorImplementation) fullActions(u *Unit) []*FullAction {
return []*FullAction{NewFullAction(u, resolvePrototype, "draw 1. exile 1")}
}
-type missionaryImplementation struct {
- cardImplementationBase
-}
+type missionaryImpl struct{ cardImplementationBase }
-func (impl *missionaryImplementation) fullActions(u *Unit) []*FullAction {
+func (impl *missionaryImpl) fullActions(u *Unit) []*FullAction {
resolvePrototype := func(a Action) ActionResolveFunc {
u := a.Source().(*Unit)
target := a.Targets().sel[0].(*Unit)
return func(s *State) {
- faith, found := target.States[UnitMarks.Faith]
- if !found {
- target.States[UnitMarks.Faith] = 2
- } else {
- target.States[UnitMarks.Faith] = faith + 2
- if faith+2 > target.Card().BuyCost {
- target.controller = u.Controller()
- }
+ target.addMarks(UnitMarks.Faith, 2)
+ if target.Marks(UnitMarks.Faith) > target.Card().BuyCost {
+ target.controller = u.Controller()
}
}
}
return []*FullAction{NewFullAction(u, resolvePrototype, "put faith ...")}
}
-type swordImplementation struct {
- cardImplementationBase
-}
+type swordImpl struct{ cardImplementationBase }
-func (*swordImplementation) onPile(p Permanent) {
+func (*swordImpl) onPile(p Permanent) {
if u, ok := p.(*Unit); ok && !INVALID_ATTACK(u.Attack) {
u.Attack.attacks[0] += 1
}
}
-func (*swordImplementation) onUnpile(p Permanent) {
+func (*swordImpl) onUnpile(p Permanent) {
if u, ok := p.(*Unit); ok && !INVALID_ATTACK(u.Attack) {
u.Attack.attacks[0] -= 1
}
}
-type tidesChangeImplementation struct {
+type tidesChangeImpl struct{ cardImplementation }
+
+func (*tidesChangeImpl) onPlay(s *State, _ *Player, _ *Targets) {
+ s.Map.redistributeStoreCards(s.Rand)
+}
+
+type dejaVuImpl struct{ cardImplementation }
+
+func (*dejaVuImpl) onPlay(s *State, _ *Player, _ *Targets) {
+ for _, p := range s.Players {
+ if !p.IsKraken() {
+ p.Resource = s.Map.ResourceGain
+ }
+ }
+}
+
+type devourThePoorImpl struct{ cardImplementation }
+
+func (*devourThePoorImpl) onPlay(s *State, kraken *Player, _ *Targets) {
+ for _, u := range s.EnemyUnits(kraken) {
+ if u.Card().BuyCost < 7 {
+ s.DestroyPermanent(u)
+ }
+ }
+}
+
+type drownedSailorImpl struct{ cardImplementation }
+
+func (*drownedSailorImpl) onPile(containing Permanent) {
+ if u, ok := containing.(*Unit); ok && u.Movement.Swimming {
+ u.Attack.attacks[0] += 1
+ }
+}
+
+func (*drownedSailorImpl) onUnpile(containing Permanent) {
+ if u, ok := containing.(*Unit); ok && u.Movement.Swimming {
+ u.Attack.attacks[0] -= 1
+ }
+}
+
+func (*drownedSailorImpl) onDrop(sailor Permanent) {
+ sailor.Controller().gameState.DestroyPermanent(sailor)
+}
+
+// TODO: implement death trigger
+// TODO: implement hook for the containing permanent on pile
+type flyingDutchmenImpl struct{ cardImplementation }
+
+type frostPylonAoE struct{}
+
+func (*frostPylonAoE) onEntering(Permanent) {
+ // TODO: prevent actions and grant new full action
+}
+
+func (*frostPylonAoE) onLeaving(Permanent) {
+ // TODO: remove effect
+}
+
+type frostPylon struct {
cardImplementation
+ aoe frostPylonAoE
}
-func (*tidesChangeImplementation) onPlay(s *State, _ *Player, _ *Targets) {
- s.Map.redistributeStoreCards(s.Rand)
+func (*frostPylon) spawnTiles(s *State, kraken *Player) (spawns []*Tile) {
+ for _, u := range s.EnemyUnits(kraken) {
+ spawns = append(spawns, TilesInRange(s.Map, u, 1)...)
+ }
+ return
+}
+
+func (*frostPylon) stateBasedActions(s *State, p Permanent) {
+ if p.Marks(PermanentMarks.Crack) >= 3 {
+ s.DestroyPermanent(p)
+ }
+}
+
+func (i *frostPylon) onEntering(t *Tile) {
+ s := t.Permanent.Controller().gameState
+ for _, tile := range TilesInRange(s.Map, t.Permanent, 1) {
+ tile.addEffect(&i.aoe)
+ }
+}
+
+func (i *frostPylon) onLeaving(t *Tile) {
+ s := t.Permanent.Controller().gameState
+ for _, tile := range TilesInRange(s.Map, t.Permanent, 1) {
+ tile.removeEffect(&i.aoe)
+ }
+}
+
+type giganticHailImpl struct{ cardImplementation }
+
+func (*giganticHailImpl) onPlay(s *State, kraken *Player, _ *Targets) {
+ emptyTiles := s.Map.FreeTiles()
+ choices := len(emptyTiles)
+ for i := 0; i < 3; i++ {
+ if choices == 0 {
+ return
+ }
+
+ choice := s.Rand.Intn(choices - 1)
+ s.AddNewArtifact(NewCard("kraken/ice_berg"), emptyTiles[choice].Position, kraken)
+ emptyTiles[choice] = emptyTiles[choices-1]
+ choices--
+ }
+}
+
+type soldOutImpl struct{ cardImplementation }
+
+func (*soldOutImpl) onPlay(s *State, kraken *Player, _ *Targets) {
+ nStores := len(s.Map.Stores)
+ storePositions := make([]Position, 0, nStores)
+ for pos := range s.Map.Stores {
+ storePositions = append(storePositions, pos)
+ }
+
+ storePos := storePositions[s.Rand.Intn(nStores-1)]
+ s.Map.StoreOn(storePos).MoveInto(s.Exile)
+
+ storeTile := s.Map.TileAt(storePos)
+ storeTile.neutralize()
+}
+
+type supriseImpl struct{ cardImplementation }
+
+func (*supriseImpl) onPlay(s *State, kraken *Player, _ *Targets) {
+ // TODO: implement modification of next spawn tile
+}
+
+type tentacleSlapImpl struct{ cardImplementation }
+
+func (*tentacleSlapImpl) onPlay(s *State, kraken *Player, _ *Targets) {
+ x := 0
+ for _, p := range s.Players {
+ if p.IsKraken() {
+ continue
+ }
+
+ x += p.Hand.Size()
+ p.discardHand()
+ }
+
+ gain := 5 - x
+ if gain > 0 {
+ kraken.gainResource(gain)
+ }
+}
+
+type unholyCannonballImpl struct{ cardImplementation }
+
+func (*unholyCannonballImpl) onPlay(s *State, kraken *Player, _ *Targets) {
+ krakenTiles := s.Map.FilterTiles(func(t *Tile) bool { return t.Raw == "the kraken" })
+ if len(krakenTiles) != 1 {
+ log.Fatal("No kraken tile found in Map")
+ }
+
+ enemiesInRange2 := []*Unit{}
+ for _, u := range s.EnemyUnits(kraken) {
+ if u.Tile() == nil {
+ continue
+ }
+
+ if IsPositionInRange(u.Tile().Position, krakenTiles[0].Position, 2) {
+ enemiesInRange2 = append(enemiesInRange2, u)
+ break
+ }
+ }
+
+ if len(enemiesInRange2) == 0 {
+ kraken.gainResource(5)
+ } else {
+ for _, u := range enemiesInRange2 {
+ u.addMarks(UnitStates.Panic, 1)
+ }
+ }
}
func init() {
cardImplementations = map[string]cardImplementation{
- "base/advisor": &advisorImplementation{},
- "base/missionary": &missionaryImplementation{},
- "base/sword": &swordImplementation{},
-
- "misc/farmer": &farmerImplementation{},
+ "base/advisor": &advisorImpl{},
+ "base/missionary": &missionaryImpl{},
+ "base/sword": &swordImpl{},
- "kraken/tides_change!": &tidesChangeImplementation{},
+ "kraken/deja_vu!": &dejaVuImpl{},
+ "kraken/devour_the_poor!": &devourThePoorImpl{},
+ "kraken/drowned_sailor": &drownedSailorImpl{},
+ "kraken/flying_dutchmen": &flyingDutchmenImpl{},
+ "kraken/frost_pylon": &frostPylon{},
+ "kraken/gigantic_hail": &giganticHailImpl{},
+ "kraken/sold_out!": &soldOutImpl{},
+ "kraken/suprise!": &supriseImpl{},
+ "kraken/tentacle_slap": &tentacleSlapImpl{},
+ "kraken/tides_change!": &tidesChangeImpl{},
+ "kraken/unholy_cannonball": &unholyCannonballImpl{},
}
}
diff --git a/go/game/cardParsing.go b/go/game/cardParsing.go
index 9fcb48aa..de1c7be4 100644
--- a/go/game/cardParsing.go
+++ b/go/game/cardParsing.go
@@ -7,64 +7,86 @@ import (
"strings"
)
-type parsedCardImplementation struct {
+type dynamicCardImplementation struct {
card *Card
spawnTilesFunc func(*State, *Player) []*Tile
genFullActions func(*Unit) []*FullAction
genFreeActions func(Permanent) []*FreeAction
- _onPlay func(*State, *Player, *Targets)
- _onPile func(Permanent)
- _onUnpile func(Permanent)
- _onDrop func(Permanent)
+
+ _stateBasedActions func(*State, Permanent)
+ _onEntering func(*Tile)
+ _onLeaving func(*Tile)
+ _onPlay func(*State, *Player, *Targets)
+ _onPile func(Permanent)
+ _onUnpile func(Permanent)
+ _onDrop func(Permanent)
}
-func (impl *parsedCardImplementation) spawnTiles(s *State, p *Player) []*Tile {
+func (impl *dynamicCardImplementation) spawnTiles(s *State, p *Player) []*Tile {
if impl.spawnTilesFunc != nil {
return impl.spawnTilesFunc(s, p)
}
return nil
}
-func (impl *parsedCardImplementation) fullActions(u *Unit) []*FullAction {
+func (impl *dynamicCardImplementation) fullActions(u *Unit) []*FullAction {
if impl.genFullActions != nil {
return impl.genFullActions(u)
}
return nil
}
-func (impl *parsedCardImplementation) freeActions(p Permanent) []*FreeAction {
+func (impl *dynamicCardImplementation) freeActions(p Permanent) []*FreeAction {
if impl.genFreeActions != nil {
return impl.genFreeActions(p)
}
return nil
}
-func (impl *parsedCardImplementation) onPlay(s *State, p *Player, t *Targets) {
+func (impl *dynamicCardImplementation) stateBasedActions(s *State, p Permanent) {
+ if impl._stateBasedActions != nil {
+ impl._stateBasedActions(s, p)
+ }
+}
+
+func (impl *dynamicCardImplementation) onEntering(t *Tile) {
+ if impl._onEntering != nil {
+ impl._onEntering(t)
+ }
+}
+
+func (impl *dynamicCardImplementation) onLeaving(t *Tile) {
+ if impl._onLeaving != nil {
+ impl._onLeaving(t)
+ }
+}
+
+func (impl *dynamicCardImplementation) onPlay(s *State, p *Player, t *Targets) {
if impl._onPlay != nil {
impl._onPlay(s, p, t)
}
}
-func (impl *parsedCardImplementation) onPile(p Permanent) {
+func (impl *dynamicCardImplementation) onPile(p Permanent) {
if impl._onPile != nil {
impl._onPile(p)
}
}
-func (impl *parsedCardImplementation) onUnpile(p Permanent) {
+func (impl *dynamicCardImplementation) onUnpile(p Permanent) {
if impl._onUnpile != nil {
impl._onUnpile(p)
}
}
-func (impl *parsedCardImplementation) onDrop(p Permanent) {
+func (impl *dynamicCardImplementation) onDrop(p Permanent) {
if impl._onDrop != nil {
impl._onDrop(p)
}
}
-func newParsedCardImplementation(c *Card) *parsedCardImplementation {
- impl := &parsedCardImplementation{card: c}
+func newParsedCardImplementation(c *Card) *dynamicCardImplementation {
+ impl := &dynamicCardImplementation{card: c}
for _, effect := range c.getEffects() {
impl.parseEffect(effect)
}
@@ -76,13 +98,13 @@ func newParsedCardImplementation(c *Card) *parsedCardImplementation {
return impl
}
-func (impl *parsedCardImplementation) parseEffect(effect string) {
+func (impl *dynamicCardImplementation) parseEffect(effect string) {
if strings.Contains(effect, "can be played") {
impl.parsePlayModification(effect)
}
}
-func (impl *parsedCardImplementation) parseFullAction(fullAction string) {
+func (impl *dynamicCardImplementation) parseFullAction(fullAction string) {
var resolveProtos []ActionFuncPrototype
gainRessourceRegex := regexp.MustCompile(`gain [1-9][0-9]* resource`)
@@ -190,7 +212,7 @@ func parseLocation(location string) func(interface{}, *State) []*Tile {
return nil
}
-func (impl *parsedCardImplementation) parsePlayModification(effect string) {
+func (impl *dynamicCardImplementation) parsePlayModification(effect string) {
tokens := strings.SplitN(effect, " can be played ", 2)
log.Println(tokens)
what, how := tokens[0], tokens[1]
diff --git a/go/game/equipment.go b/go/game/equipment.go
index af06fdb1..89821458 100644
--- a/go/game/equipment.go
+++ b/go/game/equipment.go
@@ -19,7 +19,6 @@ func newEquipment(card *Card, containing interface{}, owner *Player) *Equipment
switch containing := containing.(type) {
case *Tile:
e.tile = containing
- containing.Permanent = e
case *Unit:
AddPermanentToPile(containing, e)
}
diff --git a/go/game/map.go b/go/game/map.go
index 2115e0b0..b86debad 100644
--- a/go/game/map.go
+++ b/go/game/map.go
@@ -269,6 +269,10 @@ func (m *Map) AllTiles() []*Tile {
return m.FilterTiles(func(*Tile) bool { return true })
}
+func (m *Map) FreeTiles() []*Tile {
+ return m.FilterTiles(func(t *Tile) bool { return t.IsFree() })
+}
+
func (m *Map) distributeStoreCards(cards PileOfCards, rand *rand.Rand) {
nStores := len(m.Stores)
stores := make([]*Store, 0, nStores)
diff --git a/go/game/marks.go b/go/game/marks.go
new file mode 100644
index 00000000..a3ea0e32
--- /dev/null
+++ b/go/game/marks.go
@@ -0,0 +1,68 @@
+package game
+
+import (
+ "log"
+)
+
+type PermanentMark int
+
+const (
+ crack PermanentMark = iota + 1
+ paralysis
+ poison
+ panic_
+ rage
+ ward
+ faith
+)
+
+func (mark PermanentMark) String() string {
+ switch mark {
+ case crack:
+ return "crack"
+ case paralysis:
+ return "paralysis"
+ case poison:
+ return "poison"
+ case panic_:
+ return "panic"
+ case rage:
+ return "rage"
+ case ward:
+ return "ward"
+ case faith:
+ return "faith"
+ default:
+ log.Fatalf("Unknown permanent mark %d", mark)
+ return ""
+ }
+}
+
+var (
+ PermanentMarks = struct {
+ Crack PermanentMark
+ }{
+ Crack: crack,
+ }
+
+ unitStates = []PermanentMark{paralysis, poison, panic_, rage}
+ UnitStates = struct {
+ Paralysis PermanentMark
+ Poison PermanentMark
+ Panic PermanentMark
+ Rage PermanentMark
+ }{
+ Paralysis: paralysis,
+ Poison: poison,
+ Panic: panic_,
+ Rage: rage,
+ }
+
+ UnitMarks = struct {
+ Ward PermanentMark
+ Faith PermanentMark
+ }{
+ Ward: ward,
+ Faith: faith,
+ }
+)
diff --git a/go/game/permanent.go b/go/game/permanent.go
index 67c39e0b..9188a206 100644
--- a/go/game/permanent.go
+++ b/go/game/permanent.go
@@ -12,13 +12,17 @@ import (
type Permanent interface {
Card() *Card
Tile() *Tile
+ SetTile(*Tile)
Damage() int
+ ContainingPerm() Permanent
Pile() []Permanent
Controller() *Player
Owner() *Player
Effects() []string
HasEffect(string) bool
XEffect(string) (int, error)
+ Marks(PermanentMark) int
+ addMarks(PermanentMark, int)
String() string
Fight(p Permanent)
@@ -28,8 +32,9 @@ type Permanent interface {
AvailActions() []Action
IsAvailableTile(*Tile) bool
- SetContainingPerm(p Permanent)
- AddPermanentToPile(p Permanent)
+ setContainingPerm(p Permanent)
+ addPermanentToPile(p Permanent)
+ clearPile()
RemovePermanentFromPile(p Permanent)
onPile(containing Permanent)
onUnpile(containing Permanent)
@@ -45,6 +50,7 @@ type PermanentBase struct {
controller *Player
owner *Player
tmpEffects []string
+ marks map[PermanentMark]int
}
func NewPermanentBase(card *Card, tile *Tile, owner *Player) PermanentBase {
@@ -60,29 +66,14 @@ func NewPermanentBase(card *Card, tile *Tile, owner *Player) PermanentBase {
}
}
-func (p *PermanentBase) Card() *Card {
- return p.card
-}
-
-func (p *PermanentBase) Tile() *Tile {
- return p.tile
-}
-
-func (p *PermanentBase) Damage() int {
- return p.damage
-}
-
-func (p *PermanentBase) Pile() []Permanent {
- return p.pile
-}
-
-func (p *PermanentBase) Controller() *Player {
- return p.controller
-}
-
-func (p *PermanentBase) Owner() *Player {
- return p.owner
-}
+func (p *PermanentBase) Card() *Card { return p.card }
+func (p *PermanentBase) Tile() *Tile { return p.tile }
+func (p *PermanentBase) SetTile(t *Tile) { p.tile = t }
+func (p *PermanentBase) Damage() int { return p.damage }
+func (p *PermanentBase) ContainingPerm() Permanent { return p.containingPerm }
+func (p *PermanentBase) Pile() []Permanent { return p.pile }
+func (p *PermanentBase) Controller() *Player { return p.controller }
+func (p *PermanentBase) Owner() *Player { return p.owner }
func (p *PermanentBase) String() string {
if p.Tile() != nil {
@@ -138,18 +129,30 @@ func (p *PermanentBase) XEffect(effect string) (int, error) {
return x, err
}
-func (p *PermanentBase) IsAvailableTile(tile *Tile) bool {
- return tile.IsAvailableForCard(p.card)
+func (p *PermanentBase) Marks(mark PermanentMark) int {
+ if amount, found := p.marks[mark]; found {
+ return amount
+ }
+
+ return 0
}
-func (p *PermanentBase) SetContainingPerm(containing Permanent) {
- p.containingPerm = containing
+func (p *PermanentBase) addMarks(mark PermanentMark, amount int) {
+ if amount, found := p.marks[mark]; found {
+ p.marks[mark] += amount
+ } else {
+ p.marks[mark] = amount
+ }
}
-func (p *PermanentBase) AddPermanentToPile(a Permanent) {
- p.pile = append(p.pile, a)
+func (p *PermanentBase) IsAvailableTile(tile *Tile) bool {
+ return tile.IsAvailableForCard(p.card)
}
+func (p *PermanentBase) setContainingPerm(containing Permanent) { p.containingPerm = containing }
+func (p *PermanentBase) addPermanentToPile(a Permanent) { p.pile = append(p.pile, a) }
+func (p *PermanentBase) clearPile() { p.pile = nil }
+
func (p *PermanentBase) RemovePermanentFromPile(rm Permanent) {
for i, piled := range p.pile {
if piled != rm {
@@ -161,21 +164,45 @@ func (p *PermanentBase) RemovePermanentFromPile(rm Permanent) {
return
}
}
-func (b *PermanentBase) onPile(p Permanent) { b.Card().Impl.onPile(p) }
-func (b *PermanentBase) onUnpile(p Permanent) { b.Card().Impl.onUnpile(p) }
-func (b *PermanentBase) onDrop(p Permanent) {
- b.onUnpile(p)
- b.Card().Impl.onDrop(p)
+
+func (b *PermanentBase) onPile(containing Permanent) { b.Card().Impl.onPile(containing) }
+func (b *PermanentBase) onUnpile(containing Permanent) { b.Card().Impl.onUnpile(containing) }
+func (b *PermanentBase) onDrop(dropped Permanent) {
+ b.onUnpile(dropped.ContainingPerm())
+ b.Card().Impl.onDrop(dropped)
}
func AddPermanentToPile(containing, p Permanent) {
- p.SetContainingPerm(containing)
- containing.AddPermanentToPile(p)
+ p.setContainingPerm(containing)
+ containing.addPermanentToPile(p)
p.onPile(containing)
}
func RemovePermanentFromPile(containing, rm Permanent) {
containing.RemovePermanentFromPile(rm)
rm.onUnpile(containing)
- rm.SetContainingPerm(nil)
+ rm.setContainingPerm(nil)
+}
+
+func DropPile(containing Permanent) {
+ for _, p := range containing.Pile() {
+ p.onDrop(p)
+ p.setContainingPerm(nil)
+ }
+
+ // TODO: reset the containing's pile
+}
+
+func enterTile(p Permanent, t *Tile) {
+ p.SetTile(t)
+ p.Card().Impl.onEntering(t)
+ t.entering(p)
+}
+
+func leaveTile(p Permanent) {
+ if p.Tile() != nil {
+ p.Card().Impl.onLeaving(p.Tile())
+ p.Tile().leaving(p)
+ p.SetTile(nil)
+ }
}
diff --git a/go/game/player.go b/go/game/player.go
index b1cb6916..cb89e1ae 100644
--- a/go/game/player.go
+++ b/go/game/player.go
@@ -92,6 +92,12 @@ func (p *Player) BuyPhase() {
p.gameState.Stack.Resolve()
}
+func (p *Player) discardHand() {
+ for _, c := range p.Hand.Cards() {
+ p.DiscardCard(c)
+ }
+}
+
func (p *Player) DiscardCard(c *Card) {
p.Hand.MoveCard(c, p.DiscardPile)
}
diff --git a/go/game/state.go b/go/game/state.go
index 87e2133a..5a4714f7 100644
--- a/go/game/state.go
+++ b/go/game/state.go
@@ -24,7 +24,6 @@ type State struct {
ActivePhase PhaseType
Permanents []Permanent
Units []*Unit
- Artifacts []*Artifact
Rand *rand.Rand
}
@@ -97,9 +96,11 @@ func (s *State) Loop() []*Player {
}
func (s *State) stateBasedActions() {
- for _, u := range s.Units {
- if u.IsDestroyed() {
- s.DestroyPermanent(u)
+ for _, p := range s.Permanents {
+ p.Card().Impl.stateBasedActions(s, p)
+
+ if p.IsDestroyed() {
+ s.DestroyPermanent(p)
}
}
}
@@ -288,8 +289,10 @@ func (s *State) AddPermanent(perm Permanent) {
switch p := perm.(type) {
case *Unit:
s.Units = append(s.Units, p)
- case *Artifact:
- s.Artifacts = append(s.Artifacts, p)
+ }
+
+ if perm.Tile() != nil {
+ enterTile(perm, perm.Tile())
}
}
@@ -301,6 +304,10 @@ func (s *State) AddNewArtifact(card *Card, pos Position, owner *Player) {
s.AddPermanent(NewArtifact(card, s.Map.TileAt(pos), owner))
}
+func (s *State) addNewEquipment(card *Card, pos Position, owner *Player) {
+ s.AddPermanent(newEquipment(card, s.Map.TileAt(pos), owner))
+}
+
func (s *State) AddNewPermFromPath(cardPath string, pos Position, owner *Player) {
card := NewCard(cardPath)
switch card.Type {
@@ -320,7 +327,7 @@ func (s *State) removePermanent(p Permanent) {
}
}
-func removePtr[P *Unit | *Artifact](collection []P, ptr P) []P {
+func removePtr[P *Unit](collection []P, ptr P) []P {
for i, p := range collection {
if p == ptr {
collection[i] = collection[len(collection)-1]
@@ -331,13 +338,12 @@ func removePtr[P *Unit | *Artifact](collection []P, ptr P) []P {
}
func (s *State) DestroyPermanent(p Permanent) {
+ leaveTile(p)
s.removePermanent(p)
switch p := p.(type) {
case *Unit:
s.Units = removePtr(s.Units, p)
- case *Artifact:
- s.Artifacts = removePtr(s.Artifacts, p)
}
p.Owner().DiscardPile.AddCard(p.Card())
}
@@ -381,6 +387,10 @@ func (s *State) resolvePlay(p *Player, c *Card, targets *Targets) {
case CardTypes.Artifact:
tile := targets.sel[0].(*Tile)
s.AddNewArtifact(c, tile.Position, p)
+ case CardTypes.Equipment:
+ if tile, ok := targets.sel[0].(*Tile); ok {
+ s.addNewEquipment(c, tile.Position, p)
+ }
case CardTypes.Spell:
c.Impl.onPlay(s, p, targets)
default:
diff --git a/go/game/tile.go b/go/game/tile.go
index 3f9f9f5f..510497f8 100644
--- a/go/game/tile.go
+++ b/go/game/tile.go
@@ -77,6 +77,7 @@ type Tile struct {
Type TileType
Water bool
Raw string
+ effects []areaEffect
}
func (t *Tile) String() string {
@@ -84,7 +85,7 @@ func (t *Tile) String() string {
}
func INVALID_TILE() Tile {
- return Tile{INVALID_POSITION(), nil, 0, false, ""}
+ return Tile{Position: INVALID_POSITION()}
}
func NewTileFromString(t string, pos Position) (Tile, error) {
@@ -97,7 +98,7 @@ func NewTileFromString(t string, pos Position) (Tile, error) {
}
}
water := strings.Contains(tile, "water")
- return Tile{pos, nil, tileType, water, t}, nil
+ return Tile{Position: pos, Type: tileType, Water: water, Raw: t}, nil
}
func (t *Tile) IsFree() bool {
@@ -135,3 +136,36 @@ func (t *Tile) OnDiagonal(other *Tile) bool {
p1.X == p2.X+1 && p1.Y == p2.Y+1 ||
p1.X == p2.X-1 && p1.Y == p2.Y+1
}
+
+func (t *Tile) entering(p Permanent) {
+ t.Permanent = p
+ for _, effect := range t.effects {
+ effect.onEntering(p)
+ }
+}
+
+func (t *Tile) leaving(p Permanent) {
+ t.Permanent = nil
+ for _, effect := range t.effects {
+ effect.onLeaving(p)
+ }
+}
+
+func (t *Tile) addEffect(effect areaEffect) {
+ t.effects = append(t.effects, effect)
+}
+
+func (t *Tile) removeEffect(effect areaEffect) {
+ for i, e := range t.effects {
+ if e == effect {
+ t.effects[i] = t.effects[len(t.effects)-1]
+ t.effects = t.effects[:len(t.effects)-1]
+ break
+ }
+ }
+}
+
+func (t *Tile) neutralize() {
+ t.Type = TileTypes.Neutral
+ t.Water = false
+}
diff --git a/go/game/unit.go b/go/game/unit.go
index d38d156d..a2934296 100644
--- a/go/game/unit.go
+++ b/go/game/unit.go
@@ -4,53 +4,6 @@ import (
"log"
)
-type UnitMark int
-
-const (
- paralysis UnitMark = iota + 1
- poison
- panic_
- rage
- ward
- faith
-)
-
-func (mark UnitMark) String() string {
- switch mark {
- case paralysis:
- return "paralysis"
- case poison:
- return "poison"
- case panic_:
- return "panic"
- case rage:
- return "rage"
- case ward:
- return "ward"
- case faith:
- return "faith"
- default:
- log.Fatalf("Unhandled unit mark %d", mark)
- return ""
- }
-}
-
-var UnitMarks = struct {
- Paralysis UnitMark
- Poison UnitMark
- Panic UnitMark
- Rage UnitMark
- Ward UnitMark
- Faith UnitMark
-}{
- Paralysis: paralysis,
- Poison: poison,
- Panic: panic_,
- Rage: rage,
- Ward: ward,
- Faith: faith,
-}
-
const (
DEFAULT_AVAIL_MOVE_ACTIONS = 1
DEFAULT_AVAIL_ATTACK_ACTIONS = 1
@@ -64,7 +17,6 @@ type Unit struct {
Upkeep int
FullActions []*FullAction
FreeActions []*FreeAction
- States map[UnitMark]int
// Effects []Effects
AvailMoveActions int
additionalMoveActions int
@@ -97,7 +49,6 @@ func NewUnit(card *Card, tile *Tile, owner *Player) *Unit {
Movement: movement,
Attack: attack,
Upkeep: upkeep,
- States: make(map[UnitMark]int),
AvailMoveActions: DEFAULT_AVAIL_MOVE_ACTIONS,
AvailAttackActions: DEFAULT_AVAIL_ATTACK_ACTIONS,
}
@@ -107,7 +58,6 @@ func NewUnit(card *Card, tile *Tile, owner *Player) *Unit {
u.FullActions = card.Impl.fullActions(u)
u.FreeActions = card.Impl.freeActions(u)
- tile.Permanent = u
return u
}
@@ -121,25 +71,23 @@ func (u *Unit) resetBaseActions() {
}
func (u *Unit) onUpkeep() {
+ // TODO: handle state marks
u.resetBaseActions()
}
func (u *Unit) move(s *State, tile *Tile) {
if !u.IsAvailableTile(tile) {
- log.Fatalf("moveing to not available tile %v", tile)
+ log.Fatalf("moving to not available tile %v", tile)
}
- if u.tile != nil {
- u.tile.Permanent = nil
- } else if u.containingPerm != nil {
+ if u.containingPerm != nil {
RemovePermanentFromPile(u.containingPerm, u)
} else {
- log.Fatalf("unit has no current location")
+ leaveTile(u)
}
if tile.Permanent == nil {
- tile.Permanent = u
- u.tile = tile
+ enterTile(u, tile)
} else { // crew permanent at tile
AddPermanentToPile(tile.Permanent, u)
u.tile = nil
diff --git a/go/game/unit_test.go b/go/game/unit_test.go
index 0f16176c..53b585d8 100644
--- a/go/game/unit_test.go
+++ b/go/game/unit_test.go
@@ -20,9 +20,15 @@ symbols:
s := NewState()
r := strings.NewReader(mapDef)
s.Map, _ = readMap(r)
+
f := NewUnit(NewCard("nautics/fisher"), s.Map.TileAt(Position{2, 2}), nil)
+ s.AddPermanent(f)
+
a := NewUnit(NewCard("base/archer"), s.Map.TileAt(Position{1, 1}), nil)
+ s.AddPermanent(a)
+
sword := newEquipmentFromPath("base/sword", s.Map.TileAt(Position{0, 1}), nil)
+ s.AddPermanent(sword)
a.move(s, f.Tile())
if len(f.Pile()) != 1 {