diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2023-02-25 23:08:47 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-08-20 15:57:06 +0200 |
| commit | 3d36d1d0254b8aeecc889a2a49cbcced458cbe9c (patch) | |
| tree | 38dd0851d8eaa6535d05b97f29f65fc35655a5ef /go | |
| parent | 19fb1a7e5c5883f49e944b6499fa1ced207a3e39 (diff) | |
| download | muhqs-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/TODO | 4 | ||||
| -rw-r--r-- | go/client/game.go | 14 | ||||
| -rw-r--r-- | go/client/main.go | 4 | ||||
| -rw-r--r-- | go/game/action.go | 64 | ||||
| -rw-r--r-- | go/game/ai.go | 62 | ||||
| -rw-r--r-- | go/game/areaEffect_test.go | 2 | ||||
| -rw-r--r-- | go/game/artifact.go | 4 | ||||
| -rw-r--r-- | go/game/card.go | 4 | ||||
| -rw-r--r-- | go/game/cardImplementations.go | 204 | ||||
| -rw-r--r-- | go/game/cardParsing.go | 4 | ||||
| -rw-r--r-- | go/game/effect.go | 47 | ||||
| -rw-r--r-- | go/game/permanent.go | 79 | ||||
| -rw-r--r-- | go/game/player.go | 7 | ||||
| -rw-r--r-- | go/game/pos.go | 8 | ||||
| -rw-r--r-- | go/game/range.go | 8 | ||||
| -rw-r--r-- | go/game/state.go | 43 | ||||
| -rw-r--r-- | go/game/targets.go | 96 | ||||
| -rw-r--r-- | go/game/targets_test.go | 52 | ||||
| -rw-r--r-- | go/game/tile.go | 2 | ||||
| -rw-r--r-- | go/game/unit.go | 38 | ||||
| -rw-r--r-- | go/game/unit_test.go | 4 | ||||
| -rw-r--r-- | go/ui/button.go | 6 | ||||
| -rw-r--r-- | go/ui/hoverable.go | 6 | ||||
| -rw-r--r-- | go/utils/slices.go | 13 |
24 files changed, 649 insertions, 126 deletions
@@ -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 +} |
