package game import ( "errors" "fmt" "slices" "strconv" "strings" "muhq.space/muhqs-game/go/log" "muhq.space/muhqs-game/go/utils" ) type ( TargetConstraintFunc func(any) error TargetDesc struct { target, req string } TargetRequirement struct { min, max int } Target struct { s *LocalState desc string requirement TargetRequirement constraint TargetConstraintFunc sel []any } Targets struct { idx int ts []*Target } ) var INVALID_TARGET_DESC = TargetDesc{} func (t *Target) String() string { if len(t.sel) == 0 { return "undecided target" } if len(t.sel) == 1 { return fmt.Sprintf("%v", t.sel[0]) } s := "[" for _, sel := range t.sel { s += fmt.Sprintf("%v ", sel) } return s[:len(s)-1] + "]" } func (t *Targets) String() string { if len(t.ts) == 0 { return "no targets" } if len(t.ts) == 1 { return t.ts[0].String() } s := "[" for _, t := range t.ts { s += fmt.Sprintf("%v ", t) } return s[:len(s)-1] + "]" } func newTargetWithSel(s *LocalState, desc TargetDesc, action Action, sel []any) *Target { t := &Target{ s: s, desc: desc.target, constraint: targetConstraint(desc.target, s, action), requirement: desc.requirement(), sel: sel, } return t } func newTarget(s *LocalState, desc TargetDesc, action Action) *Target { return newTargetWithSel(s, desc, action, []any{}) } func newUnitAiTarget(s *LocalState, desc TargetDesc, ai *UnitAI) *Target { // Dummy action allowing the parsing code to determine the controller of the AI dummyAction := NewPassPriority(ai.u.controller) return newTargetWithSel(s, desc, dummyAction, []any{}) } func newConstraintTarget(s *LocalState, desc TargetDesc, constraint TargetConstraintFunc, action Action, ) *Target { return &Target{ s: s, desc: desc.target, constraint: constraint, requirement: desc.requirement(), } } func newTargets(targets ...*Target) *Targets { return &Targets{0, targets} } func newTargetDesc(desc string) TargetDesc { return TargetDesc{desc, "1"} } func newPileDropTargets(s *LocalState, a *PileDropAction) *Targets { pile := a.pile targets := newTargets() targets.ts = make([]*Target, 0, len(pile)) desc := newTargetDesc("adjacent tile") for i, p := range pile { constraints := parseTileTargetConstraint(desc.target, s, a) // The selected tile must be available for the individual dropped card constraints = append(constraints, availableTileConstraint(a, p.Card())) // The selected tile must be not be previously selected in this action constraints = append(constraints, noPreviousSelectionConstraint(targets, i)) targets.ts = append(targets.ts, newConstraintTarget(s, desc, conjunction(constraints...), a)) } return targets } func newArtifactMoveTargets(s *LocalState, a *ArtifactMoveAction) *Targets { targets := newTargets() targets.ts = make([]*Target, 0, 2) u := a.Source().(*Unit) desc := newTargetDesc("available tile") constraints := []TargetConstraintFunc{tileTargetConstraint} constraints = append(constraints, availableTileConstraint(a, u.Card())) constraints = append(constraints, moveConstraint(s, a, u)) moveTarget := newConstraintTarget(s, desc, conjunction(constraints...), a) targets.ts = append(targets.ts, moveTarget) constraints = []TargetConstraintFunc{tileTargetConstraint} constraints = append(constraints, availableTileConstraint(a, a.Artifact.Card())) constraints = append(constraints, func(t any) error { c := rangeTargetConstraint(moveTarget.sel[0], 1) return c(t) }) constraints = append(constraints, noPreviousSelectionConstraint(targets, 1)) targets.ts = append(targets.ts, newConstraintTarget(s, desc, conjunction(constraints...), a)) return targets } func (d *TargetDesc) requirement() TargetRequirement { switch d.req { case "?": return TargetRequirement{0, 1} case "+": return TargetRequirement{1, -1} case "*": return TargetRequirement{0, -1} default: if strings.Contains(d.req, "-") { tokens := strings.SplitN(d.req, "-", 2) min, err := strconv.Atoi(tokens[0]) if err != nil { log.Panicf("failed to parse target requirement %s: %v", d.req, err) } max, err := strconv.Atoi(tokens[1]) if err != nil { log.Panicf("failed to parse target requirement %s: %v", d.req, err) } return TargetRequirement{min, max} } x, err := strconv.Atoi(d.req) if err != nil { log.Panicf("failed to parse target requirement %s: %v", d.req, err) } return TargetRequirement{x, x} } } var ErrTargetAlreadySelected = errors.New("already selected") // AddSelection adds an object as selection to the target. // If the provided object does not satisfy the target contraint err != nil is returned. // Selecting the same object multiple times results in an error. func (t *Target) AddSelection(sel any) (err error) { if slices.Contains(t.sel, sel) { return ErrTargetAlreadySelected } if err = t.constraint(sel); err == nil { t.sel = append(t.sel, sel) } return } func (t *Target) RequireSelection() bool { return len(t.sel) < t.requirement.min } func (t *Target) HasSelection() bool { return len(t.sel) > 0 } func (t *Target) AllowSelection() bool { return (t.requirement.max == -1 || len(t.sel) < t.requirement.max) && len(t.Options()) > 0 } func (t *Target) ClearSelection() { t.sel = []any{} } func (t *Target) NoSelection() bool { return len(t.sel) == 0 } func (t *Target) Selection() []any { return t.sel } func (t *Target) Options() (options []any) { candidates := t.candidates() for _, candidate := range candidates { err := t.constraint(candidate) // TODO: Fix this if a single target allows multiple selections of the same object. if err == nil && slices.Contains(t.sel, candidate) { err = ErrTargetAlreadySelected } if err == nil { options = append(options, candidate) } else { log.Debug("no valid option", "candidate", candidate, "error", err) } } return options } // HasOptions reports if there is at least one possible target. func (t *Target) HasOptions() bool { for _, candidate := range t.candidates() { if err := t.constraint(candidate); err == nil { return true } } return false } func (t *Target) CheckSelection(s *LocalState) error { if t.RequireSelection() { return errors.New("number of selected targets and required ones does not match") } if t.requirement.max != -1 && len(t.sel) > t.requirement.max { return fmt.Errorf("to many selection %d (%d allowed)", len(t.sel), t.requirement.max) } for _, sel := range t.sel { if err := t.constraint(sel); err != nil { return fmt.Errorf("selected target %s does not fit its desciption %s: %s", sel, t.desc, err) } } return nil } // AddSelection adds an object as selection to the current target. func (t *Targets) AddSelection(sel any) error { return t.ts[t.idx].AddSelection(sel) } func (t *Targets) HasSelections() bool { for _, t := range t.ts { if !t.HasSelection() { return false } } return true } func (t *Targets) ClearSelections() { for _, t := range t.ts { t.ClearSelection() } } func (t *Targets) NoSelections() bool { for _, t := range t.ts { if !t.NoSelection() { return false } } return true } // Options returns the options of the current target. func (t *Targets) Options() (options []any) { return t.ts[t.idx].Options() } // HasOptions reports if each target has at least one possible target option. func (ts *Targets) HasOptions() bool { for _, t := range ts.ts { if !t.HasOptions() { return false } } return true } func (t *Targets) Cur() *Target { return t.ts[t.idx] } func (t *Targets) Next() { t.idx++ } func (t *Targets) Targets() []*Target { return t.ts } func (t *Targets) CheckTargets(s *LocalState) error { log.Debug("Check all sub targets", "targets", t.ts) for _, target := range t.ts { if err := target.CheckSelection(s); err != nil { return err } } return nil } // RequireSelection reports if any of the targets require a selection. func (targets *Targets) RequireSelection() bool { for _, t := range targets.ts { if t.RequireSelection() { return true } } return false } // AllowSelection reports if any of the targets allow a selection. func (targets *Targets) AllowSelection() bool { for _, t := range targets.ts { if t.AllowSelection() { return true } } return false } func conjunction(constraints ...TargetConstraintFunc) TargetConstraintFunc { return func(t any) error { for _, constraint := range constraints { if err := constraint(t); err != nil { return err } } return nil } } func disjunction(constraints ...TargetConstraintFunc) TargetConstraintFunc { return func(t any) error { var err error for _, constraint := range constraints { if err = constraint(t); err == nil { return nil } } return err } } func noPreviousSelectionConstraint(targets *Targets, idx int) TargetConstraintFunc { return func(t any) error { for i := 0; i < idx && i < targets.idx; i++ { target := targets.ts[i] if slices.Contains(target.sel, t) { return fmt.Errorf("Selection %v already selected for target %d", t, i) } } return nil } } func targetConstraint(desc string, s *LocalState, action Action) TargetConstraintFunc { if !strings.Contains(desc, " or ") { return singleTargetConstraint(desc, s, action) } descs := strings.Split(desc, " or ") constraints := make([]TargetConstraintFunc, 0, len(descs)) for _, desc := range descs { constraints = append(constraints, singleTargetConstraint(desc, s, action)) } return disjunction(constraints...) } func singleTargetConstraint(desc string, s *LocalState, action Action) TargetConstraintFunc { constraints := []TargetConstraintFunc{} if strings.Contains(desc, "permanent") { constraints = append(constraints, parsePermanentTargetConstraint(desc, s, action)...) } else if strings.Contains(desc, "unit") { constraints = append(constraints, parseUnitTargetConstraint(desc, s, action)...) } else if strings.Contains(desc, "artifact") { constraints = append(constraints, parseArtifactTargetConstraint(desc, s, action)...) } else if strings.Contains(desc, "tile") { constraints = append(constraints, parseTileTargetConstraint(desc, s, action)...) } else if strings.Contains(desc, "card") { constraints = append(constraints, parseCardTargetConstraint(desc, s, action)...) // } else if strings.Contains(desc, "action") { // constraints = append(constraints, parseActionTargetConstraint(desc, s, action)...) } else if strings.Contains(desc, "spell") { constraints = append(constraints, parseSpellTargetConstraint(desc, s, action)...) } return conjunction(constraints...) } func enemyPermanentTargetConstraint(action Action) TargetConstraintFunc { var sourceController *Player switch source := action.Source().(type) { case *Player: sourceController = source case Permanent: sourceController = source.Controller() default: log.Panicf("Unhandled source type %T in enemyPermanentTargetConstraint", source) } return func(t any) (err error) { p, _ := t.(Permanent) if !p.Controller().IsEnemy(sourceController) { err = fmt.Errorf("Controller %v of target %v is not an enemy of %v", p.Controller(), t, sourceController) } return } } func controlledPermanentTargetConstraint(action Action) TargetConstraintFunc { var sourceController *Player switch source := action.Source().(type) { case *Player: sourceController = source case Permanent: sourceController = source.Controller() default: log.Panicf("Unhandled source type %T in enemyPermanentTargetConstraint", source) } return func(t any) (err error) { p, _ := t.(Permanent) if p.Controller() != sourceController { err = fmt.Errorf("Controller %v of target %v is %v", p.Controller(), t, sourceController) } return } } var ( ErrTargetNotAttackable = errors.New("not attackable") ErrTargetOutOfRange = errors.New("not in range") ErrTargetRangeProtected = errors.New("range protected") ) func attackableTargetConstraint(action Action) TargetConstraintFunc { return func(t any) error { p, _ := t.(Permanent) if !p.Attackable() { return ErrTargetNotAttackable } if attackAction, ok := action.(*AttackAction); ok { attacker := attackAction.Source().(*Unit) ok, _ := attacker.AttackableTile(p.Tile()) if !ok { return ErrTargetOutOfRange } d := DistanceBetweenPermanents(attacker, p) if d > 1 && p.RangeProtected() { return ErrTargetRangeProtected } } return nil } } func posFromTileOrPermanent(tileOrPermanent any) Position { switch obj := tileOrPermanent.(type) { case Position: return obj case *Tile: return obj.Position case Permanent: return TileOrContainingPermTile(obj).Position default: log.Panicf("Unhandled source type %T in posFromTileOrPermanent", tileOrPermanent) return INVALID_POSITION() } } func rangeTargetConstraint(source any, r int) TargetConstraintFunc { return func(t any) (err error) { sourcePos := posFromTileOrPermanent(source) targetPos := posFromTileOrPermanent(t) if !IsPositionInRange(sourcePos, targetPos, r) { err = fmt.Errorf("Position %v of target %v not in range %d of source's position %v", targetPos, t, r, sourcePos) } return } } func typeTargetConstraint[T any](typeDesc string) TargetConstraintFunc { return func(t any) (err error) { _, ok := t.(T) if !ok { err = fmt.Errorf("unexpected target type %T not %s", t, typeDesc) } return } } func permanentCardTypeConstraint(f func(CardType) bool) TargetConstraintFunc { return func(t any) (err error) { p, ok := t.(Permanent) if !ok { err = fmt.Errorf("unexpected target type %T not Permanent", t) return } c := p.Card() if !f(c.Type) { err = fmt.Errorf("unexpected target card type %v", c.Type) return } return } } func cardTypeConstraint(cardType CardType) TargetConstraintFunc { return func(t any) (err error) { c, ok := t.(*Card) if !ok { err = fmt.Errorf("unexpected target type %T not *Card", t) return } if c.Type != cardType { err = fmt.Errorf("unexpected card type: %v not %v", c.Type, cardType) return } return } } var ( permanentTargetConstraint TargetConstraintFunc = typeTargetConstraint[Permanent]("Permanent") unitTargetConstraint TargetConstraintFunc = typeTargetConstraint[*Unit]("*Unit") tileTargetConstraint TargetConstraintFunc = typeTargetConstraint[*Tile]("*Tile") artifactTargetConstraint TargetConstraintFunc = permanentCardTypeConstraint(CardType.IsArtifact) cardTargetConstraint TargetConstraintFunc = typeTargetConstraint[*Card]("*Card") playActionTargetConstraint TargetConstraintFunc = typeTargetConstraint[*PlayAction]("*PlayAction") spellTargetConstraint TargetConstraintFunc = cardTypeConstraint(CardTypes.Spell) ) var ErrTargetHasShroud = errors.New("target has shroud") func shroudedTargetContraint(t any) (err error) { p := t.(Permanent) if p.HasEffect("shroud") { err = ErrTargetHasShroud } return } func parsePermanentTargetConstraint(desc string, s *LocalState, action Action) []TargetConstraintFunc { constraints := []TargetConstraintFunc{permanentTargetConstraint, shroudedTargetContraint} if strings.Contains(desc, "enemy permanent") { constraints = append(constraints, enemyPermanentTargetConstraint(action)) } if strings.Contains(desc, "attackable ") { constraints = append(constraints, attackableTargetConstraint(action)) } if strings.Contains(desc, " in range ") { tokens := strings.SplitN(desc, " in range ", 2) r, err := strconv.Atoi(tokens[1]) if err != nil { log.Panicf("Invalid range %s in target constraint %s", tokens[1], desc) } constraints = append(constraints, rangeTargetConstraint(action.Source(), r)) } return constraints } func parseUnitTargetConstraint(desc string, _ *LocalState, action Action) []TargetConstraintFunc { constraints := []TargetConstraintFunc{unitTargetConstraint, shroudedTargetContraint} if strings.Contains(desc, "enemy unit") { constraints = append(constraints, enemyPermanentTargetConstraint(action)) } if strings.Contains(desc, "you control") || strings.Contains(desc, "allied unit") { constraints = append(constraints, controlledPermanentTargetConstraint(action)) } if strings.Contains(desc, " in range ") { tokens := strings.SplitN(desc, " in range ", 2) r, err := strconv.Atoi(tokens[1]) if err != nil { log.Panicf("Invalid range %s in target constraint %s", tokens[1], desc) } constraints = append(constraints, rangeTargetConstraint(action.Source(), r)) } return constraints } func parseArtifactTargetConstraint(constraint string, s *LocalState, action Action) []TargetConstraintFunc { constraints := []TargetConstraintFunc{artifactTargetConstraint} return constraints } func _relaxedTileTarget(t any) *Tile { switch t := t.(type) { case *Tile: return t case *Unit: return t.Tile() default: log.Panicf("Not handled target type %T", t) } return nil } func relaxedTileTarget(action Action, t any) *Tile { switch action.(type) { // Some actions allow to also move to Tile occupied by a crewable permanent case *MoveAction: return _relaxedTileTarget(t) case *ArtifactMoveAction: return _relaxedTileTarget(t) case *ArtifactSwitchAction: return _relaxedTileTarget(t) // Strict case default: return t.(*Tile) } } func availableTileConstraint(action Action, card *Card) TargetConstraintFunc { return func(t any) (err error) { tile := relaxedTileTarget(action, t) if card == nil { log.Panicf("Not implemented availability tile constraint for %T", action) } if tile.IsAvailableForCard(card) { return nil } log.Debug("tile not available", "tile", tile, "card", card) return fmt.Errorf("tile %v is not available for %s", tile, card.Name) } } func moveConstraint(_ *LocalState, action Action, u *Unit) TargetConstraintFunc { return func(t any) (err error) { // Some actions allow also unit targets as though they were tiles tile := relaxedTileTarget(action, t) if slices.Contains(u.MoveRangeTiles(), tile) { return nil } return fmt.Errorf("tile %v is not in %v's movement range", tile, u) } } var ( ErrTargetWrongTileType = errors.New("wrong tile type") ErrTargetNotConnectedTile = errors.New("not connected") ) func parseTileTargetConstraint(desc string, s *LocalState, action Action) []TargetConstraintFunc { constraints := []TargetConstraintFunc{} if moveAction, ok := action.(*MoveAction); ok { u := moveAction.Source().(*Unit) constraints = append(constraints, moveConstraint(s, action, u)) } else { constraints = append(constraints, tileTargetConstraint) } var player *Player switch a := action.(type) { case *PlayAction: player = a.Source().(*Player) } var card *Card switch a := action.(type) { case *PlayAction: card = a.Card case *MoveAction: card = a.Card case *StreetAction: card = a.Card case *FullAction: card = a.Card } if strings.Contains(desc, "spawn") { if player == nil { log.Panicf("Not implemented spawn tile constraint for %T", action) } constraints = append(constraints, func(t any) (err error) { tile := relaxedTileTarget(action, t) availableSpawnTiles := s.AvailableSpawnTiles(player, card) log.Debug("check possible spawn tile", "tile", tile, "spawns", availableSpawnTiles) if slices.Contains(availableSpawnTiles, tile) { log.Debug("is spawn tile", "tile", tile) return nil } return fmt.Errorf("tile %v is no spawn tile", tile) }) } if strings.Contains(desc, "available") { constraints = append(constraints, availableTileConstraint(action, card)) } if strings.Contains(desc, "water") { constraints = append(constraints, func(t any) (err error) { tile := t.(*Tile) if tile.Water { return nil } return fmt.Errorf("tile %v is not a water tile", tile) }) } if strings.Contains(desc, "adjacent") { var origin any if pda, ok := action.(*PileDropAction); ok { origin = pda.tile } else { origin = action.Source() } constraints = append(constraints, rangeTargetConstraint(origin, 1)) } if strings.Contains(desc, "free") { constraints = append(constraints, func(t any) (err error) { tile := t.(*Tile) if tile.IsFree() { return nil } return fmt.Errorf("tile %v is occupied by %v", tile, tile.Permanent) }) } for name := range TileNames { if strings.Contains(desc, name) { // FIXME: skip spawn tile type constraint since it colides with available water spawns if name == "spawn" { continue } tileType := TileNames[name] if strings.Contains(desc, "connected") { // Find connected tiles u := action.Source().(*Unit) graph := s.Map().generateConnectedMovementRangeGraphFor(u, tileType) tiles := []*Tile{} for _, pos := range PositionsInRange(u.Tile().Position, u.Movement.Range, false) { tile := s.Map().TileAt(pos) if tile == nil || tile.Type != tileType { continue } _, err := findPathTo(graph, u, pos) if err != nil { continue } tiles = append(tiles, tile) } // Check if tile is connected constraints = append(constraints, func(t any) (err error) { tile := t.(*Tile) if !slices.Contains(tiles, tile) { err = ErrTargetNotConnectedTile } return }) } // Tile type constraint constraints = append(constraints, func(t any) error { tile := t.(*Tile) if tile.Type == tileType { return nil } return ErrTargetWrongTileType }) } } return constraints } func parseCardTargetConstraint(desc string, s *LocalState, action Action) []TargetConstraintFunc { constraints := []TargetConstraintFunc{cardTargetConstraint} player := action.Source().(*Player) origins := []*Player{player} originDesc := player.Name + "'s" if strings.Contains(desc, "opponent") { originDesc = "a opponent's" origins = []*Player{} for _, p := range s.Players() { if p.IsEnemy(player) { origins = append(origins, p) } } } if strings.Contains(desc, "hand") || strings.Contains(desc, "store") || strings.Contains(desc, "discard pile") { if player == nil { log.Panicf("Not implemented PileOfCards constraint for %T", action) } var pocs []PileOfCards pocDesc := "" if strings.Contains(desc, "hand") { pocDesc = "hand" } else if strings.Contains(desc, "store") { pocDesc = "store" } else if strings.Contains(desc, "discard pile") { pocDesc = "discard pile" } // TODO: support player specific constraints for _, p := range origins { switch pocDesc { case "hand": pocs = append(pocs, p.Hand) case "store": if p.gameState.Map().HasStores() { for _, s := range p.AvailableStores() { pocs = append(pocs, s) } } else { pocs = append(pocs, p.Store) } case "discard pile": pocs = append(pocs, p.DiscardPile) } } constraints = append(constraints, func(t any) (err error) { c := t.(*Card) for _, poc := range pocs { if poc.Contains(c) { return nil } } return fmt.Errorf("card %s is not in %s %s", c.Name, originDesc, pocDesc) }) } return constraints } func playedCardTypeConstraint(cardType CardType) TargetConstraintFunc { return func(t any) error { pa := t.(*PlayAction) if pa.Card.Type != cardType { return fmt.Errorf("played cardtype %s not %s", pa.Card.Type, cardType) } return nil } } func parseSpellTargetConstraint(desc string, s *LocalState, action Action) []TargetConstraintFunc { var constraints []TargetConstraintFunc // The target has to be a declared play action if strings.Contains(desc, "declared") { constraints = []TargetConstraintFunc{ playActionTargetConstraint, playedCardTypeConstraint(CardTypes.Spell), func(t any) error { pa := t.(*PlayAction) if !utils.InterfaceSliceContains(utils.TypedSliceToInterfaceSlice(s.stack.Actions), pa) { return fmt.Errorf("action %s not declared", pa) } return nil }, } // The target may be any spell card } else { constraints = []TargetConstraintFunc{ cardTargetConstraint, spellTargetConstraint, } } return constraints } func (t *Target) candidates() []any { c := []any{} if strings.Contains(t.desc, "unit") || strings.Contains(t.desc, "artifact") || strings.Contains(t.desc, "permanent") { c = append(c, utils.TypedSliceToInterfaceSlice(t.s.Permanents())...) } if strings.Contains(t.desc, "tile") { c = append(c, utils.TypedSliceToInterfaceSlice(t.s.Map().AllTiles())...) } if strings.Contains(t.desc, "card") { cards := NewPileOfCards() switch t.desc { case "hand card": for _, p := range t.s.Players() { cards.AddPoc(p.Hand) } case "store card": if t.s.Map().HasStores() { for _, store := range t.s.Stores() { cards.AddPoc(store) } } else { for _, p := range t.s.Players() { cards.AddPoc(p.Store) } } default: log.Panicf("Unimplemented card options for %s", t.desc) } c = append(c, utils.TypedSliceToInterfaceSlice(cards.Cards())...) } if strings.Contains(t.desc, "declared") { c = append(c, utils.TypedSliceToInterfaceSlice(t.s.stack.Actions)...) } return c }