aboutsummaryrefslogtreecommitdiff
path: root/go
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-02-25 23:08:47 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2025-08-20 15:57:06 +0200
commit3d36d1d0254b8aeecc889a2a49cbcced458cbe9c (patch)
tree38dd0851d8eaa6535d05b97f29f65fc35655a5ef /go
parent19fb1a7e5c5883f49e944b6499fa1ced207a3e39 (diff)
downloadmuhqs-game-3d36d1d0254b8aeecc889a2a49cbcced458cbe9c.tar.gz
muhqs-game-3d36d1d0254b8aeecc889a2a49cbcced458cbe9c.zip
intermediate commit
* Implement move artifact action * Fix widget update memory leak * update the highlights not during on each frame * do not update the unchanged label of a button * Allow to exit the game by pressing 'q' * Implement some cards from the magic set * Improve permanent formatting * Some tweaks to UnitAI code
Diffstat (limited to 'go')
-rw-r--r--go/TODO4
-rw-r--r--go/client/game.go14
-rw-r--r--go/client/main.go4
-rw-r--r--go/game/action.go64
-rw-r--r--go/game/ai.go62
-rw-r--r--go/game/areaEffect_test.go2
-rw-r--r--go/game/artifact.go4
-rw-r--r--go/game/card.go4
-rw-r--r--go/game/cardImplementations.go204
-rw-r--r--go/game/cardParsing.go4
-rw-r--r--go/game/effect.go47
-rw-r--r--go/game/permanent.go79
-rw-r--r--go/game/player.go7
-rw-r--r--go/game/pos.go8
-rw-r--r--go/game/range.go8
-rw-r--r--go/game/state.go43
-rw-r--r--go/game/targets.go96
-rw-r--r--go/game/targets_test.go52
-rw-r--r--go/game/tile.go2
-rw-r--r--go/game/unit.go38
-rw-r--r--go/game/unit_test.go4
-rw-r--r--go/ui/button.go6
-rw-r--r--go/ui/hoverable.go6
-rw-r--r--go/utils/slices.go13
24 files changed, 649 insertions, 126 deletions
diff --git a/go/TODO b/go/TODO
index 24825cc8..2842d87a 100644
--- a/go/TODO
+++ b/go/TODO
@@ -1,9 +1,7 @@
* Fix data race between UI and game loop (possible big state lock)
* implement different highlighting used for selection and candidates
* implement representation of next target and current seelction in prompt
-* implement artifact play equipped to next unit
-* implement artifcat action
- * move
+* implement equipment play equipped to next unit
* implement equipment actions
* drop
* implement Pile UI hint
diff --git a/go/client/game.go b/go/client/game.go
index 8dc69b25..0e516815 100644
--- a/go/client/game.go
+++ b/go/client/game.go
@@ -32,6 +32,8 @@ const (
)
type Game struct {
+ app *app
+
selectedObject interface{}
handLayer *ui.HandView
@@ -58,6 +60,7 @@ type Game struct {
func newGame(app *app) *Game {
g := &Game{
+ app: app,
Collection: ui.Collection{
Width: app.windowWidth,
Height: app.windowHeight,
@@ -110,6 +113,7 @@ func (g *Game) addActivePlayer(name string, deckList string) *Game {
func (g *Game) addPrompt(action game.Action, prompt string) {
g.prompt = ui.NewPrompt(g.Height/2, g.Width, action, prompt)
g.AddWidget(g.prompt)
+ g.updateHighlight()
}
func (g *Game) getPlayer(name string) *game.Player {
@@ -202,9 +206,11 @@ func (g *Game) addPermActionChoice(perm game.Permanent, x, y int) {
}
perms := []game.Permanent{perm}
+ labels := []string{perm.Card().Name}
for _, p := range perm.Pile() {
if len(p.CurrentlyAvailActions()) > 0 {
perms = append(perms, p)
+ labels = append(labels, p.Card().Name)
}
}
@@ -215,7 +221,7 @@ func (g *Game) addPermActionChoice(perm game.Permanent, x, y int) {
p := perms[c.GetChoosen(x, y)]
g.addActionChoice(p, x, y)
}
- g.addChoice(ui.NewPermChoice(x, y, perms, onClick))
+ g.addChoice(ui.NewChoice(x, y, utils.TypedSliceToInterfaceSlice(labels), onClick))
} else {
g.selectedObject = perms[0]
g.addActionChoice(perms[0], x, y)
@@ -289,6 +295,7 @@ func (g *Game) progressPrompt() {
targets := a.Targets()
if targets.RequireSelection() {
targets.Next()
+ g.updateHighlight()
} else {
g.declareAction(a)
g.removePrompt()
@@ -515,12 +522,16 @@ func (g *Game) Update() error {
if err := g.prompt.Add(obj); err != nil {
log.Println("Not added", obj, "to active prompt:", err)
g.handleSelection(obj, x, y)
+ } else {
+ g.updateHighlight()
}
} else {
g.handleSelection(obj, x, y)
}
} else if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
g.passButton.Click(0, 0)
+ } else if inpututil.IsKeyJustPressed(ebiten.KeyQ) {
+ return fmt.Errorf("Exit")
} else if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
g.removeChoice()
g.selectedObject = nil
@@ -541,7 +552,6 @@ func (g *Game) Update() error {
return err
}
- g.updateHighlight()
g.updatePassButton()
return nil
}
diff --git a/go/client/main.go b/go/client/main.go
index 82b60a40..86491a4d 100644
--- a/go/client/main.go
+++ b/go/client/main.go
@@ -94,6 +94,8 @@ func main() {
app.pushActivity(startMenu)
if err := ebiten.RunGame(app); err != nil {
- log.Fatal(err)
+ if err.Error() != "Exit" {
+ log.Fatal(err)
+ }
}
}
diff --git a/go/game/action.go b/go/game/action.go
index 09c3ec68..271f5f51 100644
--- a/go/game/action.go
+++ b/go/game/action.go
@@ -156,8 +156,10 @@ func NewPlayAction(p *Player, c *Card, args ...interface{}) *PlayAction {
s := p.gameState
if c.IsPermanent() {
a.targets = newTargets(newTarget(s, permanentPlayActionTarget, a))
- } else if targetDesc := c.Impl.playTargets(); targetDesc != nil {
- a.targets = newTargets(newTarget(s, *targetDesc, a))
+ } else if targetDesc := c.Impl.playTargets(); targetDesc != INVALID_TARGET_DESC {
+ a.targets = newTargets(newTarget(s, targetDesc, a))
+ } else {
+ a.targets = newTargets()
}
a.resolveFunc = func(s *State) {
@@ -182,8 +184,7 @@ type MoveAction struct {
var moveActionTargetDesc = TargetDesc{"available tile", "1"}
-func (a *MoveAction) targetTile() *Tile {
- t := a.Target()
+func targetTile(t *Target) *Tile {
if tile, ok := t.sel[0].(*Tile); ok {
return tile
}
@@ -202,8 +203,8 @@ func NewMoveAction(u *Unit) *MoveAction {
a.targets = newTargets(newTarget(u.Controller().gameState, moveActionTargetDesc, a))
a.resolveFunc = func(s *State) {
- tile := a.targetTile()
- u.move(s, tile)
+ t := targetTile(a.Target())
+ movePermanent(u, t)
}
a.costFunc = genMoveActionCost(u)
@@ -217,8 +218,8 @@ func (a *MoveAction) String() string {
return fmt.Sprintf("move %s", u.Movement.String())
}
- t := a.targetTile()
- return fmt.Sprintf("%s -> %v", u.FmtWController(), t.Position)
+ t := targetTile(a.Target())
+ return fmt.Sprintf("%v -> %v", u, t.Position)
}
func genBaseActionCost(u *Unit, attack, move int) ActionCostFunc {
@@ -274,7 +275,7 @@ func NewEquipAction(u *Unit, e *Equipment) *EquipAction {
func (a *EquipAction) String() string {
u := a.source.(*Unit)
- return fmt.Sprintf("%s equip %v", u.FmtWController(), a.equipment)
+ return fmt.Sprintf("%v equip %v", u, a.equipment)
}
type AttackAction struct {
@@ -307,7 +308,7 @@ func (a *AttackAction) String() string {
}
target := a.Target().sel[0].(Permanent)
- return fmt.Sprintf("%s x %v", u.FmtWController(), target.Tile().Position)
+ return fmt.Sprintf("%v x %v", u, target.Tile().Position)
}
type ArtifactSwitchAction struct {
@@ -334,7 +335,46 @@ func newArtifactSwitchAction(u *Unit, artifact Permanent) *ArtifactSwitchAction
}
func (a *ArtifactSwitchAction) String() string {
- return fmt.Sprintf("%s <-> %v", a.Source().(*Unit).FmtWController(), a.Artifact)
+ return fmt.Sprintf("%v <-> %v", a.Source().(*Unit), a.Artifact)
+}
+
+type ArtifactMoveAction struct {
+ ActionBase
+ Artifact Permanent
+}
+
+func newArtifactMoveAction(u *Unit, artifact Permanent) *ArtifactMoveAction {
+ a := &ArtifactMoveAction{
+ ActionBase{
+ source: u,
+ Card: u.Card(),
+ },
+ artifact,
+ }
+
+ a.targets = newArtifactMoveTargets(u.Controller().gameState, a)
+
+ a.resolveFunc = func(s *State) {
+ tile := targetTile(a.Targets().ts[0])
+ movePermanent(u, tile)
+
+ tile = targetTile(a.Targets().ts[1])
+ movePermanent(artifact, tile)
+ }
+
+ a.costFunc = genFullActionCost(u)
+
+ return a
+}
+
+func (a *ArtifactMoveAction) String() string {
+ if !a.Targets().HasSelections() {
+ return fmt.Sprintf("%v move %v", a.Source().(*Unit), a.Artifact)
+ }
+
+ t1 := targetTile(a.Targets().ts[0])
+ t2 := targetTile(a.Targets().ts[1])
+ return fmt.Sprintf("%v,%v -> %v,%v", a.Source().(*Unit), a.Artifact, t1, t2)
}
type FullAction struct {
@@ -368,7 +408,7 @@ func (a *FullAction) String() string {
return fmt.Sprintf("↻ %s", a.desc)
}
- return fmt.Sprintf("%s ↻@%v", u.FmtWController(), a.targets)
+ return fmt.Sprintf("%v↻@%v", u, a.targets)
}
type FreeAction struct {
diff --git a/go/game/ai.go b/go/game/ai.go
index 31a16363..94a9f7ec 100644
--- a/go/game/ai.go
+++ b/go/game/ai.go
@@ -15,6 +15,7 @@ type UnitAI struct {
s *State
u *Unit
actions chan Action
+ target *Target
syncGameState *sync.WaitGroup
}
@@ -44,6 +45,7 @@ func (ai *UnitAI) promptAction() {
func (ai *UnitAI) Execute(f func(*UnitAI)) {
defer close(ai.actions)
if ai.awaitGameStateSync() {
+ // TODO: Investigate hangs when the AI did not issue any actions
f(ai)
}
}
@@ -57,7 +59,12 @@ func NewUnitAI(s *State, u *Unit) *UnitAI {
c := make(chan Action)
wg := new(sync.WaitGroup)
wg.Add(1)
- ai := &UnitAI{s, u, c, wg}
+ ai := &UnitAI{
+ s: s,
+ u: u,
+ actions: c,
+ syncGameState: wg,
+ }
switch aiDesc {
case "aggressive":
@@ -72,6 +79,9 @@ func NewUnitAI(s *State, u *Unit) *UnitAI {
}
go ai.Execute(func(ai *UnitAI) { WanderingAI(ai, x) })
case "target-oriented":
+ aiDesc := u.Card().getCanonicalValues("ai")[0]
+ targetDesc := aiDesc[len("target-oriented")+1:]
+ ai.target = newTarget(s, newTargetDesc(targetDesc), nil)
go ai.Execute(TargetOrientedAI)
}
@@ -120,6 +130,36 @@ func (m *Map) generateMapGraphFor(u *Unit) *dijkstra.Graph {
return graph
}
+// func (m *Map) generateMovementRangeGraphFor(u *Unit) *dijkstra.Graph {
+// origin := TileOrContainingPermTile(u).Position
+// graph := dijkstra.NewGraph()
+// for _, t := range tilesInRangeFromOrigin(m, origin, u.Movement.Range, true) {
+// graph.AddMappedVertex(t.Position.String())
+// }
+
+// for _, t := range TilesInRange(m, u, u.Movement.Range) {
+// tId := t.Position.String()
+// for _, neighbour := range tilesInRangeFromOrigin(m, t.Position, 1, true) {
+// if !IsPositionInRange(neighbour.Position, origin, u.Movement.Range) {
+// continue
+// }
+
+// if u.IsAvailableTile(neighbour) {
+// cost := 2
+// if t.OnDiagonal(neighbour) {
+// cost = 3
+// }
+// log.Printf("add %v -> %v: %d", tId, neighbour.Position.String(), int64(cost))
+// err := graph.AddMappedArc(tId, neighbour.Position.String(), int64(cost))
+// if err != nil {
+// log.Panicf("Failed to add mapped arc: %s", err)
+// }
+// }
+// }
+// }
+// return graph
+// }
+
func findPathTo(graph *dijkstra.Graph, u *Unit, pos Position) (dijkstra.BestPath, error) {
srcId, err := graph.GetMapping(TileOrContainingPermTile(u).Position.String())
if err != nil {
@@ -304,8 +344,8 @@ out:
// }
// 3.
- if ai.u.AvailAttackActions > 0 {
- attackAttackableEnemyPerm(ai)
+ if a := attackAttackableEnemyPerm(ai); a != nil {
+ ai.actions <- a
}
}
@@ -346,12 +386,16 @@ func TargetOrientedAI(ai *UnitAI) {
// 1.
if ai.u.HasFullAction() {
fullAction(ai)
+ return
}
- // if ai.t != nil {
- // // TODO: path finding again
- // attackAttackableEnemyPerm(ai)
- // } else {
- WanderingAI(ai, 3)
- // }
+ options := ai.target.Options()
+ if len(options) > 0 {
+ // TODO: move towards nearest target
+ if a := attackAttackableEnemyPerm(ai); a != nil {
+ ai.actions <- a
+ }
+ } else {
+ WanderingAI(ai, 3)
+ }
}
diff --git a/go/game/areaEffect_test.go b/go/game/areaEffect_test.go
index f6321d30..8adca76a 100644
--- a/go/game/areaEffect_test.go
+++ b/go/game/areaEffect_test.go
@@ -50,7 +50,7 @@ symbols:
t.Fatalf("archer did not enter tile %v", t0_0)
}
- archer.move(s, t0_1)
+ movePermanent(archer, t0_1)
if !left0_0 || t0_0.Permanent != nil {
t.Fatalf("moved archer did not leave tile %v", t0_0)
}
diff --git a/go/game/artifact.go b/go/game/artifact.go
index 6edc975a..81692346 100644
--- a/go/game/artifact.go
+++ b/go/game/artifact.go
@@ -34,6 +34,10 @@ func NewArtifact(card *Card, tile *Tile, owner *Player) *Artifact {
return a
}
+func (a *Artifact) String() string {
+ return FmtPermanent(a)
+}
+
func (a *Artifact) IsDestroyed() bool {
return a.Solid > 0 && a.Damage() >= a.Solid
}
diff --git a/go/game/card.go b/go/game/card.go
index 914ccb56..b4bf5ebb 100644
--- a/go/game/card.go
+++ b/go/game/card.go
@@ -94,7 +94,7 @@ type cardImplementation interface {
fullActions(*Unit) []*FullAction
freeActions(Permanent) []*FreeAction
- playTargets() *TargetDesc
+ playTargets() TargetDesc
stateBasedActions(*State, Permanent)
@@ -118,7 +118,7 @@ func (*cardImplementationBase) spawnTiles(*State, *Player) []*Tile { return nil
func (*cardImplementationBase) fullActions(*Unit) []*FullAction { return nil }
func (*cardImplementationBase) freeActions(Permanent) []*FreeAction { return nil }
-func (*cardImplementationBase) playTargets() *TargetDesc { return nil }
+func (*cardImplementationBase) playTargets() TargetDesc { return INVALID_TARGET_DESC }
func (*cardImplementationBase) stateBasedActions(*State, Permanent) {}
diff --git a/go/game/cardImplementations.go b/go/game/cardImplementations.go
index 1c4d07a5..66c77b18 100644
--- a/go/game/cardImplementations.go
+++ b/go/game/cardImplementations.go
@@ -2,6 +2,8 @@ package game
import (
"log"
+
+ "muhq.space/muhqs-game/go/utils"
)
func adjustMelee(p Permanent, damage int) {
@@ -21,6 +23,35 @@ func adjustMelee(p Permanent, damage int) {
}
}
+func adjustAttack(p Permanent, damage int) {
+ u, ok := p.(*Unit)
+ if !ok {
+ return
+ }
+
+ if INVALID_ATTACK(u.Attack) {
+ u.Attack.attacks = append(u.Attack.attacks, damage)
+ } else {
+ // TODO: remove 0 attacks
+ for i := range u.Attack.attacks {
+ u.Attack.attacks[i] += damage
+ }
+ }
+}
+
+func adjustMovement(p Permanent, delta int) {
+ u, ok := p.(*Unit)
+ if !ok {
+ return
+ }
+
+ if u.Movement == INVALID_MOVEMENT() && delta > 0 {
+ u.Movement.Range = delta
+ } else {
+ u.Movement.Range += delta
+ }
+}
+
func adjustHealth(p Permanent, delta int) {
if u, ok := p.(*Unit); ok {
u.Health += delta
@@ -39,7 +70,7 @@ func (*advisorImpl) fullActions(u *Unit) []*FullAction {
controller.DrawN(1)
cards := controller.PromptHandCardSelection(0, 1)
if cards != nil {
- controller.Deck.MoveCard(cards[0], s.Exile)
+ controller.Hand.MoveCard(cards[0], s.Exile)
}
}
}
@@ -172,11 +203,59 @@ func (*wormtongueImpl) fullActions(u *Unit) []*FullAction {
// ====== Magic Set ======
+type attackImpl struct{ cardImplementationBase }
+
+func (*attackImpl) playTargets() TargetDesc {
+ return TargetDesc{"unit", "0-2"}
+}
+
+func (*attackImpl) onPlay(s *State, c *Card, p *Player, t *Targets) {
+ sel := t.Cur().sel
+ switch len(sel) {
+ case 1:
+ s.addEotEffect(newAttackModificationEffect(sel[0].(*Unit), 2))
+ case 2:
+ s.addEotEffect(newAttackModificationEffect(sel[0].(*Unit), 1))
+ s.addEotEffect(newAttackModificationEffect(sel[1].(*Unit), 1))
+ }
+}
+
+type healImpl struct{ cardImplementationBase }
+
+func (*healImpl) playTargets() TargetDesc {
+ return newTargetDesc("unit")
+}
+
+func (*healImpl) onPlay(s *State, c *Card, p *Player, t *Targets) {
+ u := t.Cur().sel[0].(*Unit)
+ // TODO: prompt damage or poison choice
+ u.adjustDamage(-2)
+}
+
type dieImpl struct{ cardImplementationBase }
-func (*dieImpl) playTargets() *TargetDesc {
- desc := newTargetDesc("unit")
- return &desc
+func (*dieImpl) playTargets() TargetDesc {
+ return newTargetDesc("unit")
+}
+
+type moreImpl struct{ cardImplementationBase }
+
+func (*moreImpl) onPlay(s *State, c *Card, p *Player, t *Targets) {
+ p.DrawN(2)
+}
+
+type mineImpl struct{ cardImplementationBase }
+
+func (*mineImpl) onPlay(s *State, c *Card, p *Player, t *Targets) {
+ stolen := 0
+ for _, player := range s.Players {
+ if p == player || !p.IsEnemy(player) {
+ continue
+ }
+ player.reduceResource(2)
+ stolen += 2
+ }
+ p.gainResource(stolen)
}
func (*dieImpl) onPlay(s *State, c *Card, p *Player, t *Targets) {
@@ -189,6 +268,67 @@ func (*ritualImpl) onPlay(s *State, c *Card, p *Player, t *Targets) {
p.gainResource(3)
}
+type rushImpl struct{ cardImplementationBase }
+
+func (*rushImpl) playTargets() TargetDesc {
+ return TargetDesc{"unit", "0-2"}
+}
+
+func (*rushImpl) onPlay(s *State, c *Card, p *Player, t *Targets) {
+ sel := t.Cur().sel
+ switch len(sel) {
+ case 1:
+ s.addEotEffect(newMovementModificationEffect(sel[0].(*Unit), 2))
+ case 2:
+ s.addEotEffect(newMovementModificationEffect(sel[0].(*Unit), 1))
+ s.addEotEffect(newMovementModificationEffect(sel[1].(*Unit), 1))
+ }
+}
+
+type selectImpl struct{ cardImplementationBase }
+
+func (*selectImpl) onPlay(s *State, c *Card, p *Player, t *Targets) {
+ p.DrawN(1)
+ cards := p.PromptHandCardSelection(0, 1)
+ if cards != nil {
+ p.Hand.MoveCard(cards[0], s.Exile)
+ }
+}
+
+type shroudImpl struct{ cardImplementationBase }
+
+func (*shroudImpl) playTargets() TargetDesc {
+ return newTargetDesc("unit")
+}
+
+func (*shroudImpl) onPlay(s *State, c *Card, p *Player, t *Targets) {
+ u := t.Cur().sel[0].(*Unit)
+ s.addEotEffect(newTmpEffect(u, "shroud"))
+}
+
+type switchImpl struct{ cardImplementationBase }
+
+func (*switchImpl) playTargets() TargetDesc {
+ return TargetDesc{"permanent", "2"}
+}
+
+func (*switchImpl) onPlay(s *State, c *Card, p *Player, t *Targets) {
+ perms := utils.InterfaceSliceToTypedSlice[Permanent](t.Cur().sel)
+ s.switchPermanents(perms[0], perms[1])
+}
+
+type transmuteImpl struct{ cardImplementationBase }
+
+func (*transmuteImpl) playTargets() TargetDesc {
+ return newTargetDesc("store card")
+}
+
+func (*transmuteImpl) onPlay(s *State, c *Card, p *Player, t *Targets) {
+ storeCard := t.Cur().sel[0].(*Card)
+ p.Store.MoveCard(storeCard, p.Hand)
+ s.Exile.AddCard(c)
+}
+
// ====== Nautics Set ======
var fishTrapAoE areaEffect = newGrantFullActionEffect("nautics/fisher",
@@ -290,6 +430,40 @@ func (*devourThePoorImpl) onPlay(s *State, _ *Card, kraken *Player, _ *Targets)
}
}
+type dolphinImpl struct{ cardImplementationBase }
+
+func (*dolphinImpl) fullActions(u *Unit) []*FullAction {
+ s := u.Controller().gameState
+ resolvePrototype := func(a Action) ActionResolveFunc {
+ return func(s *State) {
+ t := a.Target().sel[0].(*Unit)
+
+ // 1. +1 Attack
+ // 2. +1 Armor
+ // 3. +2 Movement
+ // 4. +1 Action
+ // 5. +2 Pierce
+ // 6. All
+ choice := s.Rand.Intn(5) + 1
+ switch choice {
+ case 1:
+ s.addEotEffect(newAttackModificationEffect(t, 1))
+ case 2:
+ case 3:
+ s.addEotEffect(newMovementModificationEffect(t, 1))
+ case 4:
+ case 5:
+ case 6:
+ }
+
+ // TODO: implement eot effects
+ }
+ }
+ a := newFullAction(u, resolvePrototype, "random dolphin buff")
+ a.targets = newTargets(newTarget(s, newTargetDesc("adjacent akkied unit"), a))
+ return []*FullAction{a}
+}
+
type drownedSailorImpl struct {
cardImplementation
sailorImpl
@@ -348,7 +522,11 @@ func (i *frostPylonImpl) onLeaving(t *Tile) {
}
}
-type giganticHailImpl struct{ cardImplementation }
+type giganticHailImpl struct {
+ cardImplementation
+}
+
+func (*giganticHailImpl) playTargets() TargetDesc { return INVALID_TARGET_DESC }
func (*giganticHailImpl) onPlay(s *State, _ *Card, kraken *Player, _ *Targets) {
emptyTiles := s.Map.FreeTiles()
@@ -408,6 +586,8 @@ func (*tentacleSlapImpl) onPlay(s *State, _ *Card, kraken *Player, _ *Targets) {
type unholyCannonballImpl struct{ cardImplementation }
+func (*unholyCannonballImpl) playTargets() TargetDesc { return INVALID_TARGET_DESC }
+
func (*unholyCannonballImpl) onPlay(s *State, _ *Card, kraken *Player, _ *Targets) {
krakenTiles := s.Map.FilterTiles(func(t *Tile) bool { return t.Raw == "the kraken" })
if len(krakenTiles) != 1 {
@@ -449,8 +629,17 @@ func init() {
"base/tower_shield": &towerShieldImpl{},
"base/wormtongue": &wormtongueImpl{},
- "magic/die!": &dieImpl{},
- "magic/ritual!": &ritualImpl{},
+ "magic/attack!": &attackImpl{},
+ "magic/die!": &dieImpl{},
+ "magic/heal!": &healImpl{},
+ "magic/more!": &moreImpl{},
+ "magic/mine!": &mineImpl{},
+ "magic/ritual!": &ritualImpl{},
+ "magic/rush!": &rushImpl{},
+ "magic/select!": &selectImpl{},
+ "magic/shroud!": &shroudImpl{},
+ "magic/switch!": &switchImpl{},
+ "magic/transmute!": &transmuteImpl{},
"nautics/captain": &captainImpl{},
"nautics/fish_trap": &fishTrapImpl{aoe: fishTrapAoE},
@@ -459,6 +648,7 @@ func init() {
"kraken/deja_vu!": &dejaVuImpl{},
"kraken/devour_the_poor!": &devourThePoorImpl{},
+ "kraken/dolphin": &dolphinImpl{},
"kraken/drowned_sailor": &drownedSailorImpl{},
"kraken/flying_dutchmen": &flyingDutchmenImpl{},
"kraken/frost_pylon": &frostPylonImpl{},
diff --git a/go/game/cardParsing.go b/go/game/cardParsing.go
index 0fde5a59..b14f3f96 100644
--- a/go/game/cardParsing.go
+++ b/go/game/cardParsing.go
@@ -13,7 +13,7 @@ type dynamicCardImplementation struct {
genFullActions func(*Unit) []*FullAction
genFreeActions func(Permanent) []*FreeAction
- targetDesc *TargetDesc
+ targetDesc TargetDesc
_stateBasedActions func(*State, Permanent)
@@ -48,7 +48,7 @@ func (impl *dynamicCardImplementation) freeActions(p Permanent) []*FreeAction {
return nil
}
-func (impl *dynamicCardImplementation) playTargets() *TargetDesc {
+func (impl *dynamicCardImplementation) playTargets() TargetDesc {
return impl.targetDesc
}
diff --git a/go/game/effect.go b/go/game/effect.go
new file mode 100644
index 00000000..1fc6d5b0
--- /dev/null
+++ b/go/game/effect.go
@@ -0,0 +1,47 @@
+package game
+
+type effect interface {
+ apply(*State)
+ end(*State)
+}
+
+type dynamicEffect struct {
+ _apply func(*State)
+ _end func(*State)
+}
+
+func (e *dynamicEffect) apply(s *State) {
+ e._apply(s)
+}
+
+func (e *dynamicEffect) end(s *State) {
+ e._end(s)
+}
+
+func newAdjustmentEffect(adjust func(Permanent, int), u *Unit, delta int) effect {
+ apply := func(*State) { adjust(u, delta) }
+ end := func(*State) { adjust(u, -delta) }
+ return &dynamicEffect{apply, end}
+}
+
+func newMeleeModificationEffect(u *Unit, delta int) effect {
+ return newAdjustmentEffect(adjustMelee, u, delta)
+}
+
+func newAttackModificationEffect(u *Unit, delta int) effect {
+ return newAdjustmentEffect(adjustAttack, u, delta)
+}
+
+func newMovementModificationEffect(u *Unit, delta int) effect {
+ return newAdjustmentEffect(adjustMovement, u, delta)
+}
+
+func newHealthModificationEffect(u *Unit, delta int) effect {
+ return newAdjustmentEffect(adjustHealth, u, delta)
+}
+
+func newTmpEffect(p Permanent, effect string) effect {
+ apply := func(*State) { p.addTmpEffect(effect) }
+ end := func(*State) { p.removeTmpEffect(effect) }
+ return &dynamicEffect{apply, end}
+}
diff --git a/go/game/permanent.go b/go/game/permanent.go
index ae94a657..dfa275a6 100644
--- a/go/game/permanent.go
+++ b/go/game/permanent.go
@@ -7,6 +7,8 @@ import (
"strings"
"golang.org/x/exp/slices"
+
+ "muhq.space/muhqs-game/go/utils"
)
type Permanent interface {
@@ -33,7 +35,9 @@ type Permanent interface {
IsAvailableTile(*Tile) bool
fight(p Permanent)
- addDamage(damage int)
+ adjustDamage(damage int)
+ addTmpEffect(tmpEffect string)
+ removeTmpEffect(tmpEffect string)
setContainingPerm(p Permanent)
addPermanentToPile(p Permanent)
clearPile()
@@ -77,13 +81,33 @@ func (p *permanentBase) Pile() []Permanent { return p.pile }
func (p *permanentBase) Controller() *Player { return p.controller }
func (p *permanentBase) Owner() *Player { return p.owner }
-func (p *permanentBase) String() string {
- if p.Tile() != nil {
- return fmt.Sprintf("%s's %s@%v",
- p.Controller().Name, p.Card().Name, p.Tile().Position)
+func FmtPermanent(p Permanent) string {
+ typeIndicator := strings.ToUpper(p.Card().Type.String()[:1])
+
+ containing := p.ContainingPerm()
+ if containing == nil {
+ return fmt.Sprintf("%s%v", typeIndicator, p.Tile().Position)
+ }
+
+ for i, p := range containing.Pile() {
+ if p == p {
+ return fmt.Sprintf("%s%v[%d]", typeIndicator, p.Tile().Position, i+1)
+ }
+ }
+
+ log.Panicf("Permanent piled at %v not found in the pile", TileOrContainingPermTile(p).Position)
+ return ""
+}
+
+func FindPermanent(s *State, desc string) Permanent {
+ var pos Position
+ _, err := fmt.Sscanf(desc[1:strings.Index(desc, ")")], POSITION_FMT, &pos.X, &pos.Y)
+ if err != nil {
+ log.Panicf("desc %q not a valid permanent description", desc)
}
- return fmt.Sprintf("%s's %s", p.Controller().Name, p.Card().Name)
+ p := s.Map.TileAt(pos).Permanent
+ return p
}
func DealDamage(src, dest Permanent, damage int) {
@@ -97,11 +121,24 @@ func DealDamage(src, dest Permanent, damage int) {
actualDamage = 0
}
}
- dest.addDamage(actualDamage)
+ dest.adjustDamage(actualDamage)
+}
+
+func (p *permanentBase) adjustDamage(damage int) {
+ p.damage += damage
+ if p.damage < 0 {
+ p.damage = 0
+ }
}
+func (p *permanentBase) fight(Permanent) {}
-func (p *permanentBase) addDamage(damage int) { p.damage += damage }
-func (p *permanentBase) fight(Permanent) {}
+func (p *permanentBase) addTmpEffect(tmpEffect string) {
+ p.tmpEffects = append(p.tmpEffects, tmpEffect)
+}
+
+func (p *permanentBase) removeTmpEffect(tmpEffect string) {
+ p.tmpEffects = utils.RemoveFromUnorderedSlice[string](p.tmpEffects, tmpEffect)
+}
func (p *permanentBase) Effects() []string {
return append(p.card.getEffects(), p.tmpEffects...)
@@ -158,15 +195,7 @@ func (p *permanentBase) addPermanentToPile(a Permanent) { p.pile = appen
func (p *permanentBase) clearPile() { p.pile = nil }
func (p *permanentBase) removePermanentFromPile(rm Permanent) {
- for i, piled := range p.pile {
- if piled != rm {
- continue
- }
-
- p.pile[i] = p.pile[len(p.pile)-1]
- p.pile = p.pile[:len(p.pile)-1]
- return
- }
+ p.pile = utils.RemoveFromUnorderedSlice[Permanent](p.pile, rm)
}
func (b *permanentBase) onPile(containing Permanent) { b.Card().Impl.onPile(containing) }
@@ -188,6 +217,20 @@ func removePermanentFromPile(containing, rm Permanent) {
rm.setContainingPerm(nil)
}
+func movePermanent(p Permanent, t *Tile) {
+ if !p.IsAvailableTile(t) {
+ log.Panicf("moving %v to not available tile %v", p, t)
+ }
+
+ if p.ContainingPerm() != nil {
+ removePermanentFromPile(p.ContainingPerm(), p)
+ } else {
+ leaveTile(p)
+ }
+
+ enterTileOrPile(p, t)
+}
+
func enterTile(p Permanent, t *Tile) {
p.SetTile(t)
t.entering(p)
diff --git a/go/game/player.go b/go/game/player.go
index 685de680..efffc295 100644
--- a/go/game/player.go
+++ b/go/game/player.go
@@ -75,6 +75,13 @@ func (p *Player) gainResource(gain int) {
p.Resource += gain
}
+func (p *Player) reduceResource(amount int) {
+ p.Resource -= amount
+ if p.Resource < 0 {
+ p.Resource = 0
+ }
+}
+
func (p *Player) upkeep() {
p.gainResource(p.resourceGain())
diff --git a/go/game/pos.go b/go/game/pos.go
index 2a646a83..133f599b 100644
--- a/go/game/pos.go
+++ b/go/game/pos.go
@@ -9,12 +9,14 @@ type Position struct {
Y int
}
-func (p *Position) String() string {
- if *p == INVALID_POSITION() {
+const POSITION_FMT = "(%d, %d)"
+
+func (p Position) String() string {
+ if p == INVALID_POSITION() {
return "INVALID_POSITION"
}
- return fmt.Sprintf("(%d, %d)", p.X, p.Y)
+ return fmt.Sprintf(POSITION_FMT, p.X, p.Y)
}
func INVALID_POSITION() Position {
diff --git a/go/game/range.go b/go/game/range.go
index 07a6e234..bc50c99b 100644
--- a/go/game/range.go
+++ b/go/game/range.go
@@ -64,9 +64,9 @@ func PositionsInRange(origin Position, r int, includeOrigin bool) []Position {
return positions
}
-func TilesInRangeFromOrigin(m *Map, origin Position, r int) []*Tile {
+func tilesInRangeFromOrigin(m *Map, origin Position, r int, includeOrigin bool) []*Tile {
tiles := []*Tile{}
- for _, pos := range PositionsInRange(origin, r, false) {
+ for _, pos := range PositionsInRange(origin, r, includeOrigin) {
tile := m.TileAt(pos)
if tile != nil {
tiles = append(tiles, tile)
@@ -75,6 +75,10 @@ func TilesInRangeFromOrigin(m *Map, origin Position, r int) []*Tile {
return tiles
}
+func TilesInRangeFromOrigin(m *Map, origin Position, r int) []*Tile {
+ return tilesInRangeFromOrigin(m, origin, r, false)
+}
+
func TilesInRange(m *Map, p Permanent, r int) []*Tile {
return TilesInRangeFromOrigin(m, TileOrContainingPermTile(p).Position, r)
}
diff --git a/go/game/state.go b/go/game/state.go
index 82dccf15..604e4530 100644
--- a/go/game/state.go
+++ b/go/game/state.go
@@ -25,6 +25,7 @@ type State struct {
Permanents []Permanent
Units []*Unit
Rand *rand.Rand
+ EotEffects []effect
}
func NewState() *State {
@@ -88,6 +89,8 @@ func (s *State) Loop() []*Player {
s.ActivePhase = Phases.DiscardPhase
p.discardPhase()
+ s.endOfTurn()
+
winners = s.Map.WinCondition(s)
}
}
@@ -233,6 +236,27 @@ func (s *State) IsValidArtifactSwitch(a *ArtifactSwitchAction) error {
return nil
}
+func (s *State) IsValidArtifactMove(a *ArtifactMoveAction) error {
+ if s.ActivePhase != Phases.ActionPhase {
+ return fmt.Errorf("Play actions can only be declared during one's actions phase")
+ }
+
+ u := a.Source().(*Unit)
+ uTile := TileOrContainingPermTile(u)
+ artifact := a.Artifact
+ aTile := TileOrContainingPermTile(artifact)
+ if !IsPositionInRange(uTile.Position, aTile.Position, 1) {
+ return fmt.Errorf("Can only switch position with adjacent equipments")
+ }
+
+ solid, err := artifact.XEffect("solid")
+ if err != nil && artifact.Controller() != u.Controller() && solid > 0 {
+ return fmt.Errorf("Can only switch with solid artifacts you control")
+ }
+
+ return nil
+}
+
func (s *State) ValidateAction(a Action) (err error) {
err = a.CheckTargets(s)
if err != nil {
@@ -250,6 +274,8 @@ func (s *State) ValidateAction(a Action) (err error) {
err = s.IsValidEquip(a)
case *ArtifactSwitchAction:
err = s.IsValidArtifactSwitch(a)
+ case *ArtifactMoveAction:
+ err = s.IsValidArtifactMove(a)
case *BuyAction:
}
@@ -407,10 +433,6 @@ func (s *State) fight(p1, p2 Permanent) {
p2.fight(p1)
}
-func (s *State) MoveUnit(u *Unit, t *Tile) {
- u.move(s, t)
-}
-
func (s *State) switchPermanents(p1 Permanent, p2 Permanent) {
t1 := TileOrContainingPermTile(p1)
t2 := TileOrContainingPermTile(p2)
@@ -555,3 +577,16 @@ func (s *State) redistributeMapStoreCards() {
p.clearKnownStore()
}
}
+
+func (s *State) addEotEffect(e effect) {
+ e.apply(s)
+ s.EotEffects = append(s.EotEffects, e)
+}
+
+func (s *State) endOfTurn() {
+ for _, e := range s.EotEffects {
+ e.end(s)
+ }
+
+ s.EotEffects = []effect{}
+}
diff --git a/go/game/targets.go b/go/game/targets.go
index 1e80c06d..fb393057 100644
--- a/go/game/targets.go
+++ b/go/game/targets.go
@@ -35,14 +35,7 @@ type (
}
)
-func fmtSel(sel interface{}) string {
- switch t := sel.(type) {
- case *Tile:
- return t.Position.String()
- default:
- return fmt.Sprintf("%d", t)
- }
-}
+var INVALID_TARGET_DESC = TargetDesc{}
func (t *Target) String() string {
if len(t.sel) == 0 {
@@ -50,12 +43,28 @@ func (t *Target) String() string {
}
if len(t.sel) == 1 {
- return fmtSel(t.sel[0])
+ return fmt.Sprintf("%v", t.sel[0])
}
s := "["
for _, sel := range t.sel {
- s += fmt.Sprintf("%s ", fmtSel(sel))
+ s += fmt.Sprintf("%v ", sel)
+ }
+ return s[:len(s)-1] + "]"
+}
+
+func (t *Targets) String() string {
+ if len(t.ts) == 0 {
+ return "no targets"
+ }
+
+ if len(t.ts) == 1 {
+ return t.ts[0].String()
+ }
+
+ s := "["
+ for _, t := range t.ts {
+ s += fmt.Sprintf("%v ", t)
}
return s[:len(s)-1] + "]"
}
@@ -114,6 +123,30 @@ func newPileDropTargets(s *State, a *PileDropAction) *Targets {
return targets
}
+func newArtifactMoveTargets(s *State, a *ArtifactMoveAction) *Targets {
+ targets := newTargets()
+ targets.ts = make([]*Target, 0, 2)
+
+ u := a.Source().(*Unit)
+ desc := newTargetDesc("available tile")
+ constraints := []TargetConstraintFunc{tileTargetConstraint}
+ constraints = append(constraints, availableTileConstraint(a, u.Card()))
+ constraints = append(constraints, moveConstraint(s, a, u))
+ moveTarget := newConstraintTarget(s, desc, conjunction(constraints...), a)
+ targets.ts = append(targets.ts, moveTarget)
+
+ constraints = []TargetConstraintFunc{tileTargetConstraint}
+ constraints = append(constraints, availableTileConstraint(a, a.Artifact.Card()))
+ constraints = append(constraints, func(t interface{}) error {
+ c := rangeTargetConstraint(moveTarget.sel[0], 1)
+ return c(t)
+ })
+ constraints = append(constraints, noPreviousSelectionConstraint(targets, 1))
+ targets.ts = append(targets.ts, newConstraintTarget(s, desc, conjunction(constraints...), a))
+
+ return targets
+}
+
func (d *TargetDesc) requirement() TargetRequirement {
switch d.req {
case "?":
@@ -458,13 +491,7 @@ func parseArtifactTargetConstraint(constraint string, s *State, action Action) [
return constraints
}
-func relaxedTileTarget(action Action, t interface{}) *Tile {
- // Strict case
- if _, ok := action.(*MoveAction); !ok {
- return t.(*Tile)
- }
-
- // Move actions allow to also move to Tile occupied by a crewable permanent
+func _relaxedTileTarget(t interface{}) *Tile {
switch t := t.(type) {
case *Tile:
return t
@@ -477,6 +504,21 @@ func relaxedTileTarget(action Action, t interface{}) *Tile {
return nil
}
+func relaxedTileTarget(action Action, t interface{}) *Tile {
+ switch action.(type) {
+ // Some actions allow to also move to Tile occupied by a crewable permanent
+ case *MoveAction:
+ return _relaxedTileTarget(t)
+ case *ArtifactMoveAction:
+ return _relaxedTileTarget(t)
+ case *ArtifactSwitchAction:
+ return _relaxedTileTarget(t)
+ // Strict case
+ default:
+ return t.(*Tile)
+ }
+}
+
func availableTileConstraint(action Action, card *Card) TargetConstraintFunc {
return func(t interface{}) (err error) {
tile := relaxedTileTarget(action, t)
@@ -492,19 +534,23 @@ func availableTileConstraint(action Action, card *Card) TargetConstraintFunc {
}
}
+func moveConstraint(s *State, action Action, u *Unit) TargetConstraintFunc {
+ return func(t interface{}) (err error) {
+ // Some actions allow also unit targets as though they were tiles
+ tile := relaxedTileTarget(action, t)
+ if slices.Contains(u.MoveRangeTiles(s.Map), tile) {
+ return nil
+ }
+ return fmt.Errorf("tile %v is not in %v's movement range", tile, u)
+ }
+}
+
func parseTileTargetConstraint(desc string, s *State, action Action) []TargetConstraintFunc {
constraints := []TargetConstraintFunc{}
if moveAction, ok := action.(*MoveAction); ok {
u := moveAction.Source().(*Unit)
- constraints = append(constraints, func(t interface{}) (err error) {
- tile := relaxedTileTarget(action, t)
- if slices.Contains(u.MoveRangeTiles(s.Map), tile) {
- return nil
- }
- return fmt.Errorf("tile %v is not in %v's movement range", tile, u)
- })
- // Move action allow also unit targets as though they were tiles
+ constraints = append(constraints, moveConstraint(s, action, u))
} else {
constraints = append(constraints, tileTargetConstraint)
}
diff --git a/go/game/targets_test.go b/go/game/targets_test.go
new file mode 100644
index 00000000..be1cdad9
--- /dev/null
+++ b/go/game/targets_test.go
@@ -0,0 +1,52 @@
+package game
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestDisjunction(t *testing.T) {
+ mapDef := `map: |1-
+ HST
+ HSF
+ TSW
+symbols:
+ T: tower
+ H: house
+ F: farm
+ S: street
+ W: deep water
+`
+ s := NewState()
+ r := strings.NewReader(mapDef)
+ s.Map, _ = readMap(r)
+
+ s.AddNewPlayer("player", NewDeck())
+ p := s.Players[0]
+
+ pioneer := NewUnit(NewCard("base/pioneer"), s.Map.TileAt(Position{1, 1}), p)
+ s.AddPermanent(pioneer)
+
+ sword := newEquipmentFromPath("base/sword", s.Map.TileAt(Position{0, 1}), p)
+ s.AddPermanent(sword)
+
+ fa := pioneer.FullActions[0]
+ targets := fa.Targets()
+ if err := targets.AddSelection(s.Map.TileAt(Position{1, 1})); err != nil {
+ t.Fatalf("TileAt(1,1) not a valid target for %v", fa)
+ }
+
+ if err := targets.CheckTargets(s); err != nil {
+ t.Fatalf("TileAt(1,1) not a valid target for %v", fa)
+ }
+
+ targets.ClearSelections()
+
+ if err := targets.AddSelection(sword); err != nil {
+ t.Fatalf("%v not a valid target for %v", sword, fa)
+ }
+
+ if err := targets.CheckTargets(s); err != nil {
+ t.Fatalf("%v not a valid target for %v", sword, fa)
+ }
+}
diff --git a/go/game/tile.go b/go/game/tile.go
index 9034203d..f8d160da 100644
--- a/go/game/tile.go
+++ b/go/game/tile.go
@@ -90,7 +90,7 @@ type Tile struct {
}
func (t *Tile) String() string {
- return fmt.Sprintf("%v@%v", t.Type, t.Position)
+ return fmt.Sprintf("%v", t.Position)
}
func INVALID_TILE() Tile {
diff --git a/go/game/unit.go b/go/game/unit.go
index 99c7c28d..f1e38078 100644
--- a/go/game/unit.go
+++ b/go/game/unit.go
@@ -1,10 +1,5 @@
package game
-import (
- "fmt"
- "log"
-)
-
const (
DEFAULT_AVAIL_MOVE_ACTIONS = 1
DEFAULT_AVAIL_ATTACK_ACTIONS = 1
@@ -62,12 +57,8 @@ func NewUnit(card *Card, tile *Tile, owner *Player) *Unit {
return u
}
-func (u *Unit) Fmt() string {
- return fmt.Sprintf("%s@%v", u.Card().Name, TileOrContainingPermTile(u))
-}
-
-func (u *Unit) FmtWController() string {
- return fmt.Sprintf("%s's %s@%v", u.Controller().Name, u.Card().Name, TileOrContainingPermTile(u))
+func (u *Unit) String() string {
+ return FmtPermanent(u)
}
func NewUnitFromPath(cardPath string, tile *Tile, owner *Player) *Unit {
@@ -88,24 +79,11 @@ func (u *Unit) onUpkeep() {
u.resetBaseActions()
}
-func (u *Unit) move(s *State, tile *Tile) {
- if !u.IsAvailableTile(tile) {
- log.Panicf("moving to not available tile %v", tile)
- }
-
- if u.containingPerm != nil {
- removePermanentFromPile(u.containingPerm, u)
- } else {
- leaveTile(u)
- }
-
- enterTileOrPile(u, tile)
-}
-
func (u *Unit) MoveRangeTiles(m *Map) []*Tile {
tiles := []*Tile{}
// TODO: build graph only containing possible tile in movement range
graph := m.generateMapGraphFor(u)
+ // graph := m.generateMovementRangeGraphFor(u)
origin := TileOrContainingPermTile(u).Position
for _, pos := range PositionsInRange(origin, u.Movement.Range, false) {
tile := m.TileAt(pos)
@@ -215,6 +193,10 @@ func (u *Unit) AvailSlowActions() (actions []Action) {
actions = append(actions, NewEquipAction(u, equipment))
}
}
+
+ if tile.Permanent.Card().Type.IsArtifact() {
+ actions = append(actions, newArtifactMoveAction(u, tile.Permanent))
+ }
}
}
@@ -288,13 +270,13 @@ func (u *Unit) removeFullAction(tag string) {
}
}
-func (u *Unit) addDamage(damage int) {
- if u.Marks(UnitMarks.Ward) > 0 {
+func (u *Unit) adjustDamage(damage int) {
+ if damage > 0 && u.Marks(UnitMarks.Ward) > 0 {
u.adjustMarks(UnitMarks.Ward, -1)
return
}
- u.permanentBase.addDamage(damage)
+ u.permanentBase.adjustDamage(damage)
}
func (u *Unit) equip(e *Equipment) {
diff --git a/go/game/unit_test.go b/go/game/unit_test.go
index 4b0ee8cc..4cfcdfb9 100644
--- a/go/game/unit_test.go
+++ b/go/game/unit_test.go
@@ -33,7 +33,7 @@ symbols:
sword := newEquipmentFromPath("base/sword", s.Map.TileAt(Position{0, 1}), p)
s.AddPermanent(sword)
- a.move(s, f.Tile())
+ movePermanent(a, f.Tile())
if len(f.Pile()) != 1 {
t.Fatalf("fisher's pile size is not 1")
}
@@ -65,7 +65,7 @@ symbols:
t.Fatalf("Piled archer has attackable tiles")
}
- a.move(s, s.Map.TileAt(Position{1, 1}))
+ movePermanent(a, s.Map.TileAt(Position{1, 1}))
if len(f.Pile()) != 0 {
t.Fatalf("fisher's pile is not empty")
}
diff --git a/go/ui/button.go b/go/ui/button.go
index 50f78a3d..e882d39c 100644
--- a/go/ui/button.go
+++ b/go/ui/button.go
@@ -46,6 +46,8 @@ func (b *SimpleButton) Click(x, y int) {
}
func (b *SimpleButton) UpdateLabel(label string) {
- b.label = label
- b.ForceRedraw()
+ if b.label != label {
+ b.label = label
+ b.ForceRedraw()
+ }
}
diff --git a/go/ui/hoverable.go b/go/ui/hoverable.go
index 54d4ec15..30415c8d 100644
--- a/go/ui/hoverable.go
+++ b/go/ui/hoverable.go
@@ -62,11 +62,13 @@ type hoverCardView struct {
func (h *hoverCardView) init(w Widget) {
h.createHint = func(x, y int) Widget {
- card := w.FindObjectAt(x, y).(*game.Card)
- if card == nil {
+ obj := w.FindObjectAt(x, y)
+ if obj == nil {
return nil
}
+ card := obj.(*game.Card)
+
wx, wy := x, y
if x+HOVER_CARD_WIDTH > h.xMax || y+HOVER_CARD_HEIGHT > h.yMax {
wx = x - HOVER_CARD_WIDTH
diff --git a/go/utils/slices.go b/go/utils/slices.go
index 3cc38c1c..eb02f782 100644
--- a/go/utils/slices.go
+++ b/go/utils/slices.go
@@ -15,3 +15,16 @@ func InterfaceSliceToTypedSlice[T any](s []interface{}) []T {
}
return ts
}
+
+func RemoveFromUnorderedSlice[T comparable](s []T, o T) []T {
+ for i, t := range s {
+ if t != o {
+ continue
+ }
+
+ s[i] = s[len(s) - 1]
+ return s[:len(s) - 1]
+ }
+
+ return s
+}