package game import ( "errors" "io" "net/http" "path" "slices" "strconv" "strings" "github.com/goccy/go-yaml" "muhq.space/muhqs-game/go/assets" "muhq.space/muhqs-game/go/log" ) type CardType int const ( undefinedCardType CardType = iota unitType bossType spellType artifactType equipmentType potionType intentionType ) func (ct CardType) String() string { switch ct { case undefinedCardType: return "undefined" case unitType: return "unit" case bossType: return "boss" case spellType: return "spell" case artifactType: return "artifact" case equipmentType: return "equipment" case potionType: return "potion" case intentionType: return "intention" } log.Panicf("Switch not exhausting %d", ct) return "" } var CardTypes = struct { Unit CardType Boss CardType Spell CardType Artifact CardType Equipment CardType Potion CardType Intention CardType }{ Unit: unitType, Boss: bossType, Spell: spellType, Artifact: artifactType, Equipment: equipmentType, Potion: potionType, Intention: intentionType, } var CardTypeNames = map[string]CardType{ "unit": unitType, "boss": unitType, "spell": spellType, "artifact": artifactType, "equipment": equipmentType, "potion": potionType, "intention": intentionType, } func (ct CardType) IsPermanent() bool { switch ct { case CardTypes.Unit, CardTypes.Boss, CardTypes.Artifact, CardTypes.Equipment, CardTypes.Potion: return true default: return false } } func (ct CardType) IsUnit() bool { switch ct { case CardTypes.Unit, CardTypes.Boss: return true default: return false } } func (ct CardType) IsArtifact() bool { switch ct { case CardTypes.Artifact, CardTypes.Equipment, CardTypes.Potion: return true default: return false } } type cardImplementation interface { spawnTiles(*LocalState, *Player) []*Tile fullActions(*Unit) []*FullAction freeActions(Permanent) []*FreeAction playTargets() TargetDesc stateBasedActions(*LocalState, Permanent) additionalPlayCosts(*PlayAction) ActionCostFunc additionalSpawnsFor(Permanent, CardType) []*Tile onEntering(*Tile) onLeaving(*Tile) onPlay(*PlayAction) onETB(*LocalState, Permanent) onPile(containing Permanent) onUnpile(containing Permanent) onDrop(dropped Permanent) onUpkeep(Permanent) } // Default implementation of the cardImplementation interface. // // Card Implementations should embed this struct to "inherit" the // default behavior. type cardImplementationBase struct{} func (*cardImplementationBase) spawnTiles(*LocalState, *Player) []*Tile { return nil } func (*cardImplementationBase) fullActions(*Unit) []*FullAction { return nil } func (*cardImplementationBase) freeActions(Permanent) []*FreeAction { return nil } func (*cardImplementationBase) playTargets() TargetDesc { return INVALID_TARGET_DESC } func (*cardImplementationBase) stateBasedActions(*LocalState, Permanent) {} func (*cardImplementationBase) additionalPlayCosts(*PlayAction) ActionCostFunc { return nil } func (*cardImplementationBase) additionalSpawnsFor(Permanent, CardType) []*Tile { return nil } func (*cardImplementationBase) onEntering(*Tile) {} func (*cardImplementationBase) onLeaving(*Tile) {} func (*cardImplementationBase) onPlay(*PlayAction) {} func (*cardImplementationBase) onETB(*LocalState, Permanent) {} func (*cardImplementationBase) onPile(Permanent) {} func (*cardImplementationBase) onUnpile(Permanent) {} func (*cardImplementationBase) onDrop(Permanent) {} func (*cardImplementationBase) onUpkeep(Permanent) {} // Map of all card implementations var cardImplementations map[string]cardImplementation func getCardImplementation(c *Card) cardImplementation { if impl, found := cardImplementations[c.Path()]; found { return impl } impl := newParsedCardImplementation(c) cardImplementations[c.Path()] = impl return impl } const ( CARD_DATA_URL string = "data/cards" ) type Card struct { Name string Set SetIdentifier Type CardType BuyCosts *ResourceCosts PlayCosts *ResourceCosts Values map[string]any Impl cardImplementation } var cardDefinitions = map[string]map[string][]byte{} func getCardDefinition(set, cardName string) ([]byte, error) { cardDefinition, found := cardDefinitions[set][cardName] if !found { var err error cardDefinition, err = retrieveCardDefinition(path.Join(set, cardName)) if err != nil { return nil, err } if cardDefinitions[set] == nil { cardDefinitions[set] = map[string][]byte{} } cardDefinitions[set][cardName] = cardDefinition } return cardDefinition, nil } var ErrUnknownCardPath = errors.New("unknown card path") func retrieveCardDefinition(cardPath string) ([]byte, error) { url := assets.PROTOCOL + path.Join(assets.BASE_URL, CARD_DATA_URL, cardPath+".yml") log.Debug("loading card from", "url", url) resp, err := http.Get(url) if err != nil { return nil, err } if resp.StatusCode == 404 { return nil, ErrUnknownCardPath } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } return data, nil } func (card *Card) parseDefinition(definition []byte) { data := make(map[any]any) err := yaml.Unmarshal(definition, &data) if err != nil { log.Panicf("parsing %s failed with: %v\n", definition, err) } name, found := data["name"] if !found { log.Panicf("no name in card yaml %s\n", definition) } delete(data, "name") switch nameYml := name.(type) { case string: card.Name = nameYml case map[string]any: cardName := nameYml["en"] card.Name = cardName.(string) default: log.Panicf("Unexpected type %T of yml card field 'Name'\n", name) } t, found := data["type"] if !found { log.Panicf("no type in card yaml %s\n", definition) } delete(data, "type") card.Type = CardTypeNames[t.(string)] bc, bcFound := data["buy"] delete(data, "buy") pc, pcFound := data["play"] delete(data, "play") for k, v := range data { card.Values[k.(string)] = v } if bcFound { card.BuyCosts = ParseCardResourceCosts(bc, card.getEffects()) } if pcFound { card.PlayCosts = ParseCardResourceCosts(pc, card.getEffects()) } else if card.Type == CardTypes.Unit { card.PlayCosts = ParseCardResourceCosts(card.Values["upkeep"], card.getEffects()) } card.Impl = getCardImplementation(card) } func (card *Card) getCanonicalValues(key string) []string { var values []string untypedValues, found := card.Values[key] if !found { return []string{} } switch typedValues := untypedValues.(type) { case map[string]any: enValues := typedValues["en"] switch typedEnValues := enValues.(type) { case string: values = []string{typedEnValues} case []string: values = typedEnValues case []any: values = []string{} for _, v := range typedEnValues { values = append(values, v.(string)) } } case string: values = []string{typedValues} case uint64: values = []string{strconv.Itoa(int(typedValues))} case []string: values = typedValues } for i, value := range values { values[i] = strings.ToLower(value) } return values } func (card *Card) getEffects() []string { effects := card.getCanonicalValues("effect") for i, effect := range effects { // Trim explanation from the effect if effect[len(effect)-1:] != ")" { continue } // Trim after " (..." effects[i] = effect[0 : strings.IndexByte(effect, '(')-1] } return effects } func (card *Card) hasEffect(effect string) bool { return slices.Contains(card.getEffects(), effect) } var ErrNoXEffect = errors.New("card has no xeffect") func (card *Card) getXEffect(effect string) (xEffect, error) { for _, e := range card.getEffects() { if !strings.Contains(e, effect) { continue } return parseXEffect(e) } return xEffect{}, ErrNoXEffect } func (card *Card) hasPlacementConstrain(effect string) bool { if slices.Contains(card.getEffects(), effect) { return true } else if !card.Type.IsUnit() { return false } movementDesc := card.getCanonicalValues("movement")[0] return strings.Contains(movementDesc, effect) } func (card *Card) getAI() string { aiDesc := card.getCanonicalValues("ai") if len(aiDesc) == 0 { return "" } return strings.Split(aiDesc[0], " ")[0] } // Create a new card from the path. // Panic if the path is not valid. func NewCard(cardPath string) *Card { card, err := NewCardSafe(cardPath) if err != nil { log.Fatal(err) } return card } // NewCardSafe safely creates a new card from the path. // Return an error if the path is invalid. func NewCardSafe(cardPath string) (*Card, error) { cardName := path.Base(cardPath) set := path.Dir(cardPath) cardDefinition, err := getCardDefinition(set, cardName) if err != nil { return nil, err } c := Card{ Name: "", Set: SetNames[set], Type: undefinedCardType, BuyCosts: nil, PlayCosts: nil, Values: map[string]any{}, } c.parseDefinition(cardDefinition) return &c, err } // FileName returns the Name of a card kn lower case and replaces spaces with underscores. func (c *Card) FileName() string { return strings.ReplaceAll(strings.ToLower(c.Name), " ", "_") } // Path returns the canonical full path of a card. // %he pa%h of a card consists of its set name followed by '/' and its file name. func (c *Card) Path() string { return path.Join(c.Set.String(), c.FileName()) } func (c *Card) IsPermanent() bool { return c.Type.IsPermanent() } func (c *Card) IsToken() bool { if v, found := c.Values["token"]; found { b, ok := v.(bool) if !ok { log.Panicf("Invalid value for %s token value: %v", c.Name, v) } return b } return false } func (c *Card) IsBuyable() bool { return c.BuyCosts != nil }