返回首頁 

Greasy Fork is available in English.

天凤牌理好形表示

在天凤牌理中显示好形率


安装此脚本?
// ==UserScript==// @name         天鳳牌理好形表示// @name:zh      天凤牌理好形表示// @name:zh-CN   天凤牌理好形表示// @name:zh-TW   天鳳牌理好形表示// @name:en      Tenhou-Pairi Kokei display// @namespace    http://tanimodori.com/// @version      0.1.0// @description  天鳳牌理で一向聴の好形率を表示する// @description:zh  在天凤牌理中显示好形率// @description:zh-CN  在天凤牌理中显示好形率// @description:zh-TW  在天鳳牌理中顯示好形率// @description:en  Display Kokei percentage of ii-shan-ten in Tenhou-Pairi// @author       Tanimodori// @match        http://tenhou.net/2/*// @match        https://tenhou.net/2/*// @include      http://tenhou.net/2/*// @include      https://tenhou.net/2/*// @grant        none// @license      MIT// ==/UserScript==(function() {"use strict";class MJ {static toArray(input) {const r###lt = [];let head, tail;for (head = tail = 0; head < input.length; ++head) {if ("mpsz".indexOf(input[head]) === -1) {continue;}for (; tail < head; ++tail) {r###lt.push(input[tail] + input[head]);}++tail;}return r###lt;}static toAka(input, force) {if (input[1] !== "m" && input[1] !== "p" && input[1] !== "s") {return input;}if (input[0] === "0" || input[0] === "5") {if (force === true) {return "0" + input[1];} else if (force === false) {return "5" + input[1];} else {return "0" === input[0] ? "5" + input[1] : "0" + input[1];}}return input;}static normalize(source) {return source.map((x) => MJ.toAka(x, false)).sort(MJ.compareTile);}static compareTile(a, b) {if (a[1] !== b[1]) {return a[1] < b[1] ? -1 : 1;} else {let aNum = a.charCodeAt(0);let bNum = b.charCodeAt(0);if (aNum === 48) {aNum = 53.5;}if (bNum === 48) {bNum = 53.5;}return aNum - bNum;}}static sub(source, ...tiles) {const r###lt = [...source];for (const tile of tiles) {const index = r###lt.findIndex((x) => MJ.toAka(x, false) === MJ.toAka(tile, false));if (index != -1) {r###lt.splice(index, 1);continue;}}return r###lt;}static remains(source, tile) {let r###lt = 4;source.forEach((x) => {if (MJ.toAka(x, false) === MJ.toAka(tile, false)) {--r###lt;}});return r###lt;}static is13Orphans(source) {const orphanTiles = MJ.toArray("19m19p19s1234567z");if (source.length !== 14) {return false;}const subbed = MJ.sub(source, ...orphanTiles);return subbed.length === 1 && orphanTiles.indexOf(subbed[0]) !== -1;}static is7Pairs(source) {if (source.length !== 14) {return false;}const sorted = MJ.normalize(source);for (let i = 0; i < source.length - 1; ++i) {if (i % 2 === 0 && sorted[i] !== sorted[i + 1]) {return false;}if (i % 2 === 1 && sorted[i] === sorted[i + 1]) {return false;}}return true;}static splitSuits(source) {const r###lt = {};for (const suit of "mpsz") {r###lt[suit] = source.filter((x) => x[1] === suit);}return r###lt;}static findSuitWithPair(suits) {let suitWithPair = null;for (const suit of "mpsz") {const lengthMod3 = suits[suit].length % 3;if (lengthMod3 === 2) {if (!suitWithPair) {suitWithPair = suit;} else {return null;}} else if (lengthMod3 === 1) {return null;}}return suitWithPair;}static _allMeldsLoop(inner, withPair) {if (inner.length === 0) {return true;}const tryComb = (comb, newWithPair = withPair) => {const subbed = MJ.sub(inner, ...comb);return subbed.length === inner.length - comb.length && MJ._allMeldsLoop(subbed, newWithPair);};if (withPair) {if (tryComb([inner[0], inner[0]], false)) {return true;}}if (inner.length >= 3) {if (tryComb([inner[0], inner[0], inner[0]])) {return true;}if (inner[0][0] < "8" && inner[0][1] !== "z") {const addToTile = (t, a) => String.fromCharCode(t.charCodeAt(0) + a) + t[1];if (tryComb([inner[0], addToTile(inner[0], 1), addToTile(inner[0], 2)])) {return true;}}}return false;}static allMelds(source, withPair) {if (source.length === 0) {return true;}withPair != null ? withPair : withPair = source.length % 3 === 2;if (withPair && source.length % 3 !== 2) {return false;}if (!withPair && source.length % 3 !== 0) {return false;}const sorted = MJ.normalize(source);return MJ._allMeldsLoop(sorted, withPair);}static isNormalWinHand(source) {if (source.length % 3 !== 2) {return false;}const suits = MJ.splitSuits(source);const suitWithPair = MJ.findSuitWithPair(suits);if (!suitWithPair) {return false;}for (const suitType of "mpsz") {if (!MJ.allMelds(suits[suitType], suitType === suitWithPair)) {return false;}}return true;}static isWinHand(source) {return MJ.is13Orphans(source) || MJ.is7Pairs(source) || MJ.isNormalWinHand(source);}static findWaitingTiles(source, predicate = MJ.isWinHand) {if (source.length % 3 !== 1)return [];const allTiles = MJ.toArray("123456789m123456789p123456789s1234567z");return allTiles.filter((tile) => MJ.remains(source, tile) > 0 && predicate([...source, tile]));}}const _Hand = class {constructor(tiles, predicate = "standard") {if (typeof tiles === "string") {this.tiles = MJ.toArray(tiles);} else {this.tiles = tiles;}this.children = [];this.predicate = predicate;}get full() {return this.tiles.length % 3 === 2;}get predicateFn() {if (typeof this.predicate === "function") {return this.predicate;} else if (this.predicate in _Hand.predicates) {return _Hand.predicates[this.predicate];} else {throw new Error(`Unknown predicate "${this.predicate}"`);}}discard(tile) {const r###lt = new _Hand(MJ.sub(this.tiles, tile), this.predicate);r###lt.parent = { hand: this, type: "discard", tile, tileCount: -1 };return r###lt;}draw(tile) {const r###lt = new _Hand([...this.tiles, tile], this.predicate);r###lt.parent = { hand: this, type: "draw", tile, tileCount: -1 };return r###lt;}remains(tile) {const deck = [...this.tiles];for (let cur = this.parent; cur; cur = cur.hand.parent) {if (cur.type === "discard") {deck.push(cur.tile);}}const r###lt = MJ.remains(deck, tile);if (r###lt < 0) {throw new Error(`tile "${tile}" has more than 4 tiles`);}return r###lt;}isWinHand() {return this.predicateFn(this.tiles);}uniqueTiles(normalize = false) {const unique = (value, index, self) => self.indexOf(value) === index;let target = this.tiles;if (normalize) {target = MJ.normalize(target);}return target.filter(unique);}_xShantenPartial(childPredicate) {if (this.tiles.length % 3 !== 1) {return [];}this.children = [];for (const tile of _Hand.allTiles) {if (this.remains(tile) <= 0) {continue;}const child = this.draw(tile);if (childPredicate.call(child)) {this.children.push(child);}}return this.children;}_0ShantenPartial() {this.shanten = 0;return this._xShantenPartial(this.isWinHand);}_1ShantenPartial() {this.shanten = 1;return this._xShantenPartial(function() {return this._0ShantenFull().length !== 0;});}_xShantenFull(childPredicate) {if (this.tiles.length % 3 !== 2) {return [];}this.children = [];for (const tile of this.uniqueTiles(true)) {const child = this.discard(tile);if (childPredicate.call(child)) {this.children.push(child);}}return this.children;}_0ShantenFull() {this.shanten = 0;return this._xShantenFull(function() {return this._0ShantenPartial().length !== 0;});}_1ShantenFull() {this.shanten = 1;return this._xShantenFull(function() {return this._1ShantenPartial().length !== 0;});}markParentTileCount() {for (const child of this.children) {child.parent.tileCount = this.remains(child.parent.tile);child.markParentTileCount();}}mockShanten(shanten) {const lengthMod3 = this.tiles.length % 3;if (lengthMod3 === 0) {throw new Error(`Invalid tiles length ${shanten} to have shantens`);}this.shanten = shanten;if (shanten === 0) {const r###lt = lengthMod3 === 2 ? this._0ShantenFull() : this._0ShantenPartial();this.markParentTileCount();return r###lt;} else if (shanten === 1) {const r###lt = lengthMod3 === 2 ? this._1ShantenFull() : this._1ShantenPartial();this.markParentTileCount();return r###lt;} else {return [];}}};let Hand = _Hand;Hand.allTiles = MJ.toArray("123456789m123456789p123456789s1234567z");Hand.predicates = {standard: MJ.isWinHand,normal: MJ.isNormalWinHand};const shantenToNumber = (text) => {text = text.trim();if (text.indexOf("\u8074\u724C") !== -1) {return 0;} else if (text.indexOf("\u548C\u4E86") !== -1) {return -1;} else {const index = text.indexOf("\u5411\u8074");if (index !== -1) {return Number.parseInt(text.substring(0, index));}}throw new Error(`"${text}" is not a valid shanten text`);};const getShantenInfo = () => {const tehaiElement = document.querySelector("#tehai");if (!tehaiElement) {throw new Error("Cannot find #tehai element");}let r###lt = null;tehaiElement.childNodes.forEach((node) => {var _a;if (!r###lt && node.nodeType === node.TEXT_NODE) {const text = (_a = node.textContent) != null ? _a : "";const pattern = /(\d向聴|聴牌|和了)/gm;const matches = text.match(pattern);if (matches) {if (matches.length === 1) {const shanten = shantenToNumber(matches[0]);r###lt = { standard: shanten, normal: shanten };} else if (matches.length === 2) {const standard = shantenToNumber(matches[0]);const normal = shantenToNumber(matches[1]);r###lt = { standard, normal };}}}});if (!r###lt) {throw new Error("Cannot find shanten info");}return r###lt;};const getTiles = () => {const pattern = /([0-9][mps]|[1-7]z).gif/;const tiles = [];document.querySelectorAll("div#tehai > a > img").forEach((element) => {const match = element.src.match(pattern);if (match) {tiles.push(match[1]);}});return tiles;};const getQueryType = () => {const elementM2A = document.querySelector("#m2 > a");if (!elementM2A) {throw new Error("Cannot get query type");}const content = elementM2A.innerHTML;if (content === "\u6A19\u6E96\u5F62") {return "normal";} else if (content === "\u4E00\u822C\u5F62") {return "standard";}throw new Error("Cannot get query type");};const parseTextareaContent = (content) => {const pattern = /([0-9]+[mps]|[1-7]+z)+/gm;const matches = content.match(pattern);const r###lt = {hand: [],waitings: []};if (matches) {r###lt.hand = MJ.toArray(matches[0]);if (content.indexOf("\u6253") !== -1) {for (let i = 1; i < matches.length; i += 2) {r###lt.waitings.push({ discard: matches[i], tiles: MJ.toArray(matches[i + 1]) });}} else {for (let i = 1; i < matches.length; ++i) {r###lt.waitings.push({ tiles: MJ.toArray(matches[i]) });}}}return r###lt;};const getTextareaTiles = () => {var _a;const textarea = document.querySelector("div#m2 > textarea");if (!textarea) {throw new Error("Cannot get textarea element");}const content = (_a = textarea.textContent) != null ? _a : "";return parseTextareaContent(content);};const getUIInfo = () => {const shanten = getShantenInfo();const hand = getTiles().sort(MJ.compareTile);const waitingInfo = getTextareaTiles();const r###lt = {shanten,...waitingInfo,hand,query: {type: getQueryType(),autofill: hand.length !== waitingInfo.hand.length}};return r###lt;};const style = ".shanten-tile {\n  position: relative;\n}\n.shanten-tile .popup {\n  display: none;\n  width: 300px;\n  background-color: #ddd;\n  color: #fff;\n  text-align: center;\n  border-radius: 6px;\n  padding: 8px 0;\n  position: absolute;\n  z-index: 1;\n  top: 125%;\n  left: 50%;\n  margin-left: -150px;\n}\n.shanten-tile .popup::before {\n  content: '';\n  position: absolute;\n  top: calc(0% - 10px);\n  left: 50%;\n  margin-left: -5px;\n  border-width: 5px;\n  border-style: solid;\n  border-color: transparent transparent #ddd transparent;\n}\n.shanten-tile .popup.show {\n  visibility: visible;\n}\n.shanten-tile .popup .popup-tile img:last-of-type {\n  margin-left: 5px;\n}\n.shanten-tile .popup table {\n  text-align: initial;\n  margin-left: auto;\n  margin-right: auto;\n}\n.shanten-tile:hover .popup {\n  display: block;\n}\n";const injectCss = () => {const styleSheet = document.createElement("style");styleSheet.setAttribute("type", "text/css");styleSheet.innerHTML = style;document.head.appendChild(styleSheet);};function getElement(arg1, arg2) {let targetDocument;let spec;if (arg2) {targetDocument = arg1;spec = arg2;} else {targetDocument = document;spec = arg1;}return getElementInner(targetDocument, spec);}function getElementInner(document2, spec) {if (typeof spec === "string") {return document2.createTextNode(spec);}const isHTMLElement = (x) => {return "tagName" in x;};if (isHTMLElement(spec)) {return spec;}const element = document2.createElement(spec["_tag"]);for (const key in spec) {if (key === "_tag") {continue;} else if (key === "_class") {element.className = spec[key];} else if (key === "_innerHTML") {element.innerHTML = spec[key];} else if (key === "_children") {const value = spec[key];const children = value.map((x) => getElementInner(document2, x));element.append(...children);} else {element.setAttribute(key, spec[key]);}}return element;}function getShantenTable(config) {config.rows.sort(compareRow);const table = getElement({_tag: "table",cellpadding: "2",cellspacing: "0",_children: [{_tag: "tbody",_children: config.rows.map(getShantenRow)}]});if (config.showHand) {return getElement({_tag: "div",_class: "popup",_children: [{ _tag: "div", _class: "popup-tile", _children: config.hand.map(getShantenRowTile) }, table]});} else {return table;}}function compareRow(a, b) {const aNum = a.tiles.reduce((acc, x) => acc + x.count, 0);const bNum = b.tiles.reduce((acc, x) => acc + x.count, 0);if (aNum != bNum) {return bNum - aNum;} else {if (a.discard && b.discard) {return MJ.compareTile(a.discard, b.discard);} else {return 0;}}}function getShantenRow(config) {const tiles = splitRowTiles(config);const tdData = [];if (config.discard) {tdData.push(["\u6253"]);tdData.push([getShantenRowTile(config.discard)]);}tdData.push([config.tenpai ? "\u5F85\u3061[" : "\u6478["]);let koukeiTotalCount;let gukeiTotalCount;const hasKoukei = tiles.koukei.length > 0;const hasGukei = tiles.gukei.length > 0;if (hasKoukei || hasGukei) {if (hasKoukei) {tdData.push(tiles.koukei.map(getShantenRowTile));koukeiTotalCount = tiles.koukei.reduce((a, x) => a + x.count, 0);tdData.push([`\u597D\u5F62${koukeiTotalCount}\u679A`]);} else {koukeiTotalCount = 0;tdData.push([]);tdData.push([]);}tdData.push([hasKoukei && hasGukei ? "+" : ""]);if (hasGukei) {tdData.push(tiles.gukei.map(getShantenRowTile));gukeiTotalCount = tiles.gukei.reduce((a, x) => a + x.count, 0);tdData.push([`\u611A\u5F62${gukeiTotalCount}\u679A`]);} else {gukeiTotalCount = 0;tdData.push([]);tdData.push([]);}tdData.push(["="]);}const hasOther = tiles.other.length > 0;if (hasOther) {tdData.push(tiles.other.map(getShantenRowTile));}const totalCount = config.tiles.reduce((a, x) => a + x.count, 0);tdData.push([`${totalCount}\u679A`]);if (koukeiTotalCount !== void 0) {const ratio = Math.round(100 * koukeiTotalCount / totalCount);tdData.push([`\uFF08\u597D\u5F62\u7387${ratio}%\uFF09`]);}tdData.push(["]"]);return getElement({_tag: "tr",_children: tdData.map((x) => ({_tag: "td",_children: x}))});}function splitRowTiles(config) {const koukei = [];const gukei = [];const other = [];for (const tile of config.tiles) {if (tile.type === "koukei") {koukei.push(tile);} else if (tile.type === "gukei") {gukei.push(tile);} else {other.push(tile);}}return { koukei, gukei, other };}function getShantenRowTile(config) {if (typeof config === "string") {return getElement({_tag: "img",src: `https://cdn.tenhou.net/2/a/${config}.gif`,border: "0",class: "D"});} else {const r###lt = getElement({_tag: "span",_class: "shanten-tile",_children: [{_tag: "a",href: config.url,_children: [getShantenRowTile(config.tile)]}]});if (config.child) {const childTable = getShantenTable(config.child);r###lt.appendChild(childTable);}return r###lt;}}const getTotalTileCounts = (children) => {return children.reduce((a, x) => a + x.parent.tileCount, 0);};const isKoukei = (hand) => {for (const child of hand.children) {const waitingCount = getTotalTileCounts(child.children);if (waitingCount > 4) {return true;}}return false;};const getHandUrl = (hand) => {const queryType = hand.predicateFn === Hand.predicates.standard ? "q" : "p";const queryStr = hand.tiles.join("");return `https://tenhou.net/2/?${queryType}=${queryStr}`;};const getRowConfigFromHand = (hand) => {const tiles = [];for (const child of hand.children) {let tileType = null;if (hand.shanten === 1) {tileType = isKoukei(child) ? "koukei" : "gukei";}const tileConfig = {type: tileType,tile: child.parent.tile,count: child.parent.tileCount,url: getHandUrl(child)};if (hand.shanten === 1) {const table = getTableConfigFromHand(child);table.showHand = true;tileConfig.child = table;}tiles.push(tileConfig);}return { discard: hand.parent.tile, tiles, tenpai: hand.shanten === 0 };};const getTableConfigFromHand = (hand) => {const config = {hand: hand.tiles,showHand: false,rows: hand.children.map(getRowConfigFromHand)};return config;};const run = () => {let uiInfo;try {uiInfo = getUIInfo();} catch (e) {return;}const queryType = uiInfo.query.type;if (uiInfo.shanten[queryType] !== 1) {return;}if (uiInfo.hand.length % 3 !== 2) {return;}const originalTable = document.querySelector("#m2 > table");if (!originalTable) {return;}injectCss();const hand = new Hand(uiInfo.hand, queryType);hand.mockShanten(1);const tableConfig = getTableConfigFromHand(hand);const table = getShantenTable(tableConfig);originalTable.after(table);originalTable.remove();};run();})();