aboutsummaryrefslogtreecommitdiff
path: root/go/game
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-02-15 13:13:06 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2025-01-27 16:43:49 +0100
commitf9ab2cfdb25eb3fcd85868a2e375326a9c8b6022 (patch)
treeb96f16e6cf4a6dcf93b9557a7219179ab2f46c8d /go/game
parent805cbf397589e5cb861f866713c2eb4bec9ce40b (diff)
downloadmuhqs-game-f9ab2cfdb25eb3fcd85868a2e375326a9c8b6022.tar.gz
muhqs-game-f9ab2cfdb25eb3fcd85868a2e375326a9c8b6022.zip
intermediate commit
* fix a lot of target and action bugs * check target selection before adding it to the prompt * use the store view for stores on the map * fix card highlighting in CardGrid * Only prompt for Upkeep-/DiscardActions if appropriate * Add helper for areaEffects granting new FullActions * Add Target() helper selecting the first target to the Action interface * Fix BuyAction and DiscardAction targets * Remember the stores a player has seen
Diffstat (limited to 'go/game')
-rw-r--r--go/game/action.go77
-rw-r--r--go/game/ai.go4
-rw-r--r--go/game/areaEffect.go24
-rw-r--r--go/game/cardImplementations.go56
-rw-r--r--go/game/deck.go4
-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.go63
-rw-r--r--go/game/playerControl.go19
-rw-r--r--go/game/stack.go5
-rw-r--r--go/game/state.go18
-rw-r--r--go/game/targets.go99
-rw-r--r--go/game/tile.go37
-rw-r--r--go/game/unit.go20
15 files changed, 299 insertions, 145 deletions
diff --git a/go/game/action.go b/go/game/action.go
index 65eb03b6..77e9743f 100644
--- a/go/game/action.go
+++ b/go/game/action.go
@@ -2,8 +2,6 @@ package game
import (
"fmt"
-
- "golang.org/x/exp/slices"
)
type (
@@ -15,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
@@ -26,6 +25,7 @@ type PassPriority struct{ player *Player }
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) {}
@@ -39,6 +39,7 @@ type TargetSelection struct {
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) {}
@@ -50,7 +51,8 @@ func newTargetSelection(player *Player, targets *Targets) Action {
func newHandCardSelection(p *Player, min, max int) Action {
a := &TargetSelection{player: p}
- a.targets = newTargets(newTarget(p.gameState, newTargetDesc("hand card"), a))
+ targetDesc := TargetDesc{"hand card", fmt.Sprintf("%d-%d", min, max)}
+ a.targets = newTargets(newTarget(p.gameState, targetDesc, a))
return a
}
@@ -62,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)
}
@@ -144,7 +138,7 @@ type MoveAction struct {
var moveActionTargetDesc = TargetDesc{"available tile", "1"}
func (a *MoveAction) targetTile() *Tile {
- t := a.Targets().ts[0]
+ t := a.Target()
if tile, ok := t.sel[0].(*Tile); ok {
return tile
}
@@ -203,7 +197,7 @@ func NewAttackAction(u *Unit) *AttackAction {
a.targets = newTargets(newTarget(u.Controller().gameState, attackActionTargetDesc, a))
a.resolveFunc = func(s *State) {
- s.Fight(u, a.targets.ts[0].sel[0].(Permanent))
+ s.Fight(u, a.Target().sel[0].(Permanent))
}
a.costFunc = func(*State) bool {
@@ -223,7 +217,7 @@ func (a *AttackAction) String() string {
return fmt.Sprintf("attack %v", u.Attack)
}
- target := a.targets.ts[0].sel[0].(Permanent)
+ target := a.Target().sel[0].(Permanent)
return fmt.Sprintf("%s x %v", u.FmtWController(), target.Tile().Position)
}
@@ -261,7 +255,7 @@ func NewFullAction(u *Unit, proto ActionFuncPrototype, desc string) *FullAction
func (a *FullAction) String() string {
u := a.source.(*Unit)
- if a.targets.RequireSelection() {
+ if a.targets == nil || a.targets.RequireSelection() {
return fmt.Sprintf("↻ %s", a.desc)
}
@@ -302,12 +296,12 @@ func (a *FreeAction) String() string {
return fmt.Sprintf("%v: %s free_action@%v", p.Controller(), p.Card().Name, a.targets)
}
-var buyActionTargetDesc = TargetDesc{"store card", "1"}
+var buyActionTargetDesc = TargetDesc{"store card", "?"}
type BuyAction struct{ TargetSelection }
func (a *BuyAction) card() *Card {
- return a.Targets().ts[0].sel[0].(*Card)
+ return a.Target().sel[0].(*Card)
}
func (a *BuyAction) CheckTargets(s *State) error {
@@ -333,7 +327,7 @@ func (a *BuyAction) PayCosts(*State) bool {
}
func (a *BuyAction) String() string {
- if a.Targets().RequireSelection() {
+ if !a.Targets().HasSelections() {
return fmt.Sprintf("%s buy", a.player.Name)
}
@@ -354,7 +348,7 @@ type UpkeepAction struct {
func (a *UpkeepAction) PayCosts(*State) bool {
costs := 0
- for _, t := range a.targets.ts[0].sel {
+ for _, t := range a.Target().sel {
u := t.(*Unit)
costs += u.UpkeepCost()
}
@@ -368,31 +362,26 @@ func (a *UpkeepAction) PayCosts(*State) bool {
}
func (a *UpkeepAction) Resolve(*State) {
- s := a.player.gameState
+ p := a.player
+ s := p.gameState
+ for _, i := range a.Target().sel {
+ u := i.(*Unit)
+ s.DestroyPermanent(u)
+ }
+
+ // Keep and pay for the rest
for _, u := range s.Units {
- if u.Controller() != a.player {
+ if u.Controller() != p {
continue
}
- if !slices.Contains(a.targets.ts[0].sel, interface{}(u)) {
- s.DestroyPermanent(u)
- }
+ p.Resource -= u.upkeep
+ u.onUpkeep()
}
}
func (a *UpkeepAction) String() string {
- disbanding := []*Unit{}
- for _, u := range a.player.gameState.Units {
- if u.Controller() != a.player {
- continue
- }
-
- if !slices.Contains(a.targets.ts[0].sel, interface{}(u)) {
- disbanding = append(disbanding, u)
- }
- }
-
- return fmt.Sprintf("upkeep disbanding: %v", disbanding)
+ return fmt.Sprintf("upkeep disbanding: %v", a.Target().sel)
}
func newUpkeepAction(p *Player) *UpkeepAction {
diff --git a/go/game/ai.go b/go/game/ai.go
index bda7356a..9b48c2f5 100644
--- a/go/game/ai.go
+++ b/go/game/ai.go
@@ -90,7 +90,7 @@ func selectRandomTargets(rand *rand.Rand, targets *Targets) error {
if len(options) > 1 {
idx = rand.Intn(len(options) - 1)
}
- t.AddSelection(options[idx])
+ _ = t.AddSelection(options[idx])
}
}
return nil
@@ -211,7 +211,7 @@ func moveTowardsNearestEnemyUnit(ai *UnitAI) Action {
}
a := NewMoveAction(ai.u)
- a.Targets().ts[0].AddSelection(ai.s.Map.TileAt(target))
+ _ = a.Target().AddSelection(ai.s.Map.TileAt(target))
return a
}
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 a9da5fb1..038ce2d6 100644
--- a/go/game/cardImplementations.go
+++ b/go/game/cardImplementations.go
@@ -34,9 +34,9 @@ type missionaryImpl struct{ cardImplementationBase }
func (*missionaryImpl) fullActions(u *Unit) []*FullAction {
resolvePrototype := func(a Action) ActionResolveFunc {
u := a.Source().(*Unit)
- target := a.Targets().ts[0].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()
}
@@ -66,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)
}
}
@@ -129,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().ts[0].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(newTarget(s, newTargetDesc("adjacent water tile"), a))
+ a.targets = newTargets(newTarget(s, newTargetDesc("adjacent free water tile"), a))
return []*FullAction{a}
}
@@ -167,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 }
@@ -330,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)
}
}
}
@@ -344,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/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 1215479f..a69f2cd3 100644
--- a/go/game/player.go
+++ b/go/game/player.go
@@ -24,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 {
@@ -42,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
}
@@ -78,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.UpkeepCost()
- unit.onUpkeep()
+ if _, ok := a.(*PassPriority); ok {
+ a = newUpkeepAction(p)
}
+
+ p.gameState.DeclareAction(a)
+ p.gameState.Stack.Resolve()
}
func (p *Player) ActionPhase() {
@@ -102,12 +115,15 @@ func (p *Player) ActionPhase() {
func (p *Player) BuyPhase() {
a := promptBuy(p.Ctrl)
- log.Println(a)
+
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() {
@@ -121,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)
@@ -136,7 +156,7 @@ func (p *Player) PromptHandCardSelection(min, max int) []*Card {
log.Fatalf("Invalid hand card selection: %v", err)
}
- return utils.InterfaceSliceToTypedSlice[*Card](a.Targets().ts[0].sel)
+ return utils.InterfaceSliceToTypedSlice[*Card](a.Target().sel)
case *PassPriority:
return nil
default:
@@ -163,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 dda1a736..eea6a19b 100644
--- a/go/game/playerControl.go
+++ b/go/game/playerControl.go
@@ -8,12 +8,28 @@ import (
type PlayerNotificationType int
const (
- DeclaredActionNotification = iota
+ DeclaredActionNotification PlayerNotificationType = iota
ResolvedActionNotification
PriorityNotification
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
}
@@ -61,7 +77,6 @@ func newUpkeepPrompt(p *Player) PlayerNotification {
func newBuyPrompt(p *Player) PlayerNotification {
a := newBuyAction(p)
prompt := newTargetSelectionPrompt(a, "Select a card to buy")
- log.Println("sending buy prompt with ctx", prompt)
return prompt
}
diff --git a/go/game/stack.go b/go/game/stack.go
index c119e81c..78a5ff41 100644
--- a/go/game/stack.go
+++ b/go/game/stack.go
@@ -33,6 +33,11 @@ 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))
diff --git a/go/game/state.go b/go/game/state.go
index 31c8ad01..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().ts[0].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().ts[0].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().ts[0].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,
@@ -471,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 99ba5085..440d8922 100644
--- a/go/game/targets.go
+++ b/go/game/targets.go
@@ -93,6 +93,19 @@ func (d *TargetDesc) requirement() TargetRequirement {
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)
@@ -101,10 +114,17 @@ func (d *TargetDesc) requirement() TargetRequirement {
}
}
-func (t *Target) RequireSelection() bool {
- return len(t.sel) < t.requirement.min
+func (t *Target) AddSelection(sel interface{}) (err error) {
+ if err = t.constraint(sel); err == nil {
+ t.sel = append(t.sel, sel)
+ }
+ return
}
+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)
@@ -116,22 +136,6 @@ func (t *Target) Options() (options []interface{}) {
return options
}
-func (t *Targets) Options() (options []interface{}) {
- return t.ts[t.idx].Options()
-}
-
-func (t *Target) AddSelection(sel interface{}) {
- t.sel = append(t.sel, sel)
-}
-
-func (t *Targets) AddSelection(sel interface{}) {
- t.ts[t.idx].AddSelection(sel)
-}
-
-func (t *Targets) Next() {
- t.idx++
-}
-
func (t *Target) CheckSelection(s *State) error {
if t.RequireSelection() {
return fmt.Errorf("Number of selected targets and required ones does not match")
@@ -151,6 +155,25 @@ func (t *Target) CheckSelection(s *State) error {
return nil
}
+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) 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++ }
+
func (t *Targets) CheckTargets(s *State) error {
for _, target := range t.ts {
if err := target.CheckSelection(s); err != nil {
@@ -240,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
}
@@ -419,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
}
@@ -461,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)
}
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 94458c07..adb608ab 100644
--- a/go/game/unit.go
+++ b/go/game/unit.go
@@ -251,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)
+}