Created
April 14, 2021 18:59
-
-
Save kicktheken/6623f49df07d5148d3cc751edcb38561 to your computer and use it in GitHub Desktop.
Genshin Impact Artifact Simulator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// domain 5star probability 1.07 | |
const PROBABILITY = { | |
main_stat: { | |
plume_of_death: { | |
'atk': 1 | |
}, | |
flower_of_life: { | |
'hp': 1 | |
}, | |
sands_of_eon: { | |
'hp%': 0.2668, | |
'atk%': 0.2666, | |
'def%': 0.2666, | |
'er%': 0.1, | |
'em': 0.1 | |
}, | |
goblet_of_eonothem: { | |
'hp%': 0.2125, | |
'atk%': 0.2125, | |
'def%': 0.2, | |
'pyro%': 0.05, | |
'electro%': 0.05, | |
'cryo%': 0.05, | |
'hydro%': 0.05, | |
'anemo%': 0.05, | |
'geo%': 0.05, | |
'phys%': 0.05, | |
'em': 0.025, | |
}, | |
circlet_of_logos: { | |
'hp%': 0.22, | |
'atk%': 0.22, | |
'def%': 0.22, | |
'cr%': 0.1, | |
'cd%': 0.1, | |
'heal%': 0.1, | |
'em': 0.04 | |
} | |
}, | |
main_stat_value: { | |
'hp': 4780, | |
'atk': 311, | |
'hp%': 46.6, | |
'atk%': 46.6, | |
'def%': 58.3, | |
'er%': 51.8, | |
'em': 187, | |
'cr%': 31.1, | |
'cd%': 62.2, | |
'heal%': 35.9, | |
'pyro%': 46.6, | |
'electro%': 46.6, | |
'cryo%': 46.6, | |
'hydro%': 46.6, | |
'anemo%': 46.6, | |
'geo%': 46.6, | |
'phys%': 58.3, | |
}, | |
sub_stat_roll: { | |
'hp': 0.15, | |
'atk': 0.15, | |
'def': 0.15, | |
'hp%': 0.1, | |
'atk%': 0.1, | |
'def%': 0.1, | |
'er%': 0.1, | |
'em': 0.1, | |
'cr%': 0.075, | |
'cd%': 0.075 | |
}, | |
sub_stat_tier: { | |
'hp': { | |
'209': 0.25, | |
'239': 0.25, | |
'269': 0.25, | |
'299': 0.25 | |
}, | |
'atk': { | |
'14': 0.25, | |
'16': 0.25, | |
'18': 0.25, | |
'19': 0.25 | |
}, | |
'def': { | |
'16': 0.25, | |
'19': 0.25, | |
'21': 0.25, | |
'23': 0.25 | |
}, | |
'hp%': { | |
'4.1': 0.25, | |
'4.7': 0.25, | |
'5.3': 0.25, | |
'5.8': 0.25 | |
}, | |
'atk%': { | |
'4.1': 0.25, | |
'4.7': 0.25, | |
'5.3': 0.25, | |
'5.8': 0.25 | |
}, | |
'def%': { | |
'5.1': 0.25, | |
'5.8': 0.25, | |
'6.6': 0.25, | |
'7.3': 0.25 | |
}, | |
'em': { | |
'16': 0.25, | |
'19': 0.25, | |
'21': 0.25, | |
'23': 0.25 | |
}, | |
'er%': { | |
'4.5': 0.25, | |
'5.2': 0.25, | |
'5.8': 0.25, | |
'6.5': 0.25 | |
}, | |
'cr%': { | |
'2.7': 0.25, | |
'3.1': 0.25, | |
'3.5': 0.25, | |
'3.9': 0.25 | |
}, | |
'cd%': { | |
'5.4': 0.25, | |
'6.2': 0.25, | |
'7.0': 0.25, | |
'7.8': 0.25 | |
} | |
} | |
}; | |
const SUB_STAT_QUALITY_WEIGHT = { | |
'hp': 0, | |
'atk': 0.3, | |
'def': 0, | |
'hp%': 0, | |
'atk%': 0.8, | |
'def%': 0, | |
'er%': 0, | |
'em': 0, | |
'cr%': 1, | |
'cd%': 1 | |
}; | |
// assume 800 base atk | |
// plume of death, 311 atk | |
// sands of eon, 46.6% atk | |
// elem%, 46.6% | |
// 31.1% cr or 62.2% cd | |
// (700 * 1.466 + 311) * 1.466 * (1 + ((0.05 + 0.311) * (0.5))) = 2314 | |
// (700 * 1.932 + 311) * (1 + ((0.05 + 0.311) * (0.5))) = 1963 | |
// ((311+565) * 1.6 + 311) * 1.616 * (1 + ((0.25 + 0.28) * (1.874))) = 5516.36 | |
// ATK ((311+565) * 1.6 + 311 + 16.75) * 1.616 * (1 + ((0.25 + 0.28) * (1.874))) = 5570.31 (+0.978%) | |
// ATK% ((311+565) * 1.64975 + 311) * 1.616 * (1 + ((0.25 + 0.28) * (1.874))) = 5656.74 (+2.54%) | |
// CR% ((311+565) * 1.6 + 311) * 1.616 * (1 + ((0.283 + 0.28) * (1.874))) = 5687.51 (+3.1%) | |
// CD% ((311+565) * 1.6 + 311) * 1.616 * (1 + ((0.25 + 0.28) * (1.94))) = 5613.17 (+1.75%) | |
// 10.3-13% on hat if 3 substats | |
// 1/7 ^ 5 = 0.000059499 | |
// (1/7) ^ 4 * 6/7 * 5 = 0.00178 | |
// (1/7) ^ 3 * (6/7) ^ 2 * 10 = 0.0214 | |
// (1/7) ^ 2 * (6/7) ^ 3 * 10 = 0.1285 | |
// 1/7 * (6/7) ^ 4 * 5 = 0.3855 | |
// (6/7) ^ 5 = 0.4626 | |
function pick(probs) { | |
let weight = 0; | |
for (const key in probs) { | |
if (typeof probs[key] === 'number') { | |
weight += probs[key]; | |
} else { | |
weight += 1; | |
} | |
} | |
const r = Math.random() * weight; | |
let acc = 0; | |
for (const key in probs) { | |
if (typeof probs[key] === 'number') { | |
acc += probs[key]; | |
} else { | |
acc += 1; | |
} | |
if (r < acc) { | |
return key; | |
} | |
} | |
throw `${probs} sums up to less than 1`; | |
} | |
let id = 1; | |
function pickArtifact(rolls = 0) { | |
const artifact_type = pick(PROBABILITY.main_stat); | |
const main_stat = pick(PROBABILITY.main_stat[artifact_type]); | |
const sub_stats = {}; | |
const sub_stat_weights = {}; | |
const starts_with_four_sub_stats = Math.random() < 0.1; | |
const num_sub_stats = rolls > 0 || starts_with_four_sub_stats ? 4 : 3; | |
let possible_rolls = PROBABILITY.sub_stat_roll; | |
let exclude_stat = main_stat; | |
for (let i = 0; i < num_sub_stats; i++) { | |
possible_rolls = { ...possible_rolls, [exclude_stat]: 0 }; | |
const sub_stat = pick(possible_rolls); | |
const sub_stat_roll = parseFloat(pick(PROBABILITY.sub_stat_tier[sub_stat])); | |
sub_stat_weights[sub_stat] = PROBABILITY.sub_stat_roll[sub_stat]; | |
sub_stats[sub_stat] = sub_stat_roll; | |
exclude_stat = sub_stat; | |
} | |
for (let i = 0; i < (rolls - (!starts_with_four_sub_stats)); i++) { | |
const sub_stat = pick(sub_stat_weights); | |
const sub_stat_roll = parseFloat(pick(PROBABILITY.sub_stat_tier[sub_stat])); | |
sub_stats[sub_stat] += sub_stat_roll; | |
} | |
return { | |
artifact_type, | |
main_stat, | |
sub_stats, | |
starts_with_four_sub_stats, | |
on_set: Math.random() < 0.5, | |
id: id++ | |
}; | |
} | |
// const on_set = { ...run_track }; | |
// const off_set = { ...run_track }; | |
const ELEMENT = 'pyro%'; | |
function isGoodArtifact({ artifact_type, main_stat, on_set, sub_stats }) { | |
// if (!on_set) { | |
// return false; | |
// } | |
switch (artifact_type) { | |
case 'sands_of_eon': return main_stat === 'atk%'; | |
case 'goblet_of_eonothem': return main_stat === ELEMENT; //return /o%/.test(main_stat); | |
case 'circlet_of_logos': return main_stat === 'cd%' || main_stat === 'cr%'; | |
default: return true; | |
} | |
} | |
// function isGoodArtifact(artifact) { | |
// // const { artifact_type, main_stat, on_set, sub_stats } = artifact; | |
// if (!on_set || !isGoodMainStat(artifact)) { | |
// return false; | |
// } | |
// let sum_quality = 0; | |
// let highest_quality = 0; | |
// let num_substats = 0; | |
// for (const substat in artifact.sub_stats) { | |
// sum_quality += SUB_STAT_QUALITY_WEIGHT[substat]; | |
// num_substats++; | |
// } | |
// if (num_substats === 3) { | |
// return | |
// } | |
// return | |
// } | |
// 14.3% (1/7) | |
// 42.9% (3/7) | |
function run_trials(total_trials) { | |
let total_runs = 0; | |
for (let trials = 0; trials < total_trials; trials++) { | |
const run_track = { | |
plume_of_death: [], | |
flower_of_life: [], | |
sands_of_eon: [], | |
goblet_of_eonothem: [], | |
circlet_of_logos: [] | |
}; | |
let runs = 0; | |
while (true) { | |
const artifact = pickArtifact(); | |
if (isGoodArtifact(artifact)) { | |
run_track[artifact.artifact_type].push(artifact); | |
} | |
if (Math.random() < 0.07) { | |
const artifact = pickArtifact(); | |
if (isGoodArtifact(artifact)) { | |
run_track[artifact.artifact_type].push(artifact); | |
} | |
} | |
runs++; | |
if ( | |
( | |
!!run_track.plume_of_death.length | |
+ !!run_track.flower_of_life.length | |
+ !!run_track.sands_of_eon.length | |
+ !!run_track.goblet_of_eonothem.length | |
+ !!run_track.circlet_of_logos.length | |
) >= 4 | |
) { | |
// console.log(run_track, runs); | |
// console.log(runs) | |
break; | |
} | |
} | |
total_runs += runs; | |
} | |
// 73 runs to get correct main stats on targeted 4 piece bonus | |
console.log('avg runs', total_runs/ total_trials); | |
} | |
// run_trials(10000); | |
const factorial = (() => { | |
const memoize = {}; | |
return n => { | |
if (memoize[n]) { | |
return memoize[n]; | |
} | |
let product = 1; | |
for (let i = 2; i <= n; i++) { | |
product *= i; | |
} | |
memoize[n] = product; | |
return product; | |
}; | |
})(); | |
const choose = (() => { | |
const memoize = {}; | |
return (n, k) => { | |
if (memoize[n] && memoize[n][k]) { | |
return memoize[n][k]; | |
} | |
const result = factorial(n) / (factorial(k) * factorial(n - k)); | |
memoize[n] = { ...memoize[n], k: result }; | |
return result; | |
}; | |
})(); | |
function roll_distributions(chance, rolls) { | |
const result = {}; | |
for (let k = 0; k <= rolls; k++) { | |
result[k] = Math.pow(chance, k) * Math.pow(1 - chance, rolls - k) * choose(rolls, k); | |
} | |
return result; | |
} | |
function calcQuality(artifact) { | |
if (!isGoodArtifact(artifact)) { | |
return -1; | |
} | |
const { main_stat, sub_stats, starts_with_four_sub_stats } = artifact; | |
let possible_rolls = { ...PROBABILITY.sub_stat_roll, [main_stat]: 0 }; | |
let total_weight = 0; | |
let quality_weight = 0; | |
let current_rolls = 0; | |
let quality_points = 0; | |
for (const sub_stat in sub_stats) { | |
const value = sub_stats[sub_stat]; | |
possible_rolls = { ...possible_rolls, [sub_stat]: 0 }; | |
total_weight += PROBABILITY.sub_stat_roll[sub_stat]; | |
if (sub_stat === 'cr%' || sub_stat === 'cd%') { | |
quality_weight += PROBABILITY.sub_stat_roll[sub_stat]; | |
} | |
const roll_value = value instanceof Array ? value.length : value * 10 / PROBABILITY.main_stat_value[sub_stat]; | |
quality_points += roll_value * SUB_STAT_QUALITY_WEIGHT[sub_stat]; | |
current_rolls += Math.round(roll_value); | |
} | |
if (current_rolls >= 4) { | |
const distributions = roll_distributions(quality_weight / total_weight, 9 - (!starts_with_four_sub_stats) - current_rolls); | |
for (const distribution in distributions) { | |
quality_points += parseInt(distribution) * distributions[distribution]; | |
} | |
} else { | |
quality_points = 0; | |
let total_possible_weight = 0; | |
for (const possible_roll in possible_rolls) { | |
total_possible_weight += possible_rolls[possible_roll]; | |
} | |
for (const possible_roll in possible_rolls) { | |
if (!possible_rolls[possible_roll]) { | |
continue; | |
} | |
const possible_artifact = { | |
...artifact, | |
sub_stats: { ...sub_stats, [possible_roll]: parseFloat(pick(PROBABILITY.sub_stat_tier[possible_roll])) } | |
}; | |
quality_points += ( | |
possible_rolls[possible_roll] | |
/ total_possible_weight | |
* calcQuality(possible_artifact) | |
) | |
} | |
} | |
return quality_points; | |
} | |
// console.log(roll_distributions(4/7, 5)); | |
function calcDamage(artifacts) { | |
const values = {}; | |
for (const stat in PROBABILITY.main_stat_value) { | |
values[stat] = 0; | |
artifacts.forEach(({ main_stat, sub_stats }) => { | |
if (main_stat === stat) { | |
values[stat] += PROBABILITY.main_stat_value[stat]; | |
} | |
if (sub_stats[stat]) { | |
if (sub_stats[stat] instanceof Array) { | |
values[stat] += sub_stats[stat].reduce((sum, v) => sum + v, 0); | |
} else { // typeof sub_stats[stat] === 'number' | |
values[stat] += sub_stats[stat]; | |
} | |
} | |
}); | |
} | |
// const base_atk = 311 + 565; // ganyu 80/90 + 90 blackcliff warbow | |
const base_atk = 295 + 510; // diluc 80/80 + 90 serpent spine | |
// const cr_bonus = 0.20; // blizzard strayer for ganyu | |
const cr_bonus = 0.192 + 0.276; // blizzard strayer for ganyu | |
// const cd_bonus = 0.368 + 0.384// blackcliff warbow + ganyu asc bonus | |
const cd_bonus = 0; | |
const overall_dmg = (base_atk * (1 + values['atk%'] / 100) + values.atk) * values[ELEMENT] / 100 * (1 + (0.05 + cr_bonus + values['cr%'] / 100) * (0.5 + cd_bonus + values['cd%'] / 100)); | |
// console.log(values, overall_dmg) | |
artifacts.forEach(artifact => { | |
if (!artifact.top_damage || artifact.top_damage < overall_dmg) { | |
artifact.top_damage = overall_dmg; | |
} | |
}); | |
return [overall_dmg, values]; | |
} | |
function findBestArtifacts(inventory, artifacts = []) { | |
if (artifacts.length === 5) { | |
const [overall_dmg, values] = calcDamage(artifacts); | |
return [artifacts, overall_dmg, values]; | |
} | |
const types = Object.keys(inventory); | |
const type = types[artifacts.length]; | |
let top_overall_dmg = 0; | |
let top_artifacts; | |
let top_values; | |
inventory[type].forEach(artifact => { | |
const [bestArtifacts, overall_dmg, values] = findBestArtifacts(inventory, [...artifacts, artifact]); | |
if (overall_dmg > top_overall_dmg) { | |
top_artifacts = bestArtifacts; | |
top_overall_dmg = overall_dmg; | |
top_values = values; | |
} | |
}); | |
return [top_artifacts, top_overall_dmg, top_values]; | |
} | |
// let top_artifact = null; | |
const inventory = {}; | |
for (const type in PROBABILITY.main_stat) { | |
inventory[type] = []; | |
} | |
let current_overall_dmg = 0; | |
let full_set = false; | |
for (let runs = 0; runs < 10000; runs++){ | |
const artifact = pickArtifact(5); | |
artifact.quality = calcQuality(artifact); | |
// const equippedArtifact = inventory[artifact.artifact_type]; | |
if (artifact.quality >= 0) { | |
if (inventory[artifact.artifact_type].length >= 10) { | |
inventory[artifact.artifact_type] = inventory[artifact.artifact_type].sort((a, b) => b.top_damage - a.top_damage).slice(0, 5); | |
// const max_quality = Math.max(...inventory[artifact.artifact_type].map(({ quality }) => Math.floor(quality))); | |
// inventory[artifact.artifact_type] = inventory[artifact.artifact_type].filter(({ quality }) => quality > max_quality / 2); | |
// console.log(artifact.artifact_type, inventory[artifact.artifact_type].map(({ quality }) => quality)) | |
} | |
// console.log(artifact.artifact_type, inventory[artifact.artifact_type].map(({ quality }) => quality)) | |
inventory[artifact.artifact_type].push(artifact); | |
// if (full_set) { | |
// const equipped = []; | |
// for (const type in inventory) { | |
// if (type === artifact.artifact_type) { | |
// equipped.push(artifact); | |
// } else { | |
// equipped.push(inventory[type]) | |
// } | |
// } | |
// const [overall_dmg, values] = calcDamage(equipped); | |
// if (overall_dmg > current_overall_dmg) { | |
// console.log('upgrade at', equipped, values, artifact.artifact_type, overall_dmg, runs); | |
// inventory[artifact.artifact_type] = artifact; | |
// current_overall_dmg = overall_dmg; | |
// } | |
// } else if (!equippedArtifact || equippedArtifact.quality < quality) { | |
// artifact.quality = quality; | |
// inventory[artifact.artifact_type].push(artifact); | |
// } | |
if (full_set) { | |
const [bestArtifacts, overall_dmg, values] = findBestArtifacts(inventory); | |
if (overall_dmg > current_overall_dmg) { | |
console.log('upgrade at', bestArtifacts, values, artifact.artifact_type, overall_dmg, runs); | |
inventory[artifact.artifact_type].push(artifact); | |
current_overall_dmg = overall_dmg; | |
} else { | |
console.log('run', runs); | |
} | |
} | |
} | |
if (!full_set && Object.keys(PROBABILITY.main_stat).every(key => inventory[key].length)) { | |
const [bestArtifacts, overall_dmg, values] = findBestArtifacts(inventory); | |
console.log('full set at', bestArtifacts, values, overall_dmg, runs); | |
current_overall_dmg = overall_dmg; | |
full_set = true; | |
// break; | |
} | |
} | |
// 2750 at 10000, 2400 at 1000 | |
// console.log(top_artifact); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment