aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--go/client/game.go24
-rw-r--r--go/game/action.go171
-rw-r--r--go/game/cardImplementations.go11
-rw-r--r--go/game/kraken.go4
-rw-r--r--go/game/player.go17
-rw-r--r--go/game/playerControl.go80
-rw-r--r--go/game/stack.go2
-rw-r--r--go/game/state.go24
-rw-r--r--go/game/targets.go154
-rw-r--r--go/game/unit.go17
-rw-r--r--go/go.mod2
-rw-r--r--go/ui/prompt.go35
-rw-r--r--go/ui/textBox.go2
-rw-r--r--go/utils/slices.go8
14 files changed, 331 insertions, 220 deletions
diff --git a/go/client/game.go b/go/client/game.go
index 91e7e365..f2a035d3 100644
--- a/go/client/game.go
+++ b/go/client/game.go
@@ -153,7 +153,7 @@ func (g *Game) getPlayer(name string) *game.Player {
func (g *Game) initPlayerUi(player *game.Player) *Game {
g.activePlayer = player
- g.playerCtrl = game.NewChanPlayerControl()
+ g.playerCtrl = game.NewChanPlayerControl(player)
player.Ctrl = g.playerCtrl
var x, y int
@@ -338,6 +338,7 @@ func (g *Game) passPriority() {
func (g *Game) handlePlayerNotifications() {
n := g.playerCtrl.RecvNotification()
for n != nil {
+ log.Println("Received notification", n)
switch n.Notification {
case game.PriorityNotification:
g.showPassButton()
@@ -355,16 +356,15 @@ func (g *Game) handlePlayerNotifications() {
g.stackBuffer.RemoveLast()
g.mapView.ForceRedraw()
- case game.BuyPrompt:
- if !g.storesOnMap {
- g.showStore()
+ case game.TargetSelectionPrompt:
+ ctx := n.Context.(game.TargetSelectionCtx)
+ if _, ok := ctx.Action.(*game.BuyAction); ok {
+ if !g.storesOnMap {
+ g.showStore()
+ }
}
- case game.HandCardSelectionPrompt:
- ctx := n.Context.([]int)
- min, max := ctx[0], ctx[1]
- promptTxt := fmt.Sprintf("Select between %d and %d hand cards", min, max)
- g.prompt = ui.NewHandCardPrompt(g.height/2, g.width, promptTxt)
+ g.prompt = ui.NewPrompt(g.height/2, g.width, ctx.Action, ctx.Prompt)
g.addWidget(g.prompt)
g.showPassButton()
}
@@ -426,12 +426,6 @@ func (g *Game) handleSelection(obj interface{}, x, y int) {
g.selectedObject = game.NewPlayAction(g.activePlayer, obj.C)
g.handLayer.HighlightCard(obj.C)
- case *game.Card:
- if g.gameState.IsActivePlayer(g.activePlayer) &&
- g.gameState.ActivePhase == game.Phases.BuyPhase {
- g.declareAction(game.NewBuyAction(g.activePlayer, obj))
- }
-
default:
log.Fatalf("Object of type %T not handled", obj)
}
diff --git a/go/game/action.go b/go/game/action.go
index 5002f58a..65eb03b6 100644
--- a/go/game/action.go
+++ b/go/game/action.go
@@ -2,6 +2,8 @@ package game
import (
"fmt"
+
+ "golang.org/x/exp/slices"
)
type (
@@ -20,9 +22,9 @@ 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) CheckTargets(*State) error { return nil }
func (*PassPriority) PayCosts(*State) bool { return true }
@@ -30,25 +32,27 @@ 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) 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}
+ a.targets = newTargets(newTarget(p.gameState, newTargetDesc("hand card"), a))
+ return a
+}
type ActionBase struct {
source interface{}
@@ -82,7 +86,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 +114,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 +130,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 +141,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.Targets().ts[0]
+ 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 +160,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 +180,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 +200,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.targets.ts[0].sel[0].(Permanent))
}
a.costFunc = func(*State) bool {
@@ -212,13 +219,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.targets.ts[0].sel[0].(Permanent)
+ return fmt.Sprintf("%s x %v", u.FmtWController(), target.Tile().Position)
}
type FullAction struct {
@@ -255,11 +261,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.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 +295,109 @@ 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", "1"}
-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.Targets().ts[0].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().RequireSelection() {
+ 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.targets.ts[0].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) Resolve(*State) {
+ s := a.player.gameState
+ for _, u := range s.Units {
+ if u.Controller() != a.player {
+ continue
+ }
+
+ if !slices.Contains(a.targets.ts[0].sel, interface{}(u)) {
+ s.DestroyPermanent(u)
+ }
+ }
+}
+
func (a *UpkeepAction) String() string {
- if a.pay {
- return fmt.Sprintf("%s keep %v", a.player.Name, a.unit.Tile().Position)
+ 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("%s disband %v", a.player.Name, a.unit.Tile().Position)
+ return fmt.Sprintf("upkeep disbanding: %v", disbanding)
}
-func NewUpkeepAction(p *Player, u *Unit, pay bool) *UpkeepAction {
- return &UpkeepAction{p, u, pay}
+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/cardImplementations.go b/go/game/cardImplementations.go
index 3a7a729a..a9da5fb1 100644
--- a/go/game/cardImplementations.go
+++ b/go/game/cardImplementations.go
@@ -34,7 +34,7 @@ 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.Targets().ts[0].sel[0].(*Unit)
return func(s *State) {
target.addMarks(UnitMarks.Faith, 2)
if target.Marks(UnitMarks.Faith) > target.Card().BuyCost {
@@ -42,7 +42,10 @@ func (*missionaryImpl) fullActions(u *Unit) []*FullAction {
}
}
}
- 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 }
@@ -126,12 +129,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.Targets().ts[0].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 water tile"), a))
return []*FullAction{a}
}
diff --git a/go/game/kraken.go b/go/game/kraken.go
index d92d204b..e2cc39e9 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)
diff --git a/go/game/player.go b/go/game/player.go
index cb89e1ae..447a6173 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 (
@@ -67,7 +69,7 @@ func (p *Player) Upkeep() {
}
// TODO: let players decide if they want to pay the upkeep
- p.Resource -= unit.Upkeep
+ p.Resource -= unit.UpkeepCost()
unit.onUpkeep()
}
}
@@ -85,6 +87,7 @@ func (p *Player) ActionPhase() {
func (p *Player) BuyPhase() {
a := promptBuy(p.Ctrl)
+ log.Println(a)
if _, ok := a.(*PassPriority); ok {
return
}
@@ -110,11 +113,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.Targets().sel)
case *PassPriority:
return nil
default:
diff --git a/go/game/playerControl.go b/go/game/playerControl.go
index e62d91b3..dda1a736 100644
--- a/go/game/playerControl.go
+++ b/go/game/playerControl.go
@@ -1,14 +1,17 @@
package game
+import (
+ "fmt"
+ "log"
+)
+
type PlayerNotificationType int
const (
DeclaredActionNotification = iota
ResolvedActionNotification
PriorityNotification
- UpkeepPrompt
- BuyPrompt
- HandCardSelectionPrompt
+ TargetSelectionPrompt
)
func (n PlayerNotification) IsPriorityNotification() bool {
@@ -21,72 +24,93 @@ 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 newUpkeepPrompt(p *Player) PlayerNotification {
+ a := newUpkeepAction(p)
+ return newTargetSelectionPrompt(a, "Select units to disband")
}
-func NewBuyPrompt() PlayerNotification {
- return PlayerNotification{BuyPrompt, nil, nil}
+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
}
-func NewHandCardSelectionPrompt(min, max int) PlayerNotification {
- return PlayerNotification{HandCardSelectionPrompt, []int{min, max}, nil}
+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..c119e81c 100644
--- a/go/game/stack.go
+++ b/go/game/stack.go
@@ -35,7 +35,7 @@ func (s *Stack) pop() {
a.Resolve(s.gameState)
}
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..ccf3ed08 100644
--- a/go/game/state.go
+++ b/go/game/state.go
@@ -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:
diff --git a/go/game/targets.go b/go/game/targets.go
index eb429c8c..c310a381 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,112 @@ 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.desc,
+ constraint: targetConstraint(desc.desc, 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, desac 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 (t *Targets) RequireTargets() bool {
- return len(t.constraints) != len(t.sel)
+func newTargetDesc(desc string) TargetDesc {
+ return TargetDesc{desc, "1"}
}
-func (t *Targets) Options() [][]interface{} {
- options := make([][]interface{}, 0, len(t.constraints))
- for _, c := range t.constraints {
- options = append(options, c.options(t.s))
- }
- return options
+func newTargetDescWithRequirement(desc, requirement string) TargetDesc {
+ return TargetDesc{desc, requirement}
}
-func (t *Targets) NextOptions() []interface{} {
- if len(t.constraints) == 0 {
- return nil
+func (d *TargetDesc) requirement() TargetRequirement {
+ switch d.req {
+ case "?":
+ return TargetRequirement{0, 1}
+ case "+":
+ return TargetRequirement{1, -1}
+ case "*":
+ return TargetRequirement{0, -1}
+ default:
+ 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}
}
+}
- next := len(t.sel)
- return t.constraints[next].options(t.s)
+func (t *Target) buildConstraints(desc TargetDesc, action Action) {
}
-func (t *Targets) AddTarget(target interface{}) {
- t.sel = append(t.sel, target)
+func (t *Target) RequireSelection() bool {
+ return len(t.sel) < t.requirement.min
}
-func (t *Targets) CheckTargets(s *State) error {
- if t.RequireTargets() {
+func (t *Target) Options() (options []interface{}) {
+ candidates := targetCandidates(t.desc, s)
+
+ for _, candidate := range candidates {
+ if err := t.constraint(candidate); err == nil {
+ options = append(options, candidate)
+ }
+ }
+ return options
+}
+
+func (t *Target) AddSelection(sel interface{}) {
+ t.sel = append(t.sel, sel)
+}
+
+func (t *Target) CheckSelection(s *State) error {
+ if t.RequireSelection() {
return fmt.Errorf("Number of selected targets and required ones does not match")
}
- for i, c := range t.constraints {
- sel := t.sel[i]
- if err := c.constraint(sel); err != nil {
+ 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, c.desc, err)
+ sel, t.desc, err)
+ }
+ }
+
+ return nil
+}
+
+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") {
@@ -421,17 +476,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/unit.go b/go/game/unit.go
index 8b57b0ae..94458c07 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
diff --git a/go/go.mod b/go/go.mod
index 24b6cb52..f905c341 100644
--- a/go/go.mod
+++ b/go/go.mod
@@ -1,6 +1,6 @@
module muhq.space/muhqs-game/go
-go 1.19
+go 1.20
require (
github.com/RyanCarrier/dijkstra v1.1.0
diff --git a/go/ui/prompt.go b/go/ui/prompt.go
index bfcd5d14..704fd05b 100644
--- a/go/ui/prompt.go
+++ b/go/ui/prompt.go
@@ -1,8 +1,6 @@
package ui
import (
- "log"
-
"github.com/hajimehoshi/ebiten/v2"
"muhq.space/muhqs-game/go/game"
@@ -11,53 +9,32 @@ import (
type promptType int
const (
- handCard promptType = iota
-
PROMPT_HEIGHT int = 50
)
type Prompt struct {
- promptType promptType
y, width int
- sel []interface{}
+ promptType promptType
+ action game.Action
components []Widget
}
-func NewHandCardPrompt(y, width int, promptText string) *Prompt {
+func NewPrompt(y, width int, action game.Action, promptText string) *Prompt {
p := &Prompt{
y: y,
width: width,
- promptType: handCard,
+ action: action,
components: []Widget{NewCenteringFixedTextBox(0, y, width, PROMPT_HEIGHT, promptText)},
}
return p
}
func (p *Prompt) Add(obj interface{}) {
- var ok bool
- switch p.promptType {
- case handCard:
- _, ok = obj.(HandCard)
- }
-
- if !ok {
- log.Fatalf("Invalid type %T for prompt %d", obj, p.promptType)
- }
-
- p.sel = append(p.sel, obj)
+ p.action.Targets().AddTarget(obj)
}
func (p *Prompt) Confirm() game.Action {
- switch p.promptType {
- case handCard:
- cards := make([]*game.Card, 0, len(p.sel))
- for _, c := range p.sel {
- cards = append(cards, c.(HandCard).C)
- }
- return game.NewHandCardSelection(cards)
- }
-
- return nil
+ return p.action
}
func (p *Prompt) Height() int {
diff --git a/go/ui/textBox.go b/go/ui/textBox.go
index 58a3e679..80e6186c 100644
--- a/go/ui/textBox.go
+++ b/go/ui/textBox.go
@@ -56,7 +56,7 @@ func NewCenteringAutoTextBox(x, y int, t string) *TextBox {
func NewUnitInfo(x, y int, u *game.Unit) *TextBox {
info := fmt.Sprintf("%s\nDamage: %d\nHealth: %d\nUpkeep: %d",
- u.String(), u.Damage(), u.Health, u.Upkeep)
+ u.String(), u.Damage(), u.Health, u.UpkeepCost())
if u.Movement != game.INVALID_MOVEMENT() {
info = fmt.Sprintf("%s\nMovement: %s", info, u.Movement.String())
diff --git a/go/utils/slices.go b/go/utils/slices.go
index d2bc04c1..3cc38c1c 100644
--- a/go/utils/slices.go
+++ b/go/utils/slices.go
@@ -7,3 +7,11 @@ func TypedSliceToInterfaceSlice[T any](s []T) []interface{} {
}
return is
}
+
+func InterfaceSliceToTypedSlice[T any](s []interface{}) []T {
+ ts := make([]T, 0, len(s))
+ for _, t := range s {
+ ts = append(ts, t.(T))
+ }
+ return ts
+}