package game import ( "errors" "fmt" "regexp" "slices" "strings" "muhq.space/muhqs-game/go/log" "muhq.space/muhqs-game/go/utils" ) type ActionSpeed int const ( slow ActionSpeed = iota fast ) var ActionSpeeds = struct { Slow ActionSpeed Fast ActionSpeed }{ Slow: slow, Fast: fast, } type ( ActionCostFunc func(*LocalState) bool ActionResolveFunc func(*LocalState) ActionFuncPrototype func(Action) ActionResolveFunc ) type Action interface { Source() any Controller() *Player Speed() ActionSpeed GameState() *LocalState Targets() *Targets // Shortcut for the first target Target() *Target NeedsActionCostChoice() bool CheckTargets(*LocalState) error // Check if an actions play costs can be payed and if so pays them PayCosts(s *LocalState) bool // Apply an actions effects to the game state resolve(s *LocalState) String() string } type PassPriority struct{ player *Player } func (a *PassPriority) Source() any { return a.player } func (a *PassPriority) Controller() *Player { return a.player } func (a *PassPriority) Speed() ActionSpeed { return fast } func (a *PassPriority) GameState() *LocalState { return a.player.gameState } func (*PassPriority) Targets() *Targets { return nil } func (*PassPriority) Target() *Target { return nil } func (*PassPriority) NeedsActionCostChoice() bool { return false } func (*PassPriority) CheckTargets(*LocalState) error { return nil } func (*PassPriority) PayCosts(*LocalState) bool { return true } func (*PassPriority) resolve(*LocalState) {} func (*PassPriority) String() string { return "pass" } func NewPassPriority(p *Player) Action { return &PassPriority{p} } type TargetSelection struct { player *Player targets *Targets } func (sel *TargetSelection) Source() any { return sel.player } func (sel *TargetSelection) Controller() *Player { return sel.player } func (sel *TargetSelection) Speed() ActionSpeed { return fast } func (sel *TargetSelection) GameState() *LocalState { return sel.player.gameState } func (sel *TargetSelection) Targets() *Targets { return sel.targets } func (sel *TargetSelection) Target() *Target { return sel.targets.ts[0] } func (*TargetSelection) NeedsActionCostChoice() bool { return false } func (sel *TargetSelection) CheckTargets(s *LocalState) error { return sel.targets.CheckTargets(s) } func (*TargetSelection) PayCosts(*LocalState) bool { return true } func (*TargetSelection) resolve(*LocalState) {} func (*TargetSelection) String() string { return "target selection" } func newTargetSelection(player *Player, targets *Targets) Action { return &TargetSelection{player, targets} } func newHandCardSelection(p *Player, min, max int) Action { a := &TargetSelection{player: p} targetDesc := TargetDesc{"hand card", fmt.Sprintf("%d-%d", min, max)} a.targets = newTargets(newTarget(p.gameState, targetDesc, a)) return a } func newAlliedUnitSelection(p *Player, min, max int) Action { a := &TargetSelection{player: p} targetDesc := TargetDesc{"unit you control", fmt.Sprintf("%d-%d", min, max)} a.targets = newTargets(newTarget(p.gameState, targetDesc, a)) return a } func newTileSelection(p *Player, constraint TargetConstraintFunc, min, max int) Action { a := &TargetSelection{player: p} desc := TargetDesc{"tile", fmt.Sprintf("%d-%d", min, max)} a.targets = newTargets(newConstraintTarget(p.gameState, desc, constraint, a)) return a } type FreeDiscardAction struct { TargetSelection } func NewFreeDiscardAction(p *Player) *FreeDiscardAction { a := &FreeDiscardAction{TargetSelection: TargetSelection{player: p}} targetDesc := TargetDesc{"hand card", "2"} a.targets = newTargets(newTarget(p.gameState, targetDesc, a)) return a } func (a *FreeDiscardAction) PayCosts(s *LocalState) bool { if a.targets.CheckTargets(s) != nil { return false } cards := utils.InterfaceSliceToTypedSlice[*Card](a.Target().Selection()) a.player.discardCards(cards) return true } func (*FreeDiscardAction) CheckTargets(*LocalState) error { return nil } func (a *FreeDiscardAction) resolve(*LocalState) { a.player.gainResource(1) } func (a *FreeDiscardAction) String() string { if a.Target().RequireSelection() { return fmt.Sprintf("%s discard two", a.player.Name) } sel := utils.InterfaceSliceToTypedSlice[*Card](a.Target().Selection()) return fmt.Sprintf("%s discard [%s,%s]", a.player.Name, sel[0].Name, sel[1].Name) } type DraftPick struct { player *Player pick *Card pack PileOfCards } func (pick *DraftPick) Source() any { return pick.player } func (pick *DraftPick) Controller() *Player { return pick.player } func (pick *DraftPick) Speed() ActionSpeed { return fast } func (pick *DraftPick) GameState() *LocalState { return pick.player.gameState } func (pick *DraftPick) Targets() *Targets { return nil } func (pick *DraftPick) Target() *Target { return nil } func (*DraftPick) NeedsActionCostChoice() bool { return false } func (pick *DraftPick) CheckTargets(s *LocalState) error { return nil } func (*DraftPick) PayCosts(*LocalState) bool { return true } func (pick *DraftPick) resolve(*LocalState) { pick.pack.MoveCard(pick.pick, pick.player.Deck) } func (*DraftPick) String() string { return "draft pick" } func (pick *DraftPick) Pack() PileOfCards { return pick.pack } func (pick *DraftPick) Pick() *Card { return pick.pick } func NewDraftPick(p *Player, pack PileOfCards, card *Card) Action { return &DraftPick{player: p, pack: pack, pick: card} } func newRandomDraftPick(p *Player, n PlayerNotification) Action { pack := n.Context.(PileOfCards) return NewDraftPick(p, pack, pack.RandomCard()) } type ActionBase struct { source any Card *Card targets *Targets resolveFunc ActionResolveFunc costFunc ActionCostFunc } func (a *ActionBase) Source() any { return a.source } func (a *ActionBase) Controller() *Player { switch t := a.source.(type) { case *Player: return t case Permanent: return t.Controller() default: log.Panicf("Unhandled source type %v", t) return nil } } func (a *ActionBase) Speed() ActionSpeed { return slow } func (a *ActionBase) GameState() *LocalState { return a.Controller().gameState } func (a *ActionBase) resolve(s *LocalState) { a.resolveFunc(s) } func (a *ActionBase) PayCosts(s *LocalState) bool { if a.costFunc != nil { return a.costFunc(s) } return true } func (a *ActionBase) Targets() *Targets { return a.targets } func (a *ActionBase) Target() *Target { return a.targets.ts[0] } func (a *ActionBase) CheckTargets(s *LocalState) error { if a.targets == nil { return nil } return a.targets.CheckTargets(s) } type TriggeredAction struct { ActionBase event Event } func newTriggeredAction(event Event, trigger Trigger, resolveFunc ActionResolveFunc, costFunc ActionCostFunc, ) *TriggeredAction { return &TriggeredAction{ ActionBase: ActionBase{ source: trigger.Source(), Card: trigger.Card(), resolveFunc: resolveFunc, costFunc: costFunc, }, event: event, } } func (*TriggeredAction) Speed() ActionSpeed { return fast } func (a *TriggeredAction) String() string { return fmt.Sprintf("%v ! %v", a.event, a.source) } // Wrapper action to pass all triggered actions through the PlayerControl type DeclareTriggeredActionsAction struct { actions []*TriggeredAction } func (sel *DeclareTriggeredActionsAction) Source() any { return nil } func (sel *DeclareTriggeredActionsAction) Controller() *Player { return nil } func (sel *DeclareTriggeredActionsAction) Speed() ActionSpeed { return fast } func (sel *DeclareTriggeredActionsAction) GameState() *LocalState { return nil } func (sel *DeclareTriggeredActionsAction) Targets() *Targets { return nil } func (sel *DeclareTriggeredActionsAction) Target() *Target { return nil } func (*DeclareTriggeredActionsAction) NeedsActionCostChoice() bool { return false } func (sel *DeclareTriggeredActionsAction) CheckTargets(s *LocalState) error { return nil } func (*DeclareTriggeredActionsAction) PayCosts(*LocalState) bool { return true } func (*DeclareTriggeredActionsAction) resolve(*LocalState) {} func (*DeclareTriggeredActionsAction) String() string { return "declared triggered actions" } func NewDeclareTriggeredActionsAction(t []*TriggeredAction) *DeclareTriggeredActionsAction { return &DeclareTriggeredActionsAction{t} } type PileDropAction struct { ActionBase pile []Permanent tile *Tile } func (a *PileDropAction) String() string { perm := a.source.(Permanent) controller := perm.Controller() if a.targets.RequireSelection() { return fmt.Sprintf("%s drop %v", controller.Name, a.tile) } s := fmt.Sprintf("%s drop [", controller.Name) for i, t := range a.targets.ts { s = fmt.Sprintf("%s %v@%v,", s, a.pile[i], t.sel[0]) } return s[:len(s)-1] + " ]" } func (*PileDropAction) Speed() ActionSpeed { return fast } func (*PileDropAction) PayCosts(*LocalState) bool { return true } func newPileDropAction(perm Permanent, tile *Tile, pile []Permanent) *PileDropAction { a := &PileDropAction{ ActionBase{ source: perm, }, pile, tile, } a.resolveFunc = func(s *LocalState) { for i, p := range pile { p.onDrop(p) p.setContainingPerm(nil) enterTileOrPile(p, a.targets.ts[i].sel[0].(*Tile)) } perm.clearPile() } a.targets = newPileDropTargets(perm.Controller().gameState, a) return a } type PlayAction struct { ActionBase ChoosenVariadicCost int } var ( permanentPlayActionTarget = TargetDesc{"available spawn tile", "1"} equipmentPlayActionTarget = TargetDesc{"available spawn tile", "?"} ) func NewPlayActionCostFunc(a *PlayAction, cost int) ActionCostFunc { player := a.source.(*Player) s := player.gameState card := a.Card return func(*LocalState) bool { if player.Resource < cost { return false } if additionalCosts := getCardImplementation(card).additionalPlayCosts(a); additionalCosts != nil { if !additionalCosts(s) { return false } } player.Resource -= cost player.Hand.RemoveCard(card) return true } } // _newPlayAction returns a new PlayAction controlled by the player for a certain card. // It is important that the returned PlayAction has no cost function yet. func _newPlayAction(p *Player, c *Card) *PlayAction { a := &PlayAction{ ActionBase{ source: p, Card: c, }, -1, } s := p.gameState if c.IsPermanent() { if c.Type == CardTypes.Equipment { a.targets = newTargets(newTarget(s, equipmentPlayActionTarget, a)) } else { a.targets = newTargets(newTarget(s, permanentPlayActionTarget, a)) } } else if targetDesc := c.Impl.playTargets(); targetDesc != INVALID_TARGET_DESC { a.targets = newTargets(newTarget(s, targetDesc, a)) } else { a.targets = newTargets() } a.resolveFunc = func(s *LocalState) { s.resolvePlay(a) } return a } func newPlayActionWithCostFunc(p *Player, c *Card, costFunc ActionCostFunc) *PlayAction { a := _newPlayAction(p, c) a.costFunc = costFunc return a } func NewPlayAction(p *Player, c *Card) *PlayAction { a := _newPlayAction(p, c) s := p.gameState if a.costFunc == nil { a.costFunc = NewPlayActionCostFunc(a, c.PlayCosts.Costs(s)) } return a } func NewPlayActionVariadicCosts(p *Player, c *Card, variadicCosts int) *PlayAction { a := _newPlayAction(p, c) a.costFunc = NewPlayActionCostFunc(a, c.PlayCosts.Costs(p.gameState, variadicCosts)) a.ChoosenVariadicCost = variadicCosts return a } func (a *PlayAction) Speed() ActionSpeed { if a.Card.IsPermanent() { return slow } return fast } func (a *PlayAction) String() string { p := a.source.(*Player) if a.targets.RequireSelection() { return fmt.Sprintf(" %s play %s", p.Name, a.Card.Name) } if a.Card.Type == CardTypes.Equipment && len(a.Target().sel) == 0 { return fmt.Sprintf("%s play %s@next unit", p.Name, a.Card.Name) } return fmt.Sprintf("%s play %s@%v", p.Name, a.Card.Name, a.targets) } type MoveAction struct { ActionBase } var moveActionTargetDesc = TargetDesc{"available tile", "1"} func targetTile(t *Target) *Tile { if tile, ok := t.sel[0].(*Tile); ok { return tile } return t.sel[0].(*Unit).Tile() } func NewMoveAction(u *Unit) *MoveAction { a := &MoveAction{ ActionBase{ source: u, Card: u.Card(), }, } a.targets = newTargets(newTarget(u.Controller().gameState, moveActionTargetDesc, a)) a.resolveFunc = func(s *LocalState) { t := targetTile(a.Target()) movePermanent(u, t) } a.costFunc = genMoveActionCost(u) return a } func (a *MoveAction) String() string { u := a.source.(*Unit) if a.targets.RequireSelection() { return fmt.Sprintf("move %s", u.Movement.String()) } t := targetTile(a.Target()) return fmt.Sprintf("%v -> %v", u, t.Position) } type StreetAction struct { ActionBase } var streetActionTargetDesc = TargetDesc{"available connected street tile", "1"} func NewStreetAction(u *Unit) *StreetAction { a := &StreetAction{ ActionBase{ source: u, Card: u.Card(), }, } a.targets = newTargets(newTarget(u.Controller().gameState, streetActionTargetDesc, a)) a.resolveFunc = func(s *LocalState) { t := targetTile(a.Target()) movePermanent(u, t) } a.costFunc = func(*LocalState) bool { ok := u.AvailStreetActions > 0 && u.Tile() != nil && u.Tile().Type == TileTypes.Street if ok { u.AvailStreetActions = u.AvailStreetActions - 1 } return ok } return a } func (a *StreetAction) String() string { u := a.source.(*Unit) if a.targets.RequireSelection() { return fmt.Sprintf("street %s", u.Movement.String()) } t := targetTile(a.Target()) return fmt.Sprintf("%v s-s> %v", u, t.Position) } func genBaseActionCost(u *Unit, attack, move int) ActionCostFunc { return func(*LocalState) bool { if u.AvailAttackActions < attack || u.AvailMoveActions < move { return false } u.AvailAttackActions -= attack u.AvailMoveActions -= move return true } } func genAttackActionCost(u *Unit) ActionCostFunc { return genBaseActionCost(u, 1, 0) } func genMoveActionCost(u *Unit) ActionCostFunc { return genBaseActionCost(u, 0, 1) } func genFullActionCost(u *Unit) ActionCostFunc { return func(*LocalState) bool { if u.AvailAttackActions < 1 || u.AvailMoveActions < 1 { return false } u.AvailAttackActions = 0 u.AvailMoveActions = 0 return true } } type EquipAction struct { ActionBase equipment *Equipment } func NewEquipAction(u *Unit, e *Equipment) *EquipAction { a := &EquipAction{ ActionBase{ source: u, Card: u.Card(), }, e, } a.resolveFunc = func(s *LocalState) { u.equip(e) } return a } func (a *EquipAction) String() string { u := a.source.(*Unit) return fmt.Sprintf("%v equip %v", u, a.equipment) } type AttackAction struct { ActionBase } var attackActionTargetDesc = TargetDesc{"attackable enemy permanent", "1"} func NewAttackAction(u *Unit) *AttackAction { a := &AttackAction{ActionBase{ source: u, Card: u.Card(), }} a.targets = newTargets(newTarget(u.Controller().gameState, attackActionTargetDesc, a)) a.resolveFunc = func(s *LocalState) { s.attack(u, a.Target().sel[0].(Permanent)) } a.costFunc = genAttackActionCost(u) return a } func (a *AttackAction) String() string { u := a.source.(*Unit) if a.targets.RequireSelection() { return fmt.Sprintf("attack %v", u.Attack) } target := a.Target().sel[0].(Permanent) return fmt.Sprintf("%v x %v", u, target.Tile().Position) } type ArtifactSwitchAction struct { ActionBase Artifact Permanent } func newArtifactSwitchAction(u *Unit, artifact Permanent) *ArtifactSwitchAction { a := &ArtifactSwitchAction{ ActionBase{ source: u, Card: u.Card(), }, artifact, } a.resolveFunc = func(s *LocalState) { s.switchPermanents(a.Source().(*Unit), a.Artifact) } a.costFunc = genMoveActionCost(u) return a } func (a *ArtifactSwitchAction) String() string { return fmt.Sprintf("%v <-> %v", a.Source().(*Unit), a.Artifact) } type ArtifactMoveAction struct { ActionBase Artifact Permanent } func newArtifactMoveAction(u *Unit, artifact Permanent) *ArtifactMoveAction { a := &ArtifactMoveAction{ ActionBase{ source: u, Card: u.Card(), }, artifact, } a.targets = newArtifactMoveTargets(u.Controller().gameState, a) a.resolveFunc = func(s *LocalState) { tile := targetTile(a.Targets().ts[0]) movePermanent(u, tile) tile = targetTile(a.Targets().ts[1]) movePermanent(artifact, tile) } a.costFunc = genFullActionCost(u) return a } func (a *ArtifactMoveAction) String() string { if !a.Targets().HasSelections() { return fmt.Sprintf("%v -(%v)>", a.Source().(*Unit), a.Artifact) } t1 := targetTile(a.Targets().ts[0]) t2 := targetTile(a.Targets().ts[1]) return fmt.Sprintf("%v -(%v)> %v,%v", a.Source().(*Unit), a.Artifact, t1, t2) } type FullAction struct { ActionBase Desc string tag string } func newFullAction(u *Unit, proto ActionFuncPrototype, desc string) *FullAction { a := &FullAction{ ActionBase: ActionBase{ source: u, Card: u.Card(), }, Desc: desc, } a.resolveFunc = func(s *LocalState) { rf := proto(a) rf(s) } a.costFunc = genFullActionCost(u) return a } func (a *FullAction) String() string { u := a.source.(*Unit) if a.targets == nil || a.targets.RequireSelection() { return fmt.Sprintf("↻ %s", a.Desc) } return fmt.Sprintf("%v↻@%v", u, a.targets) } type FreeAction struct { ActionBase Desc string } // newControllerCostFunc returns a new cost function checking the controller's resource and decreasing it accordingly. func newControllerResourceCostFunc(p Permanent, amount int) ActionCostFunc { return func(s *LocalState) bool { c := p.Controller() if c.Resource < amount { return false } c.Resource -= amount return true } } func NewFreeAction(p Permanent, resolveProto ActionFuncPrototype, costFunc ActionCostFunc, desc string, ) *FreeAction { a := &FreeAction{ ActionBase{ source: p, Card: p.Card(), costFunc: costFunc, }, desc, } a.resolveFunc = func(s *LocalState) { rf := resolveProto(a) rf(s) } return a } func (a *FreeAction) Speed() ActionSpeed { return fast } func (a *FreeAction) String() (s string) { // Format the source switch src := a.source.(type) { case *Card: s += fmt.Sprintf("%v: %s", a.Controller(), src.Name) default: s += fmt.Sprintf("%s", src) } // Format the description and potential targets if a.targets == nil || a.targets.RequireSelection() { s += fmt.Sprintf(" %s", a.Desc) } else { s += fmt.Sprintf(" %s@%v", a.Desc, a.targets) } return } var buyActionTargetDesc = TargetDesc{"store card", "?"} type BuyAction struct{ TargetSelection } func (a *BuyAction) card() *Card { return a.Target().sel[0].(*Card) } func (a *BuyAction) CheckTargets(s *LocalState) error { err := a.targets.CheckTargets(s) if err != nil { return err } return s.isValidBuy(a) } func (a *BuyAction) resolve(s *LocalState) { c := a.card() p := a.Controller() if p.Store.Contains(c) { p.Store.MoveCard(c, p.DiscardPile) // Card from a store on the map } else { for _, s := range p.AvailableStores() { if s.Contains(c) { s.MoveCard(c, p.DiscardPile) return } } } } func (a *BuyAction) PayCosts(s *LocalState) bool { cost := a.card().BuyCosts.Costs(s) if cost < 0 || cost > a.player.Resource { return false } a.player.Resource -= cost return true } func (a *BuyAction) String() string { if !a.Targets().HasSelections() { return fmt.Sprintf("%s buy", a.player.Name) } return fmt.Sprintf("%s buy %s", a.player.Name, a.card().Name) } func newBuyAction(p *Player) *BuyAction { a := &BuyAction{TargetSelection{player: p}} a.targets = newTargets(newTarget(p.gameState, buyActionTargetDesc, a)) return a } var upkeepActionTargetDesc = TargetDesc{"unit you control", "*"} type UpkeepAction struct { TargetSelection } func (a *UpkeepAction) PayCosts(*LocalState) bool { costs := 0 p := a.player disbanded := utils.InterfaceSliceToTypedSlice[*Unit](a.Target().sel) p.gameState.FilterUnits(func(u *Unit) bool { if u.controller == p && !slices.Contains(disbanded, u) { costs += u.UpkeepCost() } return false }) log.Debug("calculated total upkeep costs", "costs", costs) if a.player.Resource < costs { return false } // Paying the upkeep costs is part of the resolution return true } func (a *UpkeepAction) resolve(*LocalState) { p := a.player s := p.gameState for _, i := range a.Target().sel { u := i.(*Unit) s.destroyPermanent(u) } // Keep and pay for the rest for _, u := range s.units { if u.Controller() != p { continue } log.Debug("pay for upkeep", "costs", u.UpkeepCost(), "unit", u) p.Resource -= u.UpkeepCost() u.onUpkeep() } } func (a *UpkeepAction) String() string { return fmt.Sprintf("upkeep disbanding: %v", a.Target().sel) } func newUpkeepAction(p *Player) *UpkeepAction { a := &UpkeepAction{TargetSelection{player: p}} a.targets = newTargets(newTarget(p.gameState, upkeepActionTargetDesc, a)) return a } func (a *ActionBase) NeedsActionCostChoice() bool { return a.costFunc == nil } func UseMoveAction(a Action) { switch a := a.(type) { case *EquipAction: a.costFunc = genMoveActionCost(a.Source().(*Unit)) } } func UseAttackAction(a Action) { switch a := a.(type) { case *EquipAction: a.costFunc = genAttackActionCost(a.Source().(*Unit)) } } type ConcedeAction struct { PassPriority } func NewConcedeAction(p *Player) *ConcedeAction { return &ConcedeAction{PassPriority{p}} } func (a *ConcedeAction) resolve(*LocalState) { a.player.concede() } func (a *ConcedeAction) String() string { return fmt.Sprintf("%s concedes", a.player.Name) } // Marshal an action to plain text. // The format of the action is `SOURCE ACTION CONTEXT`. // PassPriority: PLAYER pass // ConcedeAction: PLAYER concede // DraftPick: PLAYER pick CARD:PACK func MarshalAction(_a Action) []byte { out := []byte{} switch a := _a.(type) { case *PassPriority: out = fmt.Appendf(out, "%s\u00A0pass", a.player.Name) case *ConcedeAction: out = fmt.Appendf(out, "%s\u00A0concede", a.player.Name) case *DraftPick: out = fmt.Appendf(out, "%s\u00A0pick\u00A0%s:%s", a.player.Name, a.pick.Path(), a.pack) default: log.Fatalf("MarshalAction(%s) not implemented\n", a) } return out } var ( ErrInvalidActionFormat = errors.New("invalid action format") ErrUnknownPlayer = errors.New("unknown player") ) var ActionRegex = regexp.MustCompile(`^([^\xA0]*)\xA0([^\xA0]*)\xA0?(.*)?$`) func UnmarshalAction(s State, in []byte) (a Action, err error) { m := ActionRegex.FindSubmatch(in) if len(m) < 2 || len(m) > 4 { return nil, ErrInvalidActionFormat } _action := string(m[2]) switch _action { case "pick": p := s.PlayerByName(string(m[1])) if p == nil { err = ErrUnknownPlayer return } _p := strings.Split(string(m[3]), ":") pick, err := NewCardSafe(_p[0]) if err != nil { return nil, err } pack := NewPileOfCards() err = pack.FromString(_p[1]) if err != nil { return nil, err } a = NewDraftPick(p, pack, pick) case "pass", "concede": p := s.PlayerByName(string(m[1])) if p == nil { err = ErrUnknownPlayer return } if _action == "pass" { a = NewPassPriority(p) } else { a = NewConcedeAction(p) } } return }