aboutsummaryrefslogtreecommitdiff
path: root/go/game
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-01-12 20:58:14 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2023-01-26 22:17:17 +0100
commit6acdf43bc056ed8c4c09508d700f4ff24934c920 (patch)
treecb5b8a185a1fe37038406fbe7577a359516b6818 /go/game
parent2f49c5e7030b63e8f4d2a8d0d4cb55691ef1fb3a (diff)
downloadmuhqs-game-6acdf43bc056ed8c4c09508d700f4ff24934c920.tar.gz
muhqs-game-6acdf43bc056ed8c4c09508d700f4ff24934c920.zip
implement unit movement
Diffstat (limited to 'go/game')
-rw-r--r--go/game/card.go151
-rw-r--r--go/game/deck.go68
-rw-r--r--go/game/discardPiles.go7
-rw-r--r--go/game/hand.go29
-rw-r--r--go/game/map.go8
-rw-r--r--go/game/permanent.go10
-rw-r--r--go/game/player.go23
-rw-r--r--go/game/range.go39
-rw-r--r--go/game/range_test.go35
-rw-r--r--go/game/set.go63
-rw-r--r--go/game/state.go36
-rw-r--r--go/game/tile.go4
-rw-r--r--go/game/unit.go89
13 files changed, 535 insertions, 27 deletions
diff --git a/go/game/card.go b/go/game/card.go
index 6cd20257..f77a274a 100644
--- a/go/game/card.go
+++ b/go/game/card.go
@@ -1,18 +1,151 @@
package game
+import (
+ "io"
+ "log"
+ "net/http"
+ "path"
+
+ "gopkg.in/yaml.v3"
+)
+
type CardType int
const (
- Unit CardType = iota + 1
- Spell
- Artifact
- Equipment
- Potion
- Itention
+ UndefinedCardType CardType = iota
+ UnitType
+ SpellType
+ ArtifactType
+ EquipmentType
+ PotionType
+ ItentionType
+)
+
+func (ct CardType) String() string {
+ switch ct {
+ case UndefinedCardType:
+ return "undefined"
+ case UnitType:
+ return "unit"
+ case SpellType:
+ return "spell"
+ case ArtifactType:
+ return "artifact"
+ case EquipmentType:
+ return "equipment"
+ case PotionType:
+ return "potion"
+ case ItentionType:
+ return "itention"
+ }
+ log.Panicf("Switch not exhausting %d", ct)
+ return ""
+}
+
+var CardTypes = struct {
+ Unit CardType
+ Spell CardType
+ Artifact CardType
+ Equipment CardType
+ Potion CardType
+ Itention CardType
+}{
+ Unit: UnitType,
+ Spell: SpellType,
+ Artifact: ArtifactType,
+ Equipment: EquipmentType,
+ Potion: PotionType,
+ Itention: ItentionType,
+}
+
+var CardTypeNames = map[string]CardType{
+ "unit": UnitType,
+ "spell": SpellType,
+ "artifact": ArtifactType,
+ "equipment": EquipmentType,
+ "potion": PotionType,
+ "itention": ItentionType,
+}
+
+const (
+ CARD_DATA_URL string = "data/cards"
)
type Card struct {
- Name string
- Set SetIdentifier
- Type CardType
+ Name string
+ Set SetIdentifier
+ Type CardType
+ Values map[string]interface{}
+}
+
+var cards = map[string]map[string][]byte{}
+
+func retrieveCardDefinition(cardPath string) []byte {
+ url := PROTOCOL + path.Join(BASE_URL, CARD_DATA_URL, cardPath+".yml")
+ log.Printf("loading %s\n", url)
+ resp, err := http.Get(url)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ defer resp.Body.Close()
+ data, err := io.ReadAll(resp.Body)
+ if err != nil {
+ log.Fatal(err)
+ }
+ return data
+}
+
+func (card *Card) parseDefinition(definition []byte) {
+ data := make(map[interface{}]interface{})
+ err := yaml.Unmarshal(definition, &data)
+ if err != nil {
+ log.Fatalf("parsing %s failed with: %v\n", definition, err)
+ }
+
+ name, ok := data["name"]
+ if !ok {
+ log.Fatalf("no name in card yaml %s\n", definition)
+ }
+ delete(data, "name")
+
+ switch name.(type) {
+ case string:
+ card.Name = name.(string)
+ case map[string]interface{}:
+ cardName := name.(map[string]interface{})["en"]
+ card.Name = cardName.(string)
+ default:
+ log.Fatalf("Unexpected type %T of yml card field 'Name'\n", name)
+ }
+
+ t, ok := data["type"]
+ if !ok {
+ log.Fatalf("no type in card yaml %s\n", definition)
+ }
+ delete(data, "type")
+
+ card.Type = CardTypeNames[t.(string)]
+
+ for k, v := range data {
+ card.Values[k.(string)] = v
+ }
+}
+
+func NewCard(cardPath string) *Card {
+ cardName := path.Base(cardPath)
+ set := path.Dir(cardPath)
+
+ cardDefinition, found := cards[set][cardName]
+ if !found {
+ cardDefinition = retrieveCardDefinition(cardPath)
+ if cards[set] == nil {
+ cards[set] = map[string][]byte{}
+ }
+ cards[set][cardName] = cardDefinition
+ }
+
+ c := Card{"", SetIdentifierNames[set], UndefinedCardType, map[string]interface{}{}}
+ c.parseDefinition(cardDefinition)
+ return &c
}
diff --git a/go/game/deck.go b/go/game/deck.go
index 33126ae8..f4290322 100644
--- a/go/game/deck.go
+++ b/go/game/deck.go
@@ -1,5 +1,71 @@
package game
+import (
+ "log"
+ "math/rand"
+ "strconv"
+ "strings"
+ "time"
+)
+
type Deck struct {
- Cards []Card
+ Cards []*Card
+}
+
+func NewDeck() *Deck {
+ d := Deck{[]*Card{}}
+ return &d
+}
+
+func (d *Deck) AddCards(cards []*Card) {
+ d.Cards = append(d.Cards, cards...)
+}
+
+func (d *Deck) AddCard(card *Card) {
+ d.Cards = append(d.Cards, card)
+}
+
+func (d *Deck) Size() int {
+ return len(d.Cards)
+}
+
+func (d *Deck) Shuffle() {
+ rand.Seed(time.Now().UnixNano())
+ rand.Shuffle(d.Size(), func(i, j int) {
+ d.Cards[i], d.Cards[j] = d.Cards[j], d.Cards[i]
+ })
+}
+
+func (d *Deck) Draw(amount int) []*Card {
+ drawn := d.Cards[-amount:]
+ d.Cards = d.Cards[:amount]
+ return drawn
+}
+
+func NewDeckFromCardPaths(cardPaths []string) *Deck {
+ d := NewDeck()
+ for _, cardPath := range cardPaths {
+ card := NewCard(cardPath)
+ d.AddCard(card)
+ }
+ return d
+}
+
+func NewDeckFromDeckList(deckList string) *Deck {
+ cardPaths := []string{}
+ lines := strings.Split(deckList, "\n")
+ for _, line := range lines {
+ tokens := strings.SplitN(line, " ", 2)
+ cardPath := tokens[1]
+ amount, err := strconv.Atoi(tokens[0])
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ for i := 0; i < amount; i++ {
+ cardPaths = append(cardPaths, cardPath)
+ }
+ }
+
+ return NewDeckFromCardPaths(cardPaths)
}
diff --git a/go/game/discardPiles.go b/go/game/discardPiles.go
index 9c1c1e45..f7cf8994 100644
--- a/go/game/discardPiles.go
+++ b/go/game/discardPiles.go
@@ -1,5 +1,10 @@
package game
type DiscardPile struct {
- Cards []Card
+ Cards []*Card
+}
+
+func NewDiscardPile() *DiscardPile {
+ dp := DiscardPile{[]*Card{}}
+ return &dp
}
diff --git a/go/game/hand.go b/go/game/hand.go
index 42187f5a..fde4a686 100644
--- a/go/game/hand.go
+++ b/go/game/hand.go
@@ -1,5 +1,32 @@
package game
type Hand struct {
- Cards []Card
+ Cards []*Card
+}
+
+func NewHand() *Hand {
+ h := Hand{[]*Card{}}
+ return &h
+}
+
+func (h *Hand) Size() int {
+ return len(h.Cards)
+}
+
+func (h *Hand) AddCards(cards []*Card) {
+ h.Cards = append(h.Cards, cards...)
+}
+
+func (h *Hand) AddCard(card *Card) {
+ h.Cards = append(h.Cards, card)
+}
+
+func (h *Hand) RemoveCard(card *Card) {
+ for i, c := range h.Cards {
+ if c != card {
+ continue
+ }
+
+ h.Cards = append(h.Cards[:i], h.Cards[i+1:]...)
+ }
}
diff --git a/go/game/map.go b/go/game/map.go
index cecce9ca..ebf59d25 100644
--- a/go/game/map.go
+++ b/go/game/map.go
@@ -1,8 +1,8 @@
package game
import (
- "fmt"
"errors"
+ "fmt"
"io"
"log"
"math/rand"
@@ -20,7 +20,8 @@ const (
BASE_URL string = "localhost:8000/"
MAP_URL_PART string = "html/build/maps"
- DEFAULT_RESSOURCE_GAIN int = 5
+ DEFAULT_RESSOURCE_GAIN int = 5
+ DEFAULT_START_DECK string = "3 misc/farmer"
)
type MapYml struct {
@@ -32,6 +33,7 @@ type Map struct {
Tiles [][]Tile
symbols map[string]string
RessourceGain int
+ StartDeckList string
}
func readMap(r io.Reader) (*Map, error) {
@@ -52,7 +54,7 @@ func ParseMap(data []byte) (*Map, error) {
mapYml.Symbols[" "] = "neutral"
- m := &Map{[][]Tile{}, mapYml.Symbols, DEFAULT_RESSOURCE_GAIN}
+ m := &Map{[][]Tile{}, mapYml.Symbols, DEFAULT_RESSOURCE_GAIN, DEFAULT_START_DECK}
rows := strings.Split(mapYml.Map, "\n")
for y := 0; y < len(rows); y++ {
diff --git a/go/game/permanent.go b/go/game/permanent.go
index a4418e3e..37111c4d 100644
--- a/go/game/permanent.go
+++ b/go/game/permanent.go
@@ -1,8 +1,10 @@
package game
type Permanent struct {
- Card *Card
- Tile *Tile
- Damage int
- Pile []*Permanent
+ Card *Card
+ Tile *Tile
+ Damage int
+ Pile []*Permanent
+ Controller *Player
+ Owner *Player
}
diff --git a/go/game/player.go b/go/game/player.go
index 6b9041ad..20ac13a5 100644
--- a/go/game/player.go
+++ b/go/game/player.go
@@ -1,5 +1,9 @@
package game
+const (
+ MAX_DRAW int = 3
+)
+
type Player struct {
Id int
Name string
@@ -7,4 +11,23 @@ type Player struct {
DiscardPile *DiscardPile
Deck *Deck
Ressource int
+ gameState *State
+}
+
+func NewPlayer(id int, name string, deck *Deck, gameState *State) *Player {
+ p := Player{id, name, NewHand(), NewDiscardPile(), deck, 0, gameState}
+ return &p
+}
+
+func (p *Player) Draw() {
+ toDraw := MAX_DRAW - p.Hand.Size()
+ if toDraw < 1 {
+ return
+ }
+
+ drawnCards := p.Deck.Draw(toDraw)
+ p.Hand.AddCards(drawnCards)
+}
+
+func (p *Player) PlayPermanent(card *Card) {
}
diff --git a/go/game/range.go b/go/game/range.go
new file mode 100644
index 00000000..86ec2f50
--- /dev/null
+++ b/go/game/range.go
@@ -0,0 +1,39 @@
+package game
+
+import (
+ "math"
+)
+
+func IsPositionInRange(origin Position, pos Position, r int) bool {
+ x, y := float64(pos.X-origin.X), float64(pos.Y-origin.Y)
+ ax, ay := math.Abs(x), math.Abs(y)
+ var abs float64
+ if pos.X > pos.Y {
+ abs = ax + math.Floor(ay/2)
+ } else {
+ abs = ay + math.Floor(ax/2)
+ }
+
+ return abs <= float64(r)
+}
+
+func PositionsInRange(origin Position, r int, includeOrigin bool) []Position {
+ positions := []Position{}
+ for x := -r; x <= r; x++ {
+ for y := -r; y <= r; y++ {
+ pos := Position{origin.X + x, origin.Y + y}
+ if pos.X < 0 || pos.Y < 0 {
+ continue
+ }
+
+ if !includeOrigin && pos == origin {
+ continue
+ }
+
+ if IsPositionInRange(origin, pos, r) {
+ positions = append(positions, pos)
+ }
+ }
+ }
+ return positions
+}
diff --git a/go/game/range_test.go b/go/game/range_test.go
new file mode 100644
index 00000000..afa164de
--- /dev/null
+++ b/go/game/range_test.go
@@ -0,0 +1,35 @@
+package game
+
+import (
+ "golang.org/x/exp/slices"
+ "testing"
+)
+
+func TestPositionsInRange(t *testing.T) {
+ origin := Position{0, 0}
+ positionsInRange1 := []Position{Position{0, 1}, Position{1, 0}, Position{1, 1}}
+ calculatedPositions := PositionsInRange(origin, 1, false)
+ if len(positionsInRange1) != len(calculatedPositions) {
+ t.Fatalf("Unexpected number of calculated positions")
+ }
+
+ for _, p := range positionsInRange1 {
+ if !slices.Contains(calculatedPositions, p) {
+ t.Fatalf("Position %v not in calculated positions", p)
+ }
+ }
+
+ origin = Position{2, 2}
+ positionsInRange2 := []Position{Position{0, 1}, Position{0, 2}, Position{0, 3},
+ Position{1, 0}, Position{1, 1}, Position{1, 2}, Position{1, 3}, Position{1, 4},
+ Position{2, 0}, Position{2, 1}, Position{2, 3}, Position{2, 4},
+ Position{3, 0}, Position{3, 1}, Position{3, 2}, Position{3, 3}, Position{3, 4},
+ Position{4, 1}, Position{4, 2}, Position{4, 3}}
+ calculatedPositions = PositionsInRange(origin, 2, false)
+
+ for _, p := range positionsInRange2 {
+ if !slices.Contains(calculatedPositions, p) {
+ t.Fatalf("Position %v not in calculated positions", p)
+ }
+ }
+}
diff --git a/go/game/set.go b/go/game/set.go
index 4136eac6..b0565eca 100644
--- a/go/game/set.go
+++ b/go/game/set.go
@@ -1,5 +1,9 @@
package game
+import (
+ "log"
+)
+
type SetIdentifier int
const (
@@ -13,3 +17,62 @@ const (
Tyrant
Exp1
)
+
+func (si SetIdentifier) String() string {
+ switch si {
+ case Base:
+ return "base"
+ case Magic:
+ return "magic"
+ case Equipments:
+ return "equipments"
+ case Misc:
+ return "misc"
+ case Nautics:
+ return "nautics"
+ case Potions:
+ return "potions"
+ case Kraken:
+ return "kraken"
+ case Tyrant:
+ return "tyrant"
+ case Exp1:
+ return "exp1"
+ }
+ log.Panicf("Switch not exhausting %d", si)
+ return ""
+}
+
+var SetIdentifiers = struct {
+ Base SetIdentifier
+ Magic SetIdentifier
+ Equipments SetIdentifier
+ Misc SetIdentifier
+ Nautics SetIdentifier
+ Potions SetIdentifier
+ Kraken SetIdentifier
+ Tyrant SetIdentifier
+ Exp1 SetIdentifier
+}{
+ Base: Base,
+ Magic: Magic,
+ Equipments: Equipments,
+ Misc: Misc,
+ Nautics: Nautics,
+ Potions: Potions,
+ Kraken: Kraken,
+ Tyrant: Tyrant,
+ Exp1: Exp1,
+}
+
+var SetIdentifierNames = map[string]SetIdentifier{
+ "base": Base,
+ "magic": Magic,
+ "equipments": Equipments,
+ "misc": Misc,
+ "nautics": Nautics,
+ "potions": Potions,
+ "kraken": Kraken,
+ "tyrant": Tyrant,
+ "exp1": Exp1,
+}
diff --git a/go/game/state.go b/go/game/state.go
index 9dcf73c4..caae8106 100644
--- a/go/game/state.go
+++ b/go/game/state.go
@@ -1,12 +1,32 @@
package game
type State struct {
- Decks []Deck
- Stores []Store
- Map Map
- Stack Stack
- Hands []Hand
- DiscardPiles []DiscardPile
- Players []Player
- Permanents []Permanent
+ Decks []*Deck
+ Stores []*Store
+ Map *Map
+ Stack *Stack
+ Hands []*Hand
+ DiscardPiles []*DiscardPile
+ Players []*Player
+ Permanents []*Permanent
+ Units []*Unit
+}
+
+func NewState() *State {
+ return &State{}
+}
+
+func (s *State) AddNewPlayer(name string, deck *Deck) {
+ p := NewPlayer(len(s.Players), name, deck, s)
+ 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.Units = append(s.Units, unit)
+}
+
+func (s *State) MoveUnit(u *Unit, t *Tile) {
+ u.move(s, t)
}
diff --git a/go/game/tile.go b/go/game/tile.go
index fb62420b..50688d83 100644
--- a/go/game/tile.go
+++ b/go/game/tile.go
@@ -85,3 +85,7 @@ func NewTileFromString(tile string, pos Position) (Tile, error) {
}
return t, nil
}
+
+func (t *Tile) IsFree() bool {
+ return t.Permanent == nil
+}
diff --git a/go/game/unit.go b/go/game/unit.go
new file mode 100644
index 00000000..4c07d625
--- /dev/null
+++ b/go/game/unit.go
@@ -0,0 +1,89 @@
+package game
+
+import (
+ "golang.org/x/exp/slices"
+ "log"
+ "strconv"
+ "strings"
+)
+
+type UnitStates int
+
+const (
+ Paralysis UnitStates = iota + 1
+ Poison
+ Panic
+ Rage
+ Ward
+)
+
+type Attack struct {
+ Damage int
+ Range int
+}
+
+func parseAttack(attack string) Attack {
+ tokens := strings.Split(attack, " ")
+ a, err := strconv.Atoi(tokens[0])
+ if err != nil {
+ log.Fatalf("Attack %s has no valid Damage value\n", attack)
+ }
+
+ r := 1
+ if len(tokens) == 3 {
+ r, err = strconv.Atoi(tokens[2])
+ if err != nil {
+ log.Fatalf("Attack %s has no valid Range value\n", attack)
+ }
+ }
+ return Attack{a, r}
+}
+
+type Unit struct {
+ Permanent
+ Health int
+ Movement int
+ Attack Attack
+ Upkeep int
+ // FullActions []FullActions
+ // FreeActions []FreeActions
+ States []UnitStates
+ // Effects []Effects
+}
+
+func NewUnit(cardPath string, tile *Tile, owner *Player) *Unit {
+ card := NewCard(cardPath)
+
+ return &Unit{
+ Permanent{card, tile, 0, []*Permanent{}, owner, owner},
+ card.Values["health"].(int),
+ card.Values["movement"].(int),
+ parseAttack(card.Values["attack"].(string)),
+ card.Values["upkeep"].(int),
+ []UnitStates{},
+ }
+}
+
+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)
+ }
+
+ u.Tile.Permanent = nil
+ u.Tile = tile
+}
+
+func (u *Unit) MoveRangeTiles(m *Map) []*Tile {
+ tiles := []*Tile{}
+ for _, pos := range PositionsInRange(u.Tile.Position, u.Movement, false) {
+ tile := m.TileAt(pos)
+ if u.IsAvailableTile(tile) {
+ tiles = append(tiles, tile)
+ }
+ }
+ return tiles
+}
+
+func (u *Unit) IsAvailableTile(tile *Tile) bool {
+ return tile.IsFree()
+}