您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
bad move rate
// ==UserScript== // @name Mortal Bad Move Analyzer // @namespace http://tampermonkey.net/ // @version 1.1 // @description bad move rate // @author You // @match https://mjai.ekyu.moe/* // @icon https://mjai.ekyu.moe/favicon.ico // @grant none // ==/UserScript== (function () { 'use strict'; const waitForElement = (selector, timeout = 10000) => { return new Promise((resolve, reject) => { const element = document.querySelector(selector); if (element) return resolve(element); const observer = new MutationObserver(() => { const el = document.querySelector(selector); if (el) { observer.disconnect(); resolve(el); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); reject(`Timeout: ${selector} not found`); }, timeout); }); }; const analyzeBadMoves = () => { const getTemplate = raw => { const rawObj = { raw: raw }; return (...prm) => String.raw(rawObj, ...prm); } const getDahaiStr = (playerSelectHtmlStr) => { return playerSelectHtmlStr.innerHTML .replaceAll('<span class="role">プレイヤー: </span>', '') .replaceAll('<span class="role">Player: </span>', '') .replaceAll('<span class="role">작사: </span>', '') .replaceAll('<span class="role">玩家: </span>', '') .replaceAll('<svg class="tile">', '') .replaceAll('<use class="face" href="', '') .replaceAll('"></use>', '') .replaceAll('</svg>', '') .replaceAll(' ', ''); } const getMortalSelects = (mortalSelectElements) => { const mortalSelects = []; mortalSelectElements.forEach((mortalSelect) => { const recommendInt = mortalSelect.querySelectorAll('td')[2].querySelectorAll('span')[0].innerHTML; const recommendFrac = mortalSelect.querySelectorAll('td')[2].querySelectorAll('span')[1].innerHTML; mortalSelects.push({ dahai: getDahaiStr(mortalSelect.querySelectorAll('td')[0]), recommendation: parseFloat(recommendInt + recommendFrac) }); }); return mortalSelects; } const getTehaiState = (paiElements) => { let tehai = []; let draw = { kind: 'after-fu-ro', from: null, pai: null }; paiElements.forEach(pai => { const from = pai.getAttribute('before') if (from == '自摸 ' || from == 'Draw ' || from == '쯔모 ') { draw = { kind: 'tsumo', from: 'jicha', pai: pai.querySelector('svg > use').getAttribute('href') } } else if (from == '上家打 ' || from == 'Kamicha👈 cuts ' || from == '상가👈 타 ') { draw = { kind: 'fu-ro', from: 'kamicha', pai: pai.querySelector('svg > use').getAttribute('href') } } else if (from == '対面打 ' || from == 'Toimen👆 cuts ' || from == '대면👆 타 ' || from == '对家打 ') { draw = { kind: 'fu-ro', from: 'toimen', pai: pai.querySelector('svg > use').getAttribute('href') } } else if (from == '下家打 ' || from == 'Shimocha👉 cuts ' || from == '하가👉 타 ') { draw = { kind: 'fu-ro', from: 'shimocha', pai: pai.querySelector('svg > use').getAttribute('href') } } else { tehai.push(pai.querySelector('svg > use').getAttribute('href')) }; }); return { tehai: tehai, draw: draw } } const pageBody = document.querySelector('body'); const hanchan = {}; hanchan.url = location.href; const rounds = pageBody.querySelectorAll('section'); const paifuJsonStr = rounds[0].querySelector('div > details > iframe').getAttribute('src') .replace(/https:\/\/tenhou\.net\/.*json=/, ''); const daniData = JSON.parse(paifuJsonStr); hanchan.daniData = { dan: daniData.dan, name: daniData.name, rate: daniData.rate, sx: daniData.sx }; hanchan.playerId = pageBody.querySelectorAll('details > dl > dd')[4].innerHTML; hanchan.rounds = []; rounds.forEach(round => { const turns = []; const turnDetails = Object.values(round.querySelectorAll('div > details')).slice(3); turnDetails.forEach(turnDetail => { const paiElements = turnDetail.querySelectorAll('ul > li'); const dahaiElement = Object.values(turnDetail.querySelectorAll('span')).filter(e => e.className == 'role')[0].parentElement; const mortalSelectElements = turnDetail.querySelectorAll('details > table > tbody > tr'); const textFragment = turnDetail .querySelector('summary').innerHTML .replace('<span class="turn-info">', '') .replaceAll(' ', ' ') .replace('<span class="order-loss">', '') .replaceAll('</span>', '') turns.push({ summury: turnDetail.querySelector('summary').innerHTML.replace(/<span.*/, ''), url: hanchan.url + '#:~:text=' + encodeURI(textFragment), linkName: textFragment.replaceAll(' ', ' '), tehaiState: getTehaiState(paiElements), dahai: getDahaiStr(dahaiElement), mortalSelects: getMortalSelects(mortalSelectElements) }); }); hanchan.rounds.push({ id: round.querySelector('h1 > div > a').getAttribute('href'), name: round.querySelector('h1 > div > a').innerHTML, turns: turns }); }); let dahaiCount = 0; let missCount = 0; let furoCount = 0; const missList = pageBody.querySelectorAll('fieldset > label')[4]; if (document.querySelector("body > h1").innerHTML == 'Replay Examination') { missList.insertAdjacentHTML('beforeend', '<br><span style="font-weight:700">Bad move:</span>'); } else { missList.insertAdjacentHTML('beforeend', '<br><span style="font-weight:700">悪手リスト:</span>'); } hanchan.rounds.forEach(round => { round.turns.forEach(turn => { let recommendation = 0; turn.mortalSelects.forEach(mortalSelect => { if (turn.dahai == mortalSelect.dahai) { recommendation = mortalSelect.recommendation; } }); dahaiCount++; if (turn.tehaiState.draw.kind != "fu-ro") { if (recommendation <= 5) { missCount++; console.debug(`【悪手】局:${round.name}, 巡目:${turn.summury}, 打牌:${turn.dahai}, 推奨度:${recommendation}, ${round.id}:~:text=${turn.summury}`); missList.insertAdjacentHTML('beforeend', `<br><a href="${turn.url}">${round.name}${turn.linkName}</a>`); } else { console.debug(`局:${round.name}, 巡目:${turn.summury}, 打牌:${turn.dahai}, 推奨度:${recommendation}`); } } else { furoCount++; } }); }); const missRate = new Intl.NumberFormat('ja', { style: 'percent', minimumFractionDigits: 2 }).format(missCount / (dahaiCount - furoCount)); pageBody.querySelectorAll('details > dl > dt').forEach(metadata => { if (metadata.innerHTML.includes('mjai-reviewer')) { metadata.insertAdjacentHTML('beforebegin', `<dt>Bad move rate</dt><dd>${missRate}</dd>`); } }); console.debug(`打牌選択:${dahaiCount}, 副露数:${furoCount}, 悪手率:${missCount}, 悪手率:${missRate}`); console.debug(hanchan); }; waitForElement('details > dl > dt') .then(() => { setTimeout(analyzeBadMoves, 500); // 안정성을 위해 짧은 대기 }) .catch(console.error); })();