From f4b3c0a333e22a23da99a343c779e9f25b1cf290 Mon Sep 17 00:00:00 2001 From: Alex Filippov Date: Fri, 5 Jan 2018 19:19:06 +0700 Subject: [PATCH] Add routes --- routes/mj.js | 521 ++++++++++++++++++++++++++++++++++++++++++++ routes/translate.js | 341 +++++++++++++++++++++++++++++ 2 files changed, 862 insertions(+) create mode 100644 routes/mj.js create mode 100644 routes/translate.js diff --git a/routes/mj.js b/routes/mj.js new file mode 100644 index 0000000..e7d780a --- /dev/null +++ b/routes/mj.js @@ -0,0 +1,521 @@ +let dict = require('./translate'); + +function Tile(t) { + let m = Math.floor(t / 36), + d = t % 36, + n = Math.floor(d / 4) + 1, + res; + switch (m) { + case 0: + res = 'm' + (d == 16 ? 'a' : n); + break; + case 1: + res = 'p' + (d == 16 ? 'a' : n); + break; + case 2: + res = 's' + (d == 16 ? 'a' : n); + break; + case 3: + let s = Math.floor(d / 4); + switch (s) { + case 0: + res = 'tan'; + break; + case 1: + res = 'nan'; + break; + case 2: + res = 'xia'; + break; + case 3: + res = 'pei'; + break; + case 4: + res = 'haku'; + break; + case 5: + res = 'hatsu'; + break; + case 6: + res = 'chun'; + break; + } + break; + } + return res; +} + +function Hand(h) { + return h.map(function(t) { + return Tile(t); + }); +} + +function replaceTiles(trg) { + let res = trg; + for (var r = 0; r < res.rounds.length; r++) { + // обработка конечных рук + for (var f = 0; f < res.rounds[r].finish.length; f++) { + let finish = res.rounds[r].finish[f].sort(function(a, b) { + return a - b; + }); + res.rounds[r].finish[f] = Hand(res.rounds[r].finish[f]).slice(); + } + } + return res; +} + +function decodeList(list, to_int, sort) { + if (typeof sort === 'undefined') sort = false; + let res = list.split(','); + res = (to_int ? res.map(function(a) { + return parseInt(a); + }) : res); + return (sort ? res.sort(function(a, b) { + return a - b; + }) : res); +} + +function parseScore(data, mult) { + let res = []; + for (var i = 0; i < data.length / 2; i++) { + res.push({ + begin: data[2 * i] * mult, + diff: data[2 * i + 1] * mult, + end: (data[2 * i] + data[2 * i + 1]) * mult + }); + } + return res; +} + +function parseMeld(data) { + if (data & 0x4) { + return parse_chi(data); + } else if (data & 0x18) { + return parse_pon(data); + } else if (data & 0x20) { + return parse_nuki(data); + } else { + return parse_kan(data); + } +} + +function parse_chi(data) { + let t0 = (data >> 3) & 0x3, + t1 = (data >> 5) & 0x3, + t2 = (data >> 7) & 0x3, + base_and_called = data >> 10, + base = Math.floor(base_and_called / 3), + called = base_and_called - 3 * base; + base = Math.floor(base / 7) * 9 + base % 7; + return { + type: 'chi', + fromPlayer: (data & 0x3), + called: called, + tiles: [t0 + 4 * (base + 0), t1 + 4 * (base + 1), t2 + 4 * (base + 2)] + }; +} + +function parse_pon(data) { + let t4 = (data >> 5) & 0x3, + arr = [ + [1, 2, 3], + [0, 2, 3], + [0, 1, 3], + [0, 1, 2] + ], + t0 = arr[t4][0], + t1 = arr[t4][1], + t2 = arr[t4][2], + base_and_called = data >> 9, + base = Math.floor(base_and_called / 3), + called = base_and_called - 3 * base; + if (data & 0x8) { + return { + type: 'pon', + fromPlayer: (data & 0x3), + called: called, + tiles: [t0 + 4 * base, t1 + 4 * base, t2 + 4 * base] + }; + } else { + return { + type: 'chakan', + fromPlayer: (data & 0x3), + called: called, + tiles: [t0 + 4 * base, t1 + 4 * base, t2 + 4 * base, t4 + 4 * base] + }; + } +} + +function parse_kan(data) { + let base_and_called = data >> 8, + base = Math.floor(base_and_called / 4), + called = base_and_called - 4 * base; + return { + type: 'kan', + fromPlayer: (data & 0x3), + called: called, + tiles: [4 * base, 1 + 4 * base, 2 + 4 * base, 3 + 4 * base] + }; +} + +function parse_nuki(data) { + return { + type: 'nuki', + tiles: [data >> 8] + }; +} + +function parseLog(namelog, file, lang) { + return new Promise(function(resolve, reject) { + var fs = require('fs'), + xmlparser = require('htmlparser2'); + var parser = new xmlparser.Parser({ + onopentag: function(name, attribs) { + switch (true) { + case /mjloggm/.test(name): + res = { + log: namelog, + players: [], + rounds: [] + }; + break; + case /un/.test(name) && (typeof attribs.sx !== 'undefined'): + var sex = decodeList(attribs.sx), + dan = decodeList(attribs.dan), + rate = decodeList(attribs.rate); + [0, 1, 2, 3].forEach(function(p) { + res.players[p] = { + name: decodeURIComponent(attribs['n' + p]), + sex: sex[p], + dan: parseInt(dan[p]), + rate: parseFloat(rate[p]), + connected: true + }; + }); + break; + case /bye/.test(name) && (typeof attribs.who !== 'undefined'): + res.players[parseInt(attribs.who)].connected = false; + break; + case /taikyoku/.test(name) && (typeof attribs.oya !== 'undefined'): + res.diler = parseInt(attribs.oya); + break; + case /init/.test(name) && (typeof attribs.oya !== 'undefined'): + let seed = decodeList(attribs.seed); + res.rounds.push({ + name: parseInt(seed[0]), + honba: parseInt(seed[1]), + riichi: { + count: parseInt(seed[2]), + player: [] + }, + d0: parseInt(seed[3]), + d1: parseInt(seed[4]), + dora: Hand([parseInt(seed[5])]), + uradora: [], + diler: parseInt(attribs.oya), + hands: [], + finish: [], + meld: [ + [], + [], + [], + [] + ], + events: [ + [], + [], + [], + [] + ] + }); + cur_game = res.rounds.length - 1; + cur_player = res.rounds[cur_game].diler; + cur_event = -1; + [0, 1, 2, 3].forEach(function(p) { + res.rounds[cur_game].finish[p] = decodeList(attribs['hai' + p], true, true); + res.rounds[cur_game].hands[p] = Hand(decodeList(attribs['hai' + p], true, true)); + }); + break; + case /agari/.test(name) && (typeof attribs.ba !== 'undefined'): + if (!res.rounds[cur_game].win) res.rounds[cur_game].win = []; + let ten = decodeList(attribs.ten), + hand = decodeList(attribs.hai, true, true), + machi = decodeList(attribs.machi, true, false), + wintile = hand.indexOf(machi[0]), + agari = { + type: (attribs.fromwho != attribs.who ? 'ron' : 'tsumo'), + player: parseInt(attribs.who), + hand: Hand(hand), + points: parseInt(ten[1]), + fu: parseInt(ten[0]), + dora: Hand(decodeList(attribs.dorahai, true, false)), + machi: wintile + }; + if (attribs.m) { + agari.melds = []; + decodeList(attribs.m, true, false).forEach(function(m) { + agari.melds.push(parseMeld(m)); + }); + agari.closed = []; + agari.melds.forEach(function(m) { + m.tiles = Hand(m.tiles); + if (!m.fromPlayer) { + agari.closed.push(m); + } + }); + } else { + agari.closed = true; + } + if (parseInt(ten[2]) > 0) { + agari.limit = parseInt(ten[2]); + } + if (attribs.dorahaiura) { + agari.uradora = Hand(decodeList(attribs.dorahaiura, true, false)); + res.rounds[cur_game].uradora = agari.uradora; + } + if (agari.type == 'ron') { + agari.fromPlayer = parseInt(attribs.fromwho); + res.rounds[cur_game].events[agari.fromPlayer][res.rounds[cur_game].events[agari.fromPlayer].length - 1].furikomi = 1; + } + if (attribs.yaku) { + let yakus = decodeList(attribs.yaku); + agari.yakulist = []; + for (var y = 0; y < yakus.length / 2; y++) { + agari.yakulist.push({ + yaku: parseInt(yakus[2 * y]), + han: parseInt(yakus[2 * y + 1]) + }); + } + } else if (attribs.yakuman) { + agari.yakuman = decodeList(attribs.yakuman, true, true); + } + if (attribs.sc) { + res.rounds[cur_game].score = parseScore(decodeList(attribs.sc, true, false), 100); + } + res.rounds[cur_game].win.push(agari); + res.rounds[cur_game].finish[parseInt(attribs.who)] = []; + break; + case /ryuukyoku/.test(name) && (typeof attribs.ba !== 'undefined'): + res.rounds[cur_game].draw = {}; + if (attribs.type) { + res.rounds[cur_game].draw.type = attribs.type; + } + if (attribs.sc) { + res.rounds[cur_game].score = parseScore(decodeList(attribs.sc, true, false), 100); + } + if (attribs.ba) { + let ba = decodeList(attribs.ba, true, false); + res.rounds[cur_game].draw.honba = ba[0]; + res.rounds[cur_game].draw.riichi = ba[1]; + } + if (res.rounds[cur_game].draw || res.rounds[cur_game].draw.type === 'nm') { + res.rounds[cur_game].draw.tenpai = []; + [0, 1, 2, 3].forEach(function(p) { + let hand = []; + if (attribs['hai' + p]) { + hand = Hand(decodeList(attribs['hai' + p], true, true)); + res.rounds[cur_game].finish[p] = []; + } + res.rounds[cur_game].draw.tenpai.push(hand); + }); + } + break; + case /^[t-w]\d+/.test(name): + let draw = /^([t-w])(\d+)$/.exec(name); + cur_player = ['t', 'u', 'v', 'w'].indexOf(draw[1]); + // Проверим тайл замены при кане + if (res.rounds[cur_game].events[cur_player][cur_event] && + res.rounds[cur_game].events[cur_player][cur_event].call && + (res.rounds[cur_game].events[cur_player][cur_event].call.type === 'kan' || + res.rounds[cur_game].events[cur_player][cur_event].call.type === 'chakan') + ) { + res.rounds[cur_game].events[cur_player][cur_event].draw = Tile(parseInt(draw[2])); + } else { + // начало ходов начинаем с дилера + if (cur_player === res.rounds[cur_game].diler) cur_event++; + res.rounds[cur_game].events[cur_player][cur_event] = { + draw: Tile(parseInt(draw[2])) + }; + } + res.rounds[cur_game].finish[cur_player].push(parseInt(draw[2])); + break; + case /^[d-g]\d+/.test(name): + let discard = /^([d-g])(\d+)$/.exec(name); + cur_player = ['d', 'e', 'f', 'g'].indexOf(discard[1]); + // Обработка цумогири + if (!res.rounds[cur_game].events[cur_player][cur_event].call && + !res.rounds[cur_game].events[cur_player][cur_event].reach && + res.rounds[cur_game].events[cur_player][cur_event].draw == Tile(parseInt(discard[2]))) { + delete res.rounds[cur_game].events[cur_player][cur_event].draw; + res.rounds[cur_game].events[cur_player][cur_event].tsumogiri = Tile(parseInt(discard[2])); + } else { + res.rounds[cur_game].events[cur_player][cur_event].discard = Tile(parseInt(discard[2])); + } + // Уберем из финальной руки выкинутый тайл + if (res.rounds[cur_game].finish[cur_player].indexOf(parseInt(discard[2])) >= 0) { + res.rounds[cur_game].finish[cur_player].splice( + res.rounds[cur_game].finish[cur_player].indexOf(parseInt(discard[2])), 1); + } + break; + case /dora/.test(name) && (typeof attribs.hai !== 'undefined'): + res.rounds[cur_game].events[cur_player][cur_event].dora = Tile(parseInt(attribs.hai)); + res.rounds[cur_game].dora.push(Tile(parseInt(attribs.hai))); + break; + case /n/.test(name) && (typeof attribs.m !== 'undefined'): + let meld = parseMeld(attribs.m); + meld.player = parseInt(attribs.who); + // уберем из финальной руки сет + for (var i = 0; i < meld.tiles.length; i++) { + if (res.rounds[cur_game].finish[meld.player].indexOf(parseInt(meld.tiles[i])) >= 0) { + res.rounds[cur_game].finish[meld.player].splice( + res.rounds[cur_game].finish[meld.player].indexOf(parseInt(meld.tiles[i])), 1); + } + } + meld.tiles = Hand(meld.tiles); + if (meld.type != 'chakan') { + // проверим нужно ли увеличить счетчик хода + let tmp_meld = meld.player - res.rounds[cur_game].diler, + tmp_player = cur_player - res.rounds[cur_game].diler; + tmp_meld = (tmp_meld < 0 ? tmp_meld + 4 : tmp_meld); + tmp_player = (tmp_player < 0 ? tmp_player + 4 : tmp_player); + if (tmp_meld < tmp_player) { + cur_event++; + } + meld.rotate = (meld.player - cur_player + 4) % 4; + if (meld.type === 'pon') { + meld.rotate--; + } else if (meld.type === 'kan') { + meld.rotate = [4, 0, 1, 3][meld.rotate]; + } + res.rounds[cur_game].events[meld.player][cur_event] = { + call: meld + }; + } else { + res.rounds[cur_game].meld[meld.player].forEach(function(item, i) { + if (item.type === 'pon' && item.tiles[0] === meld.tiles[0]) { + res.rounds[cur_game].meld[meld.player].splice(i, 1); + meld.rotate = item.rotate; + } + }); + res.rounds[cur_game].events[meld.player][cur_event].call = meld; + } + res.rounds[cur_game].meld[meld.player].push(meld); + cur_player = meld.player; + break; + case /reach/.test(name) && (typeof attribs.step !== 'undefined'): + if (attribs.ten && attribs.step == 2) { + res.rounds[cur_game].events[attribs.who][cur_event].reach = decodeList(attribs.ten, true, false); + res.rounds[cur_game].riichi.player.push(parseInt(attribs.who)); + } else if (attribs.step == 1) { + res.rounds[cur_game].events[attribs.who][cur_event].reach = []; + } + break; + } + }, + onclosetag: function(tagname) { + if (tagname === 'mjloggm') { + res = replaceTiles(res); + resolve(res); + } + } + }, { + xmlMode: true, + lowerCaseTags: true, + lowerCaseAttributeNames: true + }); + + fs.readFile(file, function(err, data) { + if (err) reject(err); + parser.write(data); + }); + parser.end(); + }); +} + +function workLog(req, res) { + var url = require('url'), + qs = require('querystring'), + http = require('http'), + fs = require('fs'), + log, lang, json; + if (req.method === 'POST') { + var urlobj = url.parse(req.body.url), + query = qs.parse(urlobj.query); + log = query.log; + lang = req.body.lang; + json = req.body.json; + } else if (req.method === 'GET') { + log = req.query.log; + lang = req.query.lang; + json = req.query.json; + } + lang = (Object.keys(dict).indexOf(lang) >= 0 ? lang : 'en'); + if (!log) { + res.status(200).send('Нужно передать URL на лог игры!!!'); + } else { + var options = { + host: 'e.mjv.jp', + //host: 'e0.mjv.jp', + port: 80, + //followAllRedirects: true, + path: '/0/log/plainfiles.cgi?' + log + //path: '/0/log/?' + log + }, + logfile = __dirname + '/tenhou/' + log + '.xml'; + if (!fs.existsSync(logfile)) { + http.get(options, function(responce) { + var body = ''; + responce.on('data', function(chunk) { + body += chunk; + }); + responce.on('end', function() { + let fl = fs.openSync(logfile, 'wx'); + if (fl) { + fs.writeSync(fl, body); + fs.closeSync(fl); + } + if (fs.existsSync(logfile)) { + parseLog(log, logfile, lang) + .then( + result => + (json === 1 ? + res.status(200).json(result) : res.render('paifu', { + data: result, + str: dict[lang], + lang: lang + })), + error => res.status(200).send('Error: ' + error) + ); + } + }).on('error', function(e) { + log.error("Got error: " + e.message); + }); + }); + } else { + parseLog(log, logfile, lang) + .then( + result => (json == 1 ? + res.status(200).json(result) : res.render('paifu', { + data: result, + str: dict[lang], + lang: lang + })), + //res.status(200).json(result), + error => res.status(200).send('Error: ' + error) + ); + } + } +} + +exports.index = function(req, res) { + if (!req.query.log) { + res.render('index', { + error: req.flash('error') + }); + } else { + workLog(req, res); + } +}; + +exports.parse = workLog; \ No newline at end of file diff --git a/routes/translate.js b/routes/translate.js new file mode 100644 index 0000000..2e5c0a6 --- /dev/null +++ b/routes/translate.js @@ -0,0 +1,341 @@ +module.exports = { + ru: { + title: 'Пайфу', + link: 'Прямая ссылка на пайфу', + player_info: 'Информация об игроках', + name: 'Имя', + sex: 'Пол', + rate: 'Рейтинг', + dan: 'Дан', + sexF: 'жен.', + sexM: 'муж.', + sexC: 'комп', + sex_: 'неизв.', + log: 'Лог', + dans: ['10 кю', '9 кю', '8 кю', '7 кю', '6 кю', '5 кю', '4 кю', '3 кю', '2 кю', '1 кю', + '1 дан', '2 дан', '3 дан', '4 дан', '5 дан', '6 дан', '7 дан', '8 дан', '9 дан', '10 дан' + ], + rounds: ['東1', '東2', '東3', '東4', '南1', '南2', '南3', '南4', + '西1', '西2', '西3', '西4', '北1', '北2', '北3', '北4' + ], + seat: ['東', '南', '西', '北'], + round: 'Раунд', + honba: 'Хонба', + reach: 'Риичи', + dora: 'Дора', + urador: 'Урадора', + start: 'Старт', + draw: 'Взято', + discard: 'Сброс', + final: 'Финал', + pon: 'Пон', + chi: 'Чи', + kan: 'Кан', + chakan: 'Кан', + nuki: 'Наки', + ron: 'Рон', + furikomi: 'Оплата', + tsumo: 'Цумо', + from: { + M: 'выиграл с', + F: 'выиграла с', + C: 'выиграл с' + }, + han: 'хан', + fu: 'фу', + score: 'Очков', + yakulist: 'Список яку', + ryuukyoku: { + title: "Ничья", + tenpai: "Темпай", + noten: "Нотен", + yao9: "9 терминалов и благородных", + reach4: "Четыре риичи", + ron3: "Тройной рон", + kan4: "Четыре кана", + kaze4: "Четыре ветра в сносе", + nm: "Нагаши манган" + }, + limits: ['', 'Манган', 'Ханеман', 'Байман', 'Санбайман', 'Якуман'], + yaku: ['Мензенцумо', + 'Риичи', + 'Иппацу', + 'Чанкан', + 'Риншан кайхо', + 'Хайтей', + 'Хотей', + 'Пинфу', + 'Тан-яо', + 'Иипейко', + 'Свой восток', + 'Свой юг', + 'Свой запад', + 'Свой север', + 'Восток раунда', + 'Юг раунда', + 'Запад раунда', + 'Север раунда', + 'Белый дракон', + 'Зеленый дракон', + 'Красный дракон', + 'Дабл-риичи', + 'Чиитойцу', + 'Чанта', + 'Иццу', + 'Саншоку', + 'Саншоку доко', + 'Сууканцу', + 'Тойтой', + 'Сананко', + 'Шосанген', + 'Хонрото', + 'Рянпейко', + 'Джунчан', + 'Хоницу', + 'Чиницу', + 'Ренхо', + 'Тенхо', + 'Чихо', + 'Дайсанген', + 'Сууанко', + 'Сууанко танки', + 'Цууисо', + 'Рюисо', + 'Чинрото', + 'Чууренпото', + 'Чууренпото 9-стор', + 'Кокушимусо', + 'Кокушимусо 13-стор', + 'Дайсууши', + 'Шосууши', + 'Санканцу', + 'Дора', + 'Урадора', + 'Акадора' + ] + }, + en: { + title: 'Paifu', + link: 'Direct paifu link', + player_info: 'Player information', + name: 'Name', + sex: 'Sex', + rate: 'Rating', + dan: 'Dan', + sexF: 'female', + sexM: 'male', + sexC: 'comp', + sex_: 'NA', + log: 'Log', + dans: ['10 kyuu', '9 kyuu', '8 kyuu', '7 kyuu', '6 kyuu', '5 kyuu', '4 kyuu', '3 kyuu', '2 kyuu', '1 kyuu', + '1 dan', '2 dan', '3 dan', '4 dan', '5 dan', '6 dan', '7 dan', '8 dan', '9 dan', '10 dan' + ], + rounds: ['東1', '東2', '東3', '東4', '南1', '南2', '南3', '南4', + '西1', '西2', '西3', '西4', '北1', '北2', '北3', '北4' + ], + seat: ['東', '南', '西', '北'], + round: 'Round', + honba: 'Combo', + reach: 'Reach', + dora: 'Dora', + urador: 'Uradora', + start: 'Start', + draw: 'Draw', + discard: 'Drop', + final: 'Final', + pon: 'Pon', + chi: 'Chii', + kan: 'Kan', + chakan: 'Kan', + nuki: 'Nuki', + ron: 'Ron', + furikomi: 'Furikomi', + tsumo: 'Tsumo', + from: { + M: 'win from', + F: 'win from', + C: 'win from' + }, + han: 'han', + fu: 'fu', + score: 'Score', + yakulist: 'Yaku list', + ryuukyoku: { + yao9: "9 ends", + reach4: "Four riichi", + ron3: "Triple ron", + kan4: "Four kans", + kaze4: "Wind discard", + nm: "Nagashi mangan" + }, + limits: ['', 'Mangan', 'haneman', 'baiman', 'sanbaiman', 'yakuman'], + yaku: ['Mentsumo', + 'Riichi', + 'Ippatsu', + 'Chankan', + 'Rinshan kaihou', + 'Haitei raoyue', + 'Houtei raoyui', + 'Pinfu', + 'Tanyao', + 'Iipeiko', + 'Own east', + 'Own south', + 'Own west', + 'Own north', + 'Round east', + 'Round south', + 'Round west', + 'Round north', + 'White dragon', + 'Green dragon', + 'Red dragon', + 'Daburu riichi', + 'Chiitoitsu', + 'Chanta', + 'Ittsu', + 'Sanshoku doujun', + 'Sanshoku doukou', + 'Sankantsu', + 'Toitoi', + 'Sanankou', + 'Shousangen', + 'Honroutou', + 'Ryanpeikou', + 'Junchan', + 'Honitsu', + 'Chinitsu', + 'Renhou', + 'Tenhou', + 'Chihou', + 'Daisangen', + 'Suuankou', + 'Suuankou tanki', + 'Tsuuiisou', + 'Ryuuiisou', + 'Chinroutou', + 'Chuuren pouto', + 'Chuuren pouto 9-wait', + 'Kokushi musou', + 'Kokushi musou 13-wait', + 'Daisuushi', + 'Shousuushi', + 'Suukantsu', + 'Dora', + 'Uradora', + 'Akadora' + ] + }, + jp: { + title: 'Paifu', + link: 'Direct paifu link', + player_info: 'Player information', + name: 'Name', + sex: 'Sex', + rate: 'Rating', + dan: 'Dan', + sexF: 'famale', + sexM: 'male', + sexC: 'comp', + sex_: 'NA', + log: 'Log', + dans: ['新人', '9級', '8級', '7級', '6級', '5級', '4級', '3級', '2級', '1級', + '初段', '二段', '三段', '四段', '五段', '六段', '七段', '八段', '九段', '十段', '天鳳位' + ], + rounds: ['東1', '東2', '東3', '東4', '南1', '南2', '南3', '南4', + '西1', '西2', '西3', '西4', '北1', '北2', '北3', '北4' + ], + seat: ['東', '南', '西', '北'], + round: 'Round', + honba: 'Combo', + reach: 'Reach', + dora: 'Dora', + urador: 'Uradora', + start: 'Start', + draw: 'Draw', + discard: 'Drop', + final: 'Final', + pon: 'Pon', + chi: 'Chii', + kan: 'Kan', + chakan: 'Kan', + nuki: 'Nuki', + ron: 'Ron', + furikomi: 'フリコミ', + tsumo: 'ツモ', + from: { + M: 'win from', + F: 'win from', + C: 'win from' + }, + han: 'han', + fu: 'fu', + score: 'Score', + yakulist: 'Yaku list', + ryuukyoku: { + yao9: "9 ends", + reach4: "Four riichi", + ron3: "Triple ron", + kan4: "Four kans", + kaze4: "Wind discard", + nm: "Nagashi mangan" + }, + limits: ['', 'Mangan', 'haneman', 'baiman', 'sanbaiman', 'yakuman'], + yaku: ['門前清自摸和', + '立直', + '一発', + '槍槓', + '嶺上開花', + '海底摸月', + '河底撈魚', + '平和', + '断幺九', + '一盃口', + '自風 東', + '自風 南', + '自風 西', + '自風 北', + '場風 東', + '場風 南', + '場風 西', + '場風 北', + '役牌 白', + '役牌 發', + '役牌 中', + '両立直', + '七対子', + '混全帯幺九', + '一気通貫', + '三色同順', + '三色同刻', + '三槓子', + '対々和', + '三暗刻', + '小三元', + '混老頭', + '二盃口', + '純全帯幺九', + '混一色', + '清一色', + '人和', + '天和', + '地和', + '大三元', + '四暗刻', + '四暗刻単騎', + '字一色', + '緑一色', + '清老頭', + '九蓮宝燈', + '純正九蓮宝燈', + '国士無双', + '国士無双13面', + '大四喜', + '小四喜', + '四槓子', + 'ドラ', + '裏ドラ', + '赤ドラ]' + ] + } +}; \ No newline at end of file