New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

riichi-ts

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

riichi-ts - npm Package Compare versions

Comparing version 1.0.1 to 1.0.2

dist/agari.d.ts

5

agari.test.ts

@@ -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) {

@@ -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"
}
}

@@ -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 },

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

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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc