Add routes

master
Alex Filippov 2018-01-05 19:19:06 +07:00
parent 3c936c75a6
commit f4b3c0a333
2 changed files with 862 additions and 0 deletions

521
routes/mj.js Normal file
View File

@ -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;

341
routes/translate.js Normal file
View File

@ -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面',
'大四喜',
'小四喜',
'四槓子',
'ドラ',
'裏ドラ',
'赤ドラ]'
]
}
};