// 麻雀役判定システム(正規表現版)
class MahjongHandAnalyzer {
constructor() {
// 牌の種類定義
this.tiles = {
man: '123456789', // 萬子
so: 'ABCDEFGHI', // 索子
pin: 'abcdefghi', // 筒子
honor: '><v^.#@' // 字牌(東西南北白発中)
};
// 全ての牌
this.allTiles = this.tiles.man + this.tiles.so + this.tiles.pin + this.tiles.honor;
}
// 手牌をソートする
sortHand(hand) {
const order = this.allTiles;
return hand.split('').sort((a, b) => order.indexOf(a) - order.indexOf(b)).join('');
}
// 同じ牌の個数をカウント
countTiles(hand) {
const count = {};
for (const tile of hand) {
count[tile] = (count[tile] || 0) + 1;
}
return count;
}
// 順子(連続する3枚)を検出
findShuntsu(tiles, type) {
const patterns = [];
const chars = this.tiles[type];
for (let i = 0; i < chars.length - 2; i++) {
const pattern = chars[i] + chars[i+1] + chars[i+2];
const regex = new RegExp(`[${pattern}]{3}`, 'g');
if (regex.test(tiles)) {
patterns.push(pattern);
}
}
return patterns;
}
// 刻子(同じ牌3枚)を検出
findKotsu(hand) {
const patterns = [];
const regex = /(.)\1{2}/g;
let match;
while ((match = regex.exec(hand)) !== null) {
patterns.push(match[0]);
regex.lastIndex = match.index + 1; // 重複検出のため
}
return patterns;
}
// 対子(同じ牌2枚)を検出
findToitsu(hand) {
const patterns = [];
const regex = /(.)\1/g;
let match;
while ((match = regex.exec(hand)) !== null) {
patterns.push(match[0]);
regex.lastIndex = match.index + 1;
}
return patterns;
}
// タンヤオ(中張牌のみ)判定
isTanyao(hand) {
const regex = /^[2-8A-Ha-h]+$/;
return regex.test(hand);
}
// 字牌の役判定
checkHonorYaku(hand) {
const yakuList = [];
// 白(三元牌)
if (/\.{3}/.test(hand)) yakuList.push('白');
// 発(三元牌)
if (/##{3}/.test(hand)) yakuList.push('発');
// 中(三元牌)
if (/@{3}/.test(hand)) yakuList.push('中');
// 風牌
if (/>>{3}/.test(hand)) yakuList.push('東');
if (/<<{3}/.test(hand)) yakuList.push('西');
if (/vv{3}/.test(hand)) yakuList.push('南');
if (/\^\^{3}/.test(hand)) yakuList.push('北');
return yakuList;
}
// 清一色判定
isChinitsu(hand) {
if (/^[1-9]+$/.test(hand)) return '清一色(萬子)';
if (/^[A-I]+$/.test(hand)) return '清一色(索子)';
if (/^[a-i]+$/.test(hand)) return '清一色(筒子)';
return null;
}
// 混一色判定
isHonitsu(hand) {
const hasHonor = /[><v^.#@]/.test(hand);
if (!hasHonor) return null;
const noHonor = hand.replace(/[><v^.#@]/g, '');
if (/^[1-9]+$/.test(noHonor)) return '混一色(萬子)';
if (/^[A-I]+$/.test(noHonor)) return '混一色(索子)';
if (/^[a-i]+$/.test(noHonor)) return '混一色(筒子)';
return null;
}
// 平和判定(簡易版)
isPinfu(hand) {
// 字牌がない
if (/[><v^.#@]/.test(hand)) return false;
// 端牌の対子でない(簡易チェック)
const toitsu = this.findToitsu(hand);
if (toitsu.some(t => /[19AIai><v^.#@]/.test(t[0]))) return false;
return true;
}
// 待ち牌を計算
calculateWaits(hand) {
const waits = new Set();
const sortedHand = this.sortHand(hand);
// 各牌を1枚追加してテンパイになるかチェック
for (const tile of this.allTiles) {
const testHand = this.sortHand(sortedHand + tile);
if (this.isWinningHand(testHand)) {
waits.add(tile);
}
}
return Array.from(waits);
}
// 和了形かどうか判定(簡易版)
isWinningHand(hand) {
if (hand.length !== 14) return false;
// 七対子チェック
if (this.isChitoitsu(hand)) return true;
// 通常の和了形(1雀頭 + 4面子)
const count = this.countTiles(hand);
const tiles = Object.entries(count);
// 雀頭候補を探す
for (const [tile, num] of tiles) {
if (num >= 2) {
// 雀頭を除いた残りで4面子作れるか簡易チェック
const remaining = hand.replace(new RegExp(tile, 'g'), (match, offset, string) => {
const index = string.indexOf(tile);
const secondIndex = string.indexOf(tile, index + 1);
return (offset === index || offset === secondIndex) ? '' : match;
});
if (this.canFormMentsu(remaining)) return true;
}
}
return false;
}
// 面子を作れるか(簡易判定)
canFormMentsu(tiles) {
// 長さが12(4面子分)でない場合は不可
if (tiles.length !== 12) return false;
// 全て刻子の場合
const kotsuPattern = /^((.)\2{2}){4}$/;
if (kotsuPattern.test(tiles)) return true;
// その他の組み合わせ(簡易チェック)
return tiles.length === 12;
}
// 七対子判定
isChitoitsu(hand) {
const regex = /^((.)\2){7}$/;
const sorted = this.sortHand(hand);
// 同じ牌が7組あるかチェック
const count = this.countTiles(hand);
const pairs = Object.values(count).filter(c => c === 2);
return pairs.length === 7;
}
// メイン判定関数
analyzeHand(hand) {
const sortedHand = this.sortHand(hand);
const yakuList = [];
// 役の判定
if (this.isTanyao(sortedHand)) {
yakuList.push('タンヤオ');
}
const chinItsu = this.isChinitsu(sortedHand);
if (chinItsu) {
yakuList.push(chinItsu);
}
const honItsu = this.isHonitsu(sortedHand);
if (honItsu) {
yakuList.push(honItsu);
}
const honorYaku = this.checkHonorYaku(sortedHand);
yakuList.push(...honorYaku);
if (this.isChitoitsu(sortedHand)) {
yakuList.push('七対子');
}
if (this.isPinfu(sortedHand) && yakuList.length === 0) {
yakuList.push('平和(可能性)');
}
// 待ち牌の計算
const waits = this.calculateWaits(sortedHand);
return {
hand: sortedHand,
yaku: yakuList.length > 0 ? yakuList : ['役なし'],
waits: waits,
waitDisplay: waits.map(w => {
if (this.tiles.man.includes(w)) return `${this.tiles.man.indexOf(w) + 1}萬`;
if (this.tiles.so.includes(w)) return `${this.tiles.so.indexOf(w) + 1}索`;
if (this.tiles.pin.includes(w)) return `${this.tiles.pin.indexOf(w) + 1}筒`;
if (w === '>') return '東';
if (w === '<') return '西';
if (w === 'v') return '南';
if (w === '^') return '北';
if (w === '.') return '白';
if (w === '#') return '発';
if (w === '@') return '中';
return w;
})
};
}
}
// 使用例
const analyzer = new MahjongHandAnalyzer();
// テストケース
const testHands = [
'123445567889.',
'111222333444a',
'AABBCCDDEEFFG',
'11223344556677',
'>>><<<vvv^^^..',
'123456789ABC@',
];
console.log('=== 麻雀役判定結果 ===\n');
testHands.forEach(hand => {
const result = analyzer.analyzeHand(hand);
console.log(`手牌: ${hand}`);
console.log(`整理後: ${result.hand}`);
console.log(`役: ${result.yaku.join(', ')}`);
console.log(`待ち: ${result.waitDisplay.join(', ')}`);
console.log('-------------------\n');
});