Last active
May 30, 2024 16:31
-
-
Save gittib/9a0960892d7aafc66be0907f9c829884 to your computer and use it in GitHub Desktop.
遊戯王のデッキの事故率を計算します
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
/** | |
* 遊戯王 CARD DATABASE でデッキの事故率を計算するスクリプト | |
* | |
* ■--- 使い方 ----- | |
* 1. Chromeから遊戯王 CARD DATABASEへアクセスし、事故率を調べたいデッキの詳細ページを開きます | |
* (URL例: https://www.db.yugioh-card.com/yugiohdb/member_deck.action?ope=1&cgid=3f193ef337a0999b4d57f57661d2daf8&dno=1&request_locale=ja ) | |
* 2. デベロッパーツールのコンソールを開き、このスクリプトの全文をコピペして流し込みます | |
* 3. 画面の右上に出現したボタンをクリックすると、初動に必要なカードを設定する画面が開きます | |
* 4. 任意の行の+マークをクリックし、初動に必要なカードを設定します。設定が完了したら「次へ」ボタンを押してください | |
* 5. 画面に従って計算を開始すると、初動札を初手で握れる確率が計算されます。ついでに指名者系などのうらら対策も合わせて握れていれば、そのパターンの確率も計算されます。 | |
* | |
* ※注意事項 | |
* ・計算に時間がかかる場合があります。また、お使いのPCに負荷がかかります。(遊戯王 CARD DATABASEのサイトには、特に負荷はかかりません) | |
* ・現状のサイトに合わせて作っている、非公式のスクリプトになります。サイトが更新された場合、スクリプトが使用できなくなる可能性があります。 | |
*/ | |
(() => { | |
// うらら対策札 | |
const penetrationCards = [ | |
"墓穴の指名者", | |
"抹殺の指名者", | |
"禁止令", | |
"発禁令", | |
]; | |
// 1ドロー魔法はデッキ圧縮とみなし、最初から取り除いた状態で計算する | |
const oneDrawMagics = [ | |
"成金ゴブリン", | |
"金満で謙虚な壺", | |
"強欲で謙虚な壺", | |
]; | |
// 2ドロー魔法はちゃんとドローする | |
const twoDrawMagics = [ | |
"強欲な壺", | |
"強欲で貪欲な壺", | |
"強欲で金満な壺", | |
]; | |
const extraText = $('#extra_list').text(); | |
if (extraText.indexOf('ミレニアム・アイズ・サクリファイス') >= 0) { | |
console.log('ミレニアム・アイズ・サクリファイスがあるので簡易融合もうらら対策札とする'); | |
penetrationCards.push('簡易融合'); | |
} | |
const [cards, imgs] = (() => { | |
let cards = []; | |
let imgs = []; | |
$('#detailtext_main .t_body .t_row').each(function() { | |
const $self = $(this); | |
const cardName = $self.find('.inside .card_name span.name').text(); | |
const type = (() => { | |
switch ($self.find('.inside .card_name > img[alt]').attr('alt')) { | |
case '魔法': return 'spell'; | |
case '罠': return 'trap'; | |
default: return 'monster'; | |
} | |
})(); | |
const subTypeText = $self.find('.inside .flex_2.other span').text().replace(/\r\n|\r|\n|\t|【|】/g, ''); | |
if (oneDrawMagics.includes(cardName)) { | |
console.log('"'+cardName+'" は1枚分のデッキ圧縮と見なし、デッキから除外する'); | |
} else { | |
const num = parseInt($self.find('.cards_num_set > span').text()); | |
for (let i = 0 ; i < num ; i++) { | |
let card = { | |
'type': type, | |
'tribe': subTypeText.replace(/族.*$/g, '族'), | |
'element': $self.find('.inside .element img.icon_img[alt]').attr('alt'), | |
'is2draw': twoDrawMagics.some(item => item == cardName), | |
'name': cardName, | |
}; | |
if (subTypeText.indexOf('チューナー') >= 0) { | |
card['tuner'] = 'チューナー'; | |
} | |
cards.push(card); | |
} | |
imgs.push({ | |
name: cardName, | |
src: $('img[title='+cardName+']').attr('src') | |
}); | |
} | |
}); | |
return [cards, imgs]; | |
})(); | |
const attrList = (() => { | |
let l = []; | |
cards.forEach(card => { | |
if (card.tribe) l.push(card.tribe); | |
l.push(card.element); | |
if (card.type == 'monster') { | |
if (card.tuner) l.push('チューナー'); | |
else l.push('非チューナー'); | |
} | |
}); | |
l = Array.from(new Set(l)); | |
function p(i) { | |
if (i == 'チューナー') return 1; | |
if (i == '非チューナー') return 2; | |
if (i.endsWith('属性')) return 3; | |
if (i.endsWith('族')) return 4; | |
return 5; | |
} | |
return l.sort((a, b) => p(a) - p(b)); | |
})(); | |
console.log([cards, imgs, attrList]); | |
(() => { | |
$('#katteni_dialog_style').remove(); | |
const style ='<style id="katteni_dialog_style">' + | |
"#calc_start_button {" + | |
"position: fixed; " + | |
"z-index: 99999; " + | |
"top: 48px; " + | |
"right: 8px; " + | |
"font-size: 24px; " + | |
"}" + | |
".katteni_dialog {" + | |
"position: fixed; " + | |
"inset: 0; " + | |
"z-index: 100000; " + | |
"background-color: rgba(127,127,127,0.7); " + | |
"}" + | |
".katteni_dialog > div {" + | |
"position: fixed; " + | |
"inset: 0; " + | |
"display: table; " + | |
"margin: auto; " + | |
"z-index: 100001; " + | |
"background-color: rgb(255, 255, 255); " + | |
"padding: 20px; " + | |
"font-size: 20px; " + | |
"border-radius: 16px; " + | |
"max-width: 95%; " + | |
"}" + | |
".katteni_dialog .parts_wrapper {" + | |
"border: 1px solid; " + | |
"margin: 2px 0; " + | |
"}" + | |
".katteni_dialog .card_image_list img {" + | |
"height: 40px; " + | |
"}" + | |
".katteni_dialog.add_card {" + | |
"z-index: 100005; " + | |
"}" + | |
".katteni_dialog.add_card > div {" + | |
"z-index: 100006; " + | |
"}" + | |
".katteni_dialog.add_card img {" + | |
"height: 80px; " + | |
"padding: 3px; " + | |
"margin: 0 1px; " + | |
"}" + | |
".katteni_dialog.add_card img.selected {" + | |
"background-color: red; " + | |
"}" + | |
".katteni_dialog button {" + | |
"margin-left: 16px; " + | |
"}" + | |
".katteni_dialog span.add {" + | |
"display: inline-block; " + | |
"font-weight: bold; " + | |
"}" + | |
'</style>'; | |
$('head').append(style); | |
})(); | |
$('#calc_start_button').remove(); | |
let $calcStartButton = $('<button id="calc_start_button">計算開始</button>'); | |
$('body').append($calcStartButton); | |
$calcStartButton.on('click', function () { | |
const $back = $('<div class="katteni_dialog">'); | |
$back.html( | |
'<div>' + | |
' <div class="parts_wrapper one">' + | |
' 1枚初動<br>' + | |
' <span class="card_image_list first"></span>のうちどれかを引ければ動き出せる' + | |
' </div>' + | |
' <div class="parts_wrapper two" data-num="1">' + | |
' 2枚初動 その1<br>' + | |
' <span class="card_image_list first"></span>のうちどれかと' + | |
' <span class="card_image_list second"></span>のうちどれかを同時に引ければ動き出せる' + | |
' </div>' + | |
' <div class="parts_wrapper two" data-num="2">' + | |
' 2枚初動 その2<br>' + | |
' <span class="card_image_list first"></span>のうちどれかと' + | |
' <span class="card_image_list second"></span>のうちどれかを同時に引ければ動き出せる' + | |
' </div>' + | |
' <div class="parts_wrapper two" data-num="3">' + | |
' 2枚初動 その3<br>' + | |
' <span class="card_image_list first"></span>のうちどれかと' + | |
' <span class="card_image_list second"></span>のうちどれかを同時に引ければ動き出せる' + | |
' </div>' + | |
' <div class="parts_wrapper three" data-num="1">' + | |
' 3枚初動 その1<br>' + | |
' <span class="card_image_list first"></span>のうちどれかと' + | |
' <span class="card_image_list second"></span>のうちどれかと' + | |
' <span class="card_image_list third"></span>のうちどれかを同時に引ければ動き出せる' + | |
' </div>' + | |
' <div class="parts_wrapper three" data-num="2">' + | |
' 3枚初動 その2<br>' + | |
' <span class="card_image_list first"></span>のうちどれかと' + | |
' <span class="card_image_list second"></span>のうちどれかと' + | |
' <span class="card_image_list third"></span>のうちどれかを同時に引ければ動き出せる' + | |
' </div>' + | |
' <div class="parts_wrapper three" data-num="3">' + | |
' 3枚初動 その3<br>' + | |
' <span class="card_image_list first"></span>のうちどれかと' + | |
' <span class="card_image_list second"></span>のうちどれかと' + | |
' <span class="card_image_list third"></span>のうちどれかを同時に引ければ動き出せる' + | |
' </div> ' + | |
' <button class="cancel">キャンセル</button> ' + | |
' <button class="next">次へ</button> ' + | |
'</div>'); | |
$back.find('.card_image_list').append('<span class="add">+</span>'); | |
$back.on('click', '.card_image_list img[title]', function() { | |
$(this).remove(); | |
}); | |
$back.on('click', '.card_image_list span.add', async function() { | |
const $self = $(this); | |
const $cardImgs = await addCardDialog(); | |
if ($cardImgs) { | |
const $list = $self.closest('.card_image_list'); | |
$cardImgs.forEach($cardImg => { | |
$list.find('img[title='+$cardImg.attr('title')+']').remove(); | |
$self.before($cardImg); | |
}); | |
} | |
}); | |
$('body').append($back); | |
$('.katteni_dialog button.cancel').on('click', () => { | |
$('.katteni_dialog').remove(); | |
}); | |
$('.katteni_dialog button.next').on('click', () => { | |
const mustCards = []; | |
$back.find('.one .card_image_list img[title]').each(function() { | |
mustCards.push([[$(this).attr('title')]]); | |
}); | |
$back.find('.two, .three').each(function() { | |
let m = []; | |
$(this).find('.card_image_list').each(function() { | |
let l = []; | |
$(this).find('img[title]').each(function() { | |
l.push($(this).attr('title')); | |
}); | |
m.push(l); | |
}); | |
if (m.map(item => item.length).reduce((a,b) => a<b ? a : b) > 0) { | |
mustCards.push(m); | |
} | |
}); | |
$('.katteni_dialog').remove(); | |
selectSenko(mustCards); | |
}); | |
}); | |
function addCardDialog() { | |
return new Promise(function (resolve, reject) { | |
const $back = $('<div class="katteni_dialog add_card">'); | |
const $dialog = $('<div>'); | |
imgs.forEach(item => { | |
const $img = $('<img>'); | |
$img.attr('src', item.src); | |
$img.attr('title', item.name); | |
$img.on('click', () => { | |
$img.toggleClass('selected'); | |
}); | |
$dialog.append($img); | |
}); | |
$dialog.append('<br>'); | |
const $attrSelect = $('<select class="attrSelect">'); | |
$attrSelect.append($('<option>')); | |
attrList.forEach(attr => { | |
const $opt = $('<option>'); | |
$opt.text(attr); | |
$attrSelect.append($opt); | |
}); | |
$dialog.append($attrSelect); | |
$dialog.append('を一括で'); | |
let $button = $('<button style="margin-left: 0px;">選択</button>'); | |
$button.on('click', () => { | |
const attr = $attrSelect.val(); | |
let tmp = []; | |
if (attr == 'チューナー') tmp = cards.filter(c => c.tuner); | |
else if (attr == '非チューナー') tmp = cards.filter(c => c.type == 'monster' && !c.tuner); | |
else if (attr.endsWith('族')) tmp = cards.filter(c => c.tribe == attr); | |
else if (attr.endsWith('属性') || attr == '魔法' || attr == '罠') tmp = cards.filter(c => c.element == attr); | |
tmp.forEach(card => { | |
$('.add_card img[title='+card.name+']').addClass('selected'); | |
}); | |
}); | |
$dialog.append($button); | |
$dialog.append('する'); | |
$button = $('<button>選択解除</button>'); | |
$button.on('click', () => { | |
$('.add_card img').removeClass('selected'); | |
}); | |
$dialog.append($button); | |
$dialog.append('<br>'); | |
$dialog.append('<br>'); | |
$button = $('<button>キャンセル</button>'); | |
$button.on('click', () => { | |
resolve([]); | |
$back.remove(); | |
}); | |
$dialog.append($button); | |
$button = $('<button>追加</button>'); | |
$button.on('click', () => { | |
let $imgs = []; | |
$dialog.find('img.selected').each(function() { | |
$imgs.push($(this).clone()); | |
}); | |
resolve($imgs); | |
$back.remove(); | |
}); | |
$dialog.append($button); | |
$back.append($dialog); | |
$('body').append($back); | |
}); | |
} | |
function selectSenko(mustCards) { | |
if (mustCards.length <= 0) { | |
const $d = $('<div class="katteni_dialog">'); | |
$d.append($('<div>初動札を設定してください。</div>')); | |
$('body').append($d); | |
setTimeout(() => { | |
$('.katteni_dialog').on('click', function() { $(this).remove(); }); | |
}, 100); | |
return; | |
} | |
const $back = $('<div class="confirm_back katteni_dialog">'); | |
const $msg = $('<div>先攻と後攻、どちらの場合を計算しますか?<br>後攻の場合、初手6枚で計算します。<br></div>'); | |
let chk = false; | |
const $checkDlabel = $('<label></label>'); | |
const $checkD = $('<input type="checkbox" name="check_draw2">'); | |
$checkD.on('change', function() { | |
//console.log($checkD.val(), $checkD.prop('checked'), $checkD.is(':checked'), $(this).val(), $(this).prop('checked'), $(this).is(':checked')); | |
chk = $checkD.prop('checked'); | |
}); | |
$checkDlabel.append($checkD); | |
$checkDlabel.append('一部の2ドロー魔法の処理を厳密化する(処理が重いです)<br>'); | |
if (cards.some(card => card.is2draw)) { | |
$msg.append($checkDlabel); | |
} | |
let $button = $('<button>先攻</button>'); | |
$button.on('click', () => { | |
calcFirstHand(mustCards, false, chk); | |
$back.remove(); | |
}); | |
$msg.append($button); | |
$button = $('<button>後攻</button>'); | |
$button.on('click', () => { | |
calcFirstHand(mustCards, true, chk); | |
$back.remove(); | |
}); | |
$msg.append($button); | |
$button = $('<button>キャンセル</button>'); | |
$button.on('click', () => { | |
$back.remove(); | |
}); | |
$msg.append($button); | |
$back.append($msg); | |
$('body').append($back); | |
} | |
function calcFirstHand (mustCards, isKoukou, checkDraw2) { | |
const cardsForCalc = (() => { | |
if (checkDraw2) return cards; else return cards.filter(card => !card.is2draw); | |
})(); | |
const $calcMsg = $('.katteni_dialog > div'); | |
let counter = 0; | |
function isJiko(hand) { | |
if (counter % 5000000 == 0) { | |
$calcMsg.text($calcMsg.text()+'.'); | |
console.log('チェック中…'); | |
} | |
counter++; | |
return mustCards.every(oneSet => { | |
for (let hi = 0 ; hi < hand.length ; hi++) { | |
const fnd = oneSet.find(p => p.includes(hand[hi].name)); | |
if (!fnd) continue; | |
oneSet = oneSet.filter(p => p != fnd); | |
if (oneSet.length <= 0) { | |
return false; // 初動札が揃ったので事故じゃないで確定 | |
} | |
} | |
return true; // 初動札が揃わなかったのでこのセットは事故(他のセットが事故ってなければセーフ) | |
}); | |
} | |
function penetration(hand) { | |
return hand.filter(h => penetrationCards.includes(h.name)).length; | |
} | |
function progress() { | |
const $back = $('<div class="calc_now katteni_dialog">'); | |
const $msg = $('<div>計算中…</div>'); | |
$back.append($msg); | |
$('body').append($back); | |
} | |
progress(); | |
setTimeout(() => { | |
let successWithPenetration = { 0:0, 1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, }; | |
let failure = 0; | |
function pickHand(n, idx = 0, pickedIdxs = []) { | |
for (let i = idx ; i < cardsForCalc.length ; i++) { | |
const tmp = [i].concat(pickedIdxs); | |
if (n > 1) { | |
pickHand(n-1, i+1, tmp); | |
} else { | |
let hand = tmp.map(t => cardsForCalc[t]); | |
if (hand.some(item => item.is2draw)) { | |
for (let h101 = 0 ; h101 < cardsForCalc.length ; h101++) { | |
if (tmp.includes(h101)) continue; | |
for (let h102 = h101+1 ; h102 < cardsForCalc.length ; h102++) { | |
if (tmp.includes(h102)) continue; | |
let added = [ | |
cardsForCalc[h101], | |
cardsForCalc[h102], | |
].concat(hand); | |
if (isJiko(added)) { | |
failure++; | |
} else { | |
successWithPenetration[penetration(hand)]++; | |
} | |
} | |
} | |
} else { | |
if (isJiko(hand)) { | |
failure++; | |
} else { | |
successWithPenetration[penetration(hand)]++; | |
} | |
} | |
} | |
} | |
} | |
const smsec = new Date().getTime(); | |
pickHand(isKoukou ? 6 : 5); | |
const processTime = new Date().getTime() - smsec; | |
let success = 0; | |
for (let i = 0 ; i <= 3 ; i++) { | |
success += successWithPenetration[i]; | |
} | |
const all = success + failure; | |
const calcRate = i => Math.round(i / parseFloat(all) * 10000.0) / 100.0; | |
const rate = calcRate(success); | |
const failRate = calcRate(failure); | |
let withP = [ | |
'初動展開できる確率:'+rate+'%', | |
'事故率:'+failRate+'%', | |
]; | |
let ratePerPenetration = []; | |
for (let i = 0 ; i <= 3 ; i++) { | |
ratePerPenetration[i] = calcRate(successWithPenetration[i]); | |
if (i == 0) { | |
withP.push('展開できるけどうらら対策は握れない確率:'+ratePerPenetration[i]+'%'); | |
} else { | |
withP.push('うらら対策'+i+'枚握りつつ展開できる確率:'+ratePerPenetration[i]+'%'); | |
} | |
} | |
console.log({ | |
'チェックパターン総数': all, | |
'回るパターン数': success, | |
'事故パターン数': failure, | |
'詳細': { successWithPenetration, ratePerPenetration, }, | |
'処理時間[msec]': processTime, | |
}); | |
setTimeout(() => { | |
$('.calc_now div').html(withP.join('<br>')); | |
$('.calc_now').on('click', function() { $(this).remove(); }); | |
}, 100); | |
}, 100); | |
} | |
return '画面右上に現れたボタンをクリックして下さい'; | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment