Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Random substats - POC #2133

Merged
merged 1 commit into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
84 changes: 76 additions & 8 deletions pkg/core/info/character.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,87 @@
package info

import (
"errors"
"fmt"

"github.com/genshinsim/gcsim/pkg/core/attributes"
"github.com/genshinsim/gcsim/pkg/core/keys"
)

type CharacterProfile struct {
Base CharacterBase `json:"base"`
Weapon WeaponProfile `json:"weapon"`
Talents TalentProfile `json:"talents"`
Stats []float64 `json:"stats"`
StatsByLabel map[string][]float64 `json:"stats_by_label"`
Sets Sets `json:"sets"`
SetParams map[keys.Set]map[string]int `json:"-"`
Params map[string]int `json:"-"`
Base CharacterBase `json:"base"`
Weapon WeaponProfile `json:"weapon"`
Talents TalentProfile `json:"talents"`
Stats []float64 `json:"stats"`
StatsByLabel map[string][]float64 `json:"stats_by_label"`
RandomSubstats *RandomSubstats `json:"random_substats"`
Sets Sets `json:"sets"`
SetParams map[keys.Set]map[string]int `json:"-"`
Params map[string]int `json:"-"`
}

type RandomSubstats struct {
Rarity int `json:"rarity"`
Sand attributes.Stat
Goblet attributes.Stat
Circlet attributes.Stat
}

func (r RandomSubstats) Validate() error {
//TODO: support more than just 5 stars
if r.Rarity != 5 {
return fmt.Errorf("unsupported rarity: %v", r.Rarity)
}
if r.Sand == attributes.NoStat {
return errors.New("sand main stat not specified")
}
if r.Goblet == attributes.NoStat {
return errors.New("goblet main stat not specified")
}
if r.Circlet == attributes.NoStat {
return errors.New("circlet main stat not specified")
}
// main stat have to be valid
switch r.Sand {
case attributes.HPP:
case attributes.ATKP:
case attributes.DEFP:
case attributes.EM:
case attributes.ER:
default:
return fmt.Errorf("%v is not a valid main stat for sand", r.Sand.String())
}

switch r.Goblet {
case attributes.HPP:
case attributes.ATKP:
case attributes.DEFP:
case attributes.EM:
case attributes.PyroP:
case attributes.HydroP:
case attributes.CryoP:
case attributes.ElectroP:
case attributes.AnemoP:
case attributes.GeoP:
case attributes.DendroP:
case attributes.PhyP:
default:
return fmt.Errorf("%v is not a valid main stat for sand", r.Sand.String())
}

switch r.Circlet {
case attributes.HPP:
case attributes.ATKP:
case attributes.DEFP:
case attributes.EM:
case attributes.CR:
case attributes.CD:
case attributes.Heal:
default:
return fmt.Errorf("%v is not a valid main stat for sand", r.Sand.String())
}

return nil
}

func (c *CharacterProfile) Clone() CharacterProfile {
Expand Down
63 changes: 63 additions & 0 deletions pkg/gcs/ast/parseCharacter.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,13 +283,76 @@ func parseCharAddStats(p *Parser) (parseFn, error) {
}
c.StatsByLabel[key] = m
return parseRows, nil
case itemIdentifier:
if n.Val == "random" {
return parseCharAddRandomStats(p)
}
fallthrough
default:
return nil, fmt.Errorf("ln%v: unrecognized token parsing add stats: %v", n.line, n)
}
}
return nil, errors.New("unexpected end of line while parsing character add stats")
}

func parseCharAddRandomStats(p *Parser) (parseFn, error) {
// xiangling add stats random rarity=5 sand=hp% goblet=pyro% circlet=cr

// note that plume/flower not specified and will be ignored
rs := &info.RandomSubstats{
Rarity: 5, // default to 5 star
}

for n := p.next(); n.Typ != itemEOF; n = p.next() {
switch n.Typ {
case itemTerminateLine:
// check to make sure all values are valid
err := rs.Validate()
if err != nil {
return nil, fmt.Errorf("ln%v: %w", n.line, err)
}
c := p.chars[p.currentCharKey]
c.RandomSubstats = rs
return parseRows, nil
case itemIdentifier:
switch n.Val {
case "rarity":
x, err := p.acceptSeqReturnLast(itemAssign, itemNumber)
if err != nil {
return nil, err
}
rs.Rarity, err = itemNumberToInt(x)
if err != nil {
return nil, err
}
case "sand":
x, err := p.acceptSeqReturnLast(itemAssign, itemStatKey)
if err != nil {
return nil, err
}
rs.Sand = statKeys[x.Val]
case "goblet":
x, err := p.acceptSeqReturnLast(itemAssign, itemStatKey)
if err != nil {
return nil, err
}
rs.Goblet = statKeys[x.Val]
case "circlet":
x, err := p.acceptSeqReturnLast(itemAssign, itemStatKey)
if err != nil {
return nil, err
}
rs.Circlet = statKeys[x.Val]
default:
return nil, fmt.Errorf("ln%v: unrecognized token parsing add stats random: %v", n.line, n)
}
default:
return nil, fmt.Errorf("ln%v: unrecognized token parsing add stats random: %v", n.line, n)
}
}
return nil, errors.New("unexpected end of line while parsing character add stats (with random subs)")
}

func (p *Parser) acceptLevelReturnBaseMax() (int, int, error) {
base := 0
max := 0
Expand Down
123 changes: 123 additions & 0 deletions pkg/simulation/randstats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package simulation

import (
"errors"
"log"
"math/rand"

"github.com/genshinsim/gcsim/pkg/core/attributes"
"github.com/genshinsim/gcsim/pkg/core/info"
)

var subDist [attributes.DelimBaseStat]float64
var subUpgrade [attributes.DelimBaseStat][4]float64

// mainstat at lvl 20
var mainStat = map[attributes.Stat]float64{
attributes.HP: 4780,
attributes.ATK: 311,
attributes.HPP: 0.466,
attributes.ATKP: 0.466,
attributes.DEFP: 0.583,
attributes.PyroP: 0.466,
attributes.HydroP: 0.466,
attributes.CryoP: 0.466,
attributes.ElectroP: 0.466,
attributes.AnemoP: 0.466,
attributes.GeoP: 0.466,
attributes.DendroP: 0.466,
attributes.PhyP: 0.466,
attributes.EM: 186.5,
attributes.ER: 0.518,
attributes.CD: 0.622,
attributes.CR: 0.311,
attributes.Heal: 0.359,
}

func init() {
subDist[attributes.HP] = 6
subDist[attributes.ATK] = 6
subDist[attributes.DEF] = 6
subDist[attributes.HPP] = 4
subDist[attributes.ATKP] = 4
subDist[attributes.DEFP] = 4
subDist[attributes.ER] = 4
subDist[attributes.EM] = 4
subDist[attributes.CR] = 3
subDist[attributes.CD] = 3

subUpgrade[attributes.HP] = [4]float64{209, 239, 269, 299}
subUpgrade[attributes.DEF] = [4]float64{16, 19, 21, 23}
subUpgrade[attributes.ATK] = [4]float64{14, 16, 18, 19}
subUpgrade[attributes.HPP] = [4]float64{0.041, 0.047, 0.053, 0.058}
subUpgrade[attributes.DEFP] = [4]float64{0.051, 0.058, 0.066, 0.073}
subUpgrade[attributes.ATKP] = [4]float64{0.041, 0.047, 0.053, 0.058}
subUpgrade[attributes.EM] = [4]float64{16, 19, 21, 23}
subUpgrade[attributes.ER] = [4]float64{0.045, 0.052, 0.058, 0.065}
subUpgrade[attributes.CR] = [4]float64{0.027, 0.031, 0.035, 0.039}
subUpgrade[attributes.CD] = [4]float64{0.054, 0.062, 0.07, 0.078}
}

func generateRandSubs(r *info.RandomSubstats, rng *rand.Rand) ([]float64, error) {
if r.Rarity != 5 {
return nil, errors.New("sorry only 5 star artifacts supported currently")
}
stats := make([]float64, attributes.EndStatType)
// main stats first
stats[attributes.ATK] = mainStat[attributes.ATK]
stats[attributes.HP] = mainStat[attributes.HP]
stats[r.Sand] += mainStat[r.Sand]
stats[r.Goblet] += mainStat[r.Goblet]
stats[r.Circlet] += mainStat[r.Circlet]

mains := [5]attributes.Stat{attributes.ATK, attributes.HP, r.Sand, r.Goblet, r.Circlet}

for _, m := range mains {
// weights
var weight [attributes.DelimBaseStat]float64
var picked [4]attributes.Stat
copy(weight[:], subDist[:])
weight[m] = 0

//TODO: option to use boss
upgrades := 4
if rng.Float64() <= 0.2 {
upgrades = 5
}
for i := 0; i < 4; i++ {
// pick stat from weight
s := randSub(weight, rng)
if s == attributes.NoStat {
log.Println("weights no good?")
log.Println(subDist)
log.Println(weight)
return nil, errors.New("unexpected error picking random sub; none found")
}
weight[s] = 0
stats[s] += subUpgrade[s][rng.Intn(4)]
picked[i] = s
}
for i := 0; i < upgrades; i++ {
// pick one out of 4 stats
s := picked[rng.Intn(4)]
stats[s] += subUpgrade[s][rng.Intn(4)]
}
}
return stats, nil
}

func randSub(weights [attributes.DelimBaseStat]float64, rng *rand.Rand) attributes.Stat {
var sumWeights float64
for _, v := range weights {
sumWeights += v
}
pick := rng.Float64() * sumWeights
var cum float64
for i, v := range weights {
cum += v
if pick <= cum {
return attributes.Stat(i)
}
}
return attributes.NoStat
}
9 changes: 9 additions & 0 deletions pkg/simulation/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ func SetupCharactersInCore(core *core.Core, chars []info.CharacterProfile, initi

active := -1
for i := range chars {
// if using random stats, ignore all stats except main
if chars[i].RandomSubstats != nil {
stats, err := generateRandSubs(chars[i].RandomSubstats, core.Rand)
if err != nil {
return err
}
chars[i].Stats = stats
clear(chars[i].StatsByLabel)
}
i, err := core.AddChar(chars[i])
if err != nil {
return err
Expand Down