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,209 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// A run's card pool, separate from the immutable capture and adventure bag.
|
||||
// This initial pool covers captured, pool-enabled handbook roles only. Hidden
|
||||
// roles need captured configurations before they can enter this pool.
|
||||
type CurrencyWarsCardPool struct {
|
||||
Roles map[uint32]CurrencyWarsPoolRole `json:"roles"`
|
||||
}
|
||||
|
||||
type CurrencyWarsPoolRole struct {
|
||||
Rarity uint32 `json:"rarity"`
|
||||
Capacity uint32 `json:"capacity"`
|
||||
Remaining uint32 `json:"remaining"`
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
}
|
||||
|
||||
// Draw policy is explicit server policy, not a claim that the Client defines
|
||||
// how the original server weights different roles inside a rarity bucket.
|
||||
type CurrencyWarsDrawPolicy string
|
||||
|
||||
const (
|
||||
CurrencyWarsDrawRemainingCopies CurrencyWarsDrawPolicy = "remaining_copies"
|
||||
CurrencyWarsDrawUniformRoleIDs CurrencyWarsDrawPolicy = "uniform_role_ids"
|
||||
)
|
||||
|
||||
// NewCapturedCardPool reads capacity and initial exclusions from captured
|
||||
// native constants. It never fabricates an ID or silently includes non-pool
|
||||
// roles. The caller must retain the capture-coverage limitation above.
|
||||
func (c *CurrencyWarsCatalog) NewCapturedCardPool() (*CurrencyWarsCardPool, error) {
|
||||
capacities, err := c.IntegerArrayRule("GridFight_CardPoolNumber")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
excluded, err := c.IntegerArrayRule("GridFight_InitialExceptRoleIdList")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blocked := make(map[uint32]bool, len(excluded))
|
||||
for _, id := range excluded {
|
||||
blocked[id] = true
|
||||
}
|
||||
pool := &CurrencyWarsCardPool{Roles: make(map[uint32]CurrencyWarsPoolRole)}
|
||||
if len(c.CapturedVariants) == 0 {
|
||||
return nil, fmt.Errorf("CW initial card pool needs a completed role capture")
|
||||
}
|
||||
for _, role := range c.CapturedVariants {
|
||||
r := role.Config
|
||||
if !r.IsInPool || blocked[r.ID] {
|
||||
continue
|
||||
}
|
||||
if r.SeasonID != c.ActiveSeasonID || r.Rarity == 0 || uint64(r.Rarity) > uint64(len(capacities)) || capacities[r.Rarity-1] == 0 {
|
||||
return nil, fmt.Errorf("CW role %d has no captured pool capacity", r.ID)
|
||||
}
|
||||
entry := CurrencyWarsPoolRole{Rarity: r.Rarity, Capacity: capacities[r.Rarity-1], Remaining: capacities[r.Rarity-1]}
|
||||
if old, present := pool.Roles[r.ID]; present && old != entry {
|
||||
return nil, fmt.Errorf("conflicting CW pool role %d", r.ID)
|
||||
}
|
||||
pool.Roles[r.ID] = entry
|
||||
}
|
||||
if len(pool.Roles) == 0 {
|
||||
return nil, fmt.Errorf("CW captured initial pool is empty")
|
||||
}
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
func (p *CurrencyWarsCardPool) Clone() *CurrencyWarsCardPool {
|
||||
copyPool := &CurrencyWarsCardPool{Roles: make(map[uint32]CurrencyWarsPoolRole, len(p.Roles))}
|
||||
for id, entry := range p.Roles {
|
||||
copyPool.Roles[id] = entry
|
||||
}
|
||||
return copyPool
|
||||
}
|
||||
|
||||
// ReturnCopies is internal inventory bookkeeping. The action processor must
|
||||
// prove ownership/unsold-offer identity before supplying IDs. Capacity checks
|
||||
// prevent overfilling, and validation commits no partial refund on failure.
|
||||
func (p *CurrencyWarsCardPool) ReturnCopies(ids []uint32) error {
|
||||
if p == nil {
|
||||
return fmt.Errorf("CW pool is missing")
|
||||
}
|
||||
next := p.Clone()
|
||||
for _, id := range ids {
|
||||
role, exists := next.Roles[id]
|
||||
if !exists || role.Remaining >= role.Capacity {
|
||||
return fmt.Errorf("invalid CW pool refund for role %d", id)
|
||||
}
|
||||
role.Remaining++
|
||||
next.Roles[id] = role
|
||||
}
|
||||
p.Roles = next.Roles
|
||||
return nil
|
||||
}
|
||||
|
||||
// Draw reserves copies for shop offers. Rarity weights must be the run's
|
||||
// resolved weights (including modifiers), not necessarily unmodified level
|
||||
// rows. The supplied random function returns a value in [0,upper); no seeded
|
||||
// generator or machine-specific randomness policy is hidden here.
|
||||
func (p *CurrencyWarsCardPool) Draw(count uint32, rarityWeights []uint32, policy CurrencyWarsDrawPolicy,
|
||||
random func(upper uint64) (uint64, error)) ([]uint32, error) {
|
||||
if p == nil || random == nil || len(rarityWeights) == 0 {
|
||||
return nil, fmt.Errorf("CW draw inputs are incomplete")
|
||||
}
|
||||
if policy != CurrencyWarsDrawRemainingCopies && policy != CurrencyWarsDrawUniformRoleIDs {
|
||||
return nil, fmt.Errorf("unknown CW draw policy %q", policy)
|
||||
}
|
||||
next := p.Clone()
|
||||
ids := make([]uint32, 0, len(next.Roles))
|
||||
var available uint64
|
||||
for id, role := range next.Roles {
|
||||
if id == 0 || role.Rarity == 0 || uint64(role.Rarity) > uint64(len(rarityWeights)) || role.Remaining > role.Capacity {
|
||||
return nil, fmt.Errorf("invalid CW pool role %d", id)
|
||||
}
|
||||
if role.Disabled {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, id)
|
||||
if rarityWeights[role.Rarity-1] != 0 {
|
||||
available += uint64(role.Remaining)
|
||||
}
|
||||
}
|
||||
if uint64(count) > available {
|
||||
return nil, fmt.Errorf("CW pool has too few eligible copies")
|
||||
}
|
||||
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
||||
var drawn []uint32
|
||||
for draw := uint32(0); draw < count; draw++ {
|
||||
present := make([]bool, len(rarityWeights))
|
||||
for _, id := range ids {
|
||||
role := next.Roles[id]
|
||||
if role.Remaining != 0 {
|
||||
present[role.Rarity-1] = true
|
||||
}
|
||||
}
|
||||
var total uint64
|
||||
for i, weight := range rarityWeights {
|
||||
if present[i] {
|
||||
total += uint64(weight)
|
||||
}
|
||||
}
|
||||
if total == 0 {
|
||||
return nil, fmt.Errorf("CW eligible rarity weights are empty")
|
||||
}
|
||||
roll, err := random(total)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if roll >= total {
|
||||
return nil, fmt.Errorf("CW random source returned an out-of-range rarity roll")
|
||||
}
|
||||
var rarity uint32
|
||||
for i, weight := range rarityWeights {
|
||||
if !present[i] {
|
||||
continue
|
||||
}
|
||||
if roll < uint64(weight) {
|
||||
rarity = uint32(i) + 1
|
||||
break
|
||||
}
|
||||
roll -= uint64(weight)
|
||||
}
|
||||
var roleTotal uint64
|
||||
for _, id := range ids {
|
||||
role := next.Roles[id]
|
||||
if role.Rarity == rarity && role.Remaining != 0 {
|
||||
if policy == CurrencyWarsDrawRemainingCopies {
|
||||
roleTotal += uint64(role.Remaining)
|
||||
} else {
|
||||
roleTotal++
|
||||
}
|
||||
}
|
||||
}
|
||||
roleRoll, err := random(roleTotal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if roleRoll >= roleTotal {
|
||||
return nil, fmt.Errorf("CW random source returned an out-of-range role roll")
|
||||
}
|
||||
selected := false
|
||||
for _, id := range ids {
|
||||
role := next.Roles[id]
|
||||
if role.Rarity != rarity || role.Remaining == 0 {
|
||||
continue
|
||||
}
|
||||
weight := uint64(1)
|
||||
if policy == CurrencyWarsDrawRemainingCopies {
|
||||
weight = uint64(role.Remaining)
|
||||
}
|
||||
if roleRoll < weight {
|
||||
role.Remaining--
|
||||
next.Roles[id] = role
|
||||
drawn = append(drawn, id)
|
||||
selected = true
|
||||
break
|
||||
}
|
||||
roleRoll -= weight
|
||||
}
|
||||
if !selected {
|
||||
return nil, fmt.Errorf("CW role draw did not resolve")
|
||||
}
|
||||
}
|
||||
p.Roles = next.Roles
|
||||
return drawn, nil
|
||||
}
|
||||
Reference in new issue
Block a user