diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2023-02-08 17:50:10 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-08-20 15:50:10 +0200 |
| commit | 45df9bf319d38db916ca983b8c92bb912f5ce9ee (patch) | |
| tree | d7966002dad4a20bc3ef385bd152554fe278a6df | |
| parent | eb10f6f4940ecd09fd7d881fe5e1735ff146f919 (diff) | |
| download | muhqs-game-45df9bf319d38db916ca983b8c92bb912f5ce9ee.tar.gz muhqs-game-45df9bf319d38db916ca983b8c92bb912f5ce9ee.zip | |
intermediate commit
| -rw-r--r-- | go/TODO | 3 | ||||
| -rw-r--r-- | go/client/game.go | 121 | ||||
| -rw-r--r-- | go/game/action.go | 194 | ||||
| -rw-r--r-- | go/game/ai.go | 52 | ||||
| -rw-r--r-- | go/game/areaEffect.go | 24 | ||||
| -rw-r--r-- | go/game/cardImplementations.go | 61 | ||||
| -rw-r--r-- | go/game/deck.go | 4 | ||||
| -rw-r--r-- | go/game/kraken.go | 23 | ||||
| -rw-r--r-- | go/game/map.go | 8 | ||||
| -rw-r--r-- | go/game/marks.go | 4 | ||||
| -rw-r--r-- | go/game/permanent.go | 6 | ||||
| -rw-r--r-- | go/game/player.go | 76 | ||||
| -rw-r--r-- | go/game/playerControl.go | 97 | ||||
| -rw-r--r-- | go/game/stack.go | 7 | ||||
| -rw-r--r-- | go/game/state.go | 48 | ||||
| -rw-r--r-- | go/game/targets.go | 220 | ||||
| -rw-r--r-- | go/game/tile.go | 37 | ||||
| -rw-r--r-- | go/game/unit.go | 37 | ||||
| -rw-r--r-- | go/go.mod | 2 | ||||
| -rw-r--r-- | go/ui/cardGrid.go | 3 | ||||
| -rw-r--r-- | go/ui/mapView.go | 4 | ||||
| -rw-r--r-- | go/ui/prompt.go | 38 | ||||
| -rw-r--r-- | go/ui/textBox.go | 21 | ||||
| -rw-r--r-- | go/utils/slices.go | 8 |
24 files changed, 707 insertions, 391 deletions
@@ -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) +} @@ -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 +} |
