Created
November 29, 2023 21:14
-
-
Save baskuit/237d327eba574cd0ae7665ae1377b03d to your computer and use it in GitHub Desktop.
Python script to solve Tauros v. Tauros
This file contains 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
from fractions import Fraction | |
value_table = {} | |
move_table = {} | |
max_hp = 353 | |
# Body Slam min damage | |
low_hp = 95 | |
attacks = ["bslam", "blizzard", "hyperbeam"] | |
crit = {0: Fraction(402, 512), 1: Fraction(110, 512)} | |
accs = { | |
"bslam": {0: Fraction(1, 256), 1: Fraction(255, 256)}, | |
"blizzard": {0: Fraction(27, 256), 1: Fraction(229, 256)}, | |
"hyperbeam": {0: Fraction(27, 256), 1: Fraction(229, 256)}, | |
"recharge": {0: Fraction(1, 1), 1: Fraction(0, 1)}, | |
} | |
procs = { | |
"bslam": {0: Fraction(1, 1), 1: Fraction(0, 1)}, | |
"blizzard": {0: Fraction(229, 256), 1: Fraction(27, 256)}, | |
"hyperbeam": {0: Fraction(1, 1), 1: Fraction(0, 1)}, | |
"recharge": {0: Fraction(1, 1), 1: Fraction(0, 1)}, | |
} | |
rolls = [ | |
{ | |
"bslam" : {95: 2, 96: 2, 97: 3, 98: 2, 99: 2, 100: 2, 101: 3, 102: 2, 103: 2, 104: 3, 105: 2, 106: 2, 107: 2, 108: 3, 109: 2, 110: 2, 111: 2, 112: 1}, | |
"blizzard" : {86: 1, 87: 2, 88: 3, 89: 2, 90: 3, 91: 2, 92: 3, 93: 2, 94: 3, 95: 2, 96: 3, 97: 2, 98: 3, 99: 2, 100: 3, 101: 2, 102: 1}, | |
"hyperbeam" : {166: 1, 167: 1, 168: 1, 169: 2, 170: 1, 171: 1, 172: 2, 173: 1, 174: 1, 175: 1, 176: 2, 177: 1, 178: 1, 179: 2, 180: 1, 181: 1, 182: 2, 183: 1, 184: 1, 185: 1, 186: 2, 187: 1, 188: 1, 189: 2, 190: 1, 191: 1, 192: 2, 193: 1, 194: 1, 195: 1, 196: 1}, | |
"recharge": {0: 39}, | |
}, | |
{ | |
"bslam" : {184: 1, 185: 1, 186: 1, 187: 1, 188: 2, 189: 1, 190: 1, 191: 1, 192: 1, 193: 1, 194: 2, 195: 1, 196: 1, 197: 1, 198: 1, 199: 2, 200: 1, 201: 1, 202: 1, 203: 1, 204: 1, 205: 2, 206: 1, 207: 1, 208: 1, 209: 1, 210: 1, 211: 2, 212: 1, 213: 1, 214: 1, 215: 1, 216: 1, 217: 1}, | |
"blizzard" : {168: 1, 169: 1, 170: 2, 171: 1, 172: 1, 173: 2, 174: 1, 175: 1, 176: 1, 177: 2, 178: 1, 179: 1, 180: 2, 181: 1, 182: 1, 183: 1, 184: 2, 185: 1, 186: 1, 187: 2, 188: 1, 189: 1, 190: 1, 191: 2, 192: 1, 193: 1, 194: 2, 195: 1, 196: 1, 197: 1, 198: 1}, | |
"hyperbeam" : {324: 1, 325: 1, 327: 1, 328: 1, 330: 1, 331: 1, 333: 1, 334: 1, 336: 1, 337: 1, 339: 1, 340: 1, 342: 1, 343: 1, 345: 1, 346: 1, 348: 1, 349: 1, 351: 1, 352: 1, 354: 1, 355: 1, 357: 1, 358: 1, 360: 1, 361: 1, 363: 1, 364: 1, 366: 1, 367: 1, 369: 1, 370: 1, 372: 1, 373: 1, 375: 1, 376: 1, 378: 1, 379: 1, 381: 1}, | |
"recharge": {0: 39}, | |
}, | |
{ | |
"bslam" : {0: 39}, | |
"blizzard" : {0: 39}, | |
"hyperbeam" : {0: 39}, | |
"recharge": {0: 39}, | |
}, | |
] | |
def solve(p1_hp, p2_hp, p1_recharge, p2_recharge): | |
# flip | |
if p2_hp > p1_hp: | |
return Fraction(1, 1) - solve(p2_hp, p1_hp, p2_recharge, p1_recharge) | |
if p1_hp <= low_hp and p2_hp <= low_hp: | |
if p1_recharge: | |
return Fraction(1, 512) | |
if p2_recharge: | |
return Fraction(511, 512) | |
return Fraction(1, 2) | |
# otherwise just lookup | |
return value_table[(p1_hp, p2_hp, p1_recharge, p2_recharge)] | |
def solve_q(p1_hp, p2_hp, p1_move, p2_move): | |
if p1_hp <= low_hp and p2_hp <= low_hp: | |
if p1_move == "recharge": | |
return Fraction(1, 512) | |
else: | |
if p2_move == "recharge": | |
return Fraction(511, 512) | |
else: | |
return Fraction(1, 2) | |
expected_value = Fraction(0, 1) | |
# count = 0 | |
# total_prob = Fraction(0, 1) | |
same_prob = Fraction(0, 1) | |
# p1_win_prob = Fraction(0, 1) | |
# p2_win_prob = Fraction(0, 1) | |
p1_accs = accs[p1_move] | |
p2_accs = accs[p2_move] | |
p1_procs = procs[p1_move] | |
p2_procs = procs[p2_move] | |
for speed_tie in range(2): | |
for p1_acc in p1_accs: | |
for p1_proc in p1_procs: | |
for p1_crit in crit: | |
p1_roll_idx = 2 if (p1_proc or not p1_acc) else p1_crit | |
p1_rolls = rolls[p1_roll_idx][p1_move] | |
for p1_roll in p1_rolls: | |
for p2_acc in p2_accs: | |
for p2_proc in p2_procs: | |
for p2_crit in crit: | |
p2_roll_idx = 2 if (p2_proc or not p2_acc) else p2_crit | |
p2_rolls = rolls[p2_roll_idx][p2_move] | |
for p2_roll in p2_rolls: | |
prob = ( | |
crit[p1_crit] | |
* p1_rolls[p1_roll] | |
* p1_accs[p1_acc] | |
* p1_procs[p1_proc] | |
* crit[p2_crit] | |
* p2_rolls[p2_roll] | |
* p2_accs[p2_acc] | |
* p2_procs[p2_proc] | |
* Fraction(1, 2 * 39**2) #accounting for damage rolls and speed tie | |
) | |
# total_prob += prob | |
p1_win = (p1_roll >= p2_hp) or (p1_proc and p1_acc) | |
p2_win = (p2_roll >= p1_hp) or (p2_proc and p2_acc) | |
# count += 1 | |
if p1_win: | |
if p2_win: | |
if speed_tie: | |
expected_value += prob # p1 won | |
# p1_win_prob += prob | |
else: | |
# p2_win_prob += prob | |
pass | |
else: | |
expected_value += prob | |
# p1_win_prob += prob | |
elif p2_win: | |
# p2_win_prob += prob | |
pass # done, since we add 0 * prob | |
else: | |
# Not terminal | |
p1_dmg = p1_acc * p1_roll | |
p2_dmg = p2_acc * p2_roll | |
p1_will_recharge = ( | |
p1_move == "hyperbeam" and p1_acc | |
) | |
p2_will_recharge = ( | |
p2_move == "hyperbeam" and p2_acc | |
) | |
if not p1_dmg and not p2_dmg: | |
if p1_move == "recharge" or p2_move == "recharge": | |
value = solve( | |
p1_hp, | |
p2_hp, | |
False, | |
False, | |
) | |
expected_value += value * prob | |
else: | |
# both miss | |
same_prob += prob | |
else: | |
# no result, lookup | |
value = solve( | |
p1_hp - p2_dmg, | |
p2_hp - p1_dmg, | |
p1_will_recharge, | |
p2_will_recharge, | |
) | |
expected_value += value * prob | |
# to account for "no change" | |
expected_value = expected_value / (Fraction(1, 1) - same_prob) | |
return expected_value | |
if __name__ == "__main__": | |
for p1_hp in range(low_hp + 1, max_hp + 1): | |
for p2_hp in range(1, p1_hp + 1): | |
print("P1 HP:", p1_hp, "P2 HP:", p2_hp) | |
matrix = {} | |
for p1_attack in attacks: | |
for p2_attack in attacks: | |
value = solve_q(p1_hp, p2_hp, p1_attack, p2_attack) | |
matrix[(p1_attack, p2_attack)] = value | |
# get optimal moves and value | |
worst = [] | |
for p1_attack in attacks: | |
worst_case = Fraction(1, 1) | |
for p2_attack in attacks: | |
value = matrix[(p1_attack, p2_attack)] | |
if value < worst_case: | |
worst_case = value | |
worst.append(worst_case) | |
best = [] | |
for p2_attack in attacks: | |
best_case = Fraction(0, 1) | |
for p1_attack in attacks: | |
value = matrix[(p1_attack, p2_attack)] | |
if value > best_case: | |
best_case = value | |
best.append(best_case) | |
assert(max(worst) == min(best)) | |
value = max(worst) | |
p1_best_moves = [] | |
for i, p1_attack in enumerate(attacks): | |
if worst[i] == value: | |
p1_best_moves.append(p1_attack) | |
p2_best_moves = [] | |
for i, p2_attack in enumerate(attacks): | |
if best[i] == value: | |
p2_best_moves.append(p2_attack) | |
# must be stored here for vs recharge below | |
value_table[(p1_hp, p2_hp, False, False)] = value | |
move_table[(p1_hp, p2_hp, False, False)] = (p1_best_moves, p2_best_moves) | |
p1_vs_charge_values = [] | |
for p1_attack in attacks: | |
value = solve_q(p1_hp, p2_hp, p1_attack, "recharge") | |
p1_vs_charge_values.append(value) | |
p2_vs_charge_values = [] | |
for p2_attack in attacks: | |
value = solve_q(p1_hp, p2_hp, "recharge", p2_attack) | |
p2_vs_charge_values.append(value) | |
p1_vs_charge_value = max(p1_vs_charge_values) | |
p2_vs_charge_value = min(p2_vs_charge_values) | |
p1_vs_charge_best_moves, p2_vs_charge_best_moves = [], [] | |
for i, p1_attack in enumerate(attacks): | |
if p1_vs_charge_values[i] == p1_vs_charge_value: | |
p1_vs_charge_best_moves.append(p1_attack) | |
for i, p2_attack in enumerate(attacks): | |
if p2_vs_charge_values[i] == p2_vs_charge_value: | |
p2_vs_charge_best_moves.append(p2_attack) | |
value_table[(p1_hp, p2_hp, False, True)] = p1_vs_charge_value | |
value_table[(p1_hp, p2_hp, True, False)] = p2_vs_charge_value | |
move_table[(p1_hp, p2_hp, False, True)] = (p1_vs_charge_best_moves, ["recharge"]) | |
move_table[(p1_hp, p2_hp, True, False)] = (["recharge"], p2_vs_charge_best_moves) | |
# print(p1_vs_charge_values, p2_vs_charge_values) | |
print("RESULT:", value, p1_best_moves, p2_best_moves) | |
print("RESULT (p1 vs charge):", p1_vs_charge_value, p1_vs_charge_best_moves) | |
print("RESULT (p2 vs charge):", p2_vs_charge_value, p2_vs_charge_best_moves) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment