pre cw with probe and a bunch script to read game behavior
This commit is contained in:
1 parent
5120219ae2
commit
da8a32f60d
310 files changed
+219693
-872
No files matched your search
@@ -0,0 +1,212 @@
|
||||
package common
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Shop plans are detached copies. The run action processor must commit the
|
||||
// returned shop, gold and purchased board role together in one saved revision.
|
||||
// They never mutate adventure inventory or write persistent state themselves.
|
||||
type CurrencyWarsShop struct {
|
||||
Pool *CurrencyWarsCardPool `json:"pool"`
|
||||
Offers []CurrencyWarsShopOffer `json:"offers"`
|
||||
Locked bool `json:"locked"`
|
||||
FreeRefreshTimes uint32 `json:"free_refresh_times"`
|
||||
}
|
||||
|
||||
type CurrencyWarsShopOffer struct {
|
||||
RoleID uint32 `json:"role_id"`
|
||||
Star uint32 `json:"star"`
|
||||
Cost uint32 `json:"cost"`
|
||||
Sold bool `json:"sold"`
|
||||
}
|
||||
|
||||
// Quotes and rarity weights must come from the server's current run modifiers.
|
||||
// Neither a client request price nor an implicit base-price fallback is used.
|
||||
type CurrencyWarsShopQuote func(roleID uint32) (star, cost uint32, err error)
|
||||
|
||||
// BaseShopRules returns captured values before run effects. The action
|
||||
// processor resolves modifiers and supplies the resulting rules explicitly.
|
||||
type CurrencyWarsShopRules struct {
|
||||
Slots uint32
|
||||
RefreshCost uint32
|
||||
}
|
||||
|
||||
func (c *CurrencyWarsCatalog) BaseShopRules() (CurrencyWarsShopRules, error) {
|
||||
slots, err := c.shopRule("GridFight_CardNumberPerRefresh")
|
||||
if err != nil {
|
||||
return CurrencyWarsShopRules{}, err
|
||||
}
|
||||
cost, err := c.shopRule("GridFight_LotteryRefreshGold")
|
||||
if err != nil {
|
||||
return CurrencyWarsShopRules{}, err
|
||||
}
|
||||
if slots == 0 {
|
||||
return CurrencyWarsShopRules{}, fmt.Errorf("CW shop has no configured slots")
|
||||
}
|
||||
return CurrencyWarsShopRules{Slots: slots, RefreshCost: cost}, nil
|
||||
}
|
||||
|
||||
func (s *CurrencyWarsShop) Clone() *CurrencyWarsShop {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
next := *s
|
||||
next.Offers = append([]CurrencyWarsShopOffer(nil), s.Offers...)
|
||||
if s.Pool != nil {
|
||||
next.Pool = s.Pool.Clone()
|
||||
}
|
||||
return &next
|
||||
}
|
||||
|
||||
func (s *CurrencyWarsShop) validate() error {
|
||||
if s == nil || s.Pool == nil || len(s.Pool.Roles) == 0 {
|
||||
return fmt.Errorf("CW shop card pool is missing")
|
||||
}
|
||||
reserved := make(map[uint32]uint64)
|
||||
for _, offer := range s.Offers {
|
||||
if offer.RoleID == 0 || offer.Star == 0 {
|
||||
return fmt.Errorf("CW shop offer identity is incomplete")
|
||||
}
|
||||
if _, exists := s.Pool.Roles[offer.RoleID]; !exists {
|
||||
return fmt.Errorf("CW shop role %d is absent from pool", offer.RoleID)
|
||||
}
|
||||
if !offer.Sold {
|
||||
reserved[offer.RoleID]++
|
||||
}
|
||||
}
|
||||
for id, role := range s.Pool.Roles {
|
||||
if id == 0 || role.Rarity == 0 || role.Capacity == 0 || role.Remaining > role.Capacity ||
|
||||
uint64(role.Remaining)+reserved[id] > uint64(role.Capacity) {
|
||||
return fmt.Errorf("CW shop pool accounting is invalid for role %d", id)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PlanRefresh charges only a manual refresh. Enter-node refreshes must use
|
||||
// PlanEnterNode, which also observes the lock. Both return a detached state;
|
||||
// failures (including RNG and quote failures) leave the original untouched.
|
||||
func (s *CurrencyWarsShop) PlanRefresh(c *CurrencyWarsCatalog, rules CurrencyWarsShopRules, gold uint32, weights []uint32,
|
||||
policy CurrencyWarsDrawPolicy, random func(uint64) (uint64, error), quote CurrencyWarsShopQuote,
|
||||
) (*CurrencyWarsShop, uint32, error) {
|
||||
if err := s.validate(); err != nil {
|
||||
return nil, gold, err
|
||||
}
|
||||
next := s.Clone()
|
||||
remainingGold := gold
|
||||
if next.FreeRefreshTimes > 0 {
|
||||
next.FreeRefreshTimes--
|
||||
} else {
|
||||
if gold < rules.RefreshCost {
|
||||
return nil, gold, fmt.Errorf("CW refresh gold is insufficient")
|
||||
}
|
||||
remainingGold -= rules.RefreshCost
|
||||
}
|
||||
if err := next.replaceOffers(c, rules, weights, policy, random, quote); err != nil {
|
||||
return nil, gold, err
|
||||
}
|
||||
return next, remainingGold, nil
|
||||
}
|
||||
|
||||
func (s *CurrencyWarsShop) PlanEnterNode(c *CurrencyWarsCatalog, rules CurrencyWarsShopRules, weights []uint32,
|
||||
policy CurrencyWarsDrawPolicy, random func(uint64) (uint64, error), quote CurrencyWarsShopQuote,
|
||||
) (*CurrencyWarsShop, error) {
|
||||
if err := s.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
next := s.Clone()
|
||||
// Keeping a locked shop does not reroll or refund any reserved copies.
|
||||
// The run processor decides when the lock is cleared from captured rules.
|
||||
if next.Locked {
|
||||
return next, nil
|
||||
}
|
||||
if err := next.replaceOffers(c, rules, weights, policy, random, quote); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return next, nil
|
||||
}
|
||||
|
||||
func (s *CurrencyWarsShop) replaceOffers(c *CurrencyWarsCatalog, rules CurrencyWarsShopRules, weights []uint32,
|
||||
policy CurrencyWarsDrawPolicy, random func(uint64) (uint64, error), quote CurrencyWarsShopQuote,
|
||||
) error {
|
||||
if quote == nil {
|
||||
return fmt.Errorf("CW shop quote resolver is missing")
|
||||
}
|
||||
if c == nil {
|
||||
return fmt.Errorf("CW catalog is missing")
|
||||
}
|
||||
if rules.Slots == 0 {
|
||||
return fmt.Errorf("CW shop has no configured slots")
|
||||
}
|
||||
var returned []uint32
|
||||
for _, offer := range s.Offers {
|
||||
if !offer.Sold {
|
||||
returned = append(returned, offer.RoleID)
|
||||
}
|
||||
}
|
||||
if err := s.Pool.ReturnCopies(returned); err != nil {
|
||||
return err
|
||||
}
|
||||
drawn, err := s.Pool.Draw(rules.Slots, weights, policy, random)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
offers := make([]CurrencyWarsShopOffer, len(drawn))
|
||||
for i, id := range drawn {
|
||||
star, cost, err := quote(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CW quote role %d: %w", id, err)
|
||||
}
|
||||
// One reserved pool copy represents a base role. A quoted upgraded role
|
||||
// needs an explicit copy-consumption rule before it can be offered here.
|
||||
role, err := c.CapturedRole(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if star != role.DefaultStar {
|
||||
return fmt.Errorf("CW upgraded shop offer needs a copy-consumption rule")
|
||||
}
|
||||
if _, err := c.CapturedRoleVariant(id, star); err != nil {
|
||||
return err
|
||||
}
|
||||
offers[i] = CurrencyWarsShopOffer{RoleID: id, Star: star, Cost: cost}
|
||||
}
|
||||
s.Offers = offers
|
||||
return s.validate()
|
||||
}
|
||||
|
||||
// PlanBuy transfers the already-reserved copy to the board action processor.
|
||||
// That processor must validate placement, merge and capacity before committing
|
||||
// this plan. Rejecting placement discards the whole plan, including gold debit.
|
||||
func (s *CurrencyWarsShop) PlanBuy(slot, gold uint32) (*CurrencyWarsShop, uint32, CurrencyWarsShopOffer, error) {
|
||||
var empty CurrencyWarsShopOffer
|
||||
if err := s.validate(); err != nil {
|
||||
return nil, gold, empty, err
|
||||
}
|
||||
if uint64(slot) >= uint64(len(s.Offers)) {
|
||||
return nil, gold, empty, fmt.Errorf("CW shop slot is out of range")
|
||||
}
|
||||
offer := s.Offers[slot]
|
||||
if offer.Sold {
|
||||
return nil, gold, empty, fmt.Errorf("CW shop offer was already bought")
|
||||
}
|
||||
if gold < offer.Cost {
|
||||
return nil, gold, empty, fmt.Errorf("CW purchase gold is insufficient")
|
||||
}
|
||||
next := s.Clone()
|
||||
next.Offers[slot].Sold = true
|
||||
return next, gold - offer.Cost, offer, nil
|
||||
}
|
||||
|
||||
func (c *CurrencyWarsCatalog) shopRule(name string) (uint32, error) {
|
||||
if c == nil {
|
||||
return 0, fmt.Errorf("CW catalog is missing")
|
||||
}
|
||||
value, err := c.IntegerRule(name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if value < 0 || uint64(value) > uint64(^uint32(0)) {
|
||||
return 0, fmt.Errorf("CW shop rule %s is outside uint32", name)
|
||||
}
|
||||
return uint32(value), nil
|
||||
}
|
||||
Reference in new issue
Block a user