Created
March 29, 2020 20:53
-
-
Save residentsummer/be91cf453662b2db41ab2118404ae22c to your computer and use it in GitHub Desktop.
Destiny Reduntant Gear
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
{ | |
"cells": [ | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"# Destiny Redundant Gear" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 1, | |
"metadata": { | |
"scrolled": false | |
}, | |
"outputs": [ | |
{ | |
"data": { | |
"application/vnd.jupyter.widget-view+json": { | |
"model_id": "96402c20a9224c2ca501e78dbf7b2378", | |
"version_major": 2, | |
"version_minor": 0 | |
}, | |
"text/plain": [ | |
"HBox(children=(FileUpload(value={}, description='Upload'), Label(value='Filename: '), Label(value='-')))" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
}, | |
{ | |
"data": { | |
"application/vnd.jupyter.widget-view+json": { | |
"model_id": "d03a8757315b4ce2be5cfa4f063e0e62", | |
"version_major": 2, | |
"version_minor": 0 | |
}, | |
"text/plain": [ | |
"HBox(children=(IntSlider(value=2, description='Threshold:', max=10), IntSlider(value=1, description='Max misse…" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
}, | |
{ | |
"data": { | |
"application/vnd.jupyter.widget-view+json": { | |
"model_id": "b8a39773f28f45dab6c5bd05f0a72185", | |
"version_major": 2, | |
"version_minor": 0 | |
}, | |
"text/plain": [ | |
"Output()" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"import io\n", | |
"import csv\n", | |
"import codecs\n", | |
"from collections import defaultdict\n", | |
"\n", | |
"import ipywidgets as widgets\n", | |
"\n", | |
"\n", | |
"STATS = ['Mobility', 'Resilience', 'Recovery', 'Discipline', 'Intellect', 'Strength']\n", | |
"BASE_STATS = [\"%s (Base)\" % x for x in STATS]\n", | |
"ALL_STATS_FMT = '|'.join(\"{%s:2}\" % x for x in BASE_STATS)\n", | |
"\n", | |
"\n", | |
"def format_armor(p):\n", | |
" return ('{Total (Base):2}|%s {Name} ({Power:3})' % ALL_STATS_FMT).format(**p)\n", | |
"\n", | |
"\n", | |
"_ARMOR = {}\n", | |
"def load_armor(filelike):\n", | |
" cats = defaultdict(list)\n", | |
"\n", | |
" for piece in csv.DictReader(filelike):\n", | |
" for k, v in piece.items():\n", | |
" try:\n", | |
" piece[k] = int(v)\n", | |
" except ValueError:\n", | |
" pass\n", | |
"\n", | |
" cats[piece['Type']].append(piece)\n", | |
"\n", | |
" _ARMOR.clear()\n", | |
" _ARMOR.update(cats)\n", | |
"\n", | |
"\n", | |
"def is_better(a, b, threshold, max_misses):\n", | |
" if a['Equippable'] != b['Equippable']:\n", | |
" return False\n", | |
"\n", | |
" miss = 0\n", | |
" for stat in BASE_STATS:\n", | |
" if a[stat] >= b[stat]:\n", | |
" continue\n", | |
"\n", | |
" if b[stat] - a[stat] > threshold:\n", | |
" return False\n", | |
"\n", | |
" miss += 1\n", | |
"\n", | |
" return miss <= max_misses\n", | |
"\n", | |
"\n", | |
"def find_dupes(cat, threshold, max_misses):\n", | |
" '''Finds duplicate items within a category'''\n", | |
"\n", | |
" scat = sorted(cat, key=lambda p: p['Total (Base)'], reverse=True)\n", | |
" l = len(scat)\n", | |
" for i in range(l):\n", | |
" dupes = []\n", | |
" for j in range(i+1, l):\n", | |
" if is_better(scat[i], scat[j], threshold, max_misses):\n", | |
" dupes.append(scat[j])\n", | |
" if dupes:\n", | |
" yield (scat[i], dupes)\n", | |
"\n", | |
"\n", | |
"def armor_filter(piece):\n", | |
" return (\n", | |
" piece['Tier'] == 'Legendary' and\n", | |
" # Need only armor 2.0\n", | |
" 'Energy Capacity' in piece['Masterwork Type'] and\n", | |
" # Filter out class items\n", | |
" piece['Total (Base)'] > 10\n", | |
" )\n", | |
"\n", | |
"\n", | |
"def print_dupes(armor, threshold, max_misses):\n", | |
" has_dupes = False\n", | |
"\n", | |
" for t, pieces in armor.items():\n", | |
" eligible = filter(armor_filter, pieces)\n", | |
" dupes = list(find_dupes(eligible, threshold, max_misses))\n", | |
" if not dupes:\n", | |
" continue\n", | |
"\n", | |
" has_dupes = True\n", | |
" print(t)\n", | |
" for more, lesses in dupes:\n", | |
" print(' ', format_armor(more))\n", | |
" for less in lesses:\n", | |
" print(' ', format_armor(less))\n", | |
" print()\n", | |
" \n", | |
" if not has_dupes:\n", | |
" print(\"=== No duplicates found, try to adjust sliders ===\")\n", | |
"\n", | |
"\n", | |
"### Display widgets and handle input ###\n", | |
"\n", | |
"uploader = widgets.FileUpload(multi=False)\n", | |
"threshold = widgets.IntSlider(value=2, min=0, max=10, step=1, description='Threshold:')\n", | |
"max_misses = widgets.IntSlider(value=1, min=0, max=6, step=1, description='Max misses:')\n", | |
"label = widgets.Label(value=\"-\")\n", | |
"output = widgets.Output()\n", | |
"display(widgets.HBox([uploader, widgets.Label(value=\"Filename: \"), label]),\n", | |
" widgets.HBox([threshold, max_misses]),\n", | |
" output)\n", | |
"\n", | |
"\n", | |
"def handle_upload(change, *args):\n", | |
" files = change['new']\n", | |
" if isinstance(files, dict):\n", | |
" files = list(files.values())\n", | |
"\n", | |
" if not files:\n", | |
" file_label.value = \"-\"\n", | |
" return\n", | |
" \n", | |
" f = files[-1]\n", | |
" label.value = f['metadata']['name']\n", | |
"\n", | |
" # Loads armor into a global var\n", | |
" load_armor(codecs.iterdecode(io.BytesIO(f['content']), 'utf8'))\n", | |
"\n", | |
" analyze()\n", | |
"\n", | |
"\n", | |
"uploader.observe(handle_upload, names='value')\n", | |
" \n", | |
"\n", | |
"def analyze(*args):\n", | |
" output.clear_output(wait=True)\n", | |
" \n", | |
" if not _ARMOR:\n", | |
" with output:\n", | |
" print(\"=== No data ===\")\n", | |
" return\n", | |
" \n", | |
" with output:\n", | |
" print_dupes(_ARMOR, threshold.value, max_misses.value)\n", | |
"\n", | |
"threshold.observe(analyze, names='value')\n", | |
"max_misses.observe(analyze, names='value')\n" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Instruction\n", | |
"\n", | |
"* Extract an armor spreadsheet from DIM (at the bottom of it's settings page)\n", | |
"* Upload it into this notebook via \"Upload\" button\n", | |
"* Use sliders to adjust tolerations (see below)\n", | |
"\n", | |
"#### One piece of armor is considered superior to the other if:\n", | |
"* sum of its base stats is greater or equal\n", | |
"* most* of base stats are greater or equal\n", | |
"\n", | |
"\\* **Max misses** - number of base stats that are allowed to be worse. **Threshold** - maximum deficiency of each stat that \"misses\".\n", | |
"\n", | |
"_Mods and full MW bonus (+2 to all stats) are not considered._" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 2, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"### Hide code blocks ###\n", | |
"\n", | |
"from IPython.display import display, Javascript\n", | |
"import IPython.core.display as di\n", | |
"\n", | |
"# This line will hide code by default when the notebook is loaded\n", | |
"display(Javascript('''jQuery(\".input\").hide();'''))\n", | |
"\n", | |
"# This line will add a button to toggle visibility of code blocks, for use with the HTML export version\n", | |
"di.display_html('''<button onclick=\"jQuery('.input').toggle();\">Toggle code</button>''', raw=True)" | |
] | |
} | |
], | |
"metadata": { | |
"kernelspec": { | |
"display_name": "Python 3", | |
"language": "python", | |
"name": "python3" | |
}, | |
"language_info": { | |
"codemirror_mode": { | |
"name": "ipython", | |
"version": 3 | |
}, | |
"file_extension": ".py", | |
"mimetype": "text/x-python", | |
"name": "python", | |
"nbconvert_exporter": "python", | |
"pygments_lexer": "ipython3", | |
"version": "3.7.5" | |
} | |
}, | |
"nbformat": 4, | |
"nbformat_minor": 4 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment