Comparing version 1.0.1 to 1.0.2
@@ -311,4 +311,7 @@ import { assert } from 'chai'; | ||
); | ||
assert.equal(digestAll(findAllAgariPatterns(testCases[1])), digestAll([])); | ||
assert.equal( | ||
digestAll(findAllAgariPatterns(testCases[1])), | ||
digestAll([[[0, 8, 9, 17, 18, 26, 27, 28, 28, 29, 30, 31, 32, 33]]]) | ||
); | ||
assert.equal( | ||
digestAll(findAllAgariPatterns(testCases[2])), | ||
@@ -315,0 +318,0 @@ digestAll([ |
22
agari.ts
@@ -198,3 +198,6 @@ // Inspired by and partially taken from https://github.com/takayama-lily/agari | ||
if (!checkAll(haipai)) { | ||
const canBeKokushi = check13(haipai); | ||
const canBeChiitoitsu = check7(haipai); | ||
const canBeBasicForm = check(haipai); | ||
if (!canBeKokushi && !canBeChiitoitsu && !canBeBasicForm) { | ||
return res; | ||
@@ -212,2 +215,17 @@ } | ||
// Check kokushi separately | ||
if (canBeKokushi) { | ||
res.push([ | ||
haipai.reduce((acc, v, idx) => { | ||
if (v > 0) { | ||
acc.push(idx); | ||
if (v > 1) { | ||
acc.push(idx); | ||
} | ||
} | ||
return acc; | ||
}, [] as number[]), | ||
]); | ||
} | ||
// Some questionable code below :) | ||
@@ -245,3 +263,3 @@ | ||
if (check7(haipai)) { | ||
if (canBeChiitoitsu) { | ||
res.push( | ||
@@ -248,0 +266,0 @@ haipai.reduce((acc, v, idx) => { |
@@ -220,3 +220,6 @@ // Inspired by and partially taken from https://github.com/takayama-lily/agari | ||
haipai = haipai.slice(); // clone | ||
if (!checkAll(haipai)) { | ||
var canBeKokushi = check13(haipai); | ||
var canBeChiitoitsu = check7(haipai); | ||
var canBeBasicForm = check(haipai); | ||
if (!canBeKokushi && !canBeChiitoitsu && !canBeBasicForm) { | ||
return res; | ||
@@ -232,2 +235,16 @@ } | ||
} | ||
// Check kokushi separately | ||
if (canBeKokushi) { | ||
res.push([ | ||
haipai.reduce(function (acc, v, idx) { | ||
if (v > 0) { | ||
acc.push(idx); | ||
if (v > 1) { | ||
acc.push(idx); | ||
} | ||
} | ||
return acc; | ||
}, []), | ||
]); | ||
} | ||
// Some questionable code below :) | ||
@@ -260,3 +277,3 @@ var fakePairIndex = -1; | ||
haipai[fakePairIndex] -= 2; | ||
if (check7(haipai)) { | ||
if (canBeChiitoitsu) { | ||
res.push(haipai.reduce(function (acc, v, idx) { | ||
@@ -263,0 +280,0 @@ if (v === 2) { |
@@ -52,4 +52,5 @@ var __assign = (this && this.__assign) || function () { | ||
import { ceil10, ceil100, is19, isProperOpenSet } from './interfaces'; | ||
var sortByInt = function (a1, a2) { return a1 - a2; }; | ||
var Riichi = /** @class */ (function () { | ||
function Riichi(closedPart, openPart, options, tileDiscardedBySomeone, firstTake, riichi, ippatsu, doubleRiichi, lastTile, afterKan) { | ||
function Riichi(closedPart, openPart, options, tileDiscardedBySomeone, firstTake, riichi, ippatsu, doubleRiichi, lastTile, afterKan, akaCount, allowAka, allowKuitan, withKiriage) { | ||
var e_1, _a, e_2, _b; | ||
@@ -101,2 +102,3 @@ var _c; | ||
this.allowKuitan = true; | ||
this.withKiriage = false; | ||
this.allowAka = true; | ||
@@ -110,7 +112,9 @@ this.hairi = true; | ||
this.extra.afterKan = afterKan !== null && afterKan !== void 0 ? afterKan : false; | ||
this.hai = closedPart.sort(); | ||
// TODO: introduce aka | ||
// this.aka += closedParsed.aka; | ||
this.allowAka = allowAka !== null && allowAka !== void 0 ? allowAka : false; | ||
this.allowKuitan = allowKuitan !== null && allowKuitan !== void 0 ? allowKuitan : false; | ||
this.withKiriage = withKiriage !== null && withKiriage !== void 0 ? withKiriage : false; | ||
this.hai = closedPart; | ||
this.aka += akaCount !== null && akaCount !== void 0 ? akaCount : 0; | ||
this.dora = (_c = options.dora) !== null && _c !== void 0 ? _c : []; | ||
if (tileDiscardedBySomeone !== undefined) { | ||
if (tileDiscardedBySomeone !== undefined && tileDiscardedBySomeone !== null) { | ||
this.takenTile = tileDiscardedBySomeone; | ||
@@ -125,3 +129,3 @@ this.hai.push(this.takenTile); | ||
if (isProperOpenSet(vv.tiles)) { | ||
this_1.furo.push(__spreadArray([], __read(vv.tiles.map(function (v) { return (vv.open ? v : -v); })), false).sort()); | ||
this_1.furo.push(__spreadArray([], __read(vv.tiles.map(function (v) { return (vv.open ? v : -v); })), false).sort(sortByInt)); | ||
} | ||
@@ -179,2 +183,7 @@ else { | ||
if (v.length > 2) { | ||
// closed kan | ||
if (v[0] === v[1] && 1 / v[0] < 0) { | ||
// hack: check infinity sign to support 1m closed kan | ||
continue; | ||
} | ||
return false; | ||
@@ -194,3 +203,3 @@ } | ||
Riichi.prototype.calcDora = function () { | ||
var e_4, _a, e_5, _b, e_6, _c; | ||
var e_4, _a, e_5, _b, e_6, _c, e_7, _d, e_8, _e; | ||
if (!this.tmpResult.han) { | ||
@@ -201,7 +210,20 @@ return; | ||
try { | ||
for (var _d = __values(this.hai), _e = _d.next(); !_e.done; _e = _d.next()) { | ||
var v = _e.value; | ||
if (this.dora.includes(v)) { | ||
dora++; | ||
for (var _f = __values(this.hai), _g = _f.next(); !_g.done; _g = _f.next()) { | ||
var v = _g.value; | ||
try { | ||
for (var _h = (e_5 = void 0, __values(this.dora)), _j = _h.next(); !_j.done; _j = _h.next()) { | ||
var d = _j.value; | ||
// loop over to detect multiple dora | ||
if (v === d) { | ||
dora++; | ||
} | ||
} | ||
} | ||
catch (e_5_1) { e_5 = { error: e_5_1 }; } | ||
finally { | ||
try { | ||
if (_j && !_j.done && (_b = _h.return)) _b.call(_h); | ||
} | ||
finally { if (e_5) throw e_5.error; } | ||
} | ||
} | ||
@@ -212,3 +234,3 @@ } | ||
try { | ||
if (_e && !_e.done && (_a = _d.return)) _a.call(_d); | ||
if (_g && !_g.done && (_a = _f.return)) _a.call(_f); | ||
} | ||
@@ -218,27 +240,40 @@ finally { if (e_4) throw e_4.error; } | ||
try { | ||
for (var _f = __values(this.furo), _g = _f.next(); !_g.done; _g = _f.next()) { | ||
var v = _g.value; | ||
for (var _k = __values(this.furo), _l = _k.next(); !_l.done; _l = _k.next()) { | ||
var v = _l.value; | ||
try { | ||
for (var v_1 = (e_6 = void 0, __values(v)), v_1_1 = v_1.next(); !v_1_1.done; v_1_1 = v_1.next()) { | ||
for (var v_1 = (e_7 = void 0, __values(v)), v_1_1 = v_1.next(); !v_1_1.done; v_1_1 = v_1.next()) { | ||
var vv = v_1_1.value; | ||
if (this.dora.includes(vv)) { | ||
dora++; | ||
try { | ||
for (var _m = (e_8 = void 0, __values(this.dora)), _o = _m.next(); !_o.done; _o = _m.next()) { | ||
var d = _o.value; | ||
// loop over to detect multiple dora | ||
if (Math.abs(vv) === d) { | ||
dora++; | ||
} | ||
} | ||
} | ||
catch (e_8_1) { e_8 = { error: e_8_1 }; } | ||
finally { | ||
try { | ||
if (_o && !_o.done && (_e = _m.return)) _e.call(_m); | ||
} | ||
finally { if (e_8) throw e_8.error; } | ||
} | ||
} | ||
} | ||
catch (e_6_1) { e_6 = { error: e_6_1 }; } | ||
catch (e_7_1) { e_7 = { error: e_7_1 }; } | ||
finally { | ||
try { | ||
if (v_1_1 && !v_1_1.done && (_c = v_1.return)) _c.call(v_1); | ||
if (v_1_1 && !v_1_1.done && (_d = v_1.return)) _d.call(v_1); | ||
} | ||
finally { if (e_6) throw e_6.error; } | ||
finally { if (e_7) throw e_7.error; } | ||
} | ||
} | ||
} | ||
catch (e_5_1) { e_5 = { error: e_5_1 }; } | ||
catch (e_6_1) { e_6 = { error: e_6_1 }; } | ||
finally { | ||
try { | ||
if (_g && !_g.done && (_b = _f.return)) _b.call(_f); | ||
if (_l && !_l.done && (_c = _k.return)) _c.call(_k); | ||
} | ||
finally { if (e_5) throw e_5.error; } | ||
finally { if (e_6) throw e_6.error; } | ||
} | ||
@@ -280,7 +315,26 @@ if (dora) { | ||
} | ||
else { | ||
if (this.withKiriage && | ||
((this.tmpResult.han === 4 && this.tmpResult.fu === 30) || | ||
(this.tmpResult.han === 3 && this.tmpResult.fu === 60))) { | ||
base = 2000; | ||
} | ||
} | ||
} | ||
this.tmpResult.ten = ceil100(base * 6); | ||
this.tmpResult.text += (this.tmpResult.name ? ' ' : '') + this.tmpResult.name; | ||
var oya = 0; | ||
var ko = 0; | ||
if (this.isTsumo) { | ||
oya = ceil100(base * 2) + ceil100(base * 2) + ceil100(base * 2); | ||
ko = ceil100(base * 2) + ceil100(base) + ceil100(base); | ||
} | ||
else { | ||
oya = ceil100(base * 6); | ||
ko = ceil100(base * 4); | ||
} | ||
this.tmpResult.ten = this.jikaze === 27 ? oya : ko; | ||
}; | ||
Riichi.prototype.calcFu = function () { | ||
var e_7, _a; | ||
Riichi.prototype.calcFu = function (haiExceptFuro, havePinfu) { | ||
var e_9, _a, e_10, _b, e_11, _c; | ||
if (havePinfu === void 0) { havePinfu = false; } | ||
var fu; | ||
@@ -290,2 +344,5 @@ if (this.tmpResult.yaku.chiitoitsu) { | ||
} | ||
else if (this.tmpResult.yaku.kokushimusou || this.tmpResult.yaku['kokushimusou 13 sides']) { | ||
fu = 0; | ||
} | ||
else if (this.tmpResult.yaku.pinfu) { | ||
@@ -296,14 +353,76 @@ fu = this.isTsumo ? 20 : 30; | ||
fu = 20; | ||
if (!this.isTsumo && this.isMenzen()) | ||
if (!this.isTsumo && this.isMenzen()) { | ||
fu += 10; | ||
} | ||
if (!this.currentPattern) { | ||
return; | ||
} | ||
// check waiting | ||
var canBeRyanmen = false; | ||
var canBeKanchan = false; | ||
var canBePenchan = false; | ||
var canBeShanpon = false; | ||
var canBeTanki = false; | ||
if (this.takenTile !== undefined && this.takenTile !== null) { | ||
try { | ||
for (var _d = __values(haiExceptFuro !== null && haiExceptFuro !== void 0 ? haiExceptFuro : []), _e = _d.next(); !_e.done; _e = _d.next()) { | ||
var v = _e.value; | ||
if (v.length !== 3) { | ||
if (v.length === 2 && this.takenTile === v[0]) { | ||
// tanki waits are already handled above | ||
canBeTanki = true; | ||
} | ||
continue; | ||
} | ||
if (v[0] === v[1]) { | ||
if (v[0] === this.takenTile) { | ||
canBeShanpon = true; | ||
} | ||
} | ||
else { | ||
if ((v[0] === this.takenTile && !is19(v[2])) || | ||
(v[2] === this.takenTile && !is19(v[0]))) { | ||
canBeRyanmen = true; | ||
} | ||
if ((v[0] === this.takenTile && is19(v[2])) || | ||
(v[2] === this.takenTile && is19(v[0]))) { | ||
canBePenchan = true; | ||
} | ||
if (v[1] === this.takenTile) { | ||
canBeKanchan = true; | ||
} | ||
} | ||
} | ||
} | ||
catch (e_9_1) { e_9 = { error: e_9_1 }; } | ||
finally { | ||
try { | ||
if (_e && !_e.done && (_a = _d.return)) _a.call(_d); | ||
} | ||
finally { if (e_9) throw e_9.error; } | ||
} | ||
} | ||
try { | ||
for (var _b = __values(this.currentPattern), _c = _b.next(); !_c.done; _c = _b.next()) { | ||
var v = _c.value; | ||
for (var _f = __values(this.furo), _g = _f.next(); !_g.done; _g = _f.next()) { | ||
var v = _g.value; | ||
if (v.length === 4) { | ||
fu += is19(Math.abs(v[0])) ? (v[0] > 0 ? 16 : 32) : v[0] > 0 ? 8 : 16; | ||
// hack: count infinity sign to support closed kan of 1m | ||
fu += is19(Math.abs(v[0])) ? (1 / v[0] > 0 ? 16 : 32) : 1 / v[0] > 0 ? 8 : 16; | ||
} | ||
else if (v.length === 2) { | ||
else if (v.length === 3 && v[0] === v[1]) { | ||
fu += is19(v[0]) ? 4 : 2; | ||
} | ||
} | ||
} | ||
catch (e_10_1) { e_10 = { error: e_10_1 }; } | ||
finally { | ||
try { | ||
if (_g && !_g.done && (_b = _f.return)) _b.call(_f); | ||
} | ||
finally { if (e_10) throw e_10.error; } | ||
} | ||
try { | ||
for (var haiExceptFuro_1 = __values(haiExceptFuro), haiExceptFuro_1_1 = haiExceptFuro_1.next(); !haiExceptFuro_1_1.done; haiExceptFuro_1_1 = haiExceptFuro_1.next()) { | ||
var v = haiExceptFuro_1_1.value; | ||
if (v.length === 2) { | ||
if ([this.bakaze, this.jikaze, 31, 32, 33].includes(v[0])) { | ||
@@ -322,13 +441,31 @@ // pair of yakuhai tile | ||
else if (v.length === 3 && v[0] === v[1]) { | ||
fu += is19(v[0]) ? 4 : 2; | ||
if (!this.isTsumo && this.takenTile === v[0]) { | ||
if (canBeRyanmen || canBeKanchan || canBePenchan) { | ||
fu += is19(v[0]) ? 8 : 4; | ||
} | ||
else { | ||
fu += is19(v[0]) ? 4 : 2; | ||
} | ||
} | ||
else { | ||
fu += is19(v[0]) ? 8 : 4; | ||
} | ||
} | ||
} | ||
} | ||
catch (e_7_1) { e_7 = { error: e_7_1 }; } | ||
catch (e_11_1) { e_11 = { error: e_11_1 }; } | ||
finally { | ||
try { | ||
if (_c && !_c.done && (_a = _b.return)) _a.call(_b); | ||
if (haiExceptFuro_1_1 && !haiExceptFuro_1_1.done && (_c = haiExceptFuro_1.return)) _c.call(haiExceptFuro_1); | ||
} | ||
finally { if (e_7) throw e_7.error; } | ||
finally { if (e_11) throw e_11.error; } | ||
} | ||
if (canBePenchan || canBeKanchan) { | ||
if (!havePinfu && !canBeTanki) { | ||
fu += 2; | ||
} | ||
else if (!canBeShanpon && !canBeRyanmen && !canBeTanki) { | ||
fu += 2; | ||
} | ||
} | ||
if (this.isTsumo) { | ||
@@ -410,4 +547,4 @@ fu += 2; | ||
Riichi.prototype.calc = function () { | ||
var e_8, _a; | ||
var _b, _c, _d, _e; | ||
var e_12, _a; | ||
var _b, _c, _d, _e, _f, _g, _h; | ||
if (this.tmpResult.error) { | ||
@@ -430,4 +567,4 @@ return this.tmpResult; | ||
try { | ||
for (var _f = __values(this.agariPatterns), _g = _f.next(); !_g.done; _g = _f.next()) { | ||
var v = _g.value; | ||
for (var _j = __values(this.agariPatterns), _k = _j.next(); !_k.done; _k = _j.next()) { | ||
var v = _k.value; | ||
this.currentPattern = __spreadArray(__spreadArray([], __read(v), false), __read(this.furo), false); | ||
@@ -438,23 +575,24 @@ this.calcYaku(); | ||
} | ||
if (this.tmpResult.han) { | ||
if (this.tmpResult.han || this.tmpResult.yakuman > 0) { | ||
this.calcDora(); | ||
this.calcFu(); | ||
this.calcFu(v, !!this.tmpResult.yaku.pinfu); | ||
} | ||
this.calcTen(); | ||
// Find variant with maximum points | ||
if (this.tmpResult.ten > ((_c = (_b = this.finalResult) === null || _b === void 0 ? void 0 : _b.ten) !== null && _c !== void 0 ? _c : 0)) { | ||
if (this.tmpResult.ten > ((_c = (_b = this.finalResult) === null || _b === void 0 ? void 0 : _b.ten) !== null && _c !== void 0 ? _c : 0) || | ||
(this.tmpResult.ten === ((_d = this.finalResult) === null || _d === void 0 ? void 0 : _d.ten) && | ||
this.tmpResult.han > ((_e = this.finalResult) === null || _e === void 0 ? void 0 : _e.han)) || | ||
(this.tmpResult.ten === ((_f = this.finalResult) === null || _f === void 0 ? void 0 : _f.ten) && | ||
this.tmpResult.han === ((_g = this.finalResult) === null || _g === void 0 ? void 0 : _g.han) && | ||
this.tmpResult.fu > ((_h = this.finalResult) === null || _h === void 0 ? void 0 : _h.fu))) { | ||
this.finalResult = JSON.parse(JSON.stringify(this.tmpResult)); | ||
} | ||
else if (this.tmpResult.ten === ((_d = this.finalResult) === null || _d === void 0 ? void 0 : _d.ten) && | ||
this.tmpResult.han > ((_e = this.finalResult) === null || _e === void 0 ? void 0 : _e.han)) { | ||
this.finalResult = JSON.parse(JSON.stringify(this.tmpResult)); | ||
} | ||
} | ||
} | ||
catch (e_8_1) { e_8 = { error: e_8_1 }; } | ||
catch (e_12_1) { e_12 = { error: e_12_1 }; } | ||
finally { | ||
try { | ||
if (_g && !_g.done && (_a = _f.return)) _a.call(_f); | ||
if (_k && !_k.done && (_a = _j.return)) _a.call(_j); | ||
} | ||
finally { if (e_8) throw e_8.error; } | ||
finally { if (e_12) throw e_12.error; } | ||
} | ||
@@ -461,0 +599,0 @@ if (this.finalResult && !this.finalResult.ten) { |
108
dist/yaku.js
@@ -73,3 +73,3 @@ var __values = (this && this.__values) || function(o) { | ||
var tile = occurences_1_1.value; | ||
hand[tile]++; | ||
hand[Math.abs(tile)]++; | ||
} | ||
@@ -115,3 +115,3 @@ } | ||
if (v.length <= 2 || v[0] === v[1]) { | ||
if (!allow.includes(v[0])) { | ||
if (!allow.includes(Math.abs(v[0]))) { | ||
return false; | ||
@@ -204,3 +204,3 @@ } | ||
} | ||
return o.takenTile !== null && [2, 4].includes(slices[foundSuit][o.takenTile]); | ||
return o.takenTile !== null && [2, 4].includes(slices[foundSuit][o.takenTile % 9]); | ||
}, | ||
@@ -223,3 +223,3 @@ }, | ||
} | ||
return o.takenTile !== null && [1, 3].includes(slices[foundSuit][o.takenTile]); | ||
return o.takenTile !== null && [1, 3].includes(slices[foundSuit][o.takenTile % 9]); | ||
}, | ||
@@ -237,3 +237,3 @@ }, | ||
if (o.furo.length > 0) { | ||
if (o.furo.some(function (v) { return v[0] > 0; })) { | ||
if (o.furo.some(function (v) { return 1 / v[0] > 0; })) { | ||
// open sets are not allowed | ||
@@ -277,3 +277,3 @@ return false; | ||
if (o.furo.length > 0) { | ||
if (o.furo.some(function (v) { return v[0] > 0; })) { | ||
if (o.furo.some(function (v) { return 1 / v[0] > 0; })) { | ||
// open sets are not allowed | ||
@@ -299,6 +299,6 @@ return false; | ||
} | ||
return kotsu === 4 && o.isTsumo; | ||
return kotsu === 4 && o.isTsumo && !YAKU['suuankou tanki'].check(o); | ||
}, | ||
}, | ||
daisuushii: { | ||
daisuushi: { | ||
isLocal: false, | ||
@@ -332,3 +332,3 @@ yakuman: 2, | ||
}, | ||
shosuushii: { | ||
shosuushi: { | ||
isLocal: false, | ||
@@ -395,3 +395,3 @@ yakuman: 1, | ||
}, | ||
tsuuisou: { | ||
tsuuiisou: { | ||
isLocal: false, | ||
@@ -404,3 +404,3 @@ yakuman: 1, | ||
}, | ||
ryuuisou: { | ||
ryuuiisou: { | ||
isLocal: false, | ||
@@ -456,3 +456,3 @@ yakuman: 1, | ||
check: function (o) { | ||
return o.extra.firstTake && o.isTsumo && o.jikaze === 1 && !o.furo.length; | ||
return o.extra.firstTake && o.isTsumo && o.jikaze === 27 && !o.furo.length; | ||
}, | ||
@@ -467,3 +467,3 @@ }, | ||
check: function (o) { | ||
return o.extra.firstTake && o.isTsumo && o.jikaze !== 1 && !o.furo.length; | ||
return o.extra.firstTake && o.isTsumo && o.jikaze !== 27 && !o.furo.length; | ||
}, | ||
@@ -478,3 +478,3 @@ }, | ||
check: function (o) { | ||
return o.extra.firstTake && !o.isTsumo && o.jikaze !== 1 && !o.furo.length; | ||
return o.extra.firstTake && !o.isTsumo && !o.furo.length; | ||
}, | ||
@@ -657,3 +657,3 @@ }, | ||
} | ||
return kotsuOrToitsu === 3 && !YAKU.daisangen.check(o); | ||
return kotsuOrToitsu === 3 && !YAKU.daisangen.check(o) && !YAKU.chiitoitsu.check(o); | ||
}, | ||
@@ -705,3 +705,3 @@ }, | ||
var e_18, _a, e_19, _b; | ||
var _c; | ||
var _c, _d; | ||
var kotsu = 0; | ||
@@ -711,5 +711,6 @@ // keep here all tiles that are not closed kantsu | ||
try { | ||
for (var _d = __values(o.furo), _e = _d.next(); !_e.done; _e = _d.next()) { | ||
var k = _e.value; | ||
if (k[0] > 0) { | ||
for (var _e = __values(o.furo), _f = _e.next(); !_f.done; _f = _e.next()) { | ||
var k = _f.value; | ||
if (1 / k[0] > 0 && k[0] === k[1]) { | ||
// workaround: use 1/k[0] because we can have -0 as a valid value for closed 1m kan | ||
openKotsu.add(k[0]); | ||
@@ -722,15 +723,28 @@ } | ||
try { | ||
if (_e && !_e.done && (_a = _d.return)) _a.call(_d); | ||
if (_f && !_f.done && (_a = _e.return)) _a.call(_e); | ||
} | ||
finally { if (e_18) throw e_18.error; } | ||
} | ||
var closedPart = (_c = o.currentPattern) === null || _c === void 0 ? void 0 : _c.filter(function (set) { return o.furo.find(function (meld) { return digest([set]) === digest([meld]); }) === undefined; }); | ||
var _loop_1 = function (v) { | ||
if (openKotsu.has(v[0]) || | ||
(v.length >= 3 && | ||
!o.isTsumo && | ||
o.takenTile !== undefined && | ||
o.takenTile !== null && | ||
v[0] === v[1] && | ||
v.includes(o.takenTile) && | ||
(closedPart === null || closedPart === void 0 ? void 0 : closedPart.find( | ||
// Taken tile should not be included in any other chi set, otherwise we still count is as kotsu | ||
function (set) { return o.takenTile === v[0] && set[0] !== set[1] && set.includes(o.takenTile); })) === undefined)) { | ||
return "continue"; | ||
} | ||
if (v.length >= 3 && v[0] === v[1]) { | ||
kotsu++; | ||
} | ||
}; | ||
try { | ||
for (var _f = __values((_c = o.currentPattern) !== null && _c !== void 0 ? _c : []), _g = _f.next(); !_g.done; _g = _f.next()) { | ||
var v = _g.value; | ||
if (openKotsu.has(v[0])) { | ||
continue; | ||
} | ||
if (v.length >= 3 && v[0] === v[1]) { | ||
kotsu++; | ||
} | ||
for (var _g = __values((_d = o.currentPattern) !== null && _d !== void 0 ? _d : []), _h = _g.next(); !_h.done; _h = _g.next()) { | ||
var v = _h.value; | ||
_loop_1(v); | ||
} | ||
@@ -741,3 +755,3 @@ } | ||
try { | ||
if (_g && !_g.done && (_b = _f.return)) _b.call(_f); | ||
if (_h && !_h.done && (_b = _g.return)) _b.call(_g); | ||
} | ||
@@ -765,3 +779,3 @@ finally { if (e_19) throw e_19.error; } | ||
isFuroMinus: false, | ||
check: function (o) { return o.extra.doubleRiichi && !o.furo.length; }, | ||
check: function (o) { return o.extra.doubleRiichi && !o.furo.filter(function (v) { return 1 / v[0] > 0; }).length; }, | ||
}, | ||
@@ -824,3 +838,3 @@ ittsu: { | ||
// collect shutsu count starting at current tile | ||
res[Math.floor(v[0] / 9)][Math.round((v[0] % 9) / 3)]++; | ||
res[Math.floor(v[0] / 9)][v[0] % 9]++; | ||
} | ||
@@ -885,4 +899,6 @@ } | ||
} | ||
else if (v.length === 3 && v[0] === v[1]) { | ||
fu += is19(v[0]) ? 4 : 2; | ||
else if (v.length === 3) { | ||
if (v[0] === v[1]) { | ||
fu += is19(v[0]) ? 4 : 2; | ||
} | ||
} | ||
@@ -898,12 +914,23 @@ } | ||
} | ||
if (hasPairFu) { | ||
// check kanchan/penchan | ||
var canBeRyanmen = false; | ||
var canBeShanpon = false; | ||
var canBeTanki = false; | ||
if (o.takenTile !== null && o.takenTile !== undefined) { | ||
try { | ||
// check if taken tile can be considered as an edge wait for any shuntsu in hand | ||
for (var _g = __values((_d = o.currentPattern) !== null && _d !== void 0 ? _d : []), _h = _g.next(); !_h.done; _h = _g.next()) { | ||
var v = _h.value; | ||
if (v.length === 3 && v[0] !== v[1]) { | ||
if (o.takenTile === v[0] || o.takenTile === v[2]) { | ||
hasPairFu = false; | ||
if (v.length !== 3) { | ||
if (v.length === 2 && o.takenTile === v[0]) { | ||
// tanki waits are already handled above | ||
canBeTanki = true; | ||
} | ||
continue; | ||
} | ||
if (v[0] === v[1] && v[0] === o.takenTile) { | ||
canBeShanpon = true; | ||
} | ||
if ((v[0] === o.takenTile && !is19(v[2])) || (v[2] === o.takenTile && !is19(v[0]))) { | ||
canBeRyanmen = true; | ||
} | ||
} | ||
@@ -918,4 +945,7 @@ } | ||
} | ||
if (!canBeShanpon && !canBeRyanmen && !canBeTanki) { | ||
fu += 2; | ||
} | ||
} | ||
if (hasPairFu) { | ||
if (hasPairFu && !canBeRyanmen) { | ||
fu += 2; | ||
@@ -922,0 +952,0 @@ } |
{ | ||
"name": "riichi-ts", | ||
"version": "1.0.1", | ||
"version": "1.0.2", | ||
"description": "Riichi mahjong calculations library", | ||
"type": "module", | ||
"module": "index.ts", | ||
"module": "dist/index.js", | ||
"main": "dist/index.js", | ||
"license": "GPL-3.0-or-later", | ||
@@ -15,9 +16,7 @@ "scripts": { | ||
}, | ||
"dependencies": { | ||
}, | ||
"devDependencies": { | ||
"prettier": "^3.0.0", | ||
"@types/chai": "^4.3.5", | ||
"@types/jest": "^29.5.0", | ||
"@types/node": "^18.15.1", | ||
"@types/yauzl-promise": "^2.1.1", | ||
"@typescript-eslint/eslint-plugin": "^6.1.0", | ||
@@ -29,7 +28,10 @@ "@typescript-eslint/parser": "^6.1.0", | ||
"eslint-plugin-unused-imports": "^3.0.0", | ||
"htmlparser2": "^9.0.0", | ||
"jest": "^29.5.0", | ||
"prettier": "^3.0.0", | ||
"ts-jest": "^29.0.5", | ||
"ts-node": "^10.9.1", | ||
"typescript": "^4.9.5" | ||
"typescript": "^4.9.5", | ||
"yauzl-promise": "^4.0.0" | ||
} | ||
} |
140
README.md
@@ -5,2 +5,136 @@ ## Riichi Typescript library | ||
### Usage | ||
Install the library using `npm install riichi-ts` or `yarn add riichi-ts`. | ||
Type definitions for typescript are included in the package. | ||
#### Tiles definition | ||
Tiles are defined by a single integer number according to the following table: | ||
- Man suit: `0` is 1man, ... `8` is 9man | ||
- Pin suit: `9` is 1man, ... `17` is 9man | ||
- Sou suit: `18` is 1man, ... `26` is 9man | ||
- Honors: | ||
- Winds: `27` is east, ... `30` is north | ||
- Dragons: `31` is white, `32` is green, `33` is red | ||
Usage: | ||
```javascript | ||
import { Riichi } from 'riichi-ts'; | ||
const hand = new Riichi( | ||
[10, 10, 10, 11, 11, 12, 12], // closed part of the hand. Taken tile from the wall should be the last here in case of tsumo. | ||
[ // melds | ||
{ open: true, tiles: [13, 14, 15] }, // set open: false to mark closed kan (ankan) | ||
{ open: true, tiles: [15, 16, 17] }, | ||
], | ||
{ | ||
bakaze: 27, // round wind | ||
jikaze: 28 // seat wind | ||
}, | ||
13, // tile taken from someone's discard. In case of tsumo, set this to null. | ||
false, // was this the first take? Used to determine tenhou/chinou/renhou | ||
true, // does this hand have riichi? | ||
false, // does this hand have ippatsu? | ||
false, // does this hand have daburu-riichi? | ||
false, // is it the last taken tile? Used to determine haitei/houtei | ||
false, // was it agari after a kan? Used to determine rinshan/chankan | ||
0, // akadora count in hand | ||
true, // allow akadora; if this is false, previous parameter is ignored | ||
true, // allow kuitan? | ||
false, // use kiriage mangan? | ||
); | ||
hand.disableDoubleyakuman(); // call this to count all yakumans as single | ||
hand.disableYaku('yaku name'); // You can also disable some of the yaku | ||
hand.disableHairi(); // Don't calculate improvements for the non-winning hand | ||
const result = hand.calc(); | ||
/* | ||
Result format: | ||
{ | ||
error: false, // true if hand couldn't be parsed | ||
fu: 30, | ||
han: 6, | ||
isAgari: true, // will be false if the hand is not winning | ||
ten: 12000, // amount of points won by the hand | ||
text: '', // additional info. Will contain 'no yaku' if the hand has no winning points | ||
yaku: { // list of found yaku in the hand | ||
chinitsu: 5, // yaku name and amount of han for certain yaku | ||
tanyao: 1 | ||
}, | ||
yakuman: 0, // Count of yakumans found | ||
hairi: { // shanten count and waits info | ||
now: 0, // current shanten count. 0 for tenpai. | ||
wait: [12], // tiles this hand is waiting for to improve or win | ||
waitsAfterDiscard: { // waits after discarding a certain tile | ||
12: [13, 14] | ||
} | ||
} | ||
hairi7and13: { // same as above but only for chiitoitsu and kokushimusou. | ||
now: 0, | ||
wait: [12], | ||
waitsAfterDiscard: { 12: [13, 14] } | ||
} | ||
} | ||
*/ | ||
``` | ||
#### Full list of yaku names that can be detected: | ||
- menzentsumo | ||
- riichi | ||
- ippatsu | ||
- chankan | ||
- rinshan | ||
- haitei | ||
- houtei | ||
- pinfu | ||
- tanyao | ||
- iipeikou | ||
- own wind east | ||
- own wind south | ||
- own wind west | ||
- own wind north | ||
- round wind east | ||
- round wind south | ||
- round wind west | ||
- round wind north | ||
- haku | ||
- hatsu | ||
- chun | ||
- daburu riichi | ||
- chiitoitsu | ||
- chanta | ||
- ittsu | ||
- sanshoku | ||
- sanshoku doukou | ||
- sankantsu | ||
- toitoi | ||
- sanankou | ||
- shosangen | ||
- honroutou | ||
- ryanpeikou | ||
- junchan | ||
- honitsu | ||
- chinitsu | ||
- renhou | ||
- tenhou | ||
- chihou | ||
- daisangen | ||
- suuankou | ||
- suuankou tanki | ||
- tsuuiisou | ||
- ryuuiisou | ||
- chinroutou | ||
- chuurenpoto | ||
- chuurenpoto 9 sides | ||
- kokushimusou | ||
- kokushimusou 13 sides | ||
- daisuushi | ||
- shosuushi | ||
- suukantsu | ||
- dora | ||
- uradora | ||
- akadora | ||
### Credits | ||
@@ -12,1 +146,7 @@ | ||
- https://github.com/takayama-lily/agari | ||
### Testing on real games data | ||
The library was tested against millions of real-life game logs from Tenhou.net phoenix lobby. Though we don't supply these logs in the repo, you can still download it on Tenhou.net and use it for testing. | ||
Please place zip archives with real replay logs into `logs` folder. The testing script will travers over all zip files there and will try to parse every replay file found. |
@@ -46,3 +46,3 @@ import assert from 'node:assert'; | ||
name: '', | ||
ten: 2000, | ||
ten: 2100, | ||
text: '', | ||
@@ -70,3 +70,3 @@ yaku: { pinfu: 1, menzentsumo: 1 }, | ||
name: '', | ||
ten: 7700, | ||
ten: 7800, | ||
text: '', | ||
@@ -108,7 +108,7 @@ yaku: { haku: 1, toitoi: 2 }, | ||
error: false, | ||
fu: 30, | ||
fu: 40, | ||
han: 3, | ||
isAgari: true, | ||
name: '', | ||
ten: 5800, | ||
ten: 5200, | ||
text: '', | ||
@@ -131,3 +131,3 @@ yaku: { chanta: 2, menzentsumo: 1 }, | ||
name: '', | ||
ten: 7700, | ||
ten: 5200, | ||
text: '', | ||
@@ -150,7 +150,7 @@ yaku: { pinfu: 1, ittsu: 2, menzentsumo: 1 }, | ||
error: false, | ||
fu: 40, | ||
fu: 50, | ||
han: 5, | ||
isAgari: true, | ||
name: '', | ||
ten: 12000, | ||
ten: 8000, | ||
text: '', | ||
@@ -162,3 +162,3 @@ yaku: { haku: 1, honroutou: 2, toitoi: 2 }, | ||
it('Should parse Pinfu, sanshoku, tanyao', () => { | ||
const r = new Riichi([p2, p3, p4, p5, p6, p7, m5, m6, m7, s5, s6, s7, s3, s3], [], { | ||
const r = new Riichi([p2, p3, p5, p6, p7, m5, m6, m7, s5, s6, s7, s3, s3, p4], [], { | ||
bakaze: e, | ||
@@ -174,3 +174,3 @@ jikaze: w, | ||
name: '', | ||
ten: 12000, | ||
ten: 8000, | ||
text: '', | ||
@@ -193,3 +193,3 @@ yaku: { pinfu: 1, menzentsumo: 1, sanshoku: 2, tanyao: 1 }, | ||
name: '', | ||
ten: 9600, | ||
ten: 6400, | ||
text: '', | ||
@@ -216,3 +216,3 @@ yaku: { tanyao: 1, chiitoitsu: 2, menzentsumo: 1 }, | ||
name: '', | ||
ten: 3900, | ||
ten: 2700, | ||
text: '', | ||
@@ -237,3 +237,3 @@ yaku: { haku: 1, 'round wind east': 1 }, | ||
name: '', | ||
ten: 12000, | ||
ten: 8000, | ||
text: '', | ||
@@ -256,7 +256,7 @@ yaku: { pinfu: 1, tanyao: 1, ryanpeikou: 3 }, | ||
error: false, | ||
fu: 30, | ||
fu: 40, | ||
han: 4, | ||
isAgari: true, | ||
name: '', | ||
ten: 11600, | ||
ten: 8000, | ||
text: '', | ||
@@ -275,3 +275,12 @@ yaku: { haku: 1, 'round wind east': 1, honitsu: 2 }, | ||
{ bakaze: e, jikaze: w }, | ||
p5 | ||
p5, | ||
undefined, | ||
undefined, | ||
undefined, | ||
undefined, | ||
undefined, | ||
undefined, | ||
undefined, | ||
undefined, | ||
true | ||
); | ||
@@ -285,3 +294,3 @@ const res = r.calc(); | ||
name: '', | ||
ten: 18000, | ||
ten: 12000, | ||
text: '', | ||
@@ -302,7 +311,7 @@ yaku: { chinitsu: 5, tanyao: 1 }, | ||
error: false, | ||
fu: 40, | ||
fu: 50, | ||
han: 4, | ||
isAgari: true, | ||
name: '', | ||
ten: 12000, | ||
ten: 8000, | ||
text: '', | ||
@@ -323,7 +332,7 @@ yaku: { 'round wind east': 1, honitsu: 3 }, | ||
error: false, | ||
fu: 40, | ||
fu: 50, | ||
han: 2, | ||
isAgari: true, | ||
name: '', | ||
ten: 3900, | ||
ten: 3200, | ||
text: '', | ||
@@ -346,3 +355,3 @@ yaku: { hatsu: 1, iipeikou: 1 }, | ||
name: '', | ||
ten: 18000, | ||
ten: 12000, | ||
text: '', | ||
@@ -368,7 +377,7 @@ yaku: { honitsu: 3, menzentsumo: 1, chiitoitsu: 2 }, | ||
error: false, | ||
fu: 0, | ||
fu: 90, | ||
han: 0, | ||
isAgari: true, | ||
name: '', | ||
ten: 48000, | ||
ten: 32000, | ||
text: '', | ||
@@ -396,3 +405,3 @@ yaku: { suukantsu: 13 }, | ||
name: '', | ||
ten: 12000, | ||
ten: 8000, | ||
text: '', | ||
@@ -417,3 +426,3 @@ yaku: { chun: 1, sankantsu: 2, toitoi: 2 }, | ||
name: '', | ||
ten: 12000, | ||
ten: 8000, | ||
text: '', | ||
@@ -432,7 +441,7 @@ yaku: { haku: 1, toitoi: 2, sanankou: 2 }, | ||
error: false, | ||
fu: 0, | ||
fu: 40, | ||
han: 0, | ||
isAgari: true, | ||
name: '', | ||
ten: 48000, | ||
ten: 32000, | ||
text: '', | ||
@@ -443,3 +452,3 @@ yaku: { suuankou: 13 }, | ||
}); | ||
it('Should parse Pinfu iipeiko chinitsu tanyao', () => { | ||
it('Should parse Sanankou chinitsu tanyao', () => { | ||
const r = new Riichi([s2, s2, s2, s3, s3, s3, s4, s4, s4, s5, s5, s6, s7, s8], [], { | ||
@@ -452,9 +461,9 @@ bakaze: e, | ||
error: false, | ||
fu: 20, | ||
fu: 40, | ||
han: 10, | ||
isAgari: true, | ||
name: '', | ||
ten: 24000, | ||
ten: 16000, | ||
text: '', | ||
yaku: { chinitsu: 6, iipeikou: 1, menzentsumo: 1, pinfu: 1, tanyao: 1 }, | ||
yaku: { chinitsu: 6, menzentsumo: 1, sanankou: 2, tanyao: 1 }, | ||
yakuman: 0, | ||
@@ -477,3 +486,3 @@ }); | ||
name: '', | ||
ten: 12000, | ||
ten: 8000, | ||
text: '', | ||
@@ -498,3 +507,3 @@ yaku: { junchan: 3, sanshoku: 2 }, | ||
name: '', | ||
ten: 11600, | ||
ten: 7700, | ||
text: '', | ||
@@ -519,3 +528,3 @@ yaku: { pinfu: 1, junchan: 3 }, | ||
name: '', | ||
ten: 24000, | ||
ten: 16000, | ||
text: '', | ||
@@ -544,3 +553,3 @@ yaku: { pinfu: 1, iipeikou: 1, ittsu: 2, chinitsu: 6 }, | ||
name: '', | ||
ten: 12000, | ||
ten: 8000, | ||
text: '', | ||
@@ -547,0 +556,0 @@ yaku: { toitoi: 2, 'sanshoku doukou': 2 }, |
162
riichi.ts
@@ -6,5 +6,2 @@ import { checkAll, findAllAgariPatterns } from './agari'; | ||
// TODO: next: make unit tests for hands; | ||
// TODO: next2: add testing with tenhou logs | ||
type Options = { | ||
@@ -36,2 +33,4 @@ dora?: number[]; | ||
const sortByInt = (a1: number, a2: number) => a1 - a2; | ||
export class Riichi { | ||
@@ -84,2 +83,3 @@ public hai: number[] = []; | ||
public allowKuitan = true; | ||
public withKiriage = false; | ||
public allowAka = true; | ||
@@ -98,3 +98,7 @@ public hairi = true; | ||
lastTile?: boolean, | ||
afterKan?: boolean | ||
afterKan?: boolean, | ||
akaCount?: number, | ||
allowAka?: boolean, | ||
allowKuitan?: boolean, | ||
withKiriage?: boolean | ||
) { | ||
@@ -107,8 +111,10 @@ this.extra.firstTake = firstTake ?? false; | ||
this.extra.afterKan = afterKan ?? false; | ||
this.hai = closedPart.sort(); | ||
// TODO: introduce aka | ||
// this.aka += closedParsed.aka; | ||
this.allowAka = allowAka ?? false; | ||
this.allowKuitan = allowKuitan ?? false; | ||
this.withKiriage = withKiriage ?? false; | ||
this.hai = closedPart; | ||
this.aka += akaCount ?? 0; | ||
this.dora = options.dora ?? []; | ||
if (tileDiscardedBySomeone !== undefined) { | ||
if (tileDiscardedBySomeone !== undefined && tileDiscardedBySomeone !== null) { | ||
this.takenTile = tileDiscardedBySomeone; | ||
@@ -123,3 +129,3 @@ this.hai.push(this.takenTile); | ||
if (isProperOpenSet(vv.tiles)) { | ||
this.furo.push([...vv.tiles.map((v) => (vv.open ? v : -v))].sort()); | ||
this.furo.push([...vv.tiles.map((v) => (vv.open ? v : -v))].sort(sortByInt)); | ||
} else { | ||
@@ -154,2 +160,7 @@ // TODO: not sure this is proper; there shouldn't be any incorrect sets | ||
if (v.length > 2) { | ||
// closed kan | ||
if (v[0] === v[1] && 1 / v[0] < 0) { | ||
// hack: check infinity sign to support 1m closed kan | ||
continue; | ||
} | ||
return false; | ||
@@ -168,4 +179,7 @@ } | ||
for (let v of this.hai) { | ||
if (this.dora.includes(v)) { | ||
dora++; | ||
for (let d of this.dora) { | ||
// loop over to detect multiple dora | ||
if (v === d) { | ||
dora++; | ||
} | ||
} | ||
@@ -176,4 +190,7 @@ } | ||
for (let vv of v) { | ||
if (this.dora.includes(vv)) { | ||
dora++; | ||
for (let d of this.dora) { | ||
// loop over to detect multiple dora | ||
if (Math.abs(vv) === d) { | ||
dora++; | ||
} | ||
} | ||
@@ -213,11 +230,32 @@ } | ||
} | ||
} else { | ||
if ( | ||
this.withKiriage && | ||
((this.tmpResult.han === 4 && this.tmpResult.fu === 30) || | ||
(this.tmpResult.han === 3 && this.tmpResult.fu === 60)) | ||
) { | ||
base = 2000; | ||
} | ||
} | ||
} | ||
this.tmpResult.ten = ceil100(base * 6); | ||
this.tmpResult.text += (this.tmpResult.name ? ' ' : '') + this.tmpResult.name; | ||
let oya = 0; | ||
let ko = 0; | ||
if (this.isTsumo) { | ||
oya = ceil100(base * 2) + ceil100(base * 2) + ceil100(base * 2); | ||
ko = ceil100(base * 2) + ceil100(base) + ceil100(base); | ||
} else { | ||
oya = ceil100(base * 6); | ||
ko = ceil100(base * 4); | ||
} | ||
this.tmpResult.ten = this.jikaze === 27 ? oya : ko; | ||
} | ||
calcFu() { | ||
calcFu(haiExceptFuro: number[][], havePinfu = false) { | ||
let fu: number; | ||
if (this.tmpResult.yaku.chiitoitsu) { | ||
fu = 25; | ||
} else if (this.tmpResult.yaku.kokushimusou || this.tmpResult.yaku['kokushimusou 13 sides']) { | ||
fu = 0; | ||
} else if (this.tmpResult.yaku.pinfu) { | ||
@@ -227,3 +265,5 @@ fu = this.isTsumo ? 20 : 30; | ||
fu = 20; | ||
if (!this.isTsumo && this.isMenzen()) fu += 10; | ||
if (!this.isTsumo && this.isMenzen()) { | ||
fu += 10; | ||
} | ||
if (!this.currentPattern) { | ||
@@ -233,6 +273,55 @@ return; | ||
for (let v of this.currentPattern) { | ||
// check waiting | ||
let canBeRyanmen = false; | ||
let canBeKanchan = false; | ||
let canBePenchan = false; | ||
let canBeShanpon = false; | ||
let canBeTanki = false; | ||
if (this.takenTile !== undefined && this.takenTile !== null) { | ||
for (let v of haiExceptFuro ?? []) { | ||
if (v.length !== 3) { | ||
if (v.length === 2 && this.takenTile === v[0]) { | ||
// tanki waits are already handled above | ||
canBeTanki = true; | ||
} | ||
continue; | ||
} | ||
if (v[0] === v[1]) { | ||
if (v[0] === this.takenTile) { | ||
canBeShanpon = true; | ||
} | ||
} else { | ||
if ( | ||
(v[0] === this.takenTile && !is19(v[2])) || | ||
(v[2] === this.takenTile && !is19(v[0])) | ||
) { | ||
canBeRyanmen = true; | ||
} | ||
if ( | ||
(v[0] === this.takenTile && is19(v[2])) || | ||
(v[2] === this.takenTile && is19(v[0])) | ||
) { | ||
canBePenchan = true; | ||
} | ||
if (v[1] === this.takenTile) { | ||
canBeKanchan = true; | ||
} | ||
} | ||
} | ||
} | ||
for (let v of this.furo) { | ||
if (v.length === 4) { | ||
fu += is19(Math.abs(v[0])) ? (v[0] > 0 ? 16 : 32) : v[0] > 0 ? 8 : 16; | ||
} else if (v.length === 2) { | ||
// hack: count infinity sign to support closed kan of 1m | ||
fu += is19(Math.abs(v[0])) ? (1 / v[0] > 0 ? 16 : 32) : 1 / v[0] > 0 ? 8 : 16; | ||
} else if (v.length === 3 && v[0] === v[1]) { | ||
fu += is19(v[0]) ? 4 : 2; | ||
} | ||
} | ||
for (let v of haiExceptFuro) { | ||
if (v.length === 2) { | ||
if ([this.bakaze, this.jikaze, 31, 32, 33].includes(v[0])) { | ||
@@ -250,6 +339,22 @@ // pair of yakuhai tile | ||
} else if (v.length === 3 && v[0] === v[1]) { | ||
fu += is19(v[0]) ? 4 : 2; | ||
if (!this.isTsumo && this.takenTile === v[0]) { | ||
if (canBeRyanmen || canBeKanchan || canBePenchan) { | ||
fu += is19(v[0]) ? 8 : 4; | ||
} else { | ||
fu += is19(v[0]) ? 4 : 2; | ||
} | ||
} else { | ||
fu += is19(v[0]) ? 8 : 4; | ||
} | ||
} | ||
} | ||
if (canBePenchan || canBeKanchan) { | ||
if (!havePinfu && !canBeTanki) { | ||
fu += 2; | ||
} else if (!canBeShanpon && !canBeRyanmen && !canBeTanki) { | ||
fu += 2; | ||
} | ||
} | ||
if (this.isTsumo) { | ||
@@ -368,5 +473,5 @@ fu += 2; | ||
if (this.tmpResult.han) { | ||
if (this.tmpResult.han || this.tmpResult.yakuman > 0) { | ||
this.calcDora(); | ||
this.calcFu(); | ||
this.calcFu(v, !!this.tmpResult.yaku.pinfu); | ||
} | ||
@@ -376,7 +481,10 @@ | ||
// Find variant with maximum points | ||
if (this.tmpResult.ten > (this.finalResult?.ten ?? 0)) { | ||
this.finalResult = JSON.parse(JSON.stringify(this.tmpResult)); | ||
} else if ( | ||
this.tmpResult.ten === this.finalResult?.ten && | ||
this.tmpResult.han > this.finalResult?.han | ||
if ( | ||
this.tmpResult.ten > (this.finalResult?.ten ?? 0) || | ||
(this.tmpResult.ten === this.finalResult?.ten && | ||
this.tmpResult.han > this.finalResult?.han) || | ||
(this.tmpResult.ten === this.finalResult?.ten && | ||
this.tmpResult.han === this.finalResult?.han && | ||
this.tmpResult.fu > this.finalResult?.fu) | ||
) { | ||
@@ -383,0 +491,0 @@ this.finalResult = JSON.parse(JSON.stringify(this.tmpResult)); |
89
yaku.ts
@@ -32,3 +32,3 @@ import assert from 'assert'; | ||
for (let tile of occurences) { | ||
hand[tile]++; | ||
hand[Math.abs(tile)]++; | ||
} | ||
@@ -62,3 +62,3 @@ return hand; | ||
if (v.length <= 2 || v[0] === v[1]) { | ||
if (!allow.includes(v[0])) { | ||
if (!allow.includes(Math.abs(v[0]))) { | ||
return false; | ||
@@ -139,3 +139,3 @@ } | ||
} | ||
return o.takenTile !== null && [2, 4].includes(slices[foundSuit][o.takenTile]); | ||
return o.takenTile !== null && [2, 4].includes(slices[foundSuit][o.takenTile % 9]); | ||
}, | ||
@@ -160,3 +160,3 @@ }, | ||
} | ||
return o.takenTile !== null && [1, 3].includes(slices[foundSuit][o.takenTile]); | ||
return o.takenTile !== null && [1, 3].includes(slices[foundSuit][o.takenTile % 9]); | ||
}, | ||
@@ -172,3 +172,3 @@ }, | ||
if (o.furo.length > 0) { | ||
if (o.furo.some((v) => v[0] > 0)) { | ||
if (o.furo.some((v) => 1 / v[0] > 0)) { | ||
// open sets are not allowed | ||
@@ -199,3 +199,3 @@ return false; | ||
if (o.furo.length > 0) { | ||
if (o.furo.some((v) => v[0] > 0)) { | ||
if (o.furo.some((v) => 1 / v[0] > 0)) { | ||
// open sets are not allowed | ||
@@ -212,6 +212,6 @@ return false; | ||
return kotsu === 4 && o.isTsumo; | ||
return kotsu === 4 && o.isTsumo && !YAKU['suuankou tanki'].check(o); | ||
}, | ||
}, | ||
daisuushii: { | ||
daisuushi: { | ||
isLocal: false, | ||
@@ -233,3 +233,3 @@ yakuman: 2, | ||
}, | ||
shosuushii: { | ||
shosuushi: { | ||
isLocal: false, | ||
@@ -272,3 +272,3 @@ yakuman: 1, | ||
}, | ||
tsuuisou: { | ||
tsuuiisou: { | ||
isLocal: false, | ||
@@ -281,3 +281,3 @@ yakuman: 1, | ||
}, | ||
ryuuisou: { | ||
ryuuiisou: { | ||
isLocal: false, | ||
@@ -321,3 +321,3 @@ yakuman: 1, | ||
check: (o: Riichi) => { | ||
return o.extra.firstTake && o.isTsumo && o.jikaze === 1 && !o.furo.length; | ||
return o.extra.firstTake && o.isTsumo && o.jikaze === 27 && !o.furo.length; | ||
}, | ||
@@ -332,3 +332,3 @@ }, | ||
check: (o: Riichi) => { | ||
return o.extra.firstTake && o.isTsumo && o.jikaze !== 1 && !o.furo.length; | ||
return o.extra.firstTake && o.isTsumo && o.jikaze !== 27 && !o.furo.length; | ||
}, | ||
@@ -343,3 +343,3 @@ }, | ||
check: (o: Riichi) => { | ||
return o.extra.firstTake && !o.isTsumo && o.jikaze !== 1 && !o.furo.length; | ||
return o.extra.firstTake && !o.isTsumo && !o.furo.length; | ||
}, | ||
@@ -472,3 +472,3 @@ }, | ||
} | ||
return kotsuOrToitsu === 3 && !YAKU.daisangen.check(o); | ||
return kotsuOrToitsu === 3 && !YAKU.daisangen.check(o) && !YAKU.chiitoitsu.check(o); | ||
}, | ||
@@ -513,3 +513,4 @@ }, | ||
for (let k of o.furo) { | ||
if (k[0] > 0) { | ||
if (1 / k[0] > 0 && k[0] === k[1]) { | ||
// workaround: use 1/k[0] because we can have -0 as a valid value for closed 1m kan | ||
openKotsu.add(k[0]); | ||
@@ -519,4 +520,19 @@ } | ||
const closedPart = o.currentPattern?.filter( | ||
(set) => o.furo.find((meld) => digest([set]) === digest([meld])) === undefined | ||
); | ||
for (let v of o.currentPattern ?? []) { | ||
if (openKotsu.has(v[0])) { | ||
if ( | ||
openKotsu.has(v[0]) || | ||
(v.length >= 3 && | ||
!o.isTsumo && | ||
o.takenTile !== undefined && | ||
o.takenTile !== null && | ||
v[0] === v[1] && | ||
v.includes(o.takenTile) && | ||
closedPart?.find( | ||
// Taken tile should not be included in any other chi set, otherwise we still count is as kotsu | ||
(set) => o.takenTile === v[0] && set[0] !== set[1] && set.includes(o.takenTile) | ||
) === undefined) | ||
) { | ||
continue; | ||
@@ -548,3 +564,3 @@ } | ||
isFuroMinus: false, | ||
check: (o: Riichi) => o.extra.doubleRiichi && !o.furo.length, | ||
check: (o: Riichi) => o.extra.doubleRiichi && !o.furo.filter((v) => 1 / v[0] > 0).length, | ||
}, | ||
@@ -592,3 +608,3 @@ ittsu: { | ||
// collect shutsu count starting at current tile | ||
res[Math.floor(v[0] / 9)][Math.round((v[0] % 9) / 3)]++; | ||
res[Math.floor(v[0] / 9)][v[0] % 9]++; | ||
} | ||
@@ -644,19 +660,38 @@ | ||
} | ||
} else if (v.length === 3 && v[0] === v[1]) { | ||
fu += is19(v[0]) ? 4 : 2; | ||
} else if (v.length === 3) { | ||
if (v[0] === v[1]) { | ||
fu += is19(v[0]) ? 4 : 2; | ||
} | ||
} | ||
} | ||
if (hasPairFu) { | ||
// check if taken tile can be considered as an edge wait for any shuntsu in hand | ||
// check kanchan/penchan | ||
let canBeRyanmen = false; | ||
let canBeShanpon = false; | ||
let canBeTanki = false; | ||
if (o.takenTile !== null && o.takenTile !== undefined) { | ||
for (let v of o.currentPattern ?? []) { | ||
if (v.length === 3 && v[0] !== v[1]) { | ||
if (o.takenTile === v[0] || o.takenTile === v[2]) { | ||
hasPairFu = false; | ||
if (v.length !== 3) { | ||
if (v.length === 2 && o.takenTile === v[0]) { | ||
// tanki waits are already handled above | ||
canBeTanki = true; | ||
} | ||
continue; | ||
} | ||
if (v[0] === v[1] && v[0] === o.takenTile) { | ||
canBeShanpon = true; | ||
} | ||
if ((v[0] === o.takenTile && !is19(v[2])) || (v[2] === o.takenTile && !is19(v[0]))) { | ||
canBeRyanmen = true; | ||
} | ||
} | ||
if (!canBeShanpon && !canBeRyanmen && !canBeTanki) { | ||
fu += 2; | ||
} | ||
} | ||
if (hasPairFu) { | ||
if (hasPairFu && !canBeRyanmen) { | ||
fu += 2; | ||
@@ -663,0 +698,0 @@ } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
613605
45
8033
151
17