package game import ( "regexp" "strconv" "strings" "muhq.space/muhqs-game/go/log" ) type dynamicCardImplementation struct { card *Card spawnTilesFunc func(*LocalState, *Player) []*Tile genFullActions func(*Unit) []*FullAction genFreeActions func(Permanent) []*FreeAction targetDesc TargetDesc _stateBasedActions func(*LocalState, Permanent) _additionalPlayCosts func(*PlayAction) ActionCostFunc _additionalSpawnsFor func(Permanent, CardType) []*Tile _onEntering func(*Tile) _onLeaving func(*Tile) _onPlay func(*PlayAction) _onETB func(*LocalState, Permanent) _onPile func(Permanent) _onUnpile func(Permanent) _onDrop func(Permanent) _onUpkeep func(Permanent) } func (impl *dynamicCardImplementation) spawnTiles(s *LocalState, p *Player) []*Tile { if impl.spawnTilesFunc != nil { return impl.spawnTilesFunc(s, p) } return nil } func (impl *dynamicCardImplementation) fullActions(u *Unit) []*FullAction { if impl.genFullActions != nil { return impl.genFullActions(u) } return nil } func (impl *dynamicCardImplementation) freeActions(p Permanent) []*FreeAction { if impl.genFreeActions != nil { return impl.genFreeActions(p) } return nil } func (impl *dynamicCardImplementation) playTargets() TargetDesc { return impl.targetDesc } func (impl *dynamicCardImplementation) stateBasedActions(s *LocalState, p Permanent) { if impl._stateBasedActions != nil { impl._stateBasedActions(s, p) } } func (impl *dynamicCardImplementation) additionalPlayCosts(a *PlayAction) ActionCostFunc { if impl._additionalPlayCosts != nil { return impl._additionalPlayCosts(a) } return nil } func (impl *dynamicCardImplementation) additionalSpawnsFor(p Permanent, ct CardType) []*Tile { if impl._additionalSpawnsFor != nil { return impl._additionalSpawnsFor(p, ct) } return nil } func (impl *dynamicCardImplementation) onEntering(t *Tile) { if impl._onEntering != nil { impl._onEntering(t) } } func (impl *dynamicCardImplementation) onLeaving(t *Tile) { if impl._onLeaving != nil { impl._onLeaving(t) } } func (impl *dynamicCardImplementation) onPlay(a *PlayAction) { if impl._onPlay != nil { impl._onPlay(a) } } func (impl *dynamicCardImplementation) onETB(s *LocalState, p Permanent) { if impl._onETB != nil { impl._onETB(s, p) } } func (impl *dynamicCardImplementation) onPile(p Permanent) { if impl._onPile != nil { impl._onPile(p) } } func (impl *dynamicCardImplementation) onUnpile(p Permanent) { if impl._onUnpile != nil { impl._onUnpile(p) } } func (impl *dynamicCardImplementation) onDrop(p Permanent) { if impl._onDrop != nil { impl._onDrop(p) } } func (impl *dynamicCardImplementation) onUpkeep(p Permanent) { if impl._onUpkeep != nil { impl._onUpkeep(p) } } func newParsedCardImplementation(c *Card) *dynamicCardImplementation { impl := &dynamicCardImplementation{card: c} for _, effect := range c.getEffects() { impl.parseEffect(effect) } for _, fullAction := range c.getCanonicalValues("full_action") { impl.parseFullAction(fullAction) } return impl } var additionalActionRegex = regexp.MustCompile(`has.*additional (.*) action`) func (impl *dynamicCardImplementation) parseEffect(effect string) { if strings.Contains(effect, "can be played") { impl.parsePlayModification(effect) } if m := additionalActionRegex.FindStringSubmatch(effect); m != nil { impl.parseAdditionalAction(effect, m) } if m := freeActionRegex.FindStringSubmatch(effect); m != nil { impl.parseFreeAction(m[1], m[2]) } // TODO: support gets and has effects if strings.Contains(effect, "equipped unit gets") { impl.parseEquipmentGetEffect(effect) } if strings.Contains(effect, "equipped unit has") { impl.parseEquipmentHasEffect(effect) } } func (impl *dynamicCardImplementation) parseFullAction(fullAction string) { var resolveProtos []ActionFuncPrototype gainRessourceRegex := regexp.MustCompile(`gain [1-9][0-9]* resource`) if find := gainRessourceRegex.Find([]byte(fullAction)); find != nil { tokens := strings.SplitN(string(find), " ", 3) gain, err := strconv.Atoi(tokens[1]) if err != nil { log.Panicf("Failed to parse resource gain full action: %s", err) } resolveProto := func(a Action) ActionResolveFunc { u := a.Source().(*Unit) return func(*LocalState) { u.controller.gainResource(gain) } } resolveProtos = append(resolveProtos, resolveProto) } if resolveProtos != nil { impl.genFullActions = func(u *Unit) []*FullAction { fullActions := make([]*FullAction, 0, len(resolveProtos)) for _, proto := range resolveProtos { fullActions = append(fullActions, newFullAction(u, proto, "")) } return fullActions } } } var freeActionRegex = regexp.MustCompile(`([^:]*):(.*)`) func (impl *dynamicCardImplementation) parseFreeAction(cost, action string) { log.Error("Parsing free actions is not implemented yet") } func parseUnitConstrain(constrain string) func(ctx any, u *Unit) bool { enemy := strings.Contains(constrain, "enemy") if enemy { return func(ctx any, e *Unit) bool { p := ctx.(*Player) return p.IsEnemy(e.Controller()) } } else { log.Error("Only enemy unit constrains are implemented yet") } return nil } func parseLocation(location string) func(any, *LocalState) []*Tile { r := -1 var what string if strings.Contains(location, "on") { r = 0 tokens := strings.SplitN(location, "on ", 2) what = tokens[1] } else if strings.Contains(location, "next to") { r = 1 tokens := strings.SplitN(location, "next to ", 2) what = tokens[1] } else if strings.Contains(location, "in range") { tokens := strings.SplitN(location, "in range ", 2) afterRangePattern := tokens[1] var rangeStr string tokens = strings.SplitN(afterRangePattern, " ", 2) rangeStr, what = tokens[0], tokens[1] var err error r, err = strconv.Atoi(rangeStr) if err != nil { log.Panicf("Unsupported range pattern \"%s\" in \"%s\"", "range "+rangeStr, location) } } if r == -1 || what == "" { return nil } if strings.Contains(what, "unit") { constrainFunc := parseUnitConstrain(what) if constrainFunc != nil { return func(ctx any, s *LocalState) []*Tile { origins := s.Map().FilterTiles(func(t *Tile) bool { if t.Permanent != nil && t.Permanent.Card().Type == CardTypes.Unit && constrainFunc(ctx, t.Permanent.(*Unit)) { return true } return false }) if r == 0 { return origins } else { tiles := []*Tile{} for _, origin := range origins { tiles = append(tiles, TilesInRangeFromOrigin(s.Map(), origin.Position, r)...) } return tiles } } } } log.Error("Only unit location constrains are implemented yet") return nil } func (impl *dynamicCardImplementation) parsePlayModification(effect string) { tokens := strings.SplitN(effect, " can be played ", 2) what, how := tokens[0], tokens[1] if what == strings.ToLower(impl.card.Name) { if locationFunc := parseLocation(how); locationFunc != nil { impl.spawnTilesFunc = func(p *LocalState, s *Player) []*Tile { return locationFunc(s, p) } } else { log.Error("Play modification other than locations are not implemented ") } } else { log.Error("Play modification for other cards are not implemented") } } func (impl *dynamicCardImplementation) parseAdditionalAction(effect string, matches []string) { log.Debug("additional action matches:", "matches", matches) switch matches[1] { case "attack": impl._onUpkeep = func(p Permanent) { u := p.(*Unit); u.AvailAttackActions = u.AvailAttackActions + 1 } case "move": impl._onUpkeep = func(p Permanent) { u := p.(*Unit); u.AvailMoveActions = u.AvailMoveActions + 1 } default: log.Warn("unhandled action", "action", matches[1]) } } func (impl *dynamicCardImplementation) parseEquipmentGetEffect(effect string) { tokens := strings.SplitAfterN(effect, "equipped unit gets ", 2) tokens = tokens[1:] tokens = strings.Split(tokens[0], " and ") var ( apply []func(Permanent) remove []func(Permanent) ) for _, token := range tokens { x, err := parseXEffect(token) if err == nil { if strings.Contains(x.raw, "movement") { apply = append(apply, func(p Permanent) { adjustMovement(p, x.x) }) remove = append(apply, func(p Permanent) { adjustMovement(p, -1*x.x) }) } else if strings.Contains(x.raw, "melee") { apply = append(apply, func(p Permanent) { adjustMelee(p, x.x) }) remove = append(apply, func(p Permanent) { adjustMelee(p, -1*x.x) }) } else if strings.Contains(x.raw, "health") { apply = append(apply, func(p Permanent) { adjustHealth(p, x.x) }) remove = append(apply, func(p Permanent) { adjustHealth(p, -1*x.x) }) } else if strings.Contains(x.raw, "armor") { apply = append(apply, func(p Permanent) { p.addTmpEffect(token) }) remove = append(apply, func(p Permanent) { p.removeTmpEffect(token) }) } } } if len(apply) == 0 { return } impl._onPile = func(p Permanent) { for _, a := range apply { a(p) } } impl._onUnpile = func(p Permanent) { for _, r := range remove { r(p) } } } func (impl *dynamicCardImplementation) parseEquipmentHasEffect(effect string) { tokens := strings.SplitAfterN(effect, "equipped unit has ", 2) tokens = tokens[1:] tokens = strings.Split(tokens[0], " and ") if len(tokens) == 0 { return } impl._onPile = func(p Permanent) { for _, token := range tokens { p.addTmpEffect(token) } } impl._onUnpile = func(p Permanent) { for _, token := range tokens { p.removeTmpEffect(token) } } }