aboutsummaryrefslogtreecommitdiff
path: root/go/game
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-02-08 17:50:10 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2025-08-20 15:50:10 +0200
commit45df9bf319d38db916ca983b8c92bb912f5ce9ee (patch)
treed7966002dad4a20bc3ef385bd152554fe278a6df /go/game
parenteb10f6f4940ecd09fd7d881fe5e1735ff146f919 (diff)
downloadmuhqs-game-45df9bf319d38db916ca983b8c92bb912f5ce9ee.tar.gz
muhqs-game-45df9bf319d38db916ca983b8c92bb912f5ce9ee.zip
intermediate commit
Diffstat (limited to 'go/game')
-rw-r--r--go/game/action.go194
-rw-r--r--go/game/ai.go52
-rw-r--r--go/game/areaEffect.go24
-rw-r--r--go/game/cardImplementations.go61
-rw-r--r--go/game/deck.go4
-rw-r--r--go/game/kraken.go23
-rw-r--r--go/game/map.go8
-rw-r--r--go/game/marks.go4
-rw-r--r--go/game/permanent.go6
-rw-r--r--go/game/player.go76
-rw-r--r--go/game/playerControl.go97
-rw-r--r--go/game/stack.go7
-rw-r--r--go/game/state.go48
-rw-r--r--go/game/targets.go220
-rw-r--r--go/game/tile.go37
-rw-r--r--go/game/unit.go37
16 files changed, 591 insertions, 307 deletions
diff --git a/go/game/action.go b/go/game/action.go
index 5002f58a..77e9743f 100644
--- a/go/game/action.go
+++ b/go/game/action.go
@@ -13,6 +13,7 @@ type (
type Action interface {
Source() interface{}
Targets() *Targets
+ Target() *Target // Shortcut for to select the first target
CheckTargets(*State) error
PayCosts(s *State) bool
@@ -20,35 +21,40 @@ type Action interface {
String() string
}
-type PassPriority struct{ source *Player }
+type PassPriority struct{ player *Player }
-func (a *PassPriority) Source() interface{} { return a.source }
+func (a *PassPriority) Source() interface{} { return a.player }
func (*PassPriority) Targets() *Targets { return nil }
+func (*PassPriority) Target() *Target { return nil }
func (*PassPriority) CheckTargets(*State) error { return nil }
func (*PassPriority) PayCosts(*State) bool { return true }
func (*PassPriority) Resolve(*State) {}
func (*PassPriority) String() string { return "pass" }
func NewPassPriority(p *Player) Action { return &PassPriority{p} }
-type HandCardSelection struct{ cards []*Card }
-
-func (*HandCardSelection) Source() interface{} { return nil }
-func (*HandCardSelection) Targets() *Targets { return nil }
-func (*HandCardSelection) CheckTargets(*State) error { return nil }
-func (*HandCardSelection) PayCosts(*State) bool { return true }
-func (*HandCardSelection) Resolve(*State) {}
-func (*HandCardSelection) String() string { return "hand card selection" }
-func NewHandCardSelection(cards []*Card) Action { return &HandCardSelection{cards} }
-
-type TargetSelection struct{ targets *Targets }
+type TargetSelection struct {
+ player *Player
+ targets *Targets
+}
-func (*TargetSelection) Source() interface{} { return nil }
+func (sel *TargetSelection) Source() interface{} { return sel.player }
func (sel *TargetSelection) Targets() *Targets { return sel.targets }
+func (sel *TargetSelection) Target() *Target { return sel.targets.ts[0] }
func (sel *TargetSelection) CheckTargets(s *State) error { return sel.targets.CheckTargets(s) }
func (*TargetSelection) PayCosts(*State) bool { return true }
func (*TargetSelection) Resolve(*State) {}
func (*TargetSelection) String() string { return "target selection" }
-func NewTargetSelection(targets *Targets) Action { return &TargetSelection{targets} }
+
+func newTargetSelection(player *Player, targets *Targets) Action {
+ return &TargetSelection{player, targets}
+}
+
+func newHandCardSelection(p *Player, min, max int) Action {
+ a := &TargetSelection{player: p}
+ targetDesc := TargetDesc{"hand card", fmt.Sprintf("%d-%d", min, max)}
+ a.targets = newTargets(newTarget(p.gameState, targetDesc, a))
+ return a
+}
type ActionBase struct {
source interface{}
@@ -58,23 +64,15 @@ type ActionBase struct {
costFunc ActionCostFunc
}
-func (a *ActionBase) Source() interface{} {
- return a.source
-}
-
-func (a *ActionBase) Resolve(s *State) {
- a.resolveFunc(s)
-}
-
-func (a *ActionBase) PayCosts(s *State) bool {
- return a.costFunc(s)
-}
-
-func (a *ActionBase) Targets() *Targets {
- return a.targets
-}
-
+func (a *ActionBase) Source() interface{} { return a.source }
+func (a *ActionBase) Resolve(s *State) { a.resolveFunc(s) }
+func (a *ActionBase) PayCosts(s *State) bool { return a.costFunc(s) }
+func (a *ActionBase) Targets() *Targets { return a.targets }
+func (a *ActionBase) Target() *Target { return a.targets.ts[0] }
func (a *ActionBase) CheckTargets(s *State) error {
+ if a.targets == nil {
+ return nil
+ }
return a.targets.CheckTargets(s)
}
@@ -82,7 +80,7 @@ type PlayAction struct {
ActionBase
}
-const permanentPlayActionTarget = "available spawn tile"
+var permanentPlayActionTarget = TargetDesc{"available spawn tile", "1"}
func NewPlayAction(p *Player, c *Card, args ...interface{}) *PlayAction {
a := &PlayAction{ActionBase{
@@ -110,9 +108,11 @@ func NewPlayAction(p *Player, c *Card, args ...interface{}) *PlayAction {
s := p.gameState
if c.IsPermanent() {
- a.targets = newTargets(s, []string{permanentPlayActionTarget}, a)
+ a.targets = newTargets(newTarget(s, permanentPlayActionTarget, a))
} else {
- a.targets = newTargets(s, []string{}, a)
+ // TODO: implement parsing targets to play a card
+ // a.targets = newTargets(s, c.Impl.playTargets(s, p), a)
+ a.targets = newTargets()
}
a.resolveFunc = func(s *State) {
@@ -124,7 +124,7 @@ func NewPlayAction(p *Player, c *Card, args ...interface{}) *PlayAction {
func (a *PlayAction) String() string {
p := a.source.(*Player)
- if a.targets.RequireTargets() {
+ if a.targets.RequireSelection() {
return fmt.Sprintf(" %s play %s", p.Name, a.Card.Name)
}
@@ -135,14 +135,15 @@ type MoveAction struct {
ActionBase
}
-const moveActionTargetDesc = "available tile"
+var moveActionTargetDesc = TargetDesc{"available tile", "1"}
func (a *MoveAction) targetTile() *Tile {
- if t, ok := a.targets.sel[0].(*Tile); ok {
- return t
+ t := a.Target()
+ if tile, ok := t.sel[0].(*Tile); ok {
+ return tile
}
- return a.targets.sel[0].(*Unit).Tile()
+ return t.sel[0].(*Unit).Tile()
}
func NewMoveAction(u *Unit) *MoveAction {
@@ -153,7 +154,7 @@ func NewMoveAction(u *Unit) *MoveAction {
},
}
- a.targets = newTargets(u.Controller().gameState, []string{moveActionTargetDesc}, a)
+ a.targets = newTargets(newTarget(u.Controller().gameState, moveActionTargetDesc, a))
a.resolveFunc = func(s *State) {
tile := a.targetTile()
@@ -173,19 +174,19 @@ func NewMoveAction(u *Unit) *MoveAction {
func (a *MoveAction) String() string {
u := a.source.(*Unit)
- if a.targets.RequireTargets() {
+ if a.targets.RequireSelection() {
return fmt.Sprintf("move %s", u.Movement.String())
}
t := a.targetTile()
- return fmt.Sprintf("%v: %v -> %v", u.Controller().Name, u.Card().Name, t.Position)
+ return fmt.Sprintf("%s -> %v", u.FmtWController(), t.Position)
}
type AttackAction struct {
ActionBase
}
-const attackActionTargetDesc = "attackable enemy permanent"
+var attackActionTargetDesc = TargetDesc{"attackable enemy permanent", "1"}
func NewAttackAction(u *Unit) *AttackAction {
a := &AttackAction{ActionBase{
@@ -193,10 +194,10 @@ func NewAttackAction(u *Unit) *AttackAction {
Card: u.Card(),
}}
- a.targets = newTargets(u.Controller().gameState, []string{attackActionTargetDesc}, a)
+ a.targets = newTargets(newTarget(u.Controller().gameState, attackActionTargetDesc, a))
a.resolveFunc = func(s *State) {
- s.Fight(u, a.targets.sel[0].(Permanent))
+ s.Fight(u, a.Target().sel[0].(Permanent))
}
a.costFunc = func(*State) bool {
@@ -212,13 +213,12 @@ func NewAttackAction(u *Unit) *AttackAction {
func (a *AttackAction) String() string {
u := a.source.(*Unit)
- if a.targets.RequireTargets() {
+ if a.targets.RequireSelection() {
return fmt.Sprintf("attack %v", u.Attack)
}
- target := a.targets.sel[0].(Permanent)
- return fmt.Sprintf("%v: %v x %v",
- u.Controller().Name, u.Card().Name, target.Tile().Position)
+ target := a.Target().sel[0].(Permanent)
+ return fmt.Sprintf("%s x %v", u.FmtWController(), target.Tile().Position)
}
type FullAction struct {
@@ -255,11 +255,11 @@ func NewFullAction(u *Unit, proto ActionFuncPrototype, desc string) *FullAction
func (a *FullAction) String() string {
u := a.source.(*Unit)
- if a.targets.RequireTargets() {
+ if a.targets == nil || a.targets.RequireSelection() {
return fmt.Sprintf("↻ %s", a.desc)
}
- return fmt.Sprintf("%s: %s ↻@%v", u.Controller().Name, u.Card().Name, a.targets)
+ return fmt.Sprintf("%s ↻@%v", u.FmtWController(), a.targets)
}
type FreeAction struct {
@@ -289,80 +289,104 @@ func NewFreeAction(p Permanent, resolveProto ActionFuncPrototype, costFunc Actio
func (a *FreeAction) String() string {
p := a.source.(Permanent)
- if a.targets.RequireTargets() {
+ if a.targets.RequireSelection() {
return fmt.Sprintf("free_action %s", a.desc)
}
return fmt.Sprintf("%v: %s free_action@%v", p.Controller(), p.Card().Name, a.targets)
}
-type BuyAction struct {
- player *Player
- card *Card
-}
+var buyActionTargetDesc = TargetDesc{"store card", "?"}
-func (a *BuyAction) Source() interface{} {
- return a.player
-}
+type BuyAction struct{ TargetSelection }
-func (a *BuyAction) Targets() *Targets {
- return newTargetsWithSel(a.player.gameState, []string{"store cards"}, a, []interface{}{a.card})
+func (a *BuyAction) card() *Card {
+ return a.Target().sel[0].(*Card)
}
func (a *BuyAction) CheckTargets(s *State) error {
+ err := a.targets.CheckTargets(s)
+ if err != nil {
+ return err
+ }
return s.isValidBuy(a)
}
func (a *BuyAction) Resolve(s *State) {
- a.player.Store.MoveCard(a.card, a.player.DiscardPile)
+ a.player.Store.MoveCard(a.card(), a.player.DiscardPile)
}
func (a *BuyAction) PayCosts(*State) bool {
- if a.card.BuyCost < 0 || a.card.BuyCost > a.player.Resource {
+ cost := a.card().BuyCost
+ if cost < 0 || cost > a.player.Resource {
return false
}
- a.player.Resource -= a.card.BuyCost
+ a.player.Resource -= cost
return true
}
-func (a *BuyAction) String() string { return fmt.Sprintf("%s buy %s", a.player.Name, a.card.Name) }
+func (a *BuyAction) String() string {
+ if !a.Targets().HasSelections() {
+ return fmt.Sprintf("%s buy", a.player.Name)
+ }
-func NewBuyAction(p *Player, c *Card) *BuyAction {
- return &BuyAction{p, c}
+ return fmt.Sprintf("%s buy %s", a.player.Name, a.card().Name)
}
-type UpkeepAction struct {
- player *Player
- unit *Unit
- pay bool
+func newBuyAction(p *Player) *BuyAction {
+ a := &BuyAction{TargetSelection{player: p}}
+ a.targets = newTargets(newTarget(p.gameState, buyActionTargetDesc, a))
+ return a
}
-func (a *UpkeepAction) Resolve(s *State) {
- if a.pay {
- a.player.Resource -= a.unit.Upkeep
- } else {
- s.DestroyPermanent(a.unit)
- }
+var upkeepActionTargetDesc = TargetDesc{"unit you controll", "*"}
+
+type UpkeepAction struct {
+ TargetSelection
}
func (a *UpkeepAction) PayCosts(*State) bool {
- if a.unit.Upkeep > a.player.Resource {
+ costs := 0
+ for _, t := range a.Target().sel {
+ u := t.(*Unit)
+ costs += u.UpkeepCost()
+ }
+
+ if a.player.Resource < costs {
return false
}
- a.player.Resource -= a.unit.Upkeep
+ a.player.Resource -= costs
return true
}
-func (a *UpkeepAction) String() string {
- if a.pay {
- return fmt.Sprintf("%s keep %v", a.player.Name, a.unit.Tile().Position)
+func (a *UpkeepAction) Resolve(*State) {
+ p := a.player
+ s := p.gameState
+ for _, i := range a.Target().sel {
+ u := i.(*Unit)
+ s.DestroyPermanent(u)
}
- return fmt.Sprintf("%s disband %v", a.player.Name, a.unit.Tile().Position)
+ // Keep and pay for the rest
+ for _, u := range s.Units {
+ if u.Controller() != p {
+ continue
+ }
+
+ p.Resource -= u.upkeep
+ u.onUpkeep()
+ }
}
-func NewUpkeepAction(p *Player, u *Unit, pay bool) *UpkeepAction {
- return &UpkeepAction{p, u, pay}
+func (a *UpkeepAction) String() string {
+ return fmt.Sprintf("upkeep disbanding: %v", a.Target().sel)
+}
+
+func newUpkeepAction(p *Player) *UpkeepAction {
+ a := &UpkeepAction{TargetSelection{player: p}}
+ a.targets = newTargets(newTarget(p.gameState, upkeepActionTargetDesc, a))
+
+ return a
}
diff --git a/go/game/ai.go b/go/game/ai.go
index daeeff4f..9b48c2f5 100644
--- a/go/game/ai.go
+++ b/go/game/ai.go
@@ -3,6 +3,7 @@ package game
import (
"fmt"
"log"
+ "math/rand"
"strconv"
"strings"
"sync"
@@ -77,6 +78,24 @@ func NewUnitAI(s *State, u *Unit) *UnitAI {
return ai
}
+func selectRandomTargets(rand *rand.Rand, targets *Targets) error {
+ for _, t := range targets.ts {
+ for t.RequireSelection() {
+ options := t.Options()
+ if len(options) == 0 {
+ return fmt.Errorf("No possible selection")
+ }
+
+ idx := 0
+ if len(options) > 1 {
+ idx = rand.Intn(len(options) - 1)
+ }
+ _ = t.AddSelection(options[idx])
+ }
+ }
+ return nil
+}
+
func (m *Map) generateMapGraphFor(u *Unit) *dijkstra.Graph {
graph := dijkstra.NewGraph()
for _, t := range m.AllTiles() {
@@ -143,17 +162,11 @@ func moveToRandomTile(ai *UnitAI) Action {
}
a := NewMoveAction(ai.u)
- options := a.Targets().NextOptions()
- if len(options) == 0 {
+ err := selectRandomTargets(ai.s.Rand, a.Targets())
+ if err != nil {
return nil
}
- idx := 0
- if len(options) > 1 {
- idx = ai.s.Rand.Intn(len(options) - 1)
- }
- a.Targets().AddTarget(options[idx])
-
return a
}
@@ -198,7 +211,7 @@ func moveTowardsNearestEnemyUnit(ai *UnitAI) Action {
}
a := NewMoveAction(ai.u)
- a.Targets().AddTarget(ai.s.Map.TileAt(target))
+ _ = a.Target().AddSelection(ai.s.Map.TileAt(target))
return a
}
@@ -208,16 +221,10 @@ func attackAttackableEnemyPerm(ai *UnitAI) Action {
}
a := NewAttackAction(ai.u)
- options := a.Targets().NextOptions()
- if len(options) == 0 {
+ err := selectRandomTargets(ai.s.Rand, a.Targets())
+ if err != nil {
return nil
}
-
- idx := 0
- if len(options) > 1 {
- idx = ai.s.Rand.Intn(len(options) - 1)
- }
- a.Targets().AddTarget(options[idx])
return a
}
@@ -281,15 +288,10 @@ out:
}
a := fullActions[idx]
- for options := a.Targets().NextOptions(); options != nil; options = a.Targets().NextOptions() {
- idx = 0
- if len(options) > 1 {
- idx = ai.s.Rand.Intn(len(options) - 1)
- }
- a.Targets().AddTarget(options[idx])
+ err := selectRandomTargets(ai.s.Rand, a.Targets())
+ if err == nil {
+ ai.actions <- a
}
-
- ai.actions <- a
}
// 2.
diff --git a/go/game/areaEffect.go b/go/game/areaEffect.go
index 8991bc3c..4cba29e9 100644
--- a/go/game/areaEffect.go
+++ b/go/game/areaEffect.go
@@ -25,3 +25,27 @@ func (e *dynamicAreaEffect) onLeaving(p Permanent) {
e._onLeaving(p)
}
}
+
+func newGrantFullActionEffect(cardPath string, f ActionFuncPrototype, desc, tag string,
+) *dynamicAreaEffect {
+ onEntering := func(p Permanent) {
+ if p.Card().Path() != cardPath {
+ return
+ }
+
+ u := p.(*Unit)
+ fa := NewFullAction(u, f, desc)
+ fa.tag = tag
+ u.FullActions = append(u.FullActions, fa)
+ }
+
+ onLeaving := func(p Permanent) {
+ if p.Card().Path() != cardPath {
+ return
+ }
+ u := p.(*Unit)
+ u.removeFullAction(tag)
+ }
+
+ return newDynamicAreaEffect(onEntering, onLeaving)
+}
diff --git a/go/game/cardImplementations.go b/go/game/cardImplementations.go
index 3a7a729a..038ce2d6 100644
--- a/go/game/cardImplementations.go
+++ b/go/game/cardImplementations.go
@@ -34,15 +34,18 @@ type missionaryImpl struct{ cardImplementationBase }
func (*missionaryImpl) fullActions(u *Unit) []*FullAction {
resolvePrototype := func(a Action) ActionResolveFunc {
u := a.Source().(*Unit)
- target := a.Targets().sel[0].(*Unit)
+ target := a.Target().sel[0].(*Unit)
return func(s *State) {
- target.addMarks(UnitMarks.Faith, 2)
+ target.adjustMarks(UnitMarks.Faith, 2)
if target.Marks(UnitMarks.Faith) > target.Card().BuyCost {
target.controller = u.Controller()
}
}
}
- return []*FullAction{NewFullAction(u, resolvePrototype, "put faith ...")}
+ s := u.Controller().gameState
+ a := NewFullAction(u, resolvePrototype, "put faith ...")
+ a.targets = newTargets(newTarget(s, newTargetDesc("unit"), a))
+ return []*FullAction{a}
}
type swordImpl struct{ cardImplementationBase }
@@ -63,59 +66,33 @@ func (*misinformationImpl) onPlay(s *State, c *Card, _ *Player, _ *Targets) {
// ====== Nautics Set ======
-type fishTrapAoE struct{}
-
-func (*fishTrapAoE) onEntering(p Permanent) {
- if p.Card().Name != "nautics/fisher" {
- return
- }
-
- fisher := p.(*Unit)
- fishTrapAction := func(a Action) ActionResolveFunc {
+var fishTrapAoE areaEffect = newGrantFullActionEffect("nautics/fisher",
+ func(a Action) ActionResolveFunc {
u := a.Source().(*Unit)
return func(s *State) {
controller := u.Controller()
controller.gainResource(2)
}
- }
-
- fa := NewFullAction(fisher, fishTrapAction, "gain 2 resource")
- fa.tag = "fishTrapAction"
- fisher.FullActions = append(fisher.FullActions, fa)
-}
-
-func (*fishTrapAoE) onLeaving(p Permanent) {
- if p.Card().Name != "nautics/fisher" {
- return
- }
-
- fisher := p.(*Unit)
- for i, fa := range fisher.FullActions {
- if fa.tag != "fishTrapAction" {
- continue
- }
- fisher.FullActions[i] = fisher.FullActions[len(fisher.FullActions)-1]
- fisher.FullActions = fisher.FullActions[:len(fisher.FullActions)-1]
- break
- }
-}
+ }, "gain 2 resource", "fishTrapAction")
type fishTrapImpl struct {
cardImplementation
- aoe fishTrapAoE
+ aoe areaEffect
}
+func (*fishTrapImpl) stateBasedActions(s *State, p Permanent) {}
+
func (i *fishTrapImpl) onEntering(t *Tile) {
s := t.Permanent.Controller().gameState
for _, tile := range TilesInRange(s.Map, t.Permanent, 1) {
- tile.addEffect(&i.aoe)
+ tile.addEffect(i.aoe)
}
}
func (i *fishTrapImpl) onLeaving(t *Tile) {
s := t.Permanent.Controller().gameState
for _, tile := range TilesInRange(s.Map, t.Permanent, 1) {
- tile.removeEffect(&i.aoe)
+ tile.removeEffect(i.aoe)
}
}
@@ -126,12 +103,12 @@ func (*fisherImpl) fullActions(u *Unit) []*FullAction {
resolvePrototype := func(a Action) ActionResolveFunc {
u := a.Source().(*Unit)
return func(s *State) {
- t := a.Targets().sel[0].(*Tile)
+ t := a.Target().sel[0].(*Tile)
s.AddNewArtifact(NewCard("nautics/fish_trap"), t.Position, u.Controller())
}
}
a := NewFullAction(u, resolvePrototype, "create fish trap")
- a.targets = newTargets(s, []string{"adjacent water tile"}, a)
+ a.targets = newTargets(newTarget(s, newTargetDesc("adjacent free water tile"), a))
return []*FullAction{a}
}
@@ -164,7 +141,7 @@ func (*sailorImpl) onUnpile(containing Permanent) {
type tidesChangeImpl struct{ cardImplementation }
func (*tidesChangeImpl) onPlay(s *State, _ *Card, _ *Player, _ *Targets) {
- s.Map.redistributeStoreCards(s.Rand)
+ s.redistributeMapStoreCards()
}
type dejaVuImpl struct{ cardImplementation }
@@ -327,7 +304,7 @@ func (*unholyCannonballImpl) onPlay(s *State, _ *Card, kraken *Player, _ *Target
kraken.gainResource(5)
} else {
for _, u := range enemiesInRange2 {
- u.addMarks(UnitStates.Panic, 1)
+ u.adjustMarks(UnitStates.Panic, 1)
}
}
}
@@ -341,7 +318,7 @@ func init() {
"base/misinformation": &misinformationImpl{},
"nautics/captain": &captainImpl{},
- "nautics/fish_trap": &fishTrapImpl{},
+ "nautics/fish_trap": &fishTrapImpl{aoe: fishTrapAoE},
"nautics/fisher": &fisherImpl{},
"nautics/sailor": &sailorImpl{},
diff --git a/go/game/deck.go b/go/game/deck.go
index 569a952d..1f9539a9 100644
--- a/go/game/deck.go
+++ b/go/game/deck.go
@@ -59,6 +59,10 @@ func NewDeckFromCardPaths(cardPaths []string) *Deck {
func NewDeckFromDeckList(deckList string) *Deck {
cardPaths := []string{}
+
+ // trim last trailing newline
+ deckList = strings.Trim(deckList, "\n")
+
lines := strings.Split(deckList, "\n")
for _, line := range lines {
tokens := strings.SplitN(line, " ", 2)
diff --git a/go/game/kraken.go b/go/game/kraken.go
index d92d204b..97929814 100644
--- a/go/game/kraken.go
+++ b/go/game/kraken.go
@@ -76,6 +76,10 @@ func addKrakenControl(kraken *Player) {
}()
}
+func (c *KrakenControl) Player() *Player {
+ return c.kraken
+}
+
func (ctrl *KrakenControl) awaitGameStateSync() {
ctrl.syncGameState.Wait()
ctrl.syncGameState.Add(1)
@@ -93,7 +97,6 @@ func (ctrl *KrakenControl) krakenTurn() {
log.Fatalf("Ai turn out of sync %d != %d", ctrl.turn, kraken.Turn)
}
-turn:
for {
drawn := kraken.Deck.Draw(1)
if len(drawn) == 0 {
@@ -114,19 +117,11 @@ turn:
}
a := NewPlayAction(kraken, c, costFunc)
- for a.Targets().RequireTargets() {
- options := a.Targets().NextOptions()
- if len(options) == 0 {
- log.Println("No target available for", c.Name)
- kraken.DiscardPile.AddCard(c)
- continue turn
- }
-
- idx := 0
- if len(options) > 1 {
- idx = s.Rand.Intn(len(options) - 1)
- }
- a.Targets().AddTarget(options[idx])
+ err := selectRandomTargets(s.Rand, a.Targets())
+ if err != nil {
+ log.Println("No target available for", c.Name)
+ kraken.DiscardPile.AddCard(c)
+ continue
}
ctrl.actions <- a
diff --git a/go/game/map.go b/go/game/map.go
index b86debad..5ed1335e 100644
--- a/go/game/map.go
+++ b/go/game/map.go
@@ -290,14 +290,6 @@ func (m *Map) distributeStoreCards(cards PileOfCards, rand *rand.Rand) {
}
}
-func (m *Map) redistributeStoreCards(rand *rand.Rand) {
- poc := NewPileOfCards()
- for _, store := range m.Stores {
- store.MoveInto(poc)
- }
- m.distributeStoreCards(poc, rand)
-}
-
func (m *Map) HasStores() bool {
return len(m.Stores) > 0
}
diff --git a/go/game/marks.go b/go/game/marks.go
index a3ea0e32..0764b531 100644
--- a/go/game/marks.go
+++ b/go/game/marks.go
@@ -45,17 +45,19 @@ var (
Crack: crack,
}
- unitStates = []PermanentMark{paralysis, poison, panic_, rage}
+ // unitStates = []PermanentMark{paralysis, poison, panic_, rage}
UnitStates = struct {
Paralysis PermanentMark
Poison PermanentMark
Panic PermanentMark
Rage PermanentMark
+ Ward PermanentMark
}{
Paralysis: paralysis,
Poison: poison,
Panic: panic_,
Rage: rage,
+ Ward: ward,
}
UnitMarks = struct {
diff --git a/go/game/permanent.go b/go/game/permanent.go
index f25c95b3..ecc4459a 100644
--- a/go/game/permanent.go
+++ b/go/game/permanent.go
@@ -22,7 +22,7 @@ type Permanent interface {
HasEffect(string) bool
XEffect(string) (int, error)
Marks(PermanentMark) int
- addMarks(PermanentMark, int)
+ adjustMarks(PermanentMark, int)
String() string
Fight(p Permanent)
@@ -137,7 +137,7 @@ func (p *PermanentBase) Marks(mark PermanentMark) int {
return 0
}
-func (p *PermanentBase) addMarks(mark PermanentMark, amount int) {
+func (p *PermanentBase) adjustMarks(mark PermanentMark, amount int) {
if amount, found := p.marks[mark]; found {
p.marks[mark] += amount
} else {
@@ -195,8 +195,8 @@ func DropPile(containing Permanent) {
func enterTile(p Permanent, t *Tile) {
p.SetTile(t)
- p.Card().Impl.onEntering(t)
t.entering(p)
+ p.Card().Impl.onEntering(t)
}
func leaveTile(p Permanent) {
diff --git a/go/game/player.go b/go/game/player.go
index 78b4637e..a69f2cd3 100644
--- a/go/game/player.go
+++ b/go/game/player.go
@@ -3,6 +3,8 @@ package game
import (
"image/color"
"log"
+
+ "muhq.space/muhqs-game/go/utils"
)
const (
@@ -22,6 +24,7 @@ type Player struct {
gameState *State
Color color.Color
Ctrl PlayerControl
+ knownStores map[Position]bool
}
func NewPlayer(id int, name string, deck *Deck, gameState *State) *Player {
@@ -40,6 +43,7 @@ func NewPlayer(id int, name string, deck *Deck, gameState *State) *Player {
Store: store,
gameState: gameState,
Ctrl: nil,
+ knownStores: make(map[Position]bool),
}
return &p
}
@@ -62,7 +66,7 @@ func (p *Player) UpkeepCost() int {
if u.Controller() != p {
continue
}
- cost += u.Upkeep
+ cost += u.upkeep
}
return cost
}
@@ -76,15 +80,26 @@ func (p *Player) Upkeep() {
// TODO: handle upkeep triggers
- for _, unit := range p.gameState.Units {
- if unit.controller != p {
- continue
+ // Skip upkeep prompt if player does not controll any units
+ controllsUnits := false
+ for _, u := range p.gameState.Units {
+ if u.Controller() == p {
+ controllsUnits = true
+ break
}
+ }
+ if !controllsUnits {
+ return
+ }
+
+ a := prompt(p.Ctrl, newUpkeepPrompt(p))
- // TODO: let players decide if they want to pay the upkeep
- p.Resource -= unit.Upkeep
- unit.onUpkeep()
+ if _, ok := a.(*PassPriority); ok {
+ a = newUpkeepAction(p)
}
+
+ p.gameState.DeclareAction(a)
+ p.gameState.Stack.Resolve()
}
func (p *Player) ActionPhase() {
@@ -100,11 +115,15 @@ func (p *Player) ActionPhase() {
func (p *Player) BuyPhase() {
a := promptBuy(p.Ctrl)
+
if _, ok := a.(*PassPriority); ok {
return
}
- p.gameState.DeclareAction(a)
- p.gameState.Stack.Resolve()
+
+ if a.Targets().HasSelections() {
+ p.gameState.DeclareAction(a)
+ p.gameState.Stack.Resolve()
+ }
}
func (p *Player) discardHand() {
@@ -118,6 +137,10 @@ func (p *Player) DiscardCard(c *Card) {
}
func (p *Player) DiscardPhase() {
+ if p.Hand.Size() == 0 {
+ return
+ }
+
cards := p.PromptHandCardSelection(0, p.Hand.Size())
for _, c := range cards {
p.DiscardCard(c)
@@ -125,11 +148,15 @@ func (p *Player) DiscardPhase() {
}
func (p *Player) PromptHandCardSelection(min, max int) []*Card {
- p.Ctrl.SendNotification(NewHandCardSelectionPrompt(min, max))
+ p.Ctrl.SendNotification(newHandCardSelectionPrompt(p, min, max))
switch a := p.Ctrl.RecvAction().(type) {
- case *HandCardSelection:
- // TODO: validate cards
- return a.cards
+ case *TargetSelection:
+ err := a.CheckTargets(p.gameState)
+ if err != nil {
+ log.Fatalf("Invalid hand card selection: %v", err)
+ }
+
+ return utils.InterfaceSliceToTypedSlice[*Card](a.Target().sel)
case *PassPriority:
return nil
default:
@@ -156,3 +183,26 @@ func (p *Player) IsEnemy(other *Player) bool {
// TODO: support coop / teams
return p != other
}
+
+func (p *Player) AvailableStores() (stores []*Store) {
+ m := p.gameState.Map
+ for pos, store := range m.Stores {
+ t := m.TileAt(pos)
+ if t.Permanent != nil && t.Permanent.Controller() == p {
+ stores = append(stores, store)
+ }
+ }
+ return
+}
+
+func (p *Player) KnowsStore(pos Position) bool {
+ return p.knownStores[pos]
+}
+
+func (p *Player) addKnownStore(pos Position) {
+ p.knownStores[pos] = true
+}
+
+func (p *Player) clearKnownStore() {
+ p.knownStores = make(map[Position]bool)
+}
diff --git a/go/game/playerControl.go b/go/game/playerControl.go
index e62d91b3..eea6a19b 100644
--- a/go/game/playerControl.go
+++ b/go/game/playerControl.go
@@ -1,16 +1,35 @@
package game
+import (
+ "fmt"
+ "log"
+)
+
type PlayerNotificationType int
const (
- DeclaredActionNotification = iota
+ DeclaredActionNotification PlayerNotificationType = iota
ResolvedActionNotification
PriorityNotification
- UpkeepPrompt
- BuyPrompt
- HandCardSelectionPrompt
+ TargetSelectionPrompt
)
+func (n PlayerNotificationType) String() string {
+ switch n {
+ case DeclaredActionNotification:
+ return "DeclaredActionNotification"
+ case ResolvedActionNotification:
+ return "ResolvedActionNotification"
+ case PriorityNotification:
+ return "PriorityNotification"
+ case TargetSelectionPrompt:
+ return "TargetSelectionPrompt"
+ default:
+ log.Panicf("Unhandled notification %d", n)
+ return ""
+ }
+}
+
func (n PlayerNotification) IsPriorityNotification() bool {
return n.Notification == PriorityNotification
}
@@ -21,72 +40,92 @@ type PlayerNotification struct {
Error error
}
-func NewPriorityNotification() PlayerNotification {
+func (n *PlayerNotification) String() string {
+ if n.Error != nil {
+ return fmt.Sprintf("error %v: %v", n.Notification, n.Error)
+ }
+
+ return fmt.Sprintf("%v: %v", n.Notification, n.Context)
+}
+
+type TargetSelectionCtx struct {
+ Action Action
+ Prompt string
+}
+
+func newPriorityNotification() PlayerNotification {
return PlayerNotification{PriorityNotification, nil, nil}
}
-func NewDeclaredActionNotification(a Action, err error) PlayerNotification {
+func newDeclaredActionNotification(a Action, err error) PlayerNotification {
return PlayerNotification{DeclaredActionNotification, a, err}
}
-func NewResolvedActionNotification(a Action, err error) PlayerNotification {
+func newResolvedActionNotification(a Action, err error) PlayerNotification {
return PlayerNotification{ResolvedActionNotification, a, err}
}
-func NewUpkeepPrompt(u *Unit) PlayerNotification {
- return PlayerNotification{ResolvedActionNotification, u, nil}
+func newTargetSelectionPrompt(a Action, desc string) PlayerNotification {
+ return PlayerNotification{TargetSelectionPrompt, TargetSelectionCtx{a, desc}, nil}
}
-func NewBuyPrompt() PlayerNotification {
- return PlayerNotification{BuyPrompt, nil, nil}
+func newUpkeepPrompt(p *Player) PlayerNotification {
+ a := newUpkeepAction(p)
+ return newTargetSelectionPrompt(a, "Select units to disband")
}
-func NewHandCardSelectionPrompt(min, max int) PlayerNotification {
- return PlayerNotification{HandCardSelectionPrompt, []int{min, max}, nil}
+func newBuyPrompt(p *Player) PlayerNotification {
+ a := newBuyAction(p)
+ prompt := newTargetSelectionPrompt(a, "Select a card to buy")
+ return prompt
+}
+
+func newHandCardSelectionPrompt(p *Player, min, max int) PlayerNotification {
+ a := newHandCardSelection(p, min, max)
+ desc := fmt.Sprintf("Select between %d and %d hand cards", min, max)
+ return newTargetSelectionPrompt(a, desc)
}
type PlayerControl interface {
+ Player() *Player
RecvAction() Action
SendNotification(PlayerNotification)
}
-type DummyPlayerControl struct{ P *Player }
-
-func (c *DummyPlayerControl) RecvAction() Action { return NewPassPriority(c.P) }
-func (*DummyPlayerControl) SendNotification(PlayerNotification) {}
-
type ChanPlayerControl struct {
- Actions chan Action
- Notifications chan PlayerNotification
+ player *Player
+ actions chan Action
+ notifications chan PlayerNotification
}
-func (c *ChanPlayerControl) SendAction(a Action) { c.Actions <- a }
-func (c *ChanPlayerControl) RecvAction() Action { return <-c.Actions }
-func (c *ChanPlayerControl) SendNotification(n PlayerNotification) { c.Notifications <- n }
+func (c *ChanPlayerControl) Player() *Player { return c.player }
+func (c *ChanPlayerControl) SendAction(a Action) { c.actions <- a }
+func (c *ChanPlayerControl) RecvAction() Action { return <-c.actions }
+func (c *ChanPlayerControl) SendNotification(n PlayerNotification) { c.notifications <- n }
func (c *ChanPlayerControl) RecvNotification() *PlayerNotification {
select {
- case n := <-c.Notifications:
+ case n := <-c.notifications:
return &n
default:
return nil
}
}
-func NewChanPlayerControl() *ChanPlayerControl {
+func NewChanPlayerControl(p *Player) *ChanPlayerControl {
a := make(chan Action)
n := make(chan PlayerNotification)
- return &ChanPlayerControl{a, n}
+ return &ChanPlayerControl{p, a, n}
}
func prompt(ctrl PlayerControl, notification PlayerNotification) Action {
- ctrl.SendNotification(NewPriorityNotification())
+ ctrl.SendNotification(notification)
return ctrl.RecvAction()
}
func promptBuy(ctrl PlayerControl) Action {
- return prompt(ctrl, NewBuyPrompt())
+ return prompt(ctrl, newBuyPrompt(ctrl.Player()))
}
func promptAction(ctrl PlayerControl) Action {
- return prompt(ctrl, NewPriorityNotification())
+ return prompt(ctrl, newPriorityNotification())
}
diff --git a/go/game/stack.go b/go/game/stack.go
index cf0622ce..78a5ff41 100644
--- a/go/game/stack.go
+++ b/go/game/stack.go
@@ -33,9 +33,14 @@ func (s *Stack) pop() {
err := s.gameState.ValidateAction(a)
if err == nil {
a.Resolve(s.gameState)
+ // Some action may be reused like full actions of permanents.
+ // Reset their target selection to allow possible reuse.
+ if t := a.Targets(); t != nil {
+ t.ClearSelections()
+ }
}
log.Println("Resolved", a, err)
- s.gameState.broadcastNotification(NewResolvedActionNotification(a, err))
+ s.gameState.broadcastNotification(newResolvedActionNotification(a, err))
}
func (s *Stack) Resolve() {
diff --git a/go/game/state.go b/go/game/state.go
index 98ac9c31..5c2fd053 100644
--- a/go/game/state.go
+++ b/go/game/state.go
@@ -111,7 +111,7 @@ func (s *State) IsValidPlay(a *PlayAction) error {
}
if a.Card.IsPermanent() {
- spawn := a.Targets().sel[0].(*Tile)
+ spawn := a.Target().sel[0].(*Tile)
if !slices.Contains(s.AvailableSpawnTiles(a.Source().(*Player), a.Card), spawn) {
return fmt.Errorf("Spawn %v is not a valid spawn tile for %s",
spawn.Position.String(), a.Card.Name)
@@ -127,7 +127,7 @@ func (s *State) IsValidMove(a *MoveAction) error {
}
u := a.Source().(*Unit)
- tile := relaxedTileTarget(a, a.Targets().sel[0])
+ tile := relaxedTileTarget(a, a.Target().sel[0])
if !slices.Contains(u.MoveRangeTiles(s.Map), tile) {
return fmt.Errorf("Unit %s@%v can not move to %s",
@@ -152,7 +152,7 @@ func (s *State) IsValidAttack(a *AttackAction) error {
return fmt.Errorf("%s can not attack", unit.card.Name)
}
- target := a.Targets().sel[0].(Permanent)
+ target := a.Target().sel[0].(Permanent)
if !IsPositionInRange(unit.tile.Position, target.Tile().Position, unit.Attack.MaxRange()) {
return fmt.Errorf("Attack target on %s is not in attack range %d of %s on %s",
target.Tile().Position.String(), unit.Attack.MaxRange(), unit.card.Name,
@@ -163,16 +163,17 @@ func (s *State) IsValidAttack(a *AttackAction) error {
}
func (s *State) isValidBuy(a *BuyAction) error {
+ card := a.card()
if s.Map.HasStores() {
var storeTiles []*Tile
for pos, store := range s.Map.Stores {
- if slices.Contains(store.cards, a.card) {
+ if slices.Contains(store.cards, card) {
storeTiles = append(storeTiles, s.Map.TileAt(pos))
}
}
if storeTiles == nil {
- return fmt.Errorf("no store contains %s", a.card.Name)
+ return fmt.Errorf("no store contains %s", card.Name)
}
controlsUnitOnStore := false
@@ -183,11 +184,15 @@ func (s *State) isValidBuy(a *BuyAction) error {
}
if !controlsUnitOnStore {
return fmt.Errorf("%s' controls no unit on a store containing %s",
- a.player.Name, a.card.Name)
+ a.player.Name, card.Name)
}
- } else if !slices.Contains(a.player.Store.cards, a.card) {
- return fmt.Errorf("%s's store does not contain %s", a.player.Name, a.card.Name)
+ } else if !slices.Contains(a.player.Store.cards, card) {
+ return fmt.Errorf("%s's store does not contain %s", a.player.Name, card.Name)
+ }
+
+ if !a.card().IsBuyable() {
+ return fmt.Errorf("Card %s is not buyable", card.Name)
}
return nil
@@ -226,14 +231,11 @@ func (s *State) DeclareAction(a Action) {
err = a.CheckTargets(s)
if err != nil {
- switch a := a.(type) {
+ switch a.(type) {
case *BuyAction:
if s.ActivePhase != Phases.BuyPhase {
err = fmt.Errorf("Cards can only be bought during one's buy phase")
}
- if !a.card.IsBuyable() {
- err = fmt.Errorf("Card %s is not buyable", a.card.Name)
- }
case *FreeAction:
default:
if !s.Stack.IsEmpty() {
@@ -249,7 +251,7 @@ func (s *State) DeclareAction(a Action) {
s.pushAction(a)
}
- s.broadcastNotification(NewDeclaredActionNotification(a, err))
+ s.broadcastNotification(newDeclaredActionNotification(a, err))
}
func (s *State) AllPassing(skipFirst bool) bool {
@@ -260,7 +262,7 @@ func (s *State) AllPassing(skipFirst bool) bool {
}
for ; i < nPlayers; i++ {
p := s.Players[(i+s.ActivePlayer.Id-1)%nPlayers]
- p.Ctrl.SendNotification(NewPriorityNotification())
+ p.Ctrl.SendNotification(newPriorityNotification())
a := p.Ctrl.RecvAction()
switch a.(type) {
case *PassPriority:
@@ -385,13 +387,13 @@ func (s *State) EnemyUnits(player *Player) []*Unit {
func (s *State) resolvePlay(p *Player, c *Card, targets *Targets) {
switch c.Type {
case CardTypes.Unit:
- tile := targets.sel[0].(*Tile)
+ tile := targets.ts[0].sel[0].(*Tile)
s.AddNewUnit(c, tile.Position, p)
case CardTypes.Artifact:
- tile := targets.sel[0].(*Tile)
+ tile := targets.ts[0].sel[0].(*Tile)
s.AddNewArtifact(c, tile.Position, p)
case CardTypes.Equipment:
- if tile, ok := targets.sel[0].(*Tile); ok {
+ if tile, ok := targets.ts[0].sel[0].(*Tile); ok {
s.addNewEquipment(c, tile.Position, p)
}
case CardTypes.Spell:
@@ -469,3 +471,15 @@ func (s *State) AvailableSpawnTiles(p *Player, c *Card) []*Tile {
return tiles
}
+
+func (s *State) redistributeMapStoreCards() {
+ poc := NewPileOfCards()
+ for _, store := range s.Map.Stores {
+ store.MoveInto(poc)
+ }
+ s.Map.distributeStoreCards(poc, s.Rand)
+
+ for _, p := range s.Players {
+ p.clearKnownStore()
+ }
+}
diff --git a/go/game/targets.go b/go/game/targets.go
index eb429c8c..440d8922 100644
--- a/go/game/targets.go
+++ b/go/game/targets.go
@@ -12,17 +12,28 @@ import (
type (
TargetConstraintFunc func(interface{}) error
- TargetConstraint struct {
- desc string
- constraint TargetConstraintFunc
+
+ TargetDesc struct {
+ target, req string
}
-)
-type Targets struct {
- s *State
- constraints []*TargetConstraint
- sel []interface{}
-}
+ TargetRequirement struct {
+ min, max int
+ }
+
+ Target struct {
+ s *State
+ desc string
+ requirement TargetRequirement
+ constraint TargetConstraintFunc
+ sel []interface{}
+ }
+
+ Targets struct {
+ idx int
+ ts []*Target
+ }
+)
func fmtSel(sel interface{}) string {
switch t := sel.(type) {
@@ -33,9 +44,9 @@ func fmtSel(sel interface{}) string {
}
}
-func (t *Targets) String() string {
+func (t *Target) String() string {
if len(t.sel) == 0 {
- return "undecided targets"
+ return "undecided target"
}
if len(t.sel) == 1 {
@@ -49,68 +60,140 @@ func (t *Targets) String() string {
return s[:len(s)-1] + "]"
}
-func newTargetsWithSel(s *State, targets []string, action Action, sel []interface{}) *Targets {
- t := &Targets{
- s: s,
- sel: sel,
+func newTargetWithSel(s *State, desc TargetDesc, action Action, sel []interface{}) *Target {
+ t := &Target{
+ s: s,
+ desc: desc.target,
+ constraint: targetConstraint(desc.target, s, action),
+ requirement: desc.requirement(),
+ sel: sel,
}
- t.buildConstraints(targets, action)
return t
}
-func newTargets(s *State, targets []string, action Action) *Targets {
- return newTargetsWithSel(s, targets, action, make([]interface{}, 0, len(targets)))
+func newTarget(s *State, desc TargetDesc, action Action) *Target {
+ return newTargetWithSel(s, desc, action, []interface{}{})
}
-func (t *Targets) buildConstraints(targets []string, action Action) {
- for _, desc := range targets {
- c := &TargetConstraint{desc, targetConstraint(desc, t.s, action)}
- t.constraints = append(t.constraints, c)
+func newTargets(targets ...*Target) *Targets {
+ return &Targets{0, targets}
+}
+
+func newTargetDesc(desc string) TargetDesc {
+ return TargetDesc{desc, "1"}
+}
+
+func (d *TargetDesc) requirement() TargetRequirement {
+ switch d.req {
+ case "?":
+ return TargetRequirement{0, 1}
+ case "+":
+ return TargetRequirement{1, -1}
+ case "*":
+ return TargetRequirement{0, -1}
+ default:
+ if strings.Contains(d.req, "-") {
+ tokens := strings.SplitN(d.req, "-", 2)
+ min, err := strconv.Atoi(tokens[0])
+ if err != nil {
+ log.Fatalf("failed to parse target requirement %s: %v", d.req, err)
+ }
+ max, err := strconv.Atoi(tokens[1])
+ if err != nil {
+ log.Fatalf("failed to parse target requirement %s: %v", d.req, err)
+ }
+ return TargetRequirement{min, max}
+ }
+
+ x, err := strconv.Atoi(d.req)
+ if err != nil {
+ log.Fatalf("failed to parse target requirement %s: %v", d.req, err)
+ }
+ return TargetRequirement{x, x}
}
}
-func (t *Targets) RequireTargets() bool {
- return len(t.constraints) != len(t.sel)
+func (t *Target) AddSelection(sel interface{}) (err error) {
+ if err = t.constraint(sel); err == nil {
+ t.sel = append(t.sel, sel)
+ }
+ return
}
-func (t *Targets) Options() [][]interface{} {
- options := make([][]interface{}, 0, len(t.constraints))
- for _, c := range t.constraints {
- options = append(options, c.options(t.s))
+func (t *Target) RequireSelection() bool { return len(t.sel) < t.requirement.min }
+func (t *Target) HasSelection() bool { return len(t.sel) > 0 }
+func (t *Target) ClearSelection() { t.sel = []interface{}{} }
+
+func (t *Target) Options() (options []interface{}) {
+ candidates := targetCandidates(t.desc, t.s)
+
+ for _, candidate := range candidates {
+ if err := t.constraint(candidate); err == nil {
+ options = append(options, candidate)
+ }
}
return options
}
-func (t *Targets) NextOptions() []interface{} {
- if len(t.constraints) == 0 {
- return nil
+func (t *Target) CheckSelection(s *State) error {
+ if t.RequireSelection() {
+ return fmt.Errorf("Number of selected targets and required ones does not match")
+ }
+
+ if t.requirement.max != -1 && len(t.sel) > t.requirement.max {
+ return fmt.Errorf("To many selection %d (%d allowed)", len(t.sel), t.requirement.max)
+ }
+
+ for _, sel := range t.sel {
+ if err := t.constraint(sel); err != nil {
+ return fmt.Errorf("selected target %s does not fit its desciption %s: %s",
+ sel, t.desc, err)
+ }
}
- next := len(t.sel)
- return t.constraints[next].options(t.s)
+ return nil
}
-func (t *Targets) AddTarget(target interface{}) {
- t.sel = append(t.sel, target)
+func (t *Targets) AddSelection(sel interface{}) error { return t.ts[t.idx].AddSelection(sel) }
+
+func (t *Targets) HasSelections() bool {
+ for _, t := range t.ts {
+ if !t.HasSelection() {
+ return false
+ }
+ }
+ return true
}
-func (t *Targets) CheckTargets(s *State) error {
- if t.RequireTargets() {
- return fmt.Errorf("Number of selected targets and required ones does not match")
+func (t *Targets) ClearSelections() {
+ for _, t := range t.ts {
+ t.ClearSelection()
}
+}
+func (t *Targets) Options() (options []interface{}) { return t.ts[t.idx].Options() }
+func (t *Targets) Next() { t.idx++ }
- for i, c := range t.constraints {
- sel := t.sel[i]
- if err := c.constraint(sel); err != nil {
- return fmt.Errorf("selected target %s does not fit its desciption %s: %s",
- sel, c.desc, err)
+func (t *Targets) CheckTargets(s *State) error {
+ for _, target := range t.ts {
+ if err := target.CheckSelection(s); err != nil {
+ return err
}
}
return nil
}
+func (targets *Targets) RequireSelection() bool {
+ for _, t := range targets.ts {
+ if t.RequireSelection() {
+ return true
+ }
+ }
+
+ return false
+}
+
func targetConstraint(desc string, s *State, action Action) TargetConstraintFunc {
constraints := []TargetConstraintFunc{}
if strings.Contains(desc, "permanent") {
@@ -180,22 +263,26 @@ func attackableTargetConstraint(action Action) TargetConstraintFunc {
}
}
-func rangeTargetConstraint(source interface{}, r int) TargetConstraintFunc {
- var sourcePos Position
- switch source := source.(type) {
+func posFromTileOrPermanent(tileOrPermanent interface{}) Position {
+ switch obj := tileOrPermanent.(type) {
case *Tile:
- sourcePos = source.Position
+ return obj.Position
case Permanent:
- sourcePos = source.Tile().Position
+ return obj.Tile().Position
default:
- log.Fatalf("Unhandled source type %T in rangeTargetConstraint", source)
+ log.Fatalf("Unhandled source type %T in posFromTileOrPermanent", tileOrPermanent)
+ return INVALID_POSITION()
}
+}
+
+func rangeTargetConstraint(source interface{}, r int) TargetConstraintFunc {
+ sourcePos := posFromTileOrPermanent(source)
return func(t interface{}) (err error) {
- p, _ := t.(Permanent)
- if !IsPositionInRange(sourcePos, p.Tile().Position, r) {
+ targetPos := posFromTileOrPermanent(t)
+ if !IsPositionInRange(sourcePos, targetPos, r) {
err = fmt.Errorf("Position %v of target %v not in range %d of source's position %v",
- p.Tile().Position, t, r, sourcePos)
+ targetPos, t, r, sourcePos)
}
return
}
@@ -359,6 +446,16 @@ func parseTileTargetConstraint(desc string, s *State, action Action) []TargetCon
constraints = append(constraints, rangeTargetConstraint(action.Source(), 1))
}
+ if strings.Contains(desc, "free") {
+ constraints = append(constraints, func(t interface{}) (err error) {
+ tile := t.(*Tile)
+ if tile.IsFree() {
+ return nil
+ }
+ return fmt.Errorf("tile %v is occupied by %v", tile, tile.Permanent)
+ })
+ }
+
return constraints
}
@@ -401,7 +498,13 @@ func parseCardTargetConstraint(desc string, s *State, action Action) []TargetCon
case "hand":
pocs = append(pocs, p.Hand)
case "store":
- pocs = append(pocs, p.Store)
+ if p.gameState.Map.HasStores() {
+ for _, s := range p.AvailableStores() {
+ pocs = append(pocs, s)
+ }
+ } else {
+ pocs = append(pocs, p.Store)
+ }
case "discard pile":
pocs = append(pocs, p.DiscardPile)
}
@@ -421,17 +524,6 @@ func parseCardTargetConstraint(desc string, s *State, action Action) []TargetCon
return constraints
}
-func (c *TargetConstraint) options(s *State) (options []interface{}) {
- candidates := targetCandidates(c.desc, s)
-
- for _, candidate := range candidates {
- if err := c.constraint(candidate); err == nil {
- options = append(options, candidate)
- }
- }
- return
-}
-
func targetCandidates(desc string, s *State) []interface{} {
if strings.Contains(desc, "unit") ||
strings.Contains(desc, "artifact") ||
diff --git a/go/game/tile.go b/go/game/tile.go
index 510497f8..ce7518cb 100644
--- a/go/game/tile.go
+++ b/go/game/tile.go
@@ -71,6 +71,15 @@ var TileNames = map[string]TileType{
"store": store,
}
+var farmEffect areaEffect = newGrantFullActionEffect("misc/farmer",
+ func(a Action) ActionResolveFunc {
+ u := a.Source().(*Unit)
+ return func(s *State) {
+ controller := u.Controller()
+ controller.gainResource(3)
+ }
+ }, "gain 3 resource", "farmAction")
+
type Tile struct {
Position Position
Permanent Permanent
@@ -88,8 +97,8 @@ func INVALID_TILE() Tile {
return Tile{Position: INVALID_POSITION()}
}
-func NewTileFromString(t string, pos Position) (Tile, error) {
- tile := strings.ToLower(t)
+func NewTileFromString(raw string, pos Position) (Tile, error) {
+ tile := strings.ToLower(raw)
tokens := strings.Split(tile, " ")
tileType, found := TileNames[tokens[0]]
if !found {
@@ -98,7 +107,14 @@ func NewTileFromString(t string, pos Position) (Tile, error) {
}
}
water := strings.Contains(tile, "water")
- return Tile{Position: pos, Type: tileType, Water: water, Raw: t}, nil
+
+ t := Tile{Position: pos, Type: tileType, Water: water, Raw: raw}
+
+ if tileType == TileTypes.Farm {
+ t.effects = append(t.effects, farmEffect)
+ }
+
+ return t, nil
}
func (t *Tile) IsFree() bool {
@@ -139,6 +155,11 @@ func (t *Tile) OnDiagonal(other *Tile) bool {
func (t *Tile) entering(p Permanent) {
t.Permanent = p
+
+ if t.Type == TileTypes.Store {
+ p.Controller().addKnownStore(t.Position)
+ }
+
for _, effect := range t.effects {
effect.onEntering(p)
}
@@ -153,6 +174,11 @@ func (t *Tile) leaving(p Permanent) {
func (t *Tile) addEffect(effect areaEffect) {
t.effects = append(t.effects, effect)
+
+ // Apply effect to the current Permanent
+ if t.Permanent != nil {
+ effect.onEntering(t.Permanent)
+ }
}
func (t *Tile) removeEffect(effect areaEffect) {
@@ -163,6 +189,11 @@ func (t *Tile) removeEffect(effect areaEffect) {
break
}
}
+
+ // Remove effect from the current Permanent
+ if t.Permanent != nil {
+ effect.onLeaving(t.Permanent)
+ }
}
func (t *Tile) neutralize() {
diff --git a/go/game/unit.go b/go/game/unit.go
index 8b57b0ae..adb608ab 100644
--- a/go/game/unit.go
+++ b/go/game/unit.go
@@ -1,6 +1,7 @@
package game
import (
+ "fmt"
"log"
)
@@ -14,7 +15,7 @@ type Unit struct {
Health int
Movement Movement
Attack Attack
- Upkeep int
+ upkeep int
FullActions []*FullAction
FreeActions []*FreeAction
// Effects []Effects
@@ -48,7 +49,7 @@ func NewUnit(card *Card, tile *Tile, owner *Player) *Unit {
Health: card.Values["health"].(int),
Movement: movement,
Attack: attack,
- Upkeep: upkeep,
+ upkeep: upkeep,
AvailMoveActions: DEFAULT_AVAIL_MOVE_ACTIONS,
AvailAttackActions: DEFAULT_AVAIL_ATTACK_ACTIONS,
}
@@ -61,10 +62,22 @@ func NewUnit(card *Card, tile *Tile, owner *Player) *Unit {
return u
}
+func (u *Unit) Fmt() string {
+ return fmt.Sprintf("%s@%v", u.Card().Name, u.TileOrContainingPermTile())
+}
+
+func (u *Unit) FmtWController() string {
+ return fmt.Sprintf("%s's %s@%v", u.Controller().Name, u.Card().Name, u.TileOrContainingPermTile())
+}
+
func NewUnitFromPath(cardPath string, tile *Tile, owner *Player) *Unit {
return NewUnit(NewCard(cardPath), tile, owner)
}
+func (u *Unit) UpkeepCost() int {
+ return u.upkeep
+}
+
func (u *Unit) resetBaseActions() {
u.AvailMoveActions = DEFAULT_AVAIL_MOVE_ACTIONS + u.additionalMoveActions
u.AvailAttackActions = DEFAULT_AVAIL_ATTACK_ACTIONS + u.additionalAttackActions
@@ -238,3 +251,23 @@ func (u *Unit) onUnpile(containing Permanent) {
func (u *Unit) onDrop(containing Permanent) {
u.onUnpile(containing)
}
+
+func (u *Unit) removeFullAction(tag string) {
+ for i, fa := range u.FullActions {
+ if fa.tag != tag {
+ continue
+ }
+ u.FullActions[i] = u.FullActions[len(u.FullActions)-1]
+ u.FullActions = u.FullActions[:len(u.FullActions)-1]
+ break
+ }
+}
+
+func (u *Unit) AddDamage(damage int) {
+ if u.Marks(UnitMarks.Ward) > 0 {
+ u.adjustMarks(UnitMarks.Ward, -1)
+ return
+ }
+
+ u.PermanentBase.AddDamage(damage)
+}