aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-02-08 17:50:10 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2025-08-20 15:50:10 +0200
commit45df9bf319d38db916ca983b8c92bb912f5ce9ee (patch)
treed7966002dad4a20bc3ef385bd152554fe278a6df
parenteb10f6f4940ecd09fd7d881fe5e1735ff146f919 (diff)
downloadmuhqs-game-45df9bf319d38db916ca983b8c92bb912f5ce9ee.tar.gz
muhqs-game-45df9bf319d38db916ca983b8c92bb912f5ce9ee.zip
intermediate commit
-rw-r--r--go/TODO3
-rw-r--r--go/client/game.go121
-rw-r--r--go/game/action.go194
-rw-r--r--go/game/ai.go52
-rw-r--r--go/game/areaEffect.go24
-rw-r--r--go/game/cardImplementations.go61
-rw-r--r--go/game/deck.go4
-rw-r--r--go/game/kraken.go23
-rw-r--r--go/game/map.go8
-rw-r--r--go/game/marks.go4
-rw-r--r--go/game/permanent.go6
-rw-r--r--go/game/player.go76
-rw-r--r--go/game/playerControl.go97
-rw-r--r--go/game/stack.go7
-rw-r--r--go/game/state.go48
-rw-r--r--go/game/targets.go220
-rw-r--r--go/game/tile.go37
-rw-r--r--go/game/unit.go37
-rw-r--r--go/go.mod2
-rw-r--r--go/ui/cardGrid.go3
-rw-r--r--go/ui/mapView.go4
-rw-r--r--go/ui/prompt.go38
-rw-r--r--go/ui/textBox.go21
-rw-r--r--go/utils/slices.go8
24 files changed, 707 insertions, 391 deletions
diff --git a/go/TODO b/go/TODO
index 23274884..74869443 100644
--- a/go/TODO
+++ b/go/TODO
@@ -1,8 +1,7 @@
-* implement upkeep action
-* implement target selection for triggers
* implement Pile UI hint
* implement spell target parsing / selection
* implement triggers
+ * implement target selection for triggers
* implement draft
* implement game log
* finish AIs
diff --git a/go/client/game.go b/go/client/game.go
index 76b04fa7..b459d42a 100644
--- a/go/client/game.go
+++ b/go/client/game.go
@@ -19,12 +19,15 @@ const (
RESOLVE_BUTTON_X = 500
STACK_BUFFER_WIDTH = 300
- HOVER_THRESHOLD = 60
+ HOVER_THRESHOLD = 60
+ HOVER_CARD_WIDTH = 500
+ HOVER_CARD_HEIGHT = 700
- HAND_VIEW_HEIGHT = 500
- HAND_VIEW_WIDTH = 500
+ HAND_VIEW_WIDTH = 500
STATE_BAR_WIDTH = 300
+
+ POC_BUTTON_WIDTH = 150
)
// A simple structure to detect if the cursor has not moved for HOVER_THRESHOLD
@@ -153,7 +156,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
@@ -175,7 +178,7 @@ func (g *Game) initPlayerUi(player *game.Player) *Game {
g.discardPileButton = ui.NewSimpleButton(g.stateBar.Width,
g.height-DEFAULT_BUTTON_HEIGHT,
- 150,
+ POC_BUTTON_WIDTH,
DEFAULT_BUTTON_HEIGHT,
"DiscardPile",
func(*ui.SimpleButton) {
@@ -196,7 +199,7 @@ func (g *Game) initPlayerUi(player *game.Player) *Game {
x = g.stateBar.Width + g.discardPileButton.Width
y = g.height - DEFAULT_BUTTON_HEIGHT
g.storeButton = ui.NewSimpleButton(x, y,
- 150,
+ POC_BUTTON_WIDTH,
DEFAULT_BUTTON_HEIGHT,
"Store",
func(*ui.SimpleButton) {
@@ -287,7 +290,11 @@ func (g *Game) addActionChoice(perm game.Permanent, x, y int) {
onClick := func(c *ui.Choice, x, y int) {
g.removeChoice()
a := actions[c.GetChoosen(x, y)]
- g.selectedObject = a
+ if a.Targets() != nil && a.Targets().RequireSelection() {
+ g.selectedObject = a
+ } else {
+ g.declareAction(a)
+ }
}
g.choice = ui.NewActionChoice(x, y, actions, onClick)
g.addWidget(g.choice)
@@ -303,8 +310,14 @@ func (g *Game) AddHighlight(obj interface{}) {
switch obj := obj.(type) {
case ui.HandCard:
g.handLayer.AddHighlightCard(obj.C)
+ case *game.Card:
+ g.handLayer.AddHighlightCard(obj)
case *game.Tile:
g.mapView.AddHighlightTile(obj)
+ case *game.Unit:
+ g.mapView.AddHighlightPermanent(obj)
+ default:
+ log.Fatalf("Unhandled highlight of type %T", obj)
}
}
@@ -338,6 +351,7 @@ func (g *Game) passPriority() {
func (g *Game) handlePlayerNotifications() {
n := g.playerCtrl.RecvNotification()
for n != nil {
+ log.Println("Received", n)
switch n.Notification {
case game.PriorityNotification:
g.showPassButton()
@@ -355,16 +369,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()
}
@@ -394,9 +407,12 @@ func (g *Game) findObjectAt(x, y int) interface{} {
func (g *Game) handleTargetSelection(obj interface{}) {
a := g.selectedObject.(game.Action)
- a.Targets().AddTarget(obj)
+ err := a.Targets().AddSelection(obj)
+ if err != nil {
+ log.Println("Not added", obj, "as target for", a, "because", err)
+ }
- if !a.Targets().RequireTargets() {
+ if !a.Targets().RequireSelection() {
g.declareAction(a)
g.selectedObject = nil
}
@@ -416,7 +432,14 @@ func (g *Game) handleSelection(obj interface{}, x, y int) {
obj.Click(x, y)
case *game.Tile:
- g.mapView.HighlightTile(obj)
+ if g.storesOnMap && obj.Type == game.TileTypes.Store && g.activePlayer.KnowsStore(obj.Position) {
+ if g.storeView != nil {
+ g.hideStore()
+ }
+
+ g.storeView = ui.NewPocList(x, y, g.gameState.Map.StoreOn(obj.Position))
+ g.showStore()
+ }
case game.Permanent:
perm := obj
@@ -426,12 +449,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)
}
@@ -444,7 +461,7 @@ func (g *Game) highlightTargets() {
return
}
- options := a.Targets().NextOptions()
+ options := a.Targets().Options()
for _, option := range options {
g.AddHighlight(option)
}
@@ -455,30 +472,36 @@ func (g *Game) handleHover(obj interface{}, x, y int) {
switch obj := obj.(type) {
case *game.Card:
wx, wy := x, y
- if x+500 > g.width || y+700 > g.height {
- wx = x - 500
- wy = y - 700
+ if x+HOVER_CARD_WIDTH > g.width || y+HOVER_CARD_HEIGHT > g.height {
+ wx = x - HOVER_CARD_WIDTH
+ wy = y - HOVER_CARD_HEIGHT
+ }
+ if wx < 0 {
+ wx = 0
}
- hoverHint = ui.NewScaledCardView(wx, wy, 500, 700, obj.Path())
+ if wy < 0 {
+ wy = 0
+ }
+ hoverHint = ui.NewScaledCardView(wx, wy, HOVER_CARD_WIDTH, HOVER_CARD_HEIGHT, obj.Path())
case *game.Unit:
hoverHint = ui.NewUnitInfo(x+ui.TILE_WIDTH, y, obj)
case game.Permanent:
hoverHint = ui.NewPermInfo(x+ui.TILE_WIDTH, y, obj)
- case *game.Tile:
- if g.storesOnMap && obj.Type == game.TileTypes.Store {
- poc := g.gameState.Map.StoreOn(obj.Position)
- hoverHint = ui.NewPocList(x, y, poc)
- // Set a special hoverDetector resetFunc keeping the hovering store
- // around and allow card selection until the cursor leaves the store widget.
- g.hoverDetector.resetFunc = func() bool {
- x, y := ebiten.CursorPosition()
- if hoverHint.Contains(x, y) {
- return false
- }
- g.removeWidget(hoverHint)
- return true
- }
- }
+ // case *game.Tile:
+ // if g.storesOnMap && obj.Type == game.TileTypes.Store {
+ // poc := g.gameState.Map.StoreOn(obj.Position)
+ // hoverHint = ui.NewPocList(x, y, poc)
+ // // Set a special hoverDetector resetFunc keeping the hovering store
+ // // around and allow card selection until the cursor leaves the store widget.
+ // g.hoverDetector.resetFunc = func() bool {
+ // x, y := ebiten.CursorPosition()
+ // if hoverHint.Contains(x, y) {
+ // return false
+ // }
+ // g.removeWidget(hoverHint)
+ // return true
+ // }
+ // }
}
if hoverHint != nil {
@@ -514,8 +537,12 @@ func (g *Game) Update() error {
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
if g.prompt != nil {
- g.prompt.Add(obj)
- g.AddHighlight(obj)
+ if err := g.prompt.Add(obj); err == nil {
+ g.AddHighlight(obj)
+ } else {
+ log.Println("Not added", obj, "to active prompt:", err)
+ g.handleSelection(obj, x, y)
+ }
} else {
g.handleSelection(obj, x, y)
}
@@ -529,6 +556,8 @@ func (g *Game) Update() error {
g.hoverDetector.reset()
g.clearMapHighlights()
g.handLayer.ClearHighlights()
+ g.hideStore()
+ g.removeWidget(g.discardPileView)
}
return nil
}
diff --git a/go/game/action.go b/go/game/action.go
index 5002f58a..77e9743f 100644
--- a/go/game/action.go
+++ b/go/game/action.go
@@ -13,6 +13,7 @@ type (
type Action interface {
Source() interface{}
Targets() *Targets
+ Target() *Target // Shortcut for to select the first target
CheckTargets(*State) error
PayCosts(s *State) bool
@@ -20,35 +21,40 @@ type Action interface {
String() string
}
-type PassPriority struct{ source *Player }
+type PassPriority struct{ player *Player }
-func (a *PassPriority) Source() interface{} { return a.source }
+func (a *PassPriority) Source() interface{} { return a.player }
func (*PassPriority) Targets() *Targets { return nil }
+func (*PassPriority) Target() *Target { return nil }
func (*PassPriority) CheckTargets(*State) error { return nil }
func (*PassPriority) PayCosts(*State) bool { return true }
func (*PassPriority) Resolve(*State) {}
func (*PassPriority) String() string { return "pass" }
func NewPassPriority(p *Player) Action { return &PassPriority{p} }
-type HandCardSelection struct{ cards []*Card }
-
-func (*HandCardSelection) Source() interface{} { return nil }
-func (*HandCardSelection) Targets() *Targets { return nil }
-func (*HandCardSelection) CheckTargets(*State) error { return nil }
-func (*HandCardSelection) PayCosts(*State) bool { return true }
-func (*HandCardSelection) Resolve(*State) {}
-func (*HandCardSelection) String() string { return "hand card selection" }
-func NewHandCardSelection(cards []*Card) Action { return &HandCardSelection{cards} }
-
-type TargetSelection struct{ targets *Targets }
+type TargetSelection struct {
+ player *Player
+ targets *Targets
+}
-func (*TargetSelection) Source() interface{} { return nil }
+func (sel *TargetSelection) Source() interface{} { return sel.player }
func (sel *TargetSelection) Targets() *Targets { return sel.targets }
+func (sel *TargetSelection) Target() *Target { return sel.targets.ts[0] }
func (sel *TargetSelection) CheckTargets(s *State) error { return sel.targets.CheckTargets(s) }
func (*TargetSelection) PayCosts(*State) bool { return true }
func (*TargetSelection) Resolve(*State) {}
func (*TargetSelection) String() string { return "target selection" }
-func NewTargetSelection(targets *Targets) Action { return &TargetSelection{targets} }
+
+func newTargetSelection(player *Player, targets *Targets) Action {
+ return &TargetSelection{player, targets}
+}
+
+func newHandCardSelection(p *Player, min, max int) Action {
+ a := &TargetSelection{player: p}
+ targetDesc := TargetDesc{"hand card", fmt.Sprintf("%d-%d", min, max)}
+ a.targets = newTargets(newTarget(p.gameState, targetDesc, a))
+ return a
+}
type ActionBase struct {
source interface{}
@@ -58,23 +64,15 @@ type ActionBase struct {
costFunc ActionCostFunc
}
-func (a *ActionBase) Source() interface{} {
- return a.source
-}
-
-func (a *ActionBase) Resolve(s *State) {
- a.resolveFunc(s)
-}
-
-func (a *ActionBase) PayCosts(s *State) bool {
- return a.costFunc(s)
-}
-
-func (a *ActionBase) Targets() *Targets {
- return a.targets
-}
-
+func (a *ActionBase) Source() interface{} { return a.source }
+func (a *ActionBase) Resolve(s *State) { a.resolveFunc(s) }
+func (a *ActionBase) PayCosts(s *State) bool { return a.costFunc(s) }
+func (a *ActionBase) Targets() *Targets { return a.targets }
+func (a *ActionBase) Target() *Target { return a.targets.ts[0] }
func (a *ActionBase) CheckTargets(s *State) error {
+ if a.targets == nil {
+ return nil
+ }
return a.targets.CheckTargets(s)
}
@@ -82,7 +80,7 @@ type PlayAction struct {
ActionBase
}
-const permanentPlayActionTarget = "available spawn tile"
+var permanentPlayActionTarget = TargetDesc{"available spawn tile", "1"}
func NewPlayAction(p *Player, c *Card, args ...interface{}) *PlayAction {
a := &PlayAction{ActionBase{
@@ -110,9 +108,11 @@ func NewPlayAction(p *Player, c *Card, args ...interface{}) *PlayAction {
s := p.gameState
if c.IsPermanent() {
- a.targets = newTargets(s, []string{permanentPlayActionTarget}, a)
+ a.targets = newTargets(newTarget(s, permanentPlayActionTarget, a))
} else {
- a.targets = newTargets(s, []string{}, a)
+ // TODO: implement parsing targets to play a card
+ // a.targets = newTargets(s, c.Impl.playTargets(s, p), a)
+ a.targets = newTargets()
}
a.resolveFunc = func(s *State) {
@@ -124,7 +124,7 @@ func NewPlayAction(p *Player, c *Card, args ...interface{}) *PlayAction {
func (a *PlayAction) String() string {
p := a.source.(*Player)
- if a.targets.RequireTargets() {
+ if a.targets.RequireSelection() {
return fmt.Sprintf(" %s play %s", p.Name, a.Card.Name)
}
@@ -135,14 +135,15 @@ type MoveAction struct {
ActionBase
}
-const moveActionTargetDesc = "available tile"
+var moveActionTargetDesc = TargetDesc{"available tile", "1"}
func (a *MoveAction) targetTile() *Tile {
- if t, ok := a.targets.sel[0].(*Tile); ok {
- return t
+ t := a.Target()
+ if tile, ok := t.sel[0].(*Tile); ok {
+ return tile
}
- return a.targets.sel[0].(*Unit).Tile()
+ return t.sel[0].(*Unit).Tile()
}
func NewMoveAction(u *Unit) *MoveAction {
@@ -153,7 +154,7 @@ func NewMoveAction(u *Unit) *MoveAction {
},
}
- a.targets = newTargets(u.Controller().gameState, []string{moveActionTargetDesc}, a)
+ a.targets = newTargets(newTarget(u.Controller().gameState, moveActionTargetDesc, a))
a.resolveFunc = func(s *State) {
tile := a.targetTile()
@@ -173,19 +174,19 @@ func NewMoveAction(u *Unit) *MoveAction {
func (a *MoveAction) String() string {
u := a.source.(*Unit)
- if a.targets.RequireTargets() {
+ if a.targets.RequireSelection() {
return fmt.Sprintf("move %s", u.Movement.String())
}
t := a.targetTile()
- return fmt.Sprintf("%v: %v -> %v", u.Controller().Name, u.Card().Name, t.Position)
+ return fmt.Sprintf("%s -> %v", u.FmtWController(), t.Position)
}
type AttackAction struct {
ActionBase
}
-const attackActionTargetDesc = "attackable enemy permanent"
+var attackActionTargetDesc = TargetDesc{"attackable enemy permanent", "1"}
func NewAttackAction(u *Unit) *AttackAction {
a := &AttackAction{ActionBase{
@@ -193,10 +194,10 @@ func NewAttackAction(u *Unit) *AttackAction {
Card: u.Card(),
}}
- a.targets = newTargets(u.Controller().gameState, []string{attackActionTargetDesc}, a)
+ a.targets = newTargets(newTarget(u.Controller().gameState, attackActionTargetDesc, a))
a.resolveFunc = func(s *State) {
- s.Fight(u, a.targets.sel[0].(Permanent))
+ s.Fight(u, a.Target().sel[0].(Permanent))
}
a.costFunc = func(*State) bool {
@@ -212,13 +213,12 @@ func NewAttackAction(u *Unit) *AttackAction {
func (a *AttackAction) String() string {
u := a.source.(*Unit)
- if a.targets.RequireTargets() {
+ if a.targets.RequireSelection() {
return fmt.Sprintf("attack %v", u.Attack)
}
- target := a.targets.sel[0].(Permanent)
- return fmt.Sprintf("%v: %v x %v",
- u.Controller().Name, u.Card().Name, target.Tile().Position)
+ target := a.Target().sel[0].(Permanent)
+ return fmt.Sprintf("%s x %v", u.FmtWController(), target.Tile().Position)
}
type FullAction struct {
@@ -255,11 +255,11 @@ func NewFullAction(u *Unit, proto ActionFuncPrototype, desc string) *FullAction
func (a *FullAction) String() string {
u := a.source.(*Unit)
- if a.targets.RequireTargets() {
+ if a.targets == nil || a.targets.RequireSelection() {
return fmt.Sprintf("↻ %s", a.desc)
}
- return fmt.Sprintf("%s: %s ↻@%v", u.Controller().Name, u.Card().Name, a.targets)
+ return fmt.Sprintf("%s ↻@%v", u.FmtWController(), a.targets)
}
type FreeAction struct {
@@ -289,80 +289,104 @@ func NewFreeAction(p Permanent, resolveProto ActionFuncPrototype, costFunc Actio
func (a *FreeAction) String() string {
p := a.source.(Permanent)
- if a.targets.RequireTargets() {
+ if a.targets.RequireSelection() {
return fmt.Sprintf("free_action %s", a.desc)
}
return fmt.Sprintf("%v: %s free_action@%v", p.Controller(), p.Card().Name, a.targets)
}
-type BuyAction struct {
- player *Player
- card *Card
-}
+var buyActionTargetDesc = TargetDesc{"store card", "?"}
-func (a *BuyAction) Source() interface{} {
- return a.player
-}
+type BuyAction struct{ TargetSelection }
-func (a *BuyAction) Targets() *Targets {
- return newTargetsWithSel(a.player.gameState, []string{"store cards"}, a, []interface{}{a.card})
+func (a *BuyAction) card() *Card {
+ return a.Target().sel[0].(*Card)
}
func (a *BuyAction) CheckTargets(s *State) error {
+ err := a.targets.CheckTargets(s)
+ if err != nil {
+ return err
+ }
return s.isValidBuy(a)
}
func (a *BuyAction) Resolve(s *State) {
- a.player.Store.MoveCard(a.card, a.player.DiscardPile)
+ a.player.Store.MoveCard(a.card(), a.player.DiscardPile)
}
func (a *BuyAction) PayCosts(*State) bool {
- if a.card.BuyCost < 0 || a.card.BuyCost > a.player.Resource {
+ cost := a.card().BuyCost
+ if cost < 0 || cost > a.player.Resource {
return false
}
- a.player.Resource -= a.card.BuyCost
+ a.player.Resource -= cost
return true
}
-func (a *BuyAction) String() string { return fmt.Sprintf("%s buy %s", a.player.Name, a.card.Name) }
+func (a *BuyAction) String() string {
+ if !a.Targets().HasSelections() {
+ return fmt.Sprintf("%s buy", a.player.Name)
+ }
-func NewBuyAction(p *Player, c *Card) *BuyAction {
- return &BuyAction{p, c}
+ return fmt.Sprintf("%s buy %s", a.player.Name, a.card().Name)
}
-type UpkeepAction struct {
- player *Player
- unit *Unit
- pay bool
+func newBuyAction(p *Player) *BuyAction {
+ a := &BuyAction{TargetSelection{player: p}}
+ a.targets = newTargets(newTarget(p.gameState, buyActionTargetDesc, a))
+ return a
}
-func (a *UpkeepAction) Resolve(s *State) {
- if a.pay {
- a.player.Resource -= a.unit.Upkeep
- } else {
- s.DestroyPermanent(a.unit)
- }
+var upkeepActionTargetDesc = TargetDesc{"unit you controll", "*"}
+
+type UpkeepAction struct {
+ TargetSelection
}
func (a *UpkeepAction) PayCosts(*State) bool {
- if a.unit.Upkeep > a.player.Resource {
+ costs := 0
+ for _, t := range a.Target().sel {
+ u := t.(*Unit)
+ costs += u.UpkeepCost()
+ }
+
+ if a.player.Resource < costs {
return false
}
- a.player.Resource -= a.unit.Upkeep
+ a.player.Resource -= costs
return true
}
-func (a *UpkeepAction) String() string {
- if a.pay {
- return fmt.Sprintf("%s keep %v", a.player.Name, a.unit.Tile().Position)
+func (a *UpkeepAction) Resolve(*State) {
+ p := a.player
+ s := p.gameState
+ for _, i := range a.Target().sel {
+ u := i.(*Unit)
+ s.DestroyPermanent(u)
}
- return fmt.Sprintf("%s disband %v", a.player.Name, a.unit.Tile().Position)
+ // Keep and pay for the rest
+ for _, u := range s.Units {
+ if u.Controller() != p {
+ continue
+ }
+
+ p.Resource -= u.upkeep
+ u.onUpkeep()
+ }
}
-func NewUpkeepAction(p *Player, u *Unit, pay bool) *UpkeepAction {
- return &UpkeepAction{p, u, pay}
+func (a *UpkeepAction) String() string {
+ return fmt.Sprintf("upkeep disbanding: %v", a.Target().sel)
+}
+
+func newUpkeepAction(p *Player) *UpkeepAction {
+ a := &UpkeepAction{TargetSelection{player: p}}
+ a.targets = newTargets(newTarget(p.gameState, upkeepActionTargetDesc, a))
+
+ return a
}
diff --git a/go/game/ai.go b/go/game/ai.go
index daeeff4f..9b48c2f5 100644
--- a/go/game/ai.go
+++ b/go/game/ai.go
@@ -3,6 +3,7 @@ package game
import (
"fmt"
"log"
+ "math/rand"
"strconv"
"strings"
"sync"
@@ -77,6 +78,24 @@ func NewUnitAI(s *State, u *Unit) *UnitAI {
return ai
}
+func selectRandomTargets(rand *rand.Rand, targets *Targets) error {
+ for _, t := range targets.ts {
+ for t.RequireSelection() {
+ options := t.Options()
+ if len(options) == 0 {
+ return fmt.Errorf("No possible selection")
+ }
+
+ idx := 0
+ if len(options) > 1 {
+ idx = rand.Intn(len(options) - 1)
+ }
+ _ = t.AddSelection(options[idx])
+ }
+ }
+ return nil
+}
+
func (m *Map) generateMapGraphFor(u *Unit) *dijkstra.Graph {
graph := dijkstra.NewGraph()
for _, t := range m.AllTiles() {
@@ -143,17 +162,11 @@ func moveToRandomTile(ai *UnitAI) Action {
}
a := NewMoveAction(ai.u)
- options := a.Targets().NextOptions()
- if len(options) == 0 {
+ err := selectRandomTargets(ai.s.Rand, a.Targets())
+ if err != nil {
return nil
}
- idx := 0
- if len(options) > 1 {
- idx = ai.s.Rand.Intn(len(options) - 1)
- }
- a.Targets().AddTarget(options[idx])
-
return a
}
@@ -198,7 +211,7 @@ func moveTowardsNearestEnemyUnit(ai *UnitAI) Action {
}
a := NewMoveAction(ai.u)
- a.Targets().AddTarget(ai.s.Map.TileAt(target))
+ _ = a.Target().AddSelection(ai.s.Map.TileAt(target))
return a
}
@@ -208,16 +221,10 @@ func attackAttackableEnemyPerm(ai *UnitAI) Action {
}
a := NewAttackAction(ai.u)
- options := a.Targets().NextOptions()
- if len(options) == 0 {
+ err := selectRandomTargets(ai.s.Rand, a.Targets())
+ if err != nil {
return nil
}
-
- idx := 0
- if len(options) > 1 {
- idx = ai.s.Rand.Intn(len(options) - 1)
- }
- a.Targets().AddTarget(options[idx])
return a
}
@@ -281,15 +288,10 @@ out:
}
a := fullActions[idx]
- for options := a.Targets().NextOptions(); options != nil; options = a.Targets().NextOptions() {
- idx = 0
- if len(options) > 1 {
- idx = ai.s.Rand.Intn(len(options) - 1)
- }
- a.Targets().AddTarget(options[idx])
+ err := selectRandomTargets(ai.s.Rand, a.Targets())
+ if err == nil {
+ ai.actions <- a
}
-
- ai.actions <- a
}
// 2.
diff --git a/go/game/areaEffect.go b/go/game/areaEffect.go
index 8991bc3c..4cba29e9 100644
--- a/go/game/areaEffect.go
+++ b/go/game/areaEffect.go
@@ -25,3 +25,27 @@ func (e *dynamicAreaEffect) onLeaving(p Permanent) {
e._onLeaving(p)
}
}
+
+func newGrantFullActionEffect(cardPath string, f ActionFuncPrototype, desc, tag string,
+) *dynamicAreaEffect {
+ onEntering := func(p Permanent) {
+ if p.Card().Path() != cardPath {
+ return
+ }
+
+ u := p.(*Unit)
+ fa := NewFullAction(u, f, desc)
+ fa.tag = tag
+ u.FullActions = append(u.FullActions, fa)
+ }
+
+ onLeaving := func(p Permanent) {
+ if p.Card().Path() != cardPath {
+ return
+ }
+ u := p.(*Unit)
+ u.removeFullAction(tag)
+ }
+
+ return newDynamicAreaEffect(onEntering, onLeaving)
+}
diff --git a/go/game/cardImplementations.go b/go/game/cardImplementations.go
index 3a7a729a..038ce2d6 100644
--- a/go/game/cardImplementations.go
+++ b/go/game/cardImplementations.go
@@ -34,15 +34,18 @@ type missionaryImpl struct{ cardImplementationBase }
func (*missionaryImpl) fullActions(u *Unit) []*FullAction {
resolvePrototype := func(a Action) ActionResolveFunc {
u := a.Source().(*Unit)
- target := a.Targets().sel[0].(*Unit)
+ target := a.Target().sel[0].(*Unit)
return func(s *State) {
- target.addMarks(UnitMarks.Faith, 2)
+ target.adjustMarks(UnitMarks.Faith, 2)
if target.Marks(UnitMarks.Faith) > target.Card().BuyCost {
target.controller = u.Controller()
}
}
}
- return []*FullAction{NewFullAction(u, resolvePrototype, "put faith ...")}
+ s := u.Controller().gameState
+ a := NewFullAction(u, resolvePrototype, "put faith ...")
+ a.targets = newTargets(newTarget(s, newTargetDesc("unit"), a))
+ return []*FullAction{a}
}
type swordImpl struct{ cardImplementationBase }
@@ -63,59 +66,33 @@ func (*misinformationImpl) onPlay(s *State, c *Card, _ *Player, _ *Targets) {
// ====== Nautics Set ======
-type fishTrapAoE struct{}
-
-func (*fishTrapAoE) onEntering(p Permanent) {
- if p.Card().Name != "nautics/fisher" {
- return
- }
-
- fisher := p.(*Unit)
- fishTrapAction := func(a Action) ActionResolveFunc {
+var fishTrapAoE areaEffect = newGrantFullActionEffect("nautics/fisher",
+ func(a Action) ActionResolveFunc {
u := a.Source().(*Unit)
return func(s *State) {
controller := u.Controller()
controller.gainResource(2)
}
- }
-
- fa := NewFullAction(fisher, fishTrapAction, "gain 2 resource")
- fa.tag = "fishTrapAction"
- fisher.FullActions = append(fisher.FullActions, fa)
-}
-
-func (*fishTrapAoE) onLeaving(p Permanent) {
- if p.Card().Name != "nautics/fisher" {
- return
- }
-
- fisher := p.(*Unit)
- for i, fa := range fisher.FullActions {
- if fa.tag != "fishTrapAction" {
- continue
- }
- fisher.FullActions[i] = fisher.FullActions[len(fisher.FullActions)-1]
- fisher.FullActions = fisher.FullActions[:len(fisher.FullActions)-1]
- break
- }
-}
+ }, "gain 2 resource", "fishTrapAction")
type fishTrapImpl struct {
cardImplementation
- aoe fishTrapAoE
+ aoe areaEffect
}
+func (*fishTrapImpl) stateBasedActions(s *State, p Permanent) {}
+
func (i *fishTrapImpl) onEntering(t *Tile) {
s := t.Permanent.Controller().gameState
for _, tile := range TilesInRange(s.Map, t.Permanent, 1) {
- tile.addEffect(&i.aoe)
+ tile.addEffect(i.aoe)
}
}
func (i *fishTrapImpl) onLeaving(t *Tile) {
s := t.Permanent.Controller().gameState
for _, tile := range TilesInRange(s.Map, t.Permanent, 1) {
- tile.removeEffect(&i.aoe)
+ tile.removeEffect(i.aoe)
}
}
@@ -126,12 +103,12 @@ func (*fisherImpl) fullActions(u *Unit) []*FullAction {
resolvePrototype := func(a Action) ActionResolveFunc {
u := a.Source().(*Unit)
return func(s *State) {
- t := a.Targets().sel[0].(*Tile)
+ t := a.Target().sel[0].(*Tile)
s.AddNewArtifact(NewCard("nautics/fish_trap"), t.Position, u.Controller())
}
}
a := NewFullAction(u, resolvePrototype, "create fish trap")
- a.targets = newTargets(s, []string{"adjacent water tile"}, a)
+ a.targets = newTargets(newTarget(s, newTargetDesc("adjacent free water tile"), a))
return []*FullAction{a}
}
@@ -164,7 +141,7 @@ func (*sailorImpl) onUnpile(containing Permanent) {
type tidesChangeImpl struct{ cardImplementation }
func (*tidesChangeImpl) onPlay(s *State, _ *Card, _ *Player, _ *Targets) {
- s.Map.redistributeStoreCards(s.Rand)
+ s.redistributeMapStoreCards()
}
type dejaVuImpl struct{ cardImplementation }
@@ -327,7 +304,7 @@ func (*unholyCannonballImpl) onPlay(s *State, _ *Card, kraken *Player, _ *Target
kraken.gainResource(5)
} else {
for _, u := range enemiesInRange2 {
- u.addMarks(UnitStates.Panic, 1)
+ u.adjustMarks(UnitStates.Panic, 1)
}
}
}
@@ -341,7 +318,7 @@ func init() {
"base/misinformation": &misinformationImpl{},
"nautics/captain": &captainImpl{},
- "nautics/fish_trap": &fishTrapImpl{},
+ "nautics/fish_trap": &fishTrapImpl{aoe: fishTrapAoE},
"nautics/fisher": &fisherImpl{},
"nautics/sailor": &sailorImpl{},
diff --git a/go/game/deck.go b/go/game/deck.go
index 569a952d..1f9539a9 100644
--- a/go/game/deck.go
+++ b/go/game/deck.go
@@ -59,6 +59,10 @@ func NewDeckFromCardPaths(cardPaths []string) *Deck {
func NewDeckFromDeckList(deckList string) *Deck {
cardPaths := []string{}
+
+ // trim last trailing newline
+ deckList = strings.Trim(deckList, "\n")
+
lines := strings.Split(deckList, "\n")
for _, line := range lines {
tokens := strings.SplitN(line, " ", 2)
diff --git a/go/game/kraken.go b/go/game/kraken.go
index d92d204b..97929814 100644
--- a/go/game/kraken.go
+++ b/go/game/kraken.go
@@ -76,6 +76,10 @@ func addKrakenControl(kraken *Player) {
}()
}
+func (c *KrakenControl) Player() *Player {
+ return c.kraken
+}
+
func (ctrl *KrakenControl) awaitGameStateSync() {
ctrl.syncGameState.Wait()
ctrl.syncGameState.Add(1)
@@ -93,7 +97,6 @@ func (ctrl *KrakenControl) krakenTurn() {
log.Fatalf("Ai turn out of sync %d != %d", ctrl.turn, kraken.Turn)
}
-turn:
for {
drawn := kraken.Deck.Draw(1)
if len(drawn) == 0 {
@@ -114,19 +117,11 @@ turn:
}
a := NewPlayAction(kraken, c, costFunc)
- for a.Targets().RequireTargets() {
- options := a.Targets().NextOptions()
- if len(options) == 0 {
- log.Println("No target available for", c.Name)
- kraken.DiscardPile.AddCard(c)
- continue turn
- }
-
- idx := 0
- if len(options) > 1 {
- idx = s.Rand.Intn(len(options) - 1)
- }
- a.Targets().AddTarget(options[idx])
+ err := selectRandomTargets(s.Rand, a.Targets())
+ if err != nil {
+ log.Println("No target available for", c.Name)
+ kraken.DiscardPile.AddCard(c)
+ continue
}
ctrl.actions <- a
diff --git a/go/game/map.go b/go/game/map.go
index b86debad..5ed1335e 100644
--- a/go/game/map.go
+++ b/go/game/map.go
@@ -290,14 +290,6 @@ func (m *Map) distributeStoreCards(cards PileOfCards, rand *rand.Rand) {
}
}
-func (m *Map) redistributeStoreCards(rand *rand.Rand) {
- poc := NewPileOfCards()
- for _, store := range m.Stores {
- store.MoveInto(poc)
- }
- m.distributeStoreCards(poc, rand)
-}
-
func (m *Map) HasStores() bool {
return len(m.Stores) > 0
}
diff --git a/go/game/marks.go b/go/game/marks.go
index a3ea0e32..0764b531 100644
--- a/go/game/marks.go
+++ b/go/game/marks.go
@@ -45,17 +45,19 @@ var (
Crack: crack,
}
- unitStates = []PermanentMark{paralysis, poison, panic_, rage}
+ // unitStates = []PermanentMark{paralysis, poison, panic_, rage}
UnitStates = struct {
Paralysis PermanentMark
Poison PermanentMark
Panic PermanentMark
Rage PermanentMark
+ Ward PermanentMark
}{
Paralysis: paralysis,
Poison: poison,
Panic: panic_,
Rage: rage,
+ Ward: ward,
}
UnitMarks = struct {
diff --git a/go/game/permanent.go b/go/game/permanent.go
index f25c95b3..ecc4459a 100644
--- a/go/game/permanent.go
+++ b/go/game/permanent.go
@@ -22,7 +22,7 @@ type Permanent interface {
HasEffect(string) bool
XEffect(string) (int, error)
Marks(PermanentMark) int
- addMarks(PermanentMark, int)
+ adjustMarks(PermanentMark, int)
String() string
Fight(p Permanent)
@@ -137,7 +137,7 @@ func (p *PermanentBase) Marks(mark PermanentMark) int {
return 0
}
-func (p *PermanentBase) addMarks(mark PermanentMark, amount int) {
+func (p *PermanentBase) adjustMarks(mark PermanentMark, amount int) {
if amount, found := p.marks[mark]; found {
p.marks[mark] += amount
} else {
@@ -195,8 +195,8 @@ func DropPile(containing Permanent) {
func enterTile(p Permanent, t *Tile) {
p.SetTile(t)
- p.Card().Impl.onEntering(t)
t.entering(p)
+ p.Card().Impl.onEntering(t)
}
func leaveTile(p Permanent) {
diff --git a/go/game/player.go b/go/game/player.go
index 78b4637e..a69f2cd3 100644
--- a/go/game/player.go
+++ b/go/game/player.go
@@ -3,6 +3,8 @@ package game
import (
"image/color"
"log"
+
+ "muhq.space/muhqs-game/go/utils"
)
const (
@@ -22,6 +24,7 @@ type Player struct {
gameState *State
Color color.Color
Ctrl PlayerControl
+ knownStores map[Position]bool
}
func NewPlayer(id int, name string, deck *Deck, gameState *State) *Player {
@@ -40,6 +43,7 @@ func NewPlayer(id int, name string, deck *Deck, gameState *State) *Player {
Store: store,
gameState: gameState,
Ctrl: nil,
+ knownStores: make(map[Position]bool),
}
return &p
}
@@ -62,7 +66,7 @@ func (p *Player) UpkeepCost() int {
if u.Controller() != p {
continue
}
- cost += u.Upkeep
+ cost += u.upkeep
}
return cost
}
@@ -76,15 +80,26 @@ func (p *Player) Upkeep() {
// TODO: handle upkeep triggers
- for _, unit := range p.gameState.Units {
- if unit.controller != p {
- continue
+ // Skip upkeep prompt if player does not controll any units
+ controllsUnits := false
+ for _, u := range p.gameState.Units {
+ if u.Controller() == p {
+ controllsUnits = true
+ break
}
+ }
+ if !controllsUnits {
+ return
+ }
+
+ a := prompt(p.Ctrl, newUpkeepPrompt(p))
- // TODO: let players decide if they want to pay the upkeep
- p.Resource -= unit.Upkeep
- unit.onUpkeep()
+ if _, ok := a.(*PassPriority); ok {
+ a = newUpkeepAction(p)
}
+
+ p.gameState.DeclareAction(a)
+ p.gameState.Stack.Resolve()
}
func (p *Player) ActionPhase() {
@@ -100,11 +115,15 @@ func (p *Player) ActionPhase() {
func (p *Player) BuyPhase() {
a := promptBuy(p.Ctrl)
+
if _, ok := a.(*PassPriority); ok {
return
}
- p.gameState.DeclareAction(a)
- p.gameState.Stack.Resolve()
+
+ if a.Targets().HasSelections() {
+ p.gameState.DeclareAction(a)
+ p.gameState.Stack.Resolve()
+ }
}
func (p *Player) discardHand() {
@@ -118,6 +137,10 @@ func (p *Player) DiscardCard(c *Card) {
}
func (p *Player) DiscardPhase() {
+ if p.Hand.Size() == 0 {
+ return
+ }
+
cards := p.PromptHandCardSelection(0, p.Hand.Size())
for _, c := range cards {
p.DiscardCard(c)
@@ -125,11 +148,15 @@ func (p *Player) DiscardPhase() {
}
func (p *Player) PromptHandCardSelection(min, max int) []*Card {
- p.Ctrl.SendNotification(NewHandCardSelectionPrompt(min, max))
+ p.Ctrl.SendNotification(newHandCardSelectionPrompt(p, min, max))
switch a := p.Ctrl.RecvAction().(type) {
- case *HandCardSelection:
- // TODO: validate cards
- return a.cards
+ case *TargetSelection:
+ err := a.CheckTargets(p.gameState)
+ if err != nil {
+ log.Fatalf("Invalid hand card selection: %v", err)
+ }
+
+ return utils.InterfaceSliceToTypedSlice[*Card](a.Target().sel)
case *PassPriority:
return nil
default:
@@ -156,3 +183,26 @@ func (p *Player) IsEnemy(other *Player) bool {
// TODO: support coop / teams
return p != other
}
+
+func (p *Player) AvailableStores() (stores []*Store) {
+ m := p.gameState.Map
+ for pos, store := range m.Stores {
+ t := m.TileAt(pos)
+ if t.Permanent != nil && t.Permanent.Controller() == p {
+ stores = append(stores, store)
+ }
+ }
+ return
+}
+
+func (p *Player) KnowsStore(pos Position) bool {
+ return p.knownStores[pos]
+}
+
+func (p *Player) addKnownStore(pos Position) {
+ p.knownStores[pos] = true
+}
+
+func (p *Player) clearKnownStore() {
+ p.knownStores = make(map[Position]bool)
+}
diff --git a/go/game/playerControl.go b/go/game/playerControl.go
index e62d91b3..eea6a19b 100644
--- a/go/game/playerControl.go
+++ b/go/game/playerControl.go
@@ -1,16 +1,35 @@
package game
+import (
+ "fmt"
+ "log"
+)
+
type PlayerNotificationType int
const (
- DeclaredActionNotification = iota
+ DeclaredActionNotification PlayerNotificationType = iota
ResolvedActionNotification
PriorityNotification
- UpkeepPrompt
- BuyPrompt
- HandCardSelectionPrompt
+ TargetSelectionPrompt
)
+func (n PlayerNotificationType) String() string {
+ switch n {
+ case DeclaredActionNotification:
+ return "DeclaredActionNotification"
+ case ResolvedActionNotification:
+ return "ResolvedActionNotification"
+ case PriorityNotification:
+ return "PriorityNotification"
+ case TargetSelectionPrompt:
+ return "TargetSelectionPrompt"
+ default:
+ log.Panicf("Unhandled notification %d", n)
+ return ""
+ }
+}
+
func (n PlayerNotification) IsPriorityNotification() bool {
return n.Notification == PriorityNotification
}
@@ -21,72 +40,92 @@ type PlayerNotification struct {
Error error
}
-func NewPriorityNotification() PlayerNotification {
+func (n *PlayerNotification) String() string {
+ if n.Error != nil {
+ return fmt.Sprintf("error %v: %v", n.Notification, n.Error)
+ }
+
+ return fmt.Sprintf("%v: %v", n.Notification, n.Context)
+}
+
+type TargetSelectionCtx struct {
+ Action Action
+ Prompt string
+}
+
+func newPriorityNotification() PlayerNotification {
return PlayerNotification{PriorityNotification, nil, nil}
}
-func NewDeclaredActionNotification(a Action, err error) PlayerNotification {
+func newDeclaredActionNotification(a Action, err error) PlayerNotification {
return PlayerNotification{DeclaredActionNotification, a, err}
}
-func NewResolvedActionNotification(a Action, err error) PlayerNotification {
+func newResolvedActionNotification(a Action, err error) PlayerNotification {
return PlayerNotification{ResolvedActionNotification, a, err}
}
-func NewUpkeepPrompt(u *Unit) PlayerNotification {
- return PlayerNotification{ResolvedActionNotification, u, nil}
+func newTargetSelectionPrompt(a Action, desc string) PlayerNotification {
+ return PlayerNotification{TargetSelectionPrompt, TargetSelectionCtx{a, desc}, nil}
}
-func NewBuyPrompt() PlayerNotification {
- return PlayerNotification{BuyPrompt, nil, nil}
+func newUpkeepPrompt(p *Player) PlayerNotification {
+ a := newUpkeepAction(p)
+ return newTargetSelectionPrompt(a, "Select units to disband")
}
-func NewHandCardSelectionPrompt(min, max int) PlayerNotification {
- return PlayerNotification{HandCardSelectionPrompt, []int{min, max}, nil}
+func newBuyPrompt(p *Player) PlayerNotification {
+ a := newBuyAction(p)
+ prompt := newTargetSelectionPrompt(a, "Select a card to buy")
+ return prompt
+}
+
+func newHandCardSelectionPrompt(p *Player, min, max int) PlayerNotification {
+ a := newHandCardSelection(p, min, max)
+ desc := fmt.Sprintf("Select between %d and %d hand cards", min, max)
+ return newTargetSelectionPrompt(a, desc)
}
type PlayerControl interface {
+ Player() *Player
RecvAction() Action
SendNotification(PlayerNotification)
}
-type DummyPlayerControl struct{ P *Player }
-
-func (c *DummyPlayerControl) RecvAction() Action { return NewPassPriority(c.P) }
-func (*DummyPlayerControl) SendNotification(PlayerNotification) {}
-
type ChanPlayerControl struct {
- Actions chan Action
- Notifications chan PlayerNotification
+ player *Player
+ actions chan Action
+ notifications chan PlayerNotification
}
-func (c *ChanPlayerControl) SendAction(a Action) { c.Actions <- a }
-func (c *ChanPlayerControl) RecvAction() Action { return <-c.Actions }
-func (c *ChanPlayerControl) SendNotification(n PlayerNotification) { c.Notifications <- n }
+func (c *ChanPlayerControl) Player() *Player { return c.player }
+func (c *ChanPlayerControl) SendAction(a Action) { c.actions <- a }
+func (c *ChanPlayerControl) RecvAction() Action { return <-c.actions }
+func (c *ChanPlayerControl) SendNotification(n PlayerNotification) { c.notifications <- n }
func (c *ChanPlayerControl) RecvNotification() *PlayerNotification {
select {
- case n := <-c.Notifications:
+ case n := <-c.notifications:
return &n
default:
return nil
}
}
-func NewChanPlayerControl() *ChanPlayerControl {
+func NewChanPlayerControl(p *Player) *ChanPlayerControl {
a := make(chan Action)
n := make(chan PlayerNotification)
- return &ChanPlayerControl{a, n}
+ return &ChanPlayerControl{p, a, n}
}
func prompt(ctrl PlayerControl, notification PlayerNotification) Action {
- ctrl.SendNotification(NewPriorityNotification())
+ ctrl.SendNotification(notification)
return ctrl.RecvAction()
}
func promptBuy(ctrl PlayerControl) Action {
- return prompt(ctrl, NewBuyPrompt())
+ return prompt(ctrl, newBuyPrompt(ctrl.Player()))
}
func promptAction(ctrl PlayerControl) Action {
- return prompt(ctrl, NewPriorityNotification())
+ return prompt(ctrl, newPriorityNotification())
}
diff --git a/go/game/stack.go b/go/game/stack.go
index cf0622ce..78a5ff41 100644
--- a/go/game/stack.go
+++ b/go/game/stack.go
@@ -33,9 +33,14 @@ func (s *Stack) pop() {
err := s.gameState.ValidateAction(a)
if err == nil {
a.Resolve(s.gameState)
+ // Some action may be reused like full actions of permanents.
+ // Reset their target selection to allow possible reuse.
+ if t := a.Targets(); t != nil {
+ t.ClearSelections()
+ }
}
log.Println("Resolved", a, err)
- s.gameState.broadcastNotification(NewResolvedActionNotification(a, err))
+ s.gameState.broadcastNotification(newResolvedActionNotification(a, err))
}
func (s *Stack) Resolve() {
diff --git a/go/game/state.go b/go/game/state.go
index 98ac9c31..5c2fd053 100644
--- a/go/game/state.go
+++ b/go/game/state.go
@@ -111,7 +111,7 @@ func (s *State) IsValidPlay(a *PlayAction) error {
}
if a.Card.IsPermanent() {
- spawn := a.Targets().sel[0].(*Tile)
+ spawn := a.Target().sel[0].(*Tile)
if !slices.Contains(s.AvailableSpawnTiles(a.Source().(*Player), a.Card), spawn) {
return fmt.Errorf("Spawn %v is not a valid spawn tile for %s",
spawn.Position.String(), a.Card.Name)
@@ -127,7 +127,7 @@ func (s *State) IsValidMove(a *MoveAction) error {
}
u := a.Source().(*Unit)
- tile := relaxedTileTarget(a, a.Targets().sel[0])
+ tile := relaxedTileTarget(a, a.Target().sel[0])
if !slices.Contains(u.MoveRangeTiles(s.Map), tile) {
return fmt.Errorf("Unit %s@%v can not move to %s",
@@ -152,7 +152,7 @@ func (s *State) IsValidAttack(a *AttackAction) error {
return fmt.Errorf("%s can not attack", unit.card.Name)
}
- target := a.Targets().sel[0].(Permanent)
+ target := a.Target().sel[0].(Permanent)
if !IsPositionInRange(unit.tile.Position, target.Tile().Position, unit.Attack.MaxRange()) {
return fmt.Errorf("Attack target on %s is not in attack range %d of %s on %s",
target.Tile().Position.String(), unit.Attack.MaxRange(), unit.card.Name,
@@ -163,16 +163,17 @@ func (s *State) IsValidAttack(a *AttackAction) error {
}
func (s *State) isValidBuy(a *BuyAction) error {
+ card := a.card()
if s.Map.HasStores() {
var storeTiles []*Tile
for pos, store := range s.Map.Stores {
- if slices.Contains(store.cards, a.card) {
+ if slices.Contains(store.cards, card) {
storeTiles = append(storeTiles, s.Map.TileAt(pos))
}
}
if storeTiles == nil {
- return fmt.Errorf("no store contains %s", a.card.Name)
+ return fmt.Errorf("no store contains %s", card.Name)
}
controlsUnitOnStore := false
@@ -183,11 +184,15 @@ func (s *State) isValidBuy(a *BuyAction) error {
}
if !controlsUnitOnStore {
return fmt.Errorf("%s' controls no unit on a store containing %s",
- a.player.Name, a.card.Name)
+ a.player.Name, card.Name)
}
- } else if !slices.Contains(a.player.Store.cards, a.card) {
- return fmt.Errorf("%s's store does not contain %s", a.player.Name, a.card.Name)
+ } else if !slices.Contains(a.player.Store.cards, card) {
+ return fmt.Errorf("%s's store does not contain %s", a.player.Name, card.Name)
+ }
+
+ if !a.card().IsBuyable() {
+ return fmt.Errorf("Card %s is not buyable", card.Name)
}
return nil
@@ -226,14 +231,11 @@ func (s *State) DeclareAction(a Action) {
err = a.CheckTargets(s)
if err != nil {
- switch a := a.(type) {
+ switch a.(type) {
case *BuyAction:
if s.ActivePhase != Phases.BuyPhase {
err = fmt.Errorf("Cards can only be bought during one's buy phase")
}
- if !a.card.IsBuyable() {
- err = fmt.Errorf("Card %s is not buyable", a.card.Name)
- }
case *FreeAction:
default:
if !s.Stack.IsEmpty() {
@@ -249,7 +251,7 @@ func (s *State) DeclareAction(a Action) {
s.pushAction(a)
}
- s.broadcastNotification(NewDeclaredActionNotification(a, err))
+ s.broadcastNotification(newDeclaredActionNotification(a, err))
}
func (s *State) AllPassing(skipFirst bool) bool {
@@ -260,7 +262,7 @@ func (s *State) AllPassing(skipFirst bool) bool {
}
for ; i < nPlayers; i++ {
p := s.Players[(i+s.ActivePlayer.Id-1)%nPlayers]
- p.Ctrl.SendNotification(NewPriorityNotification())
+ p.Ctrl.SendNotification(newPriorityNotification())
a := p.Ctrl.RecvAction()
switch a.(type) {
case *PassPriority:
@@ -385,13 +387,13 @@ func (s *State) EnemyUnits(player *Player) []*Unit {
func (s *State) resolvePlay(p *Player, c *Card, targets *Targets) {
switch c.Type {
case CardTypes.Unit:
- tile := targets.sel[0].(*Tile)
+ tile := targets.ts[0].sel[0].(*Tile)
s.AddNewUnit(c, tile.Position, p)
case CardTypes.Artifact:
- tile := targets.sel[0].(*Tile)
+ tile := targets.ts[0].sel[0].(*Tile)
s.AddNewArtifact(c, tile.Position, p)
case CardTypes.Equipment:
- if tile, ok := targets.sel[0].(*Tile); ok {
+ if tile, ok := targets.ts[0].sel[0].(*Tile); ok {
s.addNewEquipment(c, tile.Position, p)
}
case CardTypes.Spell:
@@ -469,3 +471,15 @@ func (s *State) AvailableSpawnTiles(p *Player, c *Card) []*Tile {
return tiles
}
+
+func (s *State) redistributeMapStoreCards() {
+ poc := NewPileOfCards()
+ for _, store := range s.Map.Stores {
+ store.MoveInto(poc)
+ }
+ s.Map.distributeStoreCards(poc, s.Rand)
+
+ for _, p := range s.Players {
+ p.clearKnownStore()
+ }
+}
diff --git a/go/game/targets.go b/go/game/targets.go
index eb429c8c..440d8922 100644
--- a/go/game/targets.go
+++ b/go/game/targets.go
@@ -12,17 +12,28 @@ import (
type (
TargetConstraintFunc func(interface{}) error
- TargetConstraint struct {
- desc string
- constraint TargetConstraintFunc
+
+ TargetDesc struct {
+ target, req string
}
-)
-type Targets struct {
- s *State
- constraints []*TargetConstraint
- sel []interface{}
-}
+ TargetRequirement struct {
+ min, max int
+ }
+
+ Target struct {
+ s *State
+ desc string
+ requirement TargetRequirement
+ constraint TargetConstraintFunc
+ sel []interface{}
+ }
+
+ Targets struct {
+ idx int
+ ts []*Target
+ }
+)
func fmtSel(sel interface{}) string {
switch t := sel.(type) {
@@ -33,9 +44,9 @@ func fmtSel(sel interface{}) string {
}
}
-func (t *Targets) String() string {
+func (t *Target) String() string {
if len(t.sel) == 0 {
- return "undecided targets"
+ return "undecided target"
}
if len(t.sel) == 1 {
@@ -49,68 +60,140 @@ func (t *Targets) String() string {
return s[:len(s)-1] + "]"
}
-func newTargetsWithSel(s *State, targets []string, action Action, sel []interface{}) *Targets {
- t := &Targets{
- s: s,
- sel: sel,
+func newTargetWithSel(s *State, desc TargetDesc, action Action, sel []interface{}) *Target {
+ t := &Target{
+ s: s,
+ desc: desc.target,
+ constraint: targetConstraint(desc.target, s, action),
+ requirement: desc.requirement(),
+ sel: sel,
}
- t.buildConstraints(targets, action)
return t
}
-func newTargets(s *State, targets []string, action Action) *Targets {
- return newTargetsWithSel(s, targets, action, make([]interface{}, 0, len(targets)))
+func newTarget(s *State, desc TargetDesc, action Action) *Target {
+ return newTargetWithSel(s, desc, action, []interface{}{})
}
-func (t *Targets) buildConstraints(targets []string, action Action) {
- for _, desc := range targets {
- c := &TargetConstraint{desc, targetConstraint(desc, t.s, action)}
- t.constraints = append(t.constraints, c)
+func newTargets(targets ...*Target) *Targets {
+ return &Targets{0, targets}
+}
+
+func newTargetDesc(desc string) TargetDesc {
+ return TargetDesc{desc, "1"}
+}
+
+func (d *TargetDesc) requirement() TargetRequirement {
+ switch d.req {
+ case "?":
+ return TargetRequirement{0, 1}
+ case "+":
+ return TargetRequirement{1, -1}
+ case "*":
+ return TargetRequirement{0, -1}
+ default:
+ if strings.Contains(d.req, "-") {
+ tokens := strings.SplitN(d.req, "-", 2)
+ min, err := strconv.Atoi(tokens[0])
+ if err != nil {
+ log.Fatalf("failed to parse target requirement %s: %v", d.req, err)
+ }
+ max, err := strconv.Atoi(tokens[1])
+ if err != nil {
+ log.Fatalf("failed to parse target requirement %s: %v", d.req, err)
+ }
+ return TargetRequirement{min, max}
+ }
+
+ x, err := strconv.Atoi(d.req)
+ if err != nil {
+ log.Fatalf("failed to parse target requirement %s: %v", d.req, err)
+ }
+ return TargetRequirement{x, x}
}
}
-func (t *Targets) RequireTargets() bool {
- return len(t.constraints) != len(t.sel)
+func (t *Target) AddSelection(sel interface{}) (err error) {
+ if err = t.constraint(sel); err == nil {
+ t.sel = append(t.sel, sel)
+ }
+ return
}
-func (t *Targets) Options() [][]interface{} {
- options := make([][]interface{}, 0, len(t.constraints))
- for _, c := range t.constraints {
- options = append(options, c.options(t.s))
+func (t *Target) RequireSelection() bool { return len(t.sel) < t.requirement.min }
+func (t *Target) HasSelection() bool { return len(t.sel) > 0 }
+func (t *Target) ClearSelection() { t.sel = []interface{}{} }
+
+func (t *Target) Options() (options []interface{}) {
+ candidates := targetCandidates(t.desc, t.s)
+
+ for _, candidate := range candidates {
+ if err := t.constraint(candidate); err == nil {
+ options = append(options, candidate)
+ }
}
return options
}
-func (t *Targets) NextOptions() []interface{} {
- if len(t.constraints) == 0 {
- return nil
+func (t *Target) CheckSelection(s *State) error {
+ if t.RequireSelection() {
+ return fmt.Errorf("Number of selected targets and required ones does not match")
+ }
+
+ if t.requirement.max != -1 && len(t.sel) > t.requirement.max {
+ return fmt.Errorf("To many selection %d (%d allowed)", len(t.sel), t.requirement.max)
+ }
+
+ for _, sel := range t.sel {
+ if err := t.constraint(sel); err != nil {
+ return fmt.Errorf("selected target %s does not fit its desciption %s: %s",
+ sel, t.desc, err)
+ }
}
- next := len(t.sel)
- return t.constraints[next].options(t.s)
+ return nil
}
-func (t *Targets) AddTarget(target interface{}) {
- t.sel = append(t.sel, target)
+func (t *Targets) AddSelection(sel interface{}) error { return t.ts[t.idx].AddSelection(sel) }
+
+func (t *Targets) HasSelections() bool {
+ for _, t := range t.ts {
+ if !t.HasSelection() {
+ return false
+ }
+ }
+ return true
}
-func (t *Targets) CheckTargets(s *State) error {
- if t.RequireTargets() {
- return fmt.Errorf("Number of selected targets and required ones does not match")
+func (t *Targets) ClearSelections() {
+ for _, t := range t.ts {
+ t.ClearSelection()
}
+}
+func (t *Targets) Options() (options []interface{}) { return t.ts[t.idx].Options() }
+func (t *Targets) Next() { t.idx++ }
- for i, c := range t.constraints {
- sel := t.sel[i]
- if err := c.constraint(sel); err != nil {
- return fmt.Errorf("selected target %s does not fit its desciption %s: %s",
- sel, c.desc, err)
+func (t *Targets) CheckTargets(s *State) error {
+ for _, target := range t.ts {
+ if err := target.CheckSelection(s); err != nil {
+ return err
}
}
return nil
}
+func (targets *Targets) RequireSelection() bool {
+ for _, t := range targets.ts {
+ if t.RequireSelection() {
+ return true
+ }
+ }
+
+ return false
+}
+
func targetConstraint(desc string, s *State, action Action) TargetConstraintFunc {
constraints := []TargetConstraintFunc{}
if strings.Contains(desc, "permanent") {
@@ -180,22 +263,26 @@ func attackableTargetConstraint(action Action) TargetConstraintFunc {
}
}
-func rangeTargetConstraint(source interface{}, r int) TargetConstraintFunc {
- var sourcePos Position
- switch source := source.(type) {
+func posFromTileOrPermanent(tileOrPermanent interface{}) Position {
+ switch obj := tileOrPermanent.(type) {
case *Tile:
- sourcePos = source.Position
+ return obj.Position
case Permanent:
- sourcePos = source.Tile().Position
+ return obj.Tile().Position
default:
- log.Fatalf("Unhandled source type %T in rangeTargetConstraint", source)
+ log.Fatalf("Unhandled source type %T in posFromTileOrPermanent", tileOrPermanent)
+ return INVALID_POSITION()
}
+}
+
+func rangeTargetConstraint(source interface{}, r int) TargetConstraintFunc {
+ sourcePos := posFromTileOrPermanent(source)
return func(t interface{}) (err error) {
- p, _ := t.(Permanent)
- if !IsPositionInRange(sourcePos, p.Tile().Position, r) {
+ targetPos := posFromTileOrPermanent(t)
+ if !IsPositionInRange(sourcePos, targetPos, r) {
err = fmt.Errorf("Position %v of target %v not in range %d of source's position %v",
- p.Tile().Position, t, r, sourcePos)
+ targetPos, t, r, sourcePos)
}
return
}
@@ -359,6 +446,16 @@ func parseTileTargetConstraint(desc string, s *State, action Action) []TargetCon
constraints = append(constraints, rangeTargetConstraint(action.Source(), 1))
}
+ if strings.Contains(desc, "free") {
+ constraints = append(constraints, func(t interface{}) (err error) {
+ tile := t.(*Tile)
+ if tile.IsFree() {
+ return nil
+ }
+ return fmt.Errorf("tile %v is occupied by %v", tile, tile.Permanent)
+ })
+ }
+
return constraints
}
@@ -401,7 +498,13 @@ func parseCardTargetConstraint(desc string, s *State, action Action) []TargetCon
case "hand":
pocs = append(pocs, p.Hand)
case "store":
- pocs = append(pocs, p.Store)
+ if p.gameState.Map.HasStores() {
+ for _, s := range p.AvailableStores() {
+ pocs = append(pocs, s)
+ }
+ } else {
+ pocs = append(pocs, p.Store)
+ }
case "discard pile":
pocs = append(pocs, p.DiscardPile)
}
@@ -421,17 +524,6 @@ func parseCardTargetConstraint(desc string, s *State, action Action) []TargetCon
return constraints
}
-func (c *TargetConstraint) options(s *State) (options []interface{}) {
- candidates := targetCandidates(c.desc, s)
-
- for _, candidate := range candidates {
- if err := c.constraint(candidate); err == nil {
- options = append(options, candidate)
- }
- }
- return
-}
-
func targetCandidates(desc string, s *State) []interface{} {
if strings.Contains(desc, "unit") ||
strings.Contains(desc, "artifact") ||
diff --git a/go/game/tile.go b/go/game/tile.go
index 510497f8..ce7518cb 100644
--- a/go/game/tile.go
+++ b/go/game/tile.go
@@ -71,6 +71,15 @@ var TileNames = map[string]TileType{
"store": store,
}
+var farmEffect areaEffect = newGrantFullActionEffect("misc/farmer",
+ func(a Action) ActionResolveFunc {
+ u := a.Source().(*Unit)
+ return func(s *State) {
+ controller := u.Controller()
+ controller.gainResource(3)
+ }
+ }, "gain 3 resource", "farmAction")
+
type Tile struct {
Position Position
Permanent Permanent
@@ -88,8 +97,8 @@ func INVALID_TILE() Tile {
return Tile{Position: INVALID_POSITION()}
}
-func NewTileFromString(t string, pos Position) (Tile, error) {
- tile := strings.ToLower(t)
+func NewTileFromString(raw string, pos Position) (Tile, error) {
+ tile := strings.ToLower(raw)
tokens := strings.Split(tile, " ")
tileType, found := TileNames[tokens[0]]
if !found {
@@ -98,7 +107,14 @@ func NewTileFromString(t string, pos Position) (Tile, error) {
}
}
water := strings.Contains(tile, "water")
- return Tile{Position: pos, Type: tileType, Water: water, Raw: t}, nil
+
+ t := Tile{Position: pos, Type: tileType, Water: water, Raw: raw}
+
+ if tileType == TileTypes.Farm {
+ t.effects = append(t.effects, farmEffect)
+ }
+
+ return t, nil
}
func (t *Tile) IsFree() bool {
@@ -139,6 +155,11 @@ func (t *Tile) OnDiagonal(other *Tile) bool {
func (t *Tile) entering(p Permanent) {
t.Permanent = p
+
+ if t.Type == TileTypes.Store {
+ p.Controller().addKnownStore(t.Position)
+ }
+
for _, effect := range t.effects {
effect.onEntering(p)
}
@@ -153,6 +174,11 @@ func (t *Tile) leaving(p Permanent) {
func (t *Tile) addEffect(effect areaEffect) {
t.effects = append(t.effects, effect)
+
+ // Apply effect to the current Permanent
+ if t.Permanent != nil {
+ effect.onEntering(t.Permanent)
+ }
}
func (t *Tile) removeEffect(effect areaEffect) {
@@ -163,6 +189,11 @@ func (t *Tile) removeEffect(effect areaEffect) {
break
}
}
+
+ // Remove effect from the current Permanent
+ if t.Permanent != nil {
+ effect.onLeaving(t.Permanent)
+ }
}
func (t *Tile) neutralize() {
diff --git a/go/game/unit.go b/go/game/unit.go
index 8b57b0ae..adb608ab 100644
--- a/go/game/unit.go
+++ b/go/game/unit.go
@@ -1,6 +1,7 @@
package game
import (
+ "fmt"
"log"
)
@@ -14,7 +15,7 @@ type Unit struct {
Health int
Movement Movement
Attack Attack
- Upkeep int
+ upkeep int
FullActions []*FullAction
FreeActions []*FreeAction
// Effects []Effects
@@ -48,7 +49,7 @@ func NewUnit(card *Card, tile *Tile, owner *Player) *Unit {
Health: card.Values["health"].(int),
Movement: movement,
Attack: attack,
- Upkeep: upkeep,
+ upkeep: upkeep,
AvailMoveActions: DEFAULT_AVAIL_MOVE_ACTIONS,
AvailAttackActions: DEFAULT_AVAIL_ATTACK_ACTIONS,
}
@@ -61,10 +62,22 @@ func NewUnit(card *Card, tile *Tile, owner *Player) *Unit {
return u
}
+func (u *Unit) Fmt() string {
+ return fmt.Sprintf("%s@%v", u.Card().Name, u.TileOrContainingPermTile())
+}
+
+func (u *Unit) FmtWController() string {
+ return fmt.Sprintf("%s's %s@%v", u.Controller().Name, u.Card().Name, u.TileOrContainingPermTile())
+}
+
func NewUnitFromPath(cardPath string, tile *Tile, owner *Player) *Unit {
return NewUnit(NewCard(cardPath), tile, owner)
}
+func (u *Unit) UpkeepCost() int {
+ return u.upkeep
+}
+
func (u *Unit) resetBaseActions() {
u.AvailMoveActions = DEFAULT_AVAIL_MOVE_ACTIONS + u.additionalMoveActions
u.AvailAttackActions = DEFAULT_AVAIL_ATTACK_ACTIONS + u.additionalAttackActions
@@ -238,3 +251,23 @@ func (u *Unit) onUnpile(containing Permanent) {
func (u *Unit) onDrop(containing Permanent) {
u.onUnpile(containing)
}
+
+func (u *Unit) removeFullAction(tag string) {
+ for i, fa := range u.FullActions {
+ if fa.tag != tag {
+ continue
+ }
+ u.FullActions[i] = u.FullActions[len(u.FullActions)-1]
+ u.FullActions = u.FullActions[:len(u.FullActions)-1]
+ break
+ }
+}
+
+func (u *Unit) AddDamage(damage int) {
+ if u.Marks(UnitMarks.Ward) > 0 {
+ u.adjustMarks(UnitMarks.Ward, -1)
+ return
+ }
+
+ u.PermanentBase.AddDamage(damage)
+}
diff --git a/go/go.mod b/go/go.mod
index 4534d920..c59303e0 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/cardGrid.go b/go/ui/cardGrid.go
index 59e25f2c..abe1d60b 100644
--- a/go/ui/cardGrid.go
+++ b/go/ui/cardGrid.go
@@ -98,6 +98,7 @@ func (w *CardGrid) render() *ebiten.Image {
op.GeoM.Translate(xOffset, yOffset)
if slices.Contains(w.highlights, card) {
+ log.Println("Highlighting card", card.Name)
op.ColorM.Scale(1, 0, 0, 0.5)
}
@@ -153,6 +154,8 @@ func (w *CardGrid) FindObjectAt(_x, _y int) interface{} {
func (w *CardGrid) setHighlights(cards []*game.Card) {
w.highlights = cards
+ // Reset the grid card count to force grid to be redrawn containing the highlights
+ w.gridCards = 0
w.ForceRedraw()
}
diff --git a/go/ui/mapView.go b/go/ui/mapView.go
index a6fd55c1..697eb774 100644
--- a/go/ui/mapView.go
+++ b/go/ui/mapView.go
@@ -266,6 +266,10 @@ func (vw *MapView) HighlightPermanents(permanents []game.Permanent) {
vw.ForceRedraw()
}
+func (vw *MapView) AddHighlightPermanent(p game.Permanent) {
+ vw.HighlightPermanents(append(vw.permanentsHighlights, p))
+}
+
func (vw *MapView) HighlightPermanent(p game.Permanent) {
vw.HighlightPermanents([]game.Permanent{p})
}
diff --git a/go/ui/prompt.go b/go/ui/prompt.go
index 1d552564..b8d60a49 100644
--- a/go/ui/prompt.go
+++ b/go/ui/prompt.go
@@ -1,63 +1,41 @@
package ui
import (
- "log"
-
"github.com/hajimehoshi/ebiten/v2"
"muhq.space/muhqs-game/go/game"
)
-type promptType int
-
const (
- handCard promptType = iota
-
PROMPT_HEIGHT int = 50
)
type Prompt struct {
- promptType promptType
y, width int
- sel []interface{}
+ 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{NewFixedTextBox(0, y, width, PROMPT_HEIGHT, promptText).Centering(true)},
}
return p
}
-func (p *Prompt) Add(obj interface{}) {
- var ok bool
- switch p.promptType {
- case handCard:
- _, ok = obj.(HandCard)
+func (p *Prompt) Add(obj interface{}) error {
+ if handCard, ok := obj.(HandCard); ok {
+ obj = handCard.C
}
- if !ok {
- log.Fatalf("Invalid type %T for prompt %d", obj, p.promptType)
- }
-
- p.sel = append(p.sel, obj)
+ return p.action.Targets().AddSelection(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 cc813a4e..2635535c 100644
--- a/go/ui/textBox.go
+++ b/go/ui/textBox.go
@@ -64,7 +64,7 @@ func (tb *TextBox) Centering(centering bool) *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())
@@ -153,15 +153,26 @@ func (w *PocList) setText() {
w.Y = w.bottomY - b.Dy() - w.YMargin
}
-func (w *PocList) FindObjectAt(x, y int) interface{} {
+func (w *PocList) FindObjectAt(_x, _y int) interface{} {
+ if !w.Contains(_x, _y) {
+ return nil
+ }
+
cards := w.poc.Cards()
if len(cards) == 0 {
return nil
}
- // b := text.BoundString(font.Font, w.Text)
- // i := b.Dy() / w.poc.Size()
- return cards[0]
+ y := _y - w.Y
+
+ b := text.BoundString(font.Font24, w.text)
+ i := y / (b.Dy() / w.poc.Size())
+
+ if i >= len(cards) {
+ return nil
+ }
+
+ return cards[i]
}
type TextInput struct {
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
+}