Skip to content

Instantly share code, notes, and snippets.

@gittib
Last active May 30, 2024 16:31
Show Gist options
  • Save gittib/9a0960892d7aafc66be0907f9c829884 to your computer and use it in GitHub Desktop.
Save gittib/9a0960892d7aafc66be0907f9c829884 to your computer and use it in GitHub Desktop.
遊戯王のデッキの事故率を計算します
/**
* 遊戯王 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