paifu/routes/mj.js
2018-01-05 19:19:06 +07:00

521 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;