diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2023-01-20 03:18:06 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-01-27 16:43:44 +0100 |
| commit | c4cb9637f5c85d03aa162968f0dfec3c193fc9dc (patch) | |
| tree | 3ad32e5dfd7c4ae72f66b86e77edef7b700a8b7a /go/game | |
| parent | 9df5cc7b55d8ac9ecd775a14a14f80a6c36c4d74 (diff) | |
| download | muhqs-game-c4cb9637f5c85d03aa162968f0dfec3c193fc9dc.tar.gz muhqs-game-c4cb9637f5c85d03aa162968f0dfec3c193fc9dc.zip | |
intermediate commit
Implement actions and multiple ui widgets
Diffstat (limited to 'go/game')
| -rw-r--r-- | go/game/action.go | 133 | ||||
| -rw-r--r-- | go/game/ai.go | 123 | ||||
| -rw-r--r-- | go/game/artifact.go | 48 | ||||
| -rw-r--r-- | go/game/card.go | 21 | ||||
| -rw-r--r-- | go/game/equipment.go | 5 | ||||
| -rw-r--r-- | go/game/hand.go | 5 | ||||
| -rw-r--r-- | go/game/permanent.go | 60 | ||||
| -rw-r--r-- | go/game/player.go | 9 | ||||
| -rw-r--r-- | go/game/pos.go | 12 | ||||
| -rw-r--r-- | go/game/range.go | 8 | ||||
| -rw-r--r-- | go/game/stack.go | 29 | ||||
| -rw-r--r-- | go/game/state.go | 77 | ||||
| -rw-r--r-- | go/game/tile.go | 15 | ||||
| -rw-r--r-- | go/game/unit.go | 100 |
14 files changed, 606 insertions, 39 deletions
diff --git a/go/game/action.go b/go/game/action.go index 723d4866..58eea8da 100644 --- a/go/game/action.go +++ b/go/game/action.go @@ -1,6 +1,133 @@ package game -type Action struct { - Owner *Player - Card *Card +import ( + "fmt" + "log" +) + +type ActionPrototype interface { + Specialize(...interface{}) Action + String() string +} + +type Action interface { + Declare(s *State) + Resolve(s *State) + String() string +} + +type FastAction interface { + Action + FastDeclare(s *State) +} + +type ActionBase struct { + Source interface{} + Card *Card + resolveFunc func(s *State) + desc string +} + +func (a *ActionBase) Declare(s *State) { + if !s.Stack.IsEmpty() { + log.Fatalf("Slow action can not be declared while other actions are on the stack") + } + s.Stack.Push(a) +} + +func (a *ActionBase) Resolve(s *State) { + a.resolveFunc(s) +} + +func (a *ActionBase) String() string { + if p, ok := a.Source.(Permanent); ok { + return fmt.Sprintf("%s: %s %s", p.GetController().Name, p.GetTile().Position.String(), a.desc) + } + return fmt.Sprintf("%s %s", a.Source, a.desc) +} + +type MoveActionPrototype struct { + Unit *Unit +} + +func NewMoveActionPrototype(u *Unit) *MoveActionPrototype { + return &MoveActionPrototype{u} +} + +func (proto *MoveActionPrototype) String() string { + return fmt.Sprintf("move %d", proto.Unit.Movement) +} + +func (proto *MoveActionPrototype) Specialize(args ...interface{}) Action { + target := args[0].(*Tile) + u := proto.Unit + + resolveFunc := func(s *State) { + u.move(s, target) + } + desc := fmt.Sprintf("-> %s", target.Position.String()) + return &ActionBase{u, u.GetCard(), resolveFunc, desc} +} + +type AttackActionPrototype struct { + Unit *Unit +} + +func NewAttackActionPrototype(u *Unit) *AttackActionPrototype { + return &AttackActionPrototype{u} +} + +func (proto *AttackActionPrototype) String() string { + return fmt.Sprintf("attack %s", proto.Unit.Attack.String()) +} + +func (proto *AttackActionPrototype) Specialize(args ...interface{}) Action { + target := args[0].(Permanent) + u := proto.Unit + resolveFunc := func(s *State) { + s.Fight(u, target) + } + desc := fmt.Sprintf("x %s", target.GetTile().Position.String()) + return &ActionBase{u, u.GetCard(), resolveFunc, desc} +} + +type FullActionPrototype struct { + Permanent Permanent +} + +func (proto *FullActionPrototype) String() string { + return "full_action" +} + +func (proto *FullActionPrototype) Specialize(args ...interface{}) Action { + p := proto.Permanent + resolveFunc := args[0].(func(s *State)) + desc := "full_action" + return &ActionBase{p, p.GetCard(), resolveFunc, desc} +} + +type FreeActionPrototype struct { + Permanent Permanent +} + +func (proto *FreeActionPrototype) String() string { + return "free_action" +} + +func (proto *FreeActionPrototype) Specialize(args ...interface{}) Action { + p := proto.Permanent + resolveFunc := args[0].(func(s *State)) + desc := "free_action" + return &FreeAction{ActionBase{p, p.GetCard(), resolveFunc, desc}} +} + +type FreeAction struct { + ActionBase +} + +func (a *FreeAction) FastDeclare(s *State) { + s.Stack.Push(a) +} + +func (a *FreeAction) Resolve(s *State) { } diff --git a/go/game/ai.go b/go/game/ai.go new file mode 100644 index 00000000..b1921639 --- /dev/null +++ b/go/game/ai.go @@ -0,0 +1,123 @@ +package game + +func moveToRandomTile(s *State, u *Unit) { + moveTargets := u.MoveRangeTiles(s.Map) + if len(moveTargets) > 0 { + moveTarget := moveTargets[s.Rand.Intn(len(moveTargets))-1] + s.MoveUnit(u, moveTarget) + } +} + +func attackAttackableEnemyPerm(s *State, u *Unit) { + attackablePerms := u.AttackablePermanents(s.Map) + if len(attackablePerms) > 0 { + attackablePerm := attackablePerms[s.Rand.Intn(len(attackablePerms))-1] + s.Fight(u, attackablePerm) + } +} + +// 1. If enemy Unit in attack Range then +// attack enemy Unit in Range +// 2. While move action available do +// 3a. If enemy unit exists +// move towards nearest enemy unit until it is in attack range +// if attack action avail then proceed at 1. +// 3b. Otherwise +// move to random tile in movement range +func AggressiveAi(s *State, u *Unit) { + // 1. + attackAttackableEnemyPerm(s, u) + + // 2. + if u.AvailMoveActions > 0 { + enemyUnits := s.EnemyUnits(u.GetController()) + if len(enemyUnits) > 0 { // 3a. + // TODO: implement path finding + if u.AvailAttackActions > 0 { + AggressiveAi(s, u) + } + } else { // 3b. + moveToRandomTile(s, u) + } + } +} + +// 2.2 Shy +// 1. If not in the attack Range of an enemy Unit then +// activate full action if available +// 2. If move action available then +// move to most distant point from all enemy units +// 3. If enemy Unit in attack Range then +// attack enemy Unit in Range +func ShyAi(s *State, u *Unit) { + // 1. + inEnemyAttackRange := false +out: + for _, enemyUnit := range s.EnemyUnits(u.GetController()) { + for _, p := range enemyUnit.AttackablePermanents(s.Map) { + if p == u { + inEnemyAttackRange = true + break out + } + } + } + if !inEnemyAttackRange && u.HasFullAction() { + u.FullAction() + } + + // 2. + if u.AvailMoveActions > 0 { + // TODO: implement path finding + } + + // 3. + if u.AvailAttackActions > 0 { + attackAttackableEnemyPerm(s, u) + } +} + +// 2.3 Wandering X +// 1. If enemy unit in Range X then +// execute aggressive AI +// 2. If move action available then +// move to random tile in movement Range +// proceed at 1. +func WanderingAi(s *State, u *Unit, x int) { + // 1. + for _, enemyUnit := range s.EnemyUnits(u.GetController()) { + if IsPositionInRange(u.GetTile().Position, enemyUnit.GetTile().Position, x) { + AggressiveAi(s, u) + break + } + } + // 2. + if u.AvailMoveActions > 0 { + moveToRandomTile(s, u) + WanderingAi(s, u, x) + } +} + +// 2.4 Target-oriented TARGET +// 1. Use full action if possible +// 2a. If a TARGET exists and is reachable +// +// Move onto or towards nearest TARGET +// 3a. If enemy Unit in attack Range +// attack enemy Unit in Range +// +// 2b. Otherwise +// +// execute wandering 3 AI +func TargetOrientedAi(s *State, u *Unit, t *Position) { + // 1. + if u.HasFullAction() { + u.FullAction() + } + + if t != nil { + // TODO: path finding again + attackAttackableEnemyPerm(s, u) + } else { + WanderingAi(s, u, 3) + } +} diff --git a/go/game/artifact.go b/go/game/artifact.go new file mode 100644 index 00000000..452d5a6a --- /dev/null +++ b/go/game/artifact.go @@ -0,0 +1,48 @@ +package game + +import ( + "log" + "strconv" + "strings" +) + +type Artifact struct { + PermanentBase + Solid int +} + +func NewArtifact(cardPath string, tile *Tile, owner *Player) *Artifact { + card := NewCard(cardPath) + + a := &Artifact{NewPermanentBase(card, tile, owner), 0} + tile.Permanent = a + + effects := card.getEffects() + for _, e := range effects { + if strings.HasPrefix(e, "Solid") { + tokens := strings.Split(e, " ") + var err error + a.Solid, err = strconv.Atoi(tokens[1]) + if err != nil { + log.Fatalf("Invalid Solid definition %s\n", e) + } + } + } + + return a +} + +func (a *Artifact) Fight(p Permanent) { +} + +func (a *Artifact) IsDestroyed() bool { + return a.Damage >= a.Solid +} + +func (a *Artifact) Attackable() bool { + return a.Solid > 0 +} + +func (a *Artifact) GetAvailActionPrototypes() []ActionPrototype { + return []ActionPrototype{} +} diff --git a/go/game/card.go b/go/game/card.go index f77a274a..bfa476a9 100644 --- a/go/game/card.go +++ b/go/game/card.go @@ -132,6 +132,27 @@ func (card *Card) parseDefinition(definition []byte) { } } +func (card *Card) getEffects() []string { + effects, found := card.Values["effect"] + if !found { + return []string{} + } + + if effect, ok := effects.(string); ok { + return []string{effect} + } + + if effectsMap, ok := effects.(map[string]interface{}); ok { + enEffects := effectsMap["en"] + if effect, ok := enEffects.(string); ok { + return []string{effect} + } + return enEffects.([]string) + } + return effects.([]string) + +} + func NewCard(cardPath string) *Card { cardName := path.Base(cardPath) set := path.Dir(cardPath) diff --git a/go/game/equipment.go b/go/game/equipment.go new file mode 100644 index 00000000..1b78c8ad --- /dev/null +++ b/go/game/equipment.go @@ -0,0 +1,5 @@ +package game + +type Equipment struct { + Artifact +} diff --git a/go/game/hand.go b/go/game/hand.go index fde4a686..169c8cb3 100644 --- a/go/game/hand.go +++ b/go/game/hand.go @@ -22,11 +22,14 @@ func (h *Hand) AddCard(card *Card) { } func (h *Hand) RemoveCard(card *Card) { + ncards := len(h.Cards) for i, c := range h.Cards { if c != card { continue } - h.Cards = append(h.Cards[:i], h.Cards[i+1:]...) + h.Cards[i] = h.Cards[ncards-1] + h.Cards = h.Cards[:ncards-1] + return } } diff --git a/go/game/permanent.go b/go/game/permanent.go index 37111c4d..7a9988e1 100644 --- a/go/game/permanent.go +++ b/go/game/permanent.go @@ -1,10 +1,66 @@ package game -type Permanent struct { +import ( + "fmt" + "log" +) + +type Permanent interface { + GetCard() *Card + GetTile() *Tile + GetDamage() int + GetPile() []Permanent + GetController() *Player + GetOwner() *Player + + String() string + Fight(p Permanent) + AddDamage(damage int) + IsDestroyed() bool + Attackable() bool + GetAvailActionPrototypes() []ActionPrototype +} + +type PermanentBase struct { Card *Card Tile *Tile Damage int - Pile []*Permanent + Pile []Permanent Controller *Player Owner *Player } + +func NewPermanentBase(card *Card, tile *Tile, owner *Player) PermanentBase { + if tile.Permanent != nil { + log.Fatal("Tile at %v already occupied by %v", tile.Position, tile.Permanent) + } + return PermanentBase{card, tile, 0, []Permanent{}, owner, owner} +} + +func (p *PermanentBase) GetCard() *Card { + return p.Card +} +func (p *PermanentBase) GetTile() *Tile { + return p.Tile +} +func (p *PermanentBase) GetDamage() int { + return p.Damage +} +func (p *PermanentBase) GetPile() []Permanent { + return p.Pile +} +func (p *PermanentBase) GetController() *Player { + return p.Controller +} +func (p *PermanentBase) GetOwner() *Player { + return p.Owner +} + +func (p *PermanentBase) String() string { + return fmt.Sprintf("%s's %s@%s", + p.GetController().Name, p.GetCard().Name, p.GetTile().Position.String()) +} + +func (p *PermanentBase) AddDamage(damage int) { + p.Damage += damage +} diff --git a/go/game/player.go b/go/game/player.go index 20ac13a5..78ba69a7 100644 --- a/go/game/player.go +++ b/go/game/player.go @@ -1,5 +1,9 @@ package game +import ( + "image/color" +) + const ( MAX_DRAW int = 3 ) @@ -12,10 +16,11 @@ type Player struct { Deck *Deck Ressource int gameState *State + Color color.Color } -func NewPlayer(id int, name string, deck *Deck, gameState *State) *Player { - p := Player{id, name, NewHand(), NewDiscardPile(), deck, 0, gameState} +func NewPlayer(id int, name string, deck *Deck, gameState *State, color color.Color) *Player { + p := Player{id, name, NewHand(), NewDiscardPile(), deck, 0, gameState, color} return &p } diff --git a/go/game/pos.go b/go/game/pos.go index 2b3459cd..2a646a83 100644 --- a/go/game/pos.go +++ b/go/game/pos.go @@ -1,10 +1,22 @@ package game +import ( + "fmt" +) + type Position struct { X int Y int } +func (p *Position) String() string { + if *p == INVALID_POSITION() { + return "INVALID_POSITION" + } + + return fmt.Sprintf("(%d, %d)", p.X, p.Y) +} + func INVALID_POSITION() Position { return Position{-1, -1} } diff --git a/go/game/range.go b/go/game/range.go index 86ec2f50..e12e1e67 100644 --- a/go/game/range.go +++ b/go/game/range.go @@ -37,3 +37,11 @@ func PositionsInRange(origin Position, r int, includeOrigin bool) []Position { } return positions } + +func TilesInRange(m *Map, p Permanent, r int) []*Tile { + tiles := []*Tile{} + for _, pos := range PositionsInRange(p.GetTile().Position, r, false) { + tiles = append(tiles, m.TileAt(pos)) + } + return tiles +} diff --git a/go/game/stack.go b/go/game/stack.go index 5d62e601..22c20fa3 100644 --- a/go/game/stack.go +++ b/go/game/stack.go @@ -1,5 +1,32 @@ package game +import ( + "log" +) + type Stack struct { - Actions []Action + gameState *State + Actions []Action +} + +func NewStack(s *State) *Stack { + return &Stack{s, []Action{}} +} + +func (s *Stack) IsEmpty() bool { + return len(s.Actions) == 0 +} + +func (s *Stack) Push(a Action) { + s.Actions = append(s.Actions, a) +} + +func (s *Stack) Pop() { + l := len(s.Actions) + if l == 0 { + log.Fatalf("Can not pop from empty stack") + } + a := s.Actions[l-1] + s.Actions = s.Actions[:l-1] + a.Resolve(s.gameState) } diff --git a/go/game/state.go b/go/game/state.go index caae8106..3d600093 100644 --- a/go/game/state.go +++ b/go/game/state.go @@ -1,5 +1,10 @@ package game +import ( + "image/color" + "math/rand" +) + type State struct { Decks []*Deck Stores []*Store @@ -8,25 +13,87 @@ type State struct { Hands []*Hand DiscardPiles []*DiscardPile Players []*Player - Permanents []*Permanent + Permanents []Permanent Units []*Unit + Artifacts []*Artifact + Rand rand.Rand } func NewState() *State { - return &State{} + s := &State{} + s.Stack = NewStack(s) + return s } -func (s *State) AddNewPlayer(name string, deck *Deck) { - p := NewPlayer(len(s.Players), name, deck, s) +func (s *State) AddNewPlayer(name string, deck *Deck, color color.Color) { + p := NewPlayer(len(s.Players), name, deck, s, color) s.Players = append(s.Players, p) } func (s *State) AddNewUnit(card string, pos Position, owner *Player) { unit := NewUnit(card, s.Map.TileAt(pos), owner) - s.Permanents = append(s.Permanents, &unit.Permanent) + s.Permanents = append(s.Permanents, unit) s.Units = append(s.Units, unit) } +func (s *State) removePermanent(p Permanent) { + for i, perm := range s.Permanents { + if perm == p { + s.Permanents[i] = s.Permanents[len(s.Permanents)-1] + s.Permanents = s.Permanents[:len(s.Permanents)-1] + } + } +} + +func removePtr[P *Unit | *Artifact](collection []P, ptr P) []P { + for i, p := range collection { + if p == ptr { + collection[i] = collection[len(collection)-1] + return collection[:len(collection)-1] + } + } + return collection +} + +func (s *State) DestroyPermanent(p Permanent) { + s.removePermanent(p) + + switch p.(type) { + case *Unit: + s.Units = removePtr(s.Units, p.(*Unit)) + case *Artifact: + s.Artifacts = removePtr(s.Artifacts, p.(*Artifact)) + } +} + +func (s *State) AddNewArtifact(card string, pos Position, owner *Player) { + artifact := NewArtifact(card, s.Map.TileAt(pos), owner) + s.Permanents = append(s.Permanents, artifact) + s.Artifacts = append(s.Artifacts, artifact) +} + +func (s *State) Fight(p1, p2 Permanent) { + p1.Fight(p2) + p2.Fight(p1) + if p1.IsDestroyed() { + s.DestroyPermanent(p1) + } + if p2.IsDestroyed() { + s.DestroyPermanent(p2) + } +} + func (s *State) MoveUnit(u *Unit, t *Tile) { u.move(s, t) } + +func (s *State) EnemyUnits(player *Player) []*Unit { + // TODO: support coop / teams + enemyUnits := []*Unit{} + for _, u := range s.Units { + if u.GetController() != player { + enemyUnits = append(enemyUnits, u) + } + } + return enemyUnits +} diff --git a/go/game/tile.go b/go/game/tile.go index 50688d83..b8e8c83f 100644 --- a/go/game/tile.go +++ b/go/game/tile.go @@ -2,7 +2,6 @@ package game import ( "log" - "strconv" "strings" ) @@ -62,27 +61,19 @@ var TileNames = map[string]TileType{ type Tile struct { Position Position - Permanent *Permanent + Permanent Permanent Type TileType Raw string - PlayerId int } func INVALID_TILE() Tile { - return Tile{INVALID_POSITION(), nil, 0, "", -1} + return Tile{INVALID_POSITION(), nil, 0, ""} } func NewTileFromString(tile string, pos Position) (Tile, error) { tokens := strings.Split(tile, " ") tileType := TileNames[strings.ToLower(tokens[0])] - t := Tile{pos, nil, tileType, tile, -1} - if tokens[0] == "spawn" { - playerId, err := strconv.Atoi(tokens[2]) - if err != nil { - return INVALID_TILE(), err - } - t.PlayerId = playerId - } + t := Tile{pos, nil, tileType, tile} return t, nil } diff --git a/go/game/unit.go b/go/game/unit.go index 4c07d625..b81ea8b3 100644 --- a/go/game/unit.go +++ b/go/game/unit.go @@ -1,16 +1,17 @@ package game import ( + "fmt" "golang.org/x/exp/slices" "log" "strconv" "strings" ) -type UnitStates int +type UnitState int const ( - Paralysis UnitStates = iota + 1 + Paralysis UnitState = iota + 1 Poison Panic Rage @@ -22,6 +23,13 @@ type Attack struct { Range int } +func (a *Attack) String() string { + if a.Range == 1 { + return fmt.Sprintf("%d", a.Damage) + } + return fmt.Sprintf("%d range %d", a.Damage, a.Range) +} + func parseAttack(attack string) Attack { tokens := strings.Split(attack, " ") a, err := strconv.Atoi(tokens[0]) @@ -40,34 +48,46 @@ func parseAttack(attack string) Attack { } type Unit struct { - Permanent - Health int - Movement int - Attack Attack - Upkeep int - // FullActions []FullActions - // FreeActions []FreeActions - States []UnitStates + PermanentBase + Health int + Movement int + Attack Attack + Upkeep int + FullActions []*FullActionPrototype + FreeActions []*FreeActionPrototype + States []UnitState // Effects []Effects + AvailMoveActions int + AvailAttackActions int } func NewUnit(cardPath string, tile *Tile, owner *Player) *Unit { card := NewCard(cardPath) - return &Unit{ - Permanent{card, tile, 0, []*Permanent{}, owner, owner}, + u := &Unit{ + NewPermanentBase(card, tile, owner), card.Values["health"].(int), card.Values["movement"].(int), parseAttack(card.Values["attack"].(string)), card.Values["upkeep"].(int), - []UnitStates{}, + []*FullActionPrototype{}, + []*FreeActionPrototype{}, + []UnitState{}, + 1, + 1, } + + tile.Permanent = u + return u } func (u *Unit) move(s *State, tile *Tile) { if !slices.Contains(u.MoveRangeTiles(s.Map), tile) { log.Panicf("Unit %s at %v can not move to %v", u.Card.Name, u.Tile.Position, tile.Position) } + if tile.Permanent != nil { + log.Panicf("Unit %s at %v can not move to occupied %v", u.Card.Name, u.Tile.Position, tile.Position) + } u.Tile.Permanent = nil u.Tile = tile @@ -84,6 +104,60 @@ func (u *Unit) MoveRangeTiles(m *Map) []*Tile { return tiles } +func (u *Unit) AttackableTiles(m *Map) []*Tile { + return TilesInRange(m, u, u.Attack.Range) +} + +func (u *Unit) AttackablePermanents(m *Map) []Permanent { + permanents := []Permanent{} + for _, t := range u.AttackableTiles(m) { + if t.Permanent != nil && t.Permanent.Attackable() { + permanents = append(permanents, t.Permanent) + } + } + return permanents +} + func (u *Unit) IsAvailableTile(tile *Tile) bool { return tile.IsFree() } + +func (u *Unit) Fight(p Permanent) { + p.AddDamage(u.Attack.Damage) +} + +func (u *Unit) IsDestroyed() bool { + return u.PermanentBase.Damage >= u.Health +} + +func (u *Unit) Attackable() bool { + return true +} + +func (u *Unit) HasFullAction() bool { + return len(u.FullActions) > 0 +} + +func (u *Unit) FullAction() { +} + +func (u *Unit) GetAvailActionPrototypes() []ActionPrototype { + prototypes := []ActionPrototype{} + if u.AvailMoveActions > 0 { + prototypes = append(prototypes, NewMoveActionPrototype(u)) + } + + if u.AvailAttackActions > 0 { + prototypes = append(prototypes, NewAttackActionPrototype(u)) + } + + for _, p := range u.FullActions { + prototypes = append(prototypes, p) + } + + for _, p := range u.FreeActions { + prototypes = append(prototypes, p) + } + + return prototypes +} |
