diff options
| -rw-r--r-- | go/client/game.go | 24 | ||||
| -rw-r--r-- | go/game/action.go | 171 | ||||
| -rw-r--r-- | go/game/cardImplementations.go | 11 | ||||
| -rw-r--r-- | go/game/kraken.go | 4 | ||||
| -rw-r--r-- | go/game/player.go | 17 | ||||
| -rw-r--r-- | go/game/playerControl.go | 80 | ||||
| -rw-r--r-- | go/game/stack.go | 2 | ||||
| -rw-r--r-- | go/game/state.go | 24 | ||||
| -rw-r--r-- | go/game/targets.go | 154 | ||||
| -rw-r--r-- | go/game/unit.go | 17 | ||||
| -rw-r--r-- | go/go.mod | 2 | ||||
| -rw-r--r-- | go/ui/prompt.go | 35 | ||||
| -rw-r--r-- | go/ui/textBox.go | 2 | ||||
| -rw-r--r-- | go/utils/slices.go | 8 |
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 @@ -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 +} |
