検索条件
全5件
(1/1ページ)



const NAMEPREF = 1 を const NAMEPREF = 0 に書き換えてください。
// Copyright 2021-2025 HASEBA Junya
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ==UserScript==
// @name soul2naga
// @namespace lions.blue
// @icon http://1.gravatar.com/avatar/6fa3836d10d691125749472297cf516a
// @version 1.0.0
// @description downloadlogsで取得する牌譜をNAGAで解析可能な形式に変換する
// @include https://mahjongsoul.game.yo-star.com/*
// @include https://game.mahjongsoul.com/*
// @include https://majsoul.union-game.com/0/*
// ==/UserScript==
(function() {
// ダウンロードリンクのhref属性において、牌譜データに先行する部分の文字列。
const DOWNLOAD_HREF_PREFIX = "data:text/plain;charset=utf-8,";
// 天鳳牌譜エディタのURLにおいて、牌譜データに先行する部分の文字列。
const EDITOR_URL_PREFIX = "https://tenhou.net/6/#json=";
// ダウンロードイベントの前に割り込んで牌譜データを書き換える。
document.addEventListener("click", function(e) {
const links = document.body.getElementsByTagName("a");
for (let i = 0; i < links.length; i++) {
if (isDownloadLink(links[i])) {
const soulJson = fetchSoulJson(links[i]);
const urls = createViewerUrls(soulJson);
links[i].href = buildDownloadHref(urls);
links[i].download = buildFileName(links[i].download);
return;
}
}
}, {capture: true});
/**
* オブジェクトをディープコピーする。
*
* @param {Object} src コピー対象のオブジェクトを指定する。
* @returns {Object} 複製したオブジェクトを返す。
*/
function deepCopy(src) {
// JSON文字列化してからオブジェクトに戻すことでディープコピーを実現する。
return JSON.parse(JSON.stringify(src));
}
/**
* ダウンロードリンクであるか判定する。
*
* @param {HTMLElement} element 判定対象の要素を指定する。
* @returns {Boolean} ダウンロードリンクの場合はtrue、それ以外の場合はfalseを返す。
*/
function isDownloadLink(element) {
// href属性の先頭部分で判定する。
return element.href.startsWith(DOWNLOAD_HREF_PREFIX);
}
/**
* ダウンロードリンクから雀魂の牌譜データを抽出する。
*
* @param {HTMLElement} element ダウンロードリンクを指定する。
* @returns {String} 雀魂の牌譜データを返す。
*/
function fetchSoulJson(element) {
// href属性から牌譜データを抽出する。
return decodeURIComponent(element.href.replace(DOWNLOAD_HREF_PREFIX, ""));
}
/**
* ダウンロードリンクのhref属性を組み立てる。
*
* @param {Array<String>} urls 牌譜エディタのURL群を指定する。
* @returns {String} ダウンロードリンクのhref属性を返す。
*/
function buildDownloadHref(urls) {
// ダウンロードリンクのhref属性を組み立てる。
return DOWNLOAD_HREF_PREFIX + encodeURIComponent(urls.join("\n"));
}
/**
* ダウンロードファイル名を組み立てる。
*
* @param {String} baseFileName もともとのダウンロードファイル名を指定する。
* @returns {String} 組み立てたダウンロードファイル名を返す。
*/
function buildFileName(baseFileName) {
// 卓名を雀魂っぽく変換し、拡張子を.txtに変更する。
return toSoulTable(baseFileName).replace(".json", ".txt");
}
/**
* 雀魂の牌譜JSONを牌譜エディタのURL群に変換する。
*
* @param {String} soulJson 雀魂の牌譜JSONを指定する。
* @returns {Array<String>} 牌譜エディタのURL群を返す。
*/
function createViewerUrls(soulJson) {
// 雀魂の牌譜JSONをオブジェクトに変換する。
const soulPaifu = JSON.parse(soulJson);
// title内の卓名を雀魂っぽく変換する。
//
// 変換前のtitle:
// "title": [ "玉の間南喰赤", "2021/10/20 20:48:01" ]
// 変換後のtitle:
// "title": [ "玉の間四人南", "2021/10/20 20:48:01" ]
const title = deepCopy(soulPaifu.title);
title[0] = toSoulTable(title[0]);
const name = deepCopy(soulPaifu.name);
const encodedName = name.map(function(v) {
return encodeURIComponent(v);
});
// rule内の卓名を雀魂っぽく変換する。
//
// 変換前のrule:
// "rule": { "disp": "玉の間南喰赤", "aka53": 1, "aka52": 1, "aka51": 1 }
// 変換後のrule:
// "rule": { "disp": "玉の間四人南", "aka53": 1, "aka52": 1, "aka51": 1 }
const rule = deepCopy(soulPaifu.rule);
rule.disp = toSoulTable(rule.disp);
// logを局ごとのデータに分割し、牌譜エディタのURL群として返す。
return soulPaifu.log.map(function(v) {
return EDITOR_URL_PREFIX + JSON.stringify({
"title": title,
"name": encodedName,
"rule": rule,
"log": [toNagaLog(v)],
});
});
}
/**
* 卓名を雀魂っぽく変換する。
*
* @param {String} tenhouTable 天鳳っぽい卓名を指定する。
* @returns {String} 雀魂っぽい卓名を返す。
*/
function toSoulTable(tenhouTable) {
// 表記の好みの問題なので、必ずしも必要となる処理ではない。
return tenhouTable.replace("南喰赤", "四人南");
}
/**
* logをNAGAが解析可能な形式に変換する。
*
* @param {Array<Array>} soulLog 雀魂形式のlogを指定する。
* @returns {Array<Array>} NAGAで解析可能な形式のlogを返す。
*/
function toNagaLog(soulLog) {
// 流局のデータは変換の必要がない。
if (soulLog[16].length < 3) {
return soulLog;
}
const nagaLog = deepCopy(soulLog);
// 当該局の場風を算出する。
//
// 局を表す数字と意味:
// 0 => 東1局, 1 => 東2局, ...
const prevalent = ["東", "南", "西", "北"][Math.floor(nagaLog[0][0] / 4)];
// 役名をNAGAが解析可能な表記に変換する。
// ダブロン・トリロンに対応するため複数回繰り返す。
for (let i = 1; i < nagaLog[16].length; i += 2) {
// 当該局における和了者の自風を設定する。
//
// 算出方法:
// (和了者のプレイヤー番号 - 親の位置 + 4) % 4
const seat = ["東", "南", "西", "北"][
(nagaLog[16][i].indexOf(Math.max(...nagaLog[16][i])) - (nagaLog[0][0] % 4) + 4) % 4
];
// 役名をNAGAが解析可能な表記に変換する。
nagaLog[16][i + 1] = nagaLog[16][i + 1].slice(0, 4).concat(
nagaLog[16][i + 1].slice(4).map(function(v) {
return toNagaHand(v, prevalent, seat);
}
));
}
// 変換後のlogを返す。
return nagaLog;
}
/**
* 役名をNAGAが解析可能な表記に変換する。
*
* @param {String} hand 和了役を指定する。
* @param {String} prevalent 場風を指定する。
* @param {String} seat 和了者の自風を指定する。
* @returns {String} NAGAで解析可能な表記の役名を返す。
*/
function toNagaHand(hand, prevalent, seat) {
// 対応が必要な役が判明次第、随時追加する。
switch(hand) {
case "役牌:場風牌(1飜)":
return `場風 ${prevalent}(1飜)`;
case "役牌:自風牌(1飜)":
return `自風 ${seat}(1飜)`;
case "ダブル立直(2飜)":
return "両立直(2飜)";
default:
return hand;
}
}
})();
S キーを押してください。設定が正しく行われていれば、牌譜エディタのURLが並んだテキストファイルをダウンロードできるはずです。https://tenhou.net/6/#json=牌譜データ という形式なので、この 牌譜データ の部分に雀魂の牌譜を突っ込むことが最終目標になります。downloadlogs.js の NAMEPREF の値を 0 に設定しておきましょう。ダウンロードする牌譜データ内の役名が日本語になります。S キーを押してください。これまでの手順に誤りがなければ、牌譜データのダウンロードが始まるはずです。
// ダウンロードした牌譜データ
{
"title": [...],
"name": [...],
"rule": [...],
"log": [
[東1局の牌譜],
[東2局の牌譜],
...
],
...
}
NAGAでは局単位の牌譜データを読み込むため、以下のように局数分のJSONに分割してあげる必要があります。
// 東1局
{
"title": [...],
"name": [...],
"rule": [...],
"log": [
[東1局の牌譜]
]
}
// 東2局
{
"title": [...],
"name": [...],
"rule": [...],
"log": [
[東2局の牌譜]
]
}
...
title 、 name 、 rule 、 log 以外の項目は削除しても問題ありません。https://tenhou.net/6/#json="title":[...],"name":[...],"rule":[...],"log":[[...]] のようなURLを組み立てられるようになりました。これでNAGAに牌譜を解析させることができますね。お疲れ様でした!
import json
import sys
def create_viewer_urls(soul_json):
"""
雀魂の牌譜JSONを牌譜エディタURL群に変換する。
Args:
soul_json (str): 雀魂の牌譜JSONを指定する。
Returns:
list[str]: 牌譜エディタURL群を返す。
"""
# 雀魂の牌譜JSONを辞書に変換する。
soul_paifu = json.loads(soul_json)
# title内の卓名を雀魂っぽく変換する。
#
# 変換前のtitle:
# "title": [ "玉の間南喰赤", "2021/10/20 20:48:01" ]
# 変換後のtitle:
# "title": [ "玉の間四人南", "2021/10/20 20:48:01" ]
title = soul_paifu['title'].copy()
title[0] = to_soul_table(title[0])
# rule内の卓名を雀魂っぽく変換する。
#
# 変換前のrule:
# "rule": { "disp": "玉の間南喰赤", "aka53": 1, "aka52": 1, "aka51": 1 }
# 変換後のrule:
# "rule": { "disp": "玉の間四人南", "aka53": 1, "aka52": 1, "aka51": 1 }
rule = soul_paifu['rule'].copy()
rule['disp'] = to_soul_table(rule['disp'])
# logを局ごとのデータに分割し、牌譜エディタURL群として返す。
return ['https://tenhou.net/6/#json=' + json.dumps({
'title': title,
'name': soul_paifu['name'],
'rule': rule,
'log': [to_naga_log(log)],
}, ensure_ascii=False, separators=(',', ':')) for log in soul_paifu['log']]
def to_soul_table(tenhou_table):
"""
卓名を雀魂っぽく変換する。
Args:
tenhou_table (str): 天鳳っぽい卓名を指定する。
Returns:
str: 雀魂っぽい卓名を返す。
"""
# 表記の好みの問題なので、必ずしも必要となる処理ではない。
return tenhou_table.replace('南喰赤', '四人南')
def to_naga_log(soul_log):
"""
局データをNAGAが解析可能な形式に変換する。
Args:
soul_log (list[list]): 雀魂形式のlogを指定する。
Returns:
list[list]: NAGAで解析可能な形式のlogを返す。
"""
# 流局時のデータは変換の必要がない。
if len(soul_log[16]) < 3:
return soul_log
naga_log = soul_log.copy()
# 当該局の場風を設定する。
#
# 局を表す数字と意味:
# 0 => 東1局, 1 => 東2局, ...
prevalent = ['東', '南', '西', '北'][naga_log[0][0] // 4]
# 当該局の和了者の自風を設定する。
#
# 算出方法:
# (和了者のプレイヤー番号 - 親の位置) % 4
seat = ['東', '南', '西', '北'][
(max(enumerate(naga_log[16][1]), key=lambda x: x[1])[0] - (naga_log[0][0] % 4)) % 4
]
# 役名をNAGAが解析可能な表記に変換する。
naga_log[16][2][4:] = [to_naga_hand(hand, prevalent, seat) for hand in naga_log[16][2][4:]]
# 変換後のlogを返す。
return naga_log
def to_naga_hand(hand, prevalent, seat):
"""
役名をNAGAが解析可能な表記に変換する。
Args:
hand (str): 和了役を指定する。
prevalent (str): 場風を指定する。
seat (str): 和了者の自風を指定する。
Returns:
str: NAGAで解析可能な表記の役名を返す。
"""
# 対応が必要な役が判明次第、随時追加する。
if hand == '役牌:場風牌(1飜)':
return f"場風 {prevalent}(1飜)"
elif hand == '役牌:自風牌(1飜)':
return f"自風 {seat}(1飜)"
elif hand == 'ダブル立直(2飜)':
return '両立直(2飜)'
else:
return hand
if __name__ == '__main__':
for url in create_viewer_urls(''.join(sys.stdin.readlines())):
print(url)
※ここに牌譜データを貼り付けるにゃ! の部分に牌譜データを埋め込んでください。
if __name__ == '__main__':
soul_json = """
※ここに牌譜データを貼り付けるにゃ!
"""
for url in create_viewer_urls(soul_json):
print(url)
続いて、 Paiza.io にアクセスします。入力欄(背景色が黒い領域)に変更後のスクリプトを貼り付け、実行ボタンを押してください。スクリプトに誤りがなければ、画面下部のコンソールに牌譜エディタのURLが表示されるはずです。