Skip to content

Commit

Permalink
Improve evaluation of furiten, if the wait is really good the bot wil…
Browse files Browse the repository at this point in the history
…l try to keep it
  • Loading branch information
Jimboom7 committed Jun 6, 2022
1 parent 6035234 commit 56861e4
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 23 deletions.
55 changes: 40 additions & 15 deletions src/ai_offense.js
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,8 @@ function getHandValues(hand, discardedTile) {
var expectedScore = { open: 0, closed: 0, riichi: 0 }; //For the expected score (only looking at hands that improve the current hand)
var yaku = { open: 0, closed: 0 }; //Expected Yaku
var doraValue = 0; //Expected Dora
var waits = 0; //Waits when in Tenpai (Or fractions of it when 1 shanten)
var waits = 0; //Waits when in Tenpai
var shape = 0; //When 1 shanten: Contains a value that indicates how good the shape of the hand is
var fu = 0;

var kita = 0;
Expand Down Expand Up @@ -553,7 +554,7 @@ function getHandValues(hand, discardedTile) {
var triples2 = triplesAndPairs2.triples;
var pairs2 = triplesAndPairs2.pairs;

if (!isClosed && (!tileCombination.winning || tile1Furiten) &&
if (!isClosed && (!tileCombination.winning) &&
getNumberOfTilesInTileArray(triples2, tile1.index, tile1.type) == 3) {
numberOfTiles1 *= 2; //More value to possible triples when hand is open (can call pons from all players)
}
Expand All @@ -576,7 +577,7 @@ function getHandValues(hand, discardedTile) {
return pv + getNumberOfTilesAvailable(cv.tile2.index, cv.tile2.type);
}, 0));
if (tile1Furiten) {
thisShanten = (0 - baseShanten);
thisShanten = 0 - baseShanten;
}
else {
thisShanten = (calculateShanten(parseInt(triples2.length / 3) + callTriples, parseInt(pairs2.length / 2), parseInt(doubles2.length / 2)) - baseShanten);
Expand All @@ -590,7 +591,10 @@ function getHandValues(hand, discardedTile) {
var thisYaku = getYaku(hand, calls[0], triplesAndPairs2);
var thisWait = numberOfTiles1 * getWaitQuality(tile1);
var thisFu = calculateFu(triples2, calls[0], pairs2, removeTilesFromTileArray(hand, triples.concat(pairs).concat(tile1)), tile1);
if (!tile1Furiten && (isClosed || thisYaku.open >= 1 || tilesLeft <= 4)) {
if (isClosed || thisYaku.open >= 1 || tilesLeft <= 4) {
if (tile1Furiten) {
thisWait = numberOfTiles1 / 6;
}
waits += thisWait;
fu += thisFu * thisWait * factor;
if (thisFu == 30 && isClosed) {
Expand Down Expand Up @@ -650,14 +654,14 @@ function getHandValues(hand, discardedTile) {
combFactor *= 2; //More value to possible triples when hand is open (can call pons from all players)
}

if (winning && !tile2Furiten) { //If this tile combination wins in 2 turns: calculate waits etc.
if (winning && !tile2Furiten) { //If this tile combination wins in 2 turns: calculate shape etc.
thisShanten = -1 - baseShanten;
if (waitTiles.filter(t => isSameTile(t, tile2)).length == 0) {
var newWait = numberOfTiles2 * getWaitQuality(tile2) * ((numberOfTiles1) / availableTiles.length);
var newShape = numberOfTiles2 * getWaitQuality(tile2) * ((numberOfTiles1) / availableTiles.length);
if (tile2Data.duplicate) {
newWait += numberOfTiles1 * getWaitQuality(tile1) * ((numberOfTiles2) / availableTiles.length);
newShape += numberOfTiles1 * getWaitQuality(tile1) * ((numberOfTiles2) / availableTiles.length);
}
waits += newWait;
shape += newShape;
}

var secondDiscard = removeTilesFromTileArray(hand, triples3.concat(pairs3))[0];
Expand Down Expand Up @@ -722,13 +726,21 @@ function getHandValues(hand, discardedTile) {
expectedScore.riichi /= numberOfTotalWaitCombinations;
fu /= numberOfTotalWaitCombinations;
}
if (waitTiles.length > 0) {
waits *= (waitTiles.length * 0.3) + 0.5; //Waiting on multiple tiles is better
}

fu = fu <= 30 ? 30 : fu;
fu = fu > 110 ? 30 : fu;

var efficiency = (shanten + (baseShanten - originalShanten)) * -1; //Percent Number that indicates how big the chance is to improve the hand (in regards to efficiency). Negative for increasing shanten with the discard
if (originalShanten == 0) { //Already in Tenpai: Look at waits instead
efficiency = waits / 10;
if (baseShanten == 0) {
efficiency = (waits + shape) / 10;
}
else {
efficiency = ((shanten / 1.5) * -1);
}
}

if (baseShanten > 0) { //When not tenpai
Expand All @@ -743,9 +755,16 @@ function getHandValues(hand, discardedTile) {
}

var priority = calculateTilePriority(efficiency, expectedScore, danger - sakigiri);

var riichiPriority = 0;
if (originalShanten == 0) { //Already in Tenpai: Look at waits instead
riichiEfficiency = waits / 10;
riichiPriority = calculateTilePriority(riichiEfficiency, expectedScore, danger - sakigiri);
}

return {
tile: discardedTile, priority: priority, shanten: baseShanten, efficiency: efficiency,
score: expectedScore, dora: doraValue, yaku: yaku, waits: waits, danger: danger, fu: fu
tile: discardedTile, priority: priority, riichiPriority: riichiPriority, shanten: baseShanten, efficiency: efficiency,
score: expectedScore, dora: doraValue, yaku: yaku, waits: waits, shape: shape, danger: danger, fu: fu
};
}

Expand Down Expand Up @@ -804,6 +823,8 @@ function chiitoitsuPriorities() {
var baseYaku = getYaku(newHand, calls[0]);
var yaku = { open: 0, closed: 0 };

var shape = 0;

//Possible Value, Yaku and Dora after Draw
handWithoutPairs.forEach(function (tile) {
var currentHand = [...handWithoutPairs];
Expand All @@ -820,6 +841,10 @@ function chiitoitsuPriorities() {
if (pairsValue + (pairs2.length / 2) == 7) { //Winning hand
waits = numberOfTiles * getWaitQuality(tile);
doraValue = getNumberOfDoras(pairs2);
if (tile.index < 3 || tile.index > 7 || tile.doraValue > 0 || getWaitQuality(tile) > 1.1 || //Good Wait
currentHand.filter(tile => tile.type == 3 || tile.index == 1 || tile.index == 9).length == 0) { //Or Tanyao
shape = 1;
}
}
}
});
Expand All @@ -845,8 +870,8 @@ function chiitoitsuPriorities() {

var priority = calculateTilePriority(efficiency, expectedScore, danger - sakigiri);
tiles.push({
tile: ownHand[i], priority: priority, shanten: baseShanten, efficiency: efficiency,
score: expectedScore, dora: doraValue, yaku: yaku, waits: waits, danger: danger, fu: 25
tile: ownHand[i], priority: priority, riichiPriority: priority, shanten: baseShanten, efficiency: efficiency,
score: expectedScore, dora: doraValue, yaku: yaku, waits: waits, shape: shape, danger: danger, fu: 25
});
}

Expand Down Expand Up @@ -904,8 +929,8 @@ function thirteenOrphansPriorities() {
var priority = calculateTilePriority(efficiency, expectedScore, danger - sakigiri);

tiles.push({
tile: ownHand[i], priority: priority, shanten: shanten, efficiency: efficiency,
score: expectedScore, dora: doraValue, yaku: yaku, waits: waits, danger: danger, fu: 30
tile: ownHand[i], priority: priority, riichiPriority: priority, shanten: shanten, efficiency: efficiency,
score: expectedScore, dora: doraValue, yaku: yaku, waits: waits, shape: 0, danger: danger, fu: 30
});

}
Expand Down
21 changes: 13 additions & 8 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -668,19 +668,20 @@ function getFoldThreshold(tilePrio, hand) {
}

var waits = tilePrio.waits;
var shape = tilePrio.shape;

// Formulas are based on this table: https://docs.google.com/spreadsheets/d/172LFySNLUtboZUiDguf8I3QpmFT-TApUfjOs5iRy3os/edit#gid=212618921
// TODO: Maybe switch to this: https://riichi-mahjong.com/2020/01/28/mahjong-strategy-push-or-fold-4-maximizing-game-ev/
if (tilePrio.shanten == 0) {
var foldValue = waits * handScore / 38;
var foldValue = (waits + shape) * handScore / 38;
if (tilesLeft < 8) { //Try to avoid no ten penalty
foldValue += 200 - (parseInt(tilesLeft / 4) * 100);
}
}
else if (tilePrio.shanten == 1 && strategy == STRATEGIES.GENERAL) {
waits = waits < 0.4 ? waits = 0.4 : waits;
waits = waits > 2 ? waits = 2 : waits;
var foldValue = waits * handScore / 45;
shape = shape < 0.4 ? shape = 0.4 : shape;
shape = shape > 2 ? shape = 2 : shape;
var foldValue = shape * handScore / 45;
}
else {
if (getCurrentDangerLevel() > 3000 && strategy == STRATEGIES.GENERAL) {
Expand Down Expand Up @@ -752,11 +753,15 @@ function shouldFold(tile, highestPrio = false) {
//Decide whether to call Riichi
//Based on: https://mahjong.guide/2018/01/28/mahjong-fundamentals-5-riichi/
function shouldRiichi(tilePrio) {
var badWait = tilePrio.waits < 6 - RIICHI;
var badWait = tilePrio.waits < 5 - RIICHI;
var lotsOfDoraIndicators = tilePrio.dora.length >= 3;

//Chiitoitsu
if (strategy == STRATEGIES.CHIITOITSU) {
if (tilePrio.shape == 0) {
log("Decline Riichi because of chiitoitsu wait that can be improved!");
return false;
}
badWait = tilePrio.waits < 3 - RIICHI;
}

Expand Down Expand Up @@ -791,19 +796,19 @@ function shouldRiichi(tilePrio) {
}

// Not Dealer & bad Wait & Riichi is only yaku
if (seatWind != 1 && badWait && tilePrio.score.riichi < 4000 - (RIICHI * 1000) && !lotsOfDoraIndicators) {
if (seatWind != 1 && badWait && tilePrio.score.riichi < 4000 - (RIICHI * 1000) && !lotsOfDoraIndicators && tilePrio.shape > 0.4) {
log("Decline Riichi because of worthless hand, bad waits and not dealer.");
return false;
}

// High Danger and hand not worth much or bad wait
if (getCurrentDangerLevel() > 5000 && (tilePrio.score.riichi < 5000 - (RIICHI * 1000) || badWait)) {
if (tilePrio.score.riichi < (getCurrentDangerLevel() - (RIICHI * 1000)) * (1 + badWait)) {
log("Decline Riichi because of worthless hand and high danger.");
return false;
}

// Hand already has enough yaku and high value (Around 6000+ depending on the wait)
if (tilePrio.yaku.closed >= 1 && tilePrio.score.closed > 4000 + (RIICHI * 1000) + (tilePrio.waits * 500)) {
if (tilePrio.yaku.closed >= 1 && tilePrio.score.closed / (seatWind == 1 ? 1.5 : 1) > 4000 + (RIICHI * 1000) + (tilePrio.waits * 500)) {
log("Decline Riichi because of high value hand with enough yaku.");
return false;
}
Expand Down

0 comments on commit 56861e4

Please sign in to comment.