Skip to content

Instantly share code, notes, and snippets.

@kicktheken
Created April 14, 2021 18:59
Show Gist options
  • Save kicktheken/6623f49df07d5148d3cc751edcb38561 to your computer and use it in GitHub Desktop.
Save kicktheken/6623f49df07d5148d3cc751edcb38561 to your computer and use it in GitHub Desktop.
Genshin Impact Artifact Simulator
// 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