diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2023-01-12 20:58:14 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2023-01-26 22:17:17 +0100 |
| commit | 6acdf43bc056ed8c4c09508d700f4ff24934c920 (patch) | |
| tree | cb5b8a185a1fe37038406fbe7577a359516b6818 /go/game | |
| parent | 2f49c5e7030b63e8f4d2a8d0d4cb55691ef1fb3a (diff) | |
| download | muhqs-game-6acdf43bc056ed8c4c09508d700f4ff24934c920.tar.gz muhqs-game-6acdf43bc056ed8c4c09508d700f4ff24934c920.zip | |
implement unit movement
Diffstat (limited to 'go/game')
| -rw-r--r-- | go/game/card.go | 151 | ||||
| -rw-r--r-- | go/game/deck.go | 68 | ||||
| -rw-r--r-- | go/game/discardPiles.go | 7 | ||||
| -rw-r--r-- | go/game/hand.go | 29 | ||||
| -rw-r--r-- | go/game/map.go | 8 | ||||
| -rw-r--r-- | go/game/permanent.go | 10 | ||||
| -rw-r--r-- | go/game/player.go | 23 | ||||
| -rw-r--r-- | go/game/range.go | 39 | ||||
| -rw-r--r-- | go/game/range_test.go | 35 | ||||
| -rw-r--r-- | go/game/set.go | 63 | ||||
| -rw-r--r-- | go/game/state.go | 36 | ||||
| -rw-r--r-- | go/game/tile.go | 4 | ||||
| -rw-r--r-- | go/game/unit.go | 89 |
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() +} |
