diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2023-02-01 18:46:32 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2023-02-01 18:46:32 +0100 |
| commit | 1b8140f9c6104309c480dc9ae8b36fd0079c38ec (patch) | |
| tree | ee3fadd0204dc1e928436ef06b8d5c56bb0afcfc | |
| parent | dd341b1472d65fb658f53d4d8aa1ca855df0b3bf (diff) | |
| download | muhqs-game-1b8140f9c6104309c480dc9ae8b36fd0079c38ec.tar.gz muhqs-game-1b8140f9c6104309c480dc9ae8b36fd0079c38ec.zip | |
intermediate commit
| -rw-r--r-- | go/client/game.go | 114 | ||||
| -rw-r--r-- | go/client/main.go | 3 | ||||
| -rw-r--r-- | go/client/startMenu.go | 10 | ||||
| -rw-r--r-- | go/game/action.go | 303 | ||||
| -rw-r--r-- | go/game/ai.go | 43 | ||||
| -rw-r--r-- | go/game/artifact.go | 4 | ||||
| -rw-r--r-- | go/game/card.go | 8 | ||||
| -rw-r--r-- | go/game/cardImplementations.go | 33 | ||||
| -rw-r--r-- | go/game/cardParsing.go | 33 | ||||
| -rw-r--r-- | go/game/kraken.go | 55 | ||||
| -rw-r--r-- | go/game/permanent.go | 7 | ||||
| -rw-r--r-- | go/game/state.go | 70 | ||||
| -rw-r--r-- | go/game/targets.go | 307 | ||||
| -rw-r--r-- | go/game/tile.go | 5 | ||||
| -rw-r--r-- | go/game/unit.go | 85 | ||||
| -rw-r--r-- | go/ui/choice.go | 2 | ||||
| -rw-r--r-- | go/ui/scrollable.go | 5 | ||||
| -rw-r--r-- | go/utils/slices.go | 9 |
18 files changed, 792 insertions, 304 deletions
diff --git a/go/client/game.go b/go/client/game.go index 2f112593..00ef4153 100644 --- a/go/client/game.go +++ b/go/client/game.go @@ -253,12 +253,13 @@ func (g *Game) removeWidgetAt(idx int) { } func (g *Game) addActionChoice(perm game.Permanent, x, y int) { - actions := perm.AvailActionPrototypes() + actions := perm.AvailActions() if len(actions) > 0 { g.selectedObject = perm onClick := func(c *ui.Choice, x, y int) { g.removeActionChoice() - g.selectedObject = actions[c.GetChoosen(x, y)] + a := actions[c.GetChoosen(x, y)] + g.selectedObject = a } g.actionChoice = ui.NewActionChoice(x, y, actions, onClick) g.addWidget(g.actionChoice) @@ -363,78 +364,63 @@ func (g *Game) findObjectAt(x, y int) interface{} { return nil } +func (g *Game) handleTargetSelection(obj interface{}) { + log.Println("Handle target selection") + a := g.selectedObject.(game.Action) + a.Targets().AddTarget(obj) + + if !a.Targets().RequireTargets() { + log.Println("declare", a) + g.declareAction(a) + g.selectedObject = nil + } +} + func (g *Game) handleSelection(obj interface{}, x, y int) { if obj == nil { fmt.Printf("No object found at cursor position (%d, %d)\n", x, y) return } - switch obj := obj.(type) { - case ui.Button: - obj.Click(x, y) + if _, ok := g.selectedObject.(game.Action); ok { + g.handleTargetSelection(obj) + } else { + switch obj := obj.(type) { + case ui.Button: + obj.Click(x, y) - case *game.Tile: - tile := obj - if g.selectedObject != nil { - var action game.Action - switch obj := g.selectedObject.(type) { - case *game.MoveActionPrototype: - action = obj.Specialize(g.gameState, tile) - case *game.Card: - if obj.IsPermanent() { - action = game.NewPlayAction(g.activePlayer, obj, tile.Position) - } - } - if action != nil { - g.declareAction(action) - g.selectedObject = nil - g.mapView.HighlightTile(tile) - } - } else { - g.mapView.HighlightTile(tile) - } + case *game.Tile: + g.mapView.HighlightTile(obj) - case game.Permanent: - perm := obj - if g.selectedObject == nil { + case game.Permanent: + perm := obj g.addActionChoice(perm, x, y) - } else { - switch obj := g.selectedObject.(type) { - case *game.AttackActionPrototype: - action := obj.Specialize(g.gameState, perm) - g.selectedObject = nil - g.declareAction(action) - g.mapView.HighlightPermanent(perm) - } - } - case ui.HandCard: - g.selectedObject = obj.C + case ui.HandCard: + 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)) - } + 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) + default: + log.Fatalf("Object of type %T not handled", obj) + } } } -func (g *Game) hightlightSelection(obj interface{}) { - switch obj := g.selectedObject.(type) { - case *game.MoveActionPrototype: - g.mapView.HighlightTiles(obj.Unit.MoveRangeTiles(g.gameState.Map)) - case *game.AttackActionPrototype: - perms := obj.Unit.AttackablePermanents(g.gameState.Map) - g.mapView.HighlightPermanents(perms) - case *game.Card: - g.handLayer.HighlightCard(obj) - if obj.IsPermanent() { - spawnTiles := g.gameState.AvailableSpawnTiles(g.activePlayer, obj) - g.mapView.HighlightTiles(spawnTiles) - } +func (g *Game) highlightTargets() { + a, ok := g.selectedObject.(game.Action) + if !ok { + return + } + + options := a.Targets().NextOptions() + for _, option := range options { + g.AddHighlight(option) } } @@ -489,6 +475,15 @@ func (g *Game) Update() error { obj := g.findObjectAt(x, y) + scrollX, scrollY := ebiten.Wheel() + if scrollX != 0 || scrollY != 0 { + if scrollable, ok := obj.(ui.Scrollable); ok { + scrollable.Scroll(int(scrollX), int(scrollY)) + } + } + + g.highlightTargets() + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { if g.prompt != nil { g.prompt.Add(obj) @@ -496,7 +491,6 @@ func (g *Game) Update() error { } else { g.handleSelection(obj, x, y) } - g.hightlightSelection(obj) } else if g.hoverDetector.startedHovering() && g.selectedObject == nil && obj != nil { g.handleHover(obj, x, y) } else if inpututil.IsKeyJustPressed(ebiten.KeySpace) { diff --git a/go/client/main.go b/go/client/main.go index 63fcab00..3635404f 100644 --- a/go/client/main.go +++ b/go/client/main.go @@ -31,7 +31,7 @@ type app struct { windowWidth int windowHeight int - active ebiten.Game + active ebiten.Game running bool } @@ -63,7 +63,6 @@ func main() { flag.StringVar(&startMenu.remote, "remote", "", "address of remote game to connect to") flag.Parse() - ebiten.SetWindowSize(app.windowWidth, app.windowHeight) ebiten.SetWindowTitle("Muhq's Game") diff --git a/go/client/startMenu.go b/go/client/startMenu.go index 07c4f92b..735753b0 100644 --- a/go/client/startMenu.go +++ b/go/client/startMenu.go @@ -10,12 +10,12 @@ import ( ) const ( - START_BUTTON_WIDTH = 150 + START_BUTTON_WIDTH = 150 START_BUTTON_HEIGHT = 50 ) type StartMenu struct { - app *app + app *app width, height int playerName string @@ -31,10 +31,10 @@ func NewStartMenu(app *app) *StartMenu { return m } -func (m *StartMenu) build () { +func (m *StartMenu) build() { m.widgets = append(m.widgets, ui.NewSimpleButton( - (m.width - START_BUTTON_WIDTH) / 2, - (m.height - START_BUTTON_HEIGHT) / 2, + (m.width-START_BUTTON_WIDTH)/2, + (m.height-START_BUTTON_HEIGHT)/2, 150, 50, "Start", diff --git a/go/game/action.go b/go/game/action.go index 0f7a1294..4894d47d 100644 --- a/go/game/action.go +++ b/go/game/action.go @@ -4,40 +4,52 @@ import ( "fmt" ) -type ActionPrototype interface { - Specialize(*State, ...interface{}) Action - String() string -} +type ( + ActionCostFunc func(*State) bool + ActionResolveFunc func(*State) + ActionFuncPrototype func(Action) ActionResolveFunc +) type Action interface { - Resolve(s *State) + Source() interface{} + Targets() *Targets + + CheckTargets(*State) error PayCosts(s *State) bool + Resolve(s *State) String() string } -type PassPriority struct{ Source *Player } +type PassPriority struct{ source *Player } -func (*PassPriority) Resolve(*State) {} -func (*PassPriority) PayCosts(*State) bool { return true } -func (*PassPriority) String() string { return "pass" } -func NewPassPriority(p *Player) Action { return &PassPriority{p} } +func (a *PassPriority) Source() interface{} { return a.source } +func (*PassPriority) Targets() *Targets { 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 -} +type HandCardSelection struct{ cards []*Card } -func (*HandCardSelection) Resolve(*State) {} -func (*HandCardSelection) PayCosts(*State) bool { return true } -func (*HandCardSelection) String() string { return "hand card selection" } -func NewHandCardSelection(cards []*Card) Action { return &HandCardSelection{cards} } +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 ActionBase struct { - Source interface{} + source interface{} Card *Card - Targets interface{} - resolveFunc func(s *State) - costFunc func(s *State) bool - desc string + targets *Targets + resolveFunc ActionResolveFunc + costFunc ActionCostFunc +} + +func (a *ActionBase) Source() interface{} { + return a.source } func (a *ActionBase) Resolve(s *State) { @@ -48,73 +60,91 @@ func (a *ActionBase) PayCosts(s *State) bool { return a.costFunc(s) } -func (a *ActionBase) String() string { - switch source := a.Source.(type) { - case Permanent: - p := source - return fmt.Sprintf("%s: %s %s", p.Controller().Name, p.Tile().Position.String(), a.desc) - case *Player: - return fmt.Sprintf("%s %s", source.Name, a.desc) - default: - return fmt.Sprintf("%v %s", source, a.desc) - } +func (a *ActionBase) Targets() *Targets { + return a.targets +} + +func (a *ActionBase) CheckTargets(s *State) error { + return a.targets.CheckTargets(s) } type PlayAction struct { ActionBase } -func NewPlayAction(p *Player, c *Card, targets interface{}) Action { - costFunc := func(*State) bool { - cost := c.PlayCosts() +const permanentPlayActionTarget = "available spawn tile" - if p.Resource < cost { - return false +func NewPlayAction(p *Player, c *Card, args ...interface{}) *PlayAction { + a := &PlayAction{ActionBase{ + source: p, + Card: c, + }} + + if len(args) > 0 { + if costFunc, ok := args[0].(ActionCostFunc); ok { + a.costFunc = costFunc } - p.Resource -= cost - return true } - return NewPlayActionWithSpecialCosts(p, c, targets, costFunc) -} -func NewPlayActionWithSpecialCosts(p *Player, c *Card, targets interface{}, - costFunc func(*State) bool, -) Action { + if a.costFunc == nil { + a.costFunc = func(*State) bool { + cost := c.PlayCosts() + + if p.Resource < cost { + return false + } + p.Resource -= cost + return true + } + } + + s := p.gameState + if c.IsPermanent() { + a.targets = newTargets(s, []string{permanentPlayActionTarget}, a) + } else { + a.targets = newTargets(s, []string{}, a) + } + p.Hand.RemoveCard(c) - resolveFunc := func(s *State) { - s.ResolvePlay(p, c, targets) + a.resolveFunc = func(s *State) { + s.resolvePlay(p, c, a.targets) } - desc := fmt.Sprintf("play %s@%v", c.Name, targets) - return &PlayAction{ActionBase{p, c, targets, resolveFunc, costFunc, desc}} + return a } -type MoveAction struct { - ActionBase -} +func (a *PlayAction) String() string { + p := a.source.(*Player) + if a.targets.RequireTargets() { + return fmt.Sprintf(" %s play %s", p.Name, a.Card.Name) + } -type MoveActionPrototype struct { - Unit *Unit + return fmt.Sprintf("%s play %s@%v", p.Name, a.Card.Name, a.targets) } -func NewMoveActionPrototype(u *Unit) *MoveActionPrototype { - return &MoveActionPrototype{u} +type MoveAction struct { + ActionBase } -func (proto *MoveActionPrototype) String() string { - return fmt.Sprintf("move %s", proto.Unit.Movement.String()) -} +const moveActionTargetDesc = "available tile" + +func NewMoveAction(u *Unit) *MoveAction { + a := &MoveAction{ + ActionBase{ + source: u, + Card: u.Card(), + }, + } -func (proto *MoveActionPrototype) Specialize(s *State, args ...interface{}) Action { - target := args[0].(*Tile) - u := proto.Unit + a.targets = newTargets(u.Controller().gameState, []string{moveActionTargetDesc}, a) - resolveFunc := func(s *State) { - u.move(s, target) + a.resolveFunc = func(s *State) { + tile := a.targets.sel[0].(*Tile) + u.move(s, tile) } - costFunc := func(*State) bool { + a.costFunc = func(*State) bool { if u.AvailMoveActions < 1 { return false } @@ -122,35 +152,38 @@ func (proto *MoveActionPrototype) Specialize(s *State, args ...interface{}) Acti return true } - desc := fmt.Sprintf("-> %s", target.Position.String()) - return &MoveAction{ActionBase{u, u.Card(), target, resolveFunc, costFunc, desc}} + return a } -type AttackAction struct { - ActionBase -} +func (a *MoveAction) String() string { + u := a.source.(*Unit) + if a.targets.RequireTargets() { + return fmt.Sprintf("move %s", u.Movement.String()) + } -type AttackActionPrototype struct { - Unit *Unit + t := a.targets.sel[0].(*Tile) + return fmt.Sprintf("%s: %s -> %s", u.Controller().Name, u.Card().Name, t.Position.String()) } -func NewAttackActionPrototype(u *Unit) *AttackActionPrototype { - return &AttackActionPrototype{u} +type AttackAction struct { + ActionBase } -func (proto *AttackActionPrototype) String() string { - return fmt.Sprintf("attack %s", proto.Unit.Attack.String()) -} +const attackActionTargetDesc = "attackable permanent" + +func NewAttackAction(u *Unit) *AttackAction { + a := &AttackAction{ActionBase{ + source: u, + Card: u.Card(), + }} -func (proto *AttackActionPrototype) Specialize(s *State, args ...interface{}) Action { - target := args[0].(Permanent) - u := proto.Unit + a.targets = newTargets(u.Controller().gameState, []string{attackActionTargetDesc}, a) - resolveFunc := func(s *State) { - s.Fight(u, target) + a.resolveFunc = func(s *State) { + s.Fight(u, a.targets.sel[0].(Permanent)) } - costFunc := func(*State) bool { + a.costFunc = func(*State) bool { if u.AvailAttackActions < 1 { return false } @@ -158,27 +191,41 @@ func (proto *AttackActionPrototype) Specialize(s *State, args ...interface{}) Ac return true } - desc := fmt.Sprintf("x %s", target.Tile().Position.String()) - return &AttackAction{ActionBase{u, u.Card(), target, resolveFunc, costFunc, desc}} + return a } -type FullActionPrototype struct { - Unit *Unit - resolveFuncPrototype func(*State, *Unit) func(*State) -} +func (a *AttackAction) String() string { + u := a.source.(*Unit) + if a.targets.RequireTargets() { + return fmt.Sprintf("attack %s", u.Attack.String()) + } -func NewFullActionPrototype(u *Unit, proto func(*State, *Unit) func(*State)) *FullActionPrototype { - return &FullActionPrototype{u, proto} + target := a.targets.sel[0].(Permanent) + return fmt.Sprintf("%s: %s x %s", + u.Controller().Name, u.Card().Name, target.Tile().Position.String()) } -func (proto *FullActionPrototype) String() string { - return "full_action" +type FullAction struct { + ActionBase + desc string } -func (proto *FullActionPrototype) Specialize(s *State, args ...interface{}) Action { - u := proto.Unit - resolveFunc := proto.resolveFuncPrototype(s, u) - costFunc := func(*State) bool { +func NewFullAction(u *Unit, proto ActionFuncPrototype, desc string) *FullAction { + a := &FullAction{ + ActionBase{ + source: u, + Card: u.Card(), + }, + + desc, + } + + a.resolveFunc = func(s *State) { + rf := proto(a) + rf(s) + } + + a.costFunc = func(*State) bool { if u.AvailAttackActions < 1 && u.AvailMoveActions < 1 { return false } @@ -186,28 +233,50 @@ func (proto *FullActionPrototype) Specialize(s *State, args ...interface{}) Acti u.AvailMoveActions-- return true } - desc := "full_action" - return &ActionBase{u, u.Card(), nil, resolveFunc, costFunc, desc} -} - -type FreeActionPrototype struct { - Permanent Permanent + return a } -func (proto *FreeActionPrototype) String() string { - return "free_action" -} +func (a *FullAction) String() string { + u := a.source.(*Unit) + if a.targets.RequireTargets() { + return fmt.Sprintf("↻ %s", a.desc) + } -func (proto *FreeActionPrototype) Specialize(s *State, args ...interface{}) Action { - p := proto.Permanent - resolveFunc := args[0].(func(s *State)) - costFunc := args[1].(func(s *State) bool) - desc := "free_action" - return &FreeAction{ActionBase{p, p.Card(), nil, resolveFunc, costFunc, desc}} + return fmt.Sprintf("%s: %s ↻@%s", u.Controller(), u.Card().Name, a.targets) } type FreeAction struct { ActionBase + desc string +} + +func NewFreeAction(p Permanent, resolveProto ActionFuncPrototype, costFunc ActionCostFunc, + desc string, +) *FreeAction { + a := &FreeAction{ + ActionBase{ + source: p, + Card: p.Card(), + costFunc: costFunc, + }, + desc, + } + + a.resolveFunc = func(s *State) { + rf := resolveProto(a) + rf(s) + } + + return a +} + +func (a *FreeAction) String() string { + p := a.source.(Permanent) + if a.targets.RequireTargets() { + return fmt.Sprintf("free_action %s", a.desc) + } + + return fmt.Sprintf("%s: %s free_action@%s", p.Controller(), p.Card().Name, a.targets) } type BuyAction struct { @@ -215,6 +284,18 @@ type BuyAction struct { card *Card } +func (a *BuyAction) Source() interface{} { + return a.player +} + +func (a *BuyAction) Targets() *Targets { + return newTargetsWithSel(a.player.gameState, []string{"store cards"}, a, []interface{}{a.card}) +} + +func (a *BuyAction) CheckTargets(s *State) error { + return s.isValidBuy(a) +} + func (a *BuyAction) Resolve(s *State) { a.player.Store.MoveCard(a.card, a.player.DiscardPile) } diff --git a/go/game/ai.go b/go/game/ai.go index 01820a19..0adce722 100644 --- a/go/game/ai.go +++ b/go/game/ai.go @@ -138,17 +138,18 @@ func moveToRandomTile(ai *UnitAI) Action { return nil } - moveTargets := ai.u.MoveRangeTiles(ai.s.Map) - if len(moveTargets) == 0 { + a := NewMoveAction(ai.u) + options := a.Targets().NextOptions() + if len(options) == 0 { return nil } idx := 0 - if len(moveTargets) > 1 { - idx = ai.s.Rand.Intn(len(moveTargets) - 1) + if len(options) > 1 { + idx = ai.s.Rand.Intn(len(options) - 1) } - moveTarget := moveTargets[idx] - a := NewMoveActionPrototype(ai.u).Specialize(ai.s, moveTarget) + a.Targets().AddTarget(options[idx]) + return a } @@ -191,22 +192,28 @@ func moveTowardsNearestEnemyUnit(ai *UnitAI) Action { break } } - a := NewMoveActionPrototype(ai.u).Specialize(ai.s, ai.s.Map.TileAt(target)) + + a := NewMoveAction(ai.u) + a.Targets().AddTarget(ai.s.Map.TileAt(target)) return a } func attackAttackableEnemyPerm(ai *UnitAI) Action { - attackablePerms := ai.u.AttackableEnemyPermanents(ai.s.Map) - if ai.u.AvailAttackActions == 0 || len(attackablePerms) == 0 { + if ai.u.AvailAttackActions == 0 { + return nil + } + + a := NewAttackAction(ai.u) + options := a.Targets().NextOptions() + if len(options) == 0 { return nil } idx := 0 - if len(attackablePerms) > 1 { - idx = ai.s.Rand.Intn(len(attackablePerms) - 1) + if len(options) > 1 { + idx = ai.s.Rand.Intn(len(options) - 1) } - attackablePerm := attackablePerms[idx] - a := NewAttackActionPrototype(ai.u).Specialize(ai.s, attackablePerm) + a.Targets().AddTarget(options[idx]) return a } @@ -263,12 +270,18 @@ out: } } if !inEnemyAttackRange && ai.u.HasFullAction() { - fullActions := ai.u.Card().Impl.fullActionPrototypes(ai.u) + fullActions := ai.u.Card().Impl.fullActions(ai.u) idx := 0 if len(fullActions) > 1 { idx = ai.s.Rand.Intn(len(fullActions) - 1) } - a := fullActions[idx].Specialize(ai.s, ai.u) + + a := fullActions[idx] + options := a.Targets().NextOptions() + if len(options) > 1 { + idx = ai.s.Rand.Intn(len(options) - 1) + } + a.Targets().AddTarget(options[idx]) ai.actions <- a } diff --git a/go/game/artifact.go b/go/game/artifact.go index 23b653a6..acbaeb74 100644 --- a/go/game/artifact.go +++ b/go/game/artifact.go @@ -46,6 +46,6 @@ func (a *Artifact) Attackable() bool { return a.Solid > 0 } -func (a *Artifact) AvailActionPrototypes() []ActionPrototype { - return []ActionPrototype{} +func (a *Artifact) AvailActions() []Action { + return []Action{} } diff --git a/go/game/card.go b/go/game/card.go index d4515676..9013a059 100644 --- a/go/game/card.go +++ b/go/game/card.go @@ -73,8 +73,8 @@ var CardTypeNames = map[string]CardType{ type cardImplementation interface { spawnTiles(*State, *Player) []*Tile - fullActionPrototypes(*Unit) []*FullActionPrototype - freeActionPrototypes(Permanent) []*FreeActionPrototype + fullActions(*Unit) []*FullAction + freeActions(Permanent) []*FreeAction } // Default implementation of the cardImplementation interface. @@ -87,11 +87,11 @@ func (impl *cardImplementationBase) spawnTiles(*State, *Player) []*Tile { return nil } -func (impl *cardImplementationBase) fullActionPrototypes(*Unit) []*FullActionPrototype { +func (impl *cardImplementationBase) fullActions(*Unit) []*FullAction { return nil } -func (impl *cardImplementationBase) freeActionPrototypes(Permanent) []*FreeActionPrototype { +func (impl *cardImplementationBase) freeActions(Permanent) []*FreeAction { return nil } diff --git a/go/game/cardImplementations.go b/go/game/cardImplementations.go index d1f8de70..8024e4fc 100644 --- a/go/game/cardImplementations.go +++ b/go/game/cardImplementations.go @@ -8,8 +8,9 @@ type advisorImplementation struct { cardImplementationBase } -func (impl *advisorImplementation) fullActionPrototypes(u *Unit) []*FullActionPrototype { - resolvePrototype := func(s *State, u *Unit) func(*State) { +func (impl *advisorImplementation) fullAction(u *Unit) []*FullAction { + resolvePrototype := func(a Action) ActionResolveFunc { + u := a.Source().(*Unit) return func(s *State) { controller := u.Controller() controller.DrawN(1) @@ -19,12 +20,36 @@ func (impl *advisorImplementation) fullActionPrototypes(u *Unit) []*FullActionPr } } } - return []*FullActionPrototype{NewFullActionPrototype(u, resolvePrototype)} + return []*FullAction{NewFullAction(u, resolvePrototype, "draw 1. exile 1")} +} + +type missionaryImplementation struct { + cardImplementationBase +} + +func (impl *missionaryImplementation) fullAction(u *Unit) []*FullAction { + resolvePrototype := func(a Action) ActionResolveFunc { + u := a.Source().(*Unit) + target := a.Targets().sel[0].(*Unit) + return func(s *State) { + faith, found := target.States[UnitMarks.Faith] + if !found { + target.States[UnitMarks.Faith] = 2 + } else { + target.States[UnitMarks.Faith] = faith + 2 + if faith+2 > target.Card().BuyCost { + target.controller = u.Controller() + } + } + } + } + return []*FullAction{NewFullAction(u, resolvePrototype, "put faith ...")} } func init() { cardImplementations = map[string]cardImplementation{ - "base/advisor": &advisorImplementation{}, + "base/advisor": &advisorImplementation{}, + "base/missionary": &missionaryImplementation{}, "misc/farmer": &farmerImplementation{}, } diff --git a/go/game/cardParsing.go b/go/game/cardParsing.go index 3a316c4f..afdefe54 100644 --- a/go/game/cardParsing.go +++ b/go/game/cardParsing.go @@ -8,10 +8,10 @@ import ( ) type parsedCardImplementation struct { - card *Card - spawnTilesFunc func(*State, *Player) []*Tile - genFullActionPrototypes func(*Unit) []*FullActionPrototype - genFreeActionPrototypes func(Permanent) []*FreeActionPrototype + card *Card + spawnTilesFunc func(*State, *Player) []*Tile + genFullActions func(*Unit) []*FullAction + genFreeActions func(Permanent) []*FreeAction } func (impl *parsedCardImplementation) spawnTiles(s *State, p *Player) []*Tile { @@ -21,16 +21,16 @@ func (impl *parsedCardImplementation) spawnTiles(s *State, p *Player) []*Tile { return nil } -func (impl *parsedCardImplementation) fullActionPrototypes(u *Unit) []*FullActionPrototype { - if impl.genFullActionPrototypes != nil { - return impl.genFullActionPrototypes(u) +func (impl *parsedCardImplementation) fullActions(u *Unit) []*FullAction { + if impl.genFullActions != nil { + return impl.genFullActions(u) } return nil } -func (impl *parsedCardImplementation) freeActionPrototypes(p Permanent) []*FreeActionPrototype { - if impl.genFreeActionPrototypes != nil { - return impl.genFreeActionPrototypes(p) +func (impl *parsedCardImplementation) freeActions(p Permanent) []*FreeAction { + if impl.genFreeActions != nil { + return impl.genFreeActions(p) } return nil } @@ -55,7 +55,7 @@ func (impl *parsedCardImplementation) parseEffect(effect string) { } func (impl *parsedCardImplementation) parseFullAction(fullAction string) { - var resolveProtos []func(*State, *Unit) func(*State) + var resolveProtos []ActionFuncPrototype gainRessourceRegex := regexp.MustCompile(`gain [1-9][0-9]* resource`) if find := gainRessourceRegex.Find([]byte(fullAction)); find != nil { @@ -65,7 +65,8 @@ func (impl *parsedCardImplementation) parseFullAction(fullAction string) { log.Fatalf("Failed to parse resource gain full action: %s", err) } - resolveProto := func(_ *State, u *Unit) func(*State) { + resolveProto := func(a Action) ActionResolveFunc { + u := a.Source().(*Unit) return func(*State) { u.controller.gainResource(gain) } @@ -74,14 +75,14 @@ func (impl *parsedCardImplementation) parseFullAction(fullAction string) { } if resolveProtos != nil { - impl.genFullActionPrototypes = func(u *Unit) []*FullActionPrototype { - fullActionPrototypes := make([]*FullActionPrototype, 0, len(resolveProtos)) + impl.genFullActions = func(u *Unit) []*FullAction { + fullActions := make([]*FullAction, 0, len(resolveProtos)) for _, proto := range resolveProtos { - fullActionPrototypes = append(fullActionPrototypes, NewFullActionPrototype(u, proto)) + fullActions = append(fullActions, NewFullAction(u, proto, "")) } - return fullActionPrototypes + return fullActions } } } diff --git a/go/game/kraken.go b/go/game/kraken.go index 6ea22876..19adfd3c 100644 --- a/go/game/kraken.go +++ b/go/game/kraken.go @@ -10,23 +10,21 @@ const ( KRAKEN_NAME = "Kraken" ) -// var KrakenDecklist = `2 kraken/angry_squid -// 2 kraken/chachalot -// 2 kraken/clownfish -// 2 kraken/deja_vu! -// 2 kraken/devour_the_poor! -// 2 kraken/dolphin -// 2 kraken/flying_dutchmen -// 2 kraken/frost_pylon -// 2 kraken/gigantic_hail -// 2 kraken/sailfish -// 2 kraken/sold_out! -// 2 kraken/suprise! -// 2 kraken/tentacle_slap -// 2 kraken/tides_change! -// 2 kraken/unholy_cannonball` - -var KrakenDecklist = `2 kraken/clownfish` +var KrakenDecklist = `2 kraken/angry_squid +2 kraken/chachalot +2 kraken/clownfish +2 kraken/deja_vu! +2 kraken/devour_the_poor! +2 kraken/dolphin +2 kraken/flying_dutchmen +2 kraken/frost_pylon +2 kraken/gigantic_hail +2 kraken/sailfish +2 kraken/sold_out! +2 kraken/suprise! +2 kraken/tentacle_slap +2 kraken/tides_change! +2 kraken/unholy_cannonball` func NewKraken(id int, s *State) *Player { kraken := NewPlayer(id, KRAKEN_NAME, NewDeck(), s) @@ -88,6 +86,7 @@ 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 { @@ -107,23 +106,23 @@ func (ctrl *KrakenControl) krakenTurn() { return true } - // TODO: select targets - var targets interface{} - if c.IsPermanent() { - spawns := s.AvailableSpawnTiles(kraken, c) - if len(spawns) == 0 { - log.Println("No spawn available for", c.Name) + 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 + continue turn } idx := 0 - if len(spawns) > 1 { - idx = s.Rand.Intn(len(spawns) - 1) + if len(options) > 1 { + idx = s.Rand.Intn(len(options) - 1) } - targets = spawns[idx].Position + a.Targets().AddTarget(options[idx]) } - ctrl.actions <- NewPlayActionWithSpecialCosts(kraken, c, targets, costFunc) + + ctrl.actions <- a ctrl.awaitGameStateSync() } diff --git a/go/game/permanent.go b/go/game/permanent.go index 21655d71..b4090592 100644 --- a/go/game/permanent.go +++ b/go/game/permanent.go @@ -25,7 +25,8 @@ type Permanent interface { AddDamage(damage int) IsDestroyed() bool Attackable() bool - AvailActionPrototypes() []ActionPrototype + AvailActions() []Action + IsAvailableTile(*Tile) bool } type PermanentBase struct { @@ -124,3 +125,7 @@ func (p *PermanentBase) XEffect(effect string) (int, error) { return x, err } + +func (p *PermanentBase) IsAvailableTile(tile *Tile) bool { + return tile.IsAvailableForCard(p.card) +} diff --git a/go/game/state.go b/go/game/state.go index 816fc0d0..1d4b644f 100644 --- a/go/game/state.go +++ b/go/game/state.go @@ -102,11 +102,10 @@ func (s *State) IsValidPlay(a *PlayAction) error { } if a.Card.IsPermanent() { - pos := a.Targets.(Position) - spawn := s.Map.TileAt(pos) - if !slices.Contains(s.AvailableSpawnTiles(a.Source.(*Player), a.Card), spawn) { + spawn := a.Targets().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", - pos.String(), a.Card.Name) + spawn.Position.String(), a.Card.Name) } } @@ -118,8 +117,8 @@ func (s *State) IsValidMove(a *MoveAction) error { return fmt.Errorf("Move actions can only be declared during one's actions phase") } - u := a.Source.(*Unit) - tile := a.Targets.(*Tile) + u := a.Source().(*Unit) + tile := a.Targets().sel[0].(*Tile) if !slices.Contains(u.MoveRangeTiles(s.Map), tile) { return fmt.Errorf("Unit %s at %s can not move to %s", @@ -139,16 +138,12 @@ func (s *State) IsValidAttack(a *AttackAction) error { return fmt.Errorf("Attack actions can only be declared during one's actions phase") } - unit := a.Source.(Unit) + unit := a.Source().(Unit) if unit.Attack == INVALID_ATTACK() { return fmt.Errorf("%s can not attack", unit.card.Name) } - target := a.Targets.(Permanent) - if !target.Attackable() { - return fmt.Errorf("Target %s is not attackable", target.Tile().Position.String()) - } - + target := a.Targets().sel[0].(Permanent) if !IsPositionInRange(unit.tile.Position, target.Tile().Position, unit.Attack.Range) { return fmt.Errorf("Attack target on %s is not in attack range %d of %s on %s", target.Tile().Position.String(), unit.Attack.Range, unit.card.Name, @@ -158,7 +153,7 @@ func (s *State) IsValidAttack(a *AttackAction) error { return nil } -func (s *State) IsValidBuy(a *BuyAction) error { +func (s *State) isValidBuy(a *BuyAction) error { if s.Map.HasStores() { var storeTiles []*Tile for pos, store := range s.Map.Stores { @@ -189,8 +184,12 @@ func (s *State) IsValidBuy(a *BuyAction) error { return nil } -func (s *State) ValidateAction(a Action) error { - var err error = nil +func (s *State) ValidateAction(a Action) (err error) { + err = a.CheckTargets(s) + if err != nil { + return + } + switch a := a.(type) { case *PlayAction: err = s.IsValidPlay(a) @@ -199,9 +198,9 @@ func (s *State) ValidateAction(a Action) error { case *AttackAction: err = s.IsValidAttack(a) case *BuyAction: - err = s.IsValidBuy(a) } - return err + + return } func (s *State) PushAction(a Action) { @@ -214,18 +213,23 @@ func (s *State) PushAction(a Action) { func (s *State) DeclareAction(a Action) { var err error = nil - switch a := 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() { - err = fmt.Errorf("Slow action can not be declared while other actions are on the stack") + + err = a.CheckTargets(s) + + if err != nil { + switch a := 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() { + err = fmt.Errorf("Slow action can not be declared while other actions are on the stack") + } } } @@ -367,12 +371,14 @@ func (s *State) EnemyUnits(player *Player) []*Unit { }) } -func (s *State) ResolvePlay(p *Player, c *Card, targets interface{}) { +func (s *State) resolvePlay(p *Player, c *Card, targets *Targets) { switch c.Type { case CardTypes.Unit: - s.AddNewUnit(c, targets.(Position), p) + tile := targets.sel[0].(*Tile) + s.AddNewUnit(c, tile.Position, p) case CardTypes.Artifact: - s.AddNewArtifact(c, targets.(Position), p) + tile := targets.sel[0].(*Tile) + s.AddNewArtifact(c, tile.Position, p) default: log.Printf("Resolving unhandled card type: %s\n", c.Type.String()) } diff --git a/go/game/targets.go b/go/game/targets.go new file mode 100644 index 00000000..1a8c7ab3 --- /dev/null +++ b/go/game/targets.go @@ -0,0 +1,307 @@ +package game + +import ( + "fmt" + "log" + "strconv" + "strings" + + "golang.org/x/exp/slices" + "muhq.space/muhqs-game/go/utils" +) + +type ( + TargetConstrainFunc func(interface{}) error + TargetConstrain struct { + desc string + constrain TargetConstrainFunc + } +) + +type Targets struct { + s *State + action Action + constrains []*TargetConstrain + sel []interface{} +} + +func fmtSel(sel interface{}) string { + switch t := sel.(type) { + case *Tile: + return t.Position.String() + default: + return fmt.Sprintf("%d", t) + } +} + +func (t *Targets) String() string { + if len(t.sel) == 0 { + return "undecided targets" + } + + if len(t.sel) == 1 { + return fmt.Sprintf("%s", fmtSel(t.sel[0])) + } + + s := "[" + for _, sel := range t.sel { + s += fmt.Sprintf("%s ", fmtSel(sel)) + } + return s[:len(s)-1] + "]" +} + +func newTargetsWithSel(s *State, targets []string, action Action, sel []interface{}) *Targets { + t := &Targets{ + s: s, + sel: sel, + } + + t.buildConstrains(targets, action) + return t +} + +func newTargets(s *State, targets []string, action Action) *Targets { + return newTargetsWithSel(s, targets, action, make([]interface{}, 0, len(targets))) +} + +func (t *Targets) buildConstrains(targets []string, action Action) { + for _, desc := range targets { + c := &TargetConstrain{desc, targetConstrain(desc, t.s, action)} + t.constrains = append(t.constrains, c) + } +} + +func (t *Targets) RequireTargets() bool { + return len(t.constrains) != len(t.sel) +} + +func (t *Targets) Options() [][]interface{} { + options := make([][]interface{}, 0, len(t.constrains)) + for _, c := range t.constrains { + options = append(options, c.options(t.s)) + } + return options +} + +func (t *Targets) NextOptions() []interface{} { + next := len(t.sel) + return t.constrains[next].options(t.s) +} + +func (t *Targets) AddTarget(target interface{}) { + t.sel = append(t.sel, target) +} + +func (t *Targets) CheckTargets(s *State) error { + if t.RequireTargets() { + return fmt.Errorf("Number of selected targets and required ones does not match") + } + + for i, c := range t.constrains { + sel := t.sel[i] + if err := c.constrain(sel); err != nil { + return fmt.Errorf("selected target %s does not fit its desciption %s: %s", + sel, c.desc, err) + } + } + + return nil +} + +func targetConstrain(desc string, s *State, action Action) TargetConstrainFunc { + constrains := []TargetConstrainFunc{} + if strings.Contains(desc, "unit") { + constrains = append(constrains, parseUnitTargetConstrain(desc, s, action)...) + } else if strings.Contains(desc, "artifact") { + constrains = append(constrains, parseArtifactTargetConstrain(desc, s, action)...) + } else if strings.Contains(desc, "tile") { + constrains = append(constrains, parseTileTargetConstrain(desc, s, action)...) + // } else if strings.Contains("action") { + // contrains = append(contrains, parseActionTargetConstrain(desc, s, action)...) + // } else if strings.Contains("spell") { + // contrains = append(contrains, parseSpellTargetConstrain(desc, s, action)...) + } + + return func(t interface{}) error { + for _, constrain := range constrains { + if err := constrain(t); err != nil { + return err + } + } + + return nil + } +} + +func enemyPermanentTargetConstrain(action Action) TargetConstrainFunc { + var sourceController *Player + switch source := action.Source().(type) { + case *Player: + sourceController = source + case Permanent: + sourceController = source.Controller() + default: + log.Fatalf("Unhandled source type %T in enemyPermanentTargetConstrain", source) + } + + return func(t interface{}) (err error) { + p, _ := t.(Permanent) + if !p.Controller().IsEnemy(sourceController) { + err = fmt.Errorf("Controller %s of target %t is not an enemy of %s", + p.Controller(), t, sourceController) + } + return + } +} + +func rangeTargetConstrain(source interface{}, r int) TargetConstrainFunc { + var sourcePos Position + switch source := source.(type) { + case *Tile: + sourcePos = source.Position + case Permanent: + sourcePos = source.Tile().Position + default: + log.Fatalf("Unhandled source type %T in rangeTargetConstrain", source) + } + + return func(t interface{}) (err error) { + p, _ := t.(Permanent) + if !IsPositionInRange(sourcePos, p.Tile().Position, r) { + err = fmt.Errorf("Position %s of target %s not in range %d of source's position %s", + p.Tile().Position, t, r, sourcePos) + } + return + } +} + +var unitTargetConstrain TargetConstrainFunc = func(t interface{}) (err error) { + _, ok := t.(*Unit) + if !ok { + err = fmt.Errorf("target is %T not *Unit", t) + } + return +} + +func parseUnitTargetConstrain(desc string, s *State, action Action) []TargetConstrainFunc { + constrains := []TargetConstrainFunc{unitTargetConstrain} + + if strings.Contains(desc, "enemy unit") { + constrains = append(constrains, enemyPermanentTargetConstrain(action)) + } + + if strings.Contains(desc, " in range ") { + tokens := strings.SplitN(desc, " in range ", 2) + r, err := strconv.Atoi(tokens[1]) + if err != nil { + log.Fatalf("Invalid range %s in target constrain %s", tokens[1], desc) + } + constrains = append(constrains, rangeTargetConstrain(action, r)) + } + + return constrains +} + +var artifactTargetConstrain TargetConstrainFunc = func(t interface{}) (err error) { + _, ok := t.(*Artifact) + if !ok { + err = fmt.Errorf("target is %T not *Artifact", t) + } + return +} + +func parseArtifactTargetConstrain(constrain string, s *State, action Action) []TargetConstrainFunc { + constrains := []TargetConstrainFunc{artifactTargetConstrain} + return constrains +} + +var tileTargetConstrain TargetConstrainFunc = func(t interface{}) (err error) { + _, ok := t.(*Tile) + if !ok { + err = fmt.Errorf("target is %T not *Tile", t) + } + return +} + +func parseTileTargetConstrain(desc string, s *State, action Action) []TargetConstrainFunc { + log.Println("parsing", desc) + constrains := []TargetConstrainFunc{tileTargetConstrain} + + if moveAction, ok := action.(*MoveAction); ok { + u := moveAction.Source().(*Unit) + constrains = append(constrains, func(t interface{}) (err error) { + tile := t.(*Tile) + if slices.Contains(u.MoveRangeTiles(s.Map), tile) { + return nil + } + return fmt.Errorf("tile %s is not in %d movement range", tile) + }) + } + + var player *Player + switch a := action.(type) { + case *PlayAction: + player = a.Source().(*Player) + } + + var card *Card + switch a := action.(type) { + case *PlayAction: + card = a.Card + case *MoveAction: + card = a.Card + } + + if strings.Contains(desc, "spawn") { + if player == nil { + log.Fatalf("Not implemented spawn tile constrain for %T", action) + } + + constrains = append(constrains, func(t interface{}) (err error) { + tile := t.(*Tile) + if slices.Contains(s.AvailableSpawnTiles(player, card), tile) { + return nil + } + return fmt.Errorf("tile %s is no spawn tile", tile) + }) + } + + if strings.Contains(desc, "available") { + constrains = append(constrains, func(t interface{}) (err error) { + tile := t.(*Tile) + + if card == nil { + log.Fatalf("Not implemented availability tile constrain for %T", action) + } + + if tile.IsAvailableForCard(card) { + return nil + } + return fmt.Errorf("tile %s is not available for %s", tile, card.Name) + }) + } + + return constrains +} + +func (c *TargetConstrain) options(s *State) (options []interface{}) { + candidates := targetCandidates(c.desc, s) + + for _, candidate := range candidates { + if err := c.constrain(candidate); err == nil { + options = append(options, candidate) + } + } + return +} + +func targetCandidates(desc string, s *State) []interface{} { + if strings.Contains(desc, "unit") || strings.Contains(desc, "artifact") { + return utils.TypedSliceToInterfaceSlice(s.Permanents) + } else if strings.Contains(desc, "tile") { + return utils.TypedSliceToInterfaceSlice(s.Map.AllTiles()) + } + + log.Fatalf("Unimplemented target options for %s", desc) + return nil +} diff --git a/go/game/tile.go b/go/game/tile.go index 06af795c..260747d1 100644 --- a/go/game/tile.go +++ b/go/game/tile.go @@ -1,6 +1,7 @@ package game import ( + "fmt" "strings" ) @@ -78,6 +79,10 @@ type Tile struct { Raw string } +func (t *Tile) String() string { + return fmt.Sprintf("%d@%d", t.Type, t.Position.String()) +} + func INVALID_TILE() Tile { return Tile{INVALID_POSITION(), nil, 0, false, ""} } diff --git a/go/game/unit.go b/go/game/unit.go index f4f7d9cc..2bc90992 100644 --- a/go/game/unit.go +++ b/go/game/unit.go @@ -1,24 +1,65 @@ package game -type UnitState int +import ( + "log" +) + +type UnitMark int const ( - Paralysis UnitState = iota + 1 - Poison - Panic - Rage - Ward + paralysis UnitMark = iota + 1 + poison + panic_ + rage + ward + faith ) +func (mark UnitMark) String() string { + switch mark { + case paralysis: + return "paralysis" + case poison: + return "poison" + case panic_: + return "panic" + case rage: + return "rage" + case ward: + return "ward" + case faith: + return "faith" + default: + log.Fatalf("Unhandled unit mark %d", mark) + return "" + } +} + +var UnitMarks = struct { + Paralysis UnitMark + Poison UnitMark + Panic UnitMark + Rage UnitMark + Ward UnitMark + Faith UnitMark +}{ + Paralysis: paralysis, + Poison: poison, + Panic: panic_, + Rage: rage, + Ward: ward, + Faith: faith, +} + type Unit struct { PermanentBase Health int Movement Movement Attack Attack Upkeep int - FullActions []*FullActionPrototype - FreeActions []*FreeActionPrototype - States []UnitState + FullActions []*FullAction + FreeActions []*FreeAction + States map[UnitMark]int // Effects []Effects AvailMoveActions int AvailAttackActions int @@ -52,12 +93,13 @@ func NewUnit(card *Card, tile *Tile, owner *Player) *Unit { Movement: movement, Attack: attack, Upkeep: upkeep, + States: make(map[UnitMark]int), AvailMoveActions: 1, AvailAttackActions: 1, } - u.FullActions = card.Impl.fullActionPrototypes(u) - u.FreeActions = card.Impl.freeActionPrototypes(u) + u.FullActions = card.Impl.fullActions(u) + u.FreeActions = card.Impl.freeActions(u) tile.Permanent = u return u @@ -124,10 +166,6 @@ func (u *Unit) AttackableEnemyPermanents(m *Map) []Permanent { return permanents } -func (u *Unit) IsAvailableTile(tile *Tile) bool { - return tile.IsAvailableForCard(u.card) -} - func (u *Unit) Fight(p Permanent) { DealDamage(u, p, u.Attack.Damage) } @@ -147,26 +185,27 @@ func (u *Unit) HasFullAction() bool { func (u *Unit) FullAction() { } -func (u *Unit) AvailActionPrototypes() []ActionPrototype { - prototypes := []ActionPrototype{} +func (u *Unit) AvailActions() (actions []Action) { if u.AvailMoveActions > 0 && u.Movement != INVALID_MOVEMENT() { - prototypes = append(prototypes, NewMoveActionPrototype(u)) + actions = append(actions, NewMoveAction(u)) } m := u.Controller().gameState.Map if u.AvailAttackActions > 0 && u.Attack != INVALID_ATTACK() && len(u.AttackableEnemyPermanents(m)) > 0 { - prototypes = append(prototypes, NewAttackActionPrototype(u)) + actions = append(actions, NewAttackAction(u)) } - for _, p := range u.FullActions { - prototypes = append(prototypes, p) + if u.AvailAttackActions > 0 && u.AvailMoveActions > 0 { + for _, p := range u.FullActions { + actions = append(actions, p) + } } for _, p := range u.FreeActions { - prototypes = append(prototypes, p) + actions = append(actions, p) } - return prototypes + return actions } diff --git a/go/ui/choice.go b/go/ui/choice.go index 71308d1b..003c2a6a 100644 --- a/go/ui/choice.go +++ b/go/ui/choice.go @@ -59,7 +59,7 @@ func (c *Choice) Render() *ebiten.Image { return nil } -func NewActionChoice(x, y int, actions []game.ActionPrototype, onClick func(*Choice, int, int)) *Choice { +func NewActionChoice(x, y int, actions []game.Action, onClick func(*Choice, int, int)) *Choice { // TODO: use dynamic with width and height width := 200 choiceHeight := 40 diff --git a/go/ui/scrollable.go b/go/ui/scrollable.go new file mode 100644 index 00000000..90e0a3a5 --- /dev/null +++ b/go/ui/scrollable.go @@ -0,0 +1,5 @@ +package ui + +type Scrollable interface { + Scroll(x, y int) +} diff --git a/go/utils/slices.go b/go/utils/slices.go new file mode 100644 index 00000000..d2bc04c1 --- /dev/null +++ b/go/utils/slices.go @@ -0,0 +1,9 @@ +package utils + +func TypedSliceToInterfaceSlice[T any](s []T) []interface{} { + is := make([]interface{}, 0, len(s)) + for _, t := range s { + is = append(is, t) + } + return is +} |
