commonMain.mahjongutils.shanten.FuroChanceShanten.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mahjong-utils-jvm Show documentation
Show all versions of mahjong-utils-jvm Show documentation
Mahjong Utils (for Japanese Riichi Mahjong)
package mahjongutils.shanten
import mahjongutils.CalcContext
import mahjongutils.models.Chi
import mahjongutils.models.Kan
import mahjongutils.models.Kanchan
import mahjongutils.models.Penchan
import mahjongutils.models.Pon
import mahjongutils.models.Ryanmen
import mahjongutils.models.TatsuType
import mahjongutils.models.Tile
import mahjongutils.models.TileType
import mahjongutils.models.countAsCodeArray
import mahjongutils.shanten.helpers.normalizeTiles
import kotlin.math.min
internal data class InternalFuroChanceShantenArgs(
val tiles: List,
val chanceTile: Tile,
val allowChi: Boolean = true,
val calcAdvanceNum: Boolean = true,
val bestShantenOnly: Boolean = false,
val allowKuikae: Boolean = false
)
/**
* 副露判断向听分析
* @param tiles 门前的牌
* @param chanceTile 副露机会牌(能够吃、碰的牌)
* @param allowChi 是否允许吃
* @param bestShantenOnly 仅计算最优向听数的打法(不计算退向打法)
* @param allowKuikae 是否允许食替
* @return 向听分析结果(其中shantenInfo必定为ShantenWithFuroChance类型)
*/
fun furoChanceShanten(
tiles: List,
chanceTile: Tile,
allowChi: Boolean = true,
bestShantenOnly: Boolean = false,
allowKuikae: Boolean = false
): FuroChanceShantenResult {
return furoChanceShanten(
FuroChanceShantenArgs(
tiles = tiles,
chanceTile = chanceTile,
allowChi = allowChi,
bestShantenOnly = bestShantenOnly,
allowKuikae = allowKuikae
)
)
}
fun furoChanceShanten(
args: FuroChanceShantenArgs
): FuroChanceShantenResult {
args.throwOnValidationError()
val internalShantenArgs = InternalFuroChanceShantenArgs(
tiles = args.tiles,
chanceTile = args.chanceTile,
allowChi = args.allowChi,
bestShantenOnly = args.bestShantenOnly,
allowKuikae = args.allowKuikae,
)
val context = CalcContext()
return context.furoChanceShanten(internalShantenArgs)
}
internal fun CalcContext.furoChanceShanten(
args: InternalFuroChanceShantenArgs
): FuroChanceShantenResult = memo(Pair("furoChanceShanten", args)) {
with(args) {
val tiles = normalizeTiles(tiles)
val tilesCount = tiles.countAsCodeArray()
val passShanten = regularShanten(InternalShantenArgs(tiles))
val pass = passShanten.shantenInfo.asWithoutGot
val canRon = pass.shantenNum == 0 && chanceTile in pass.advance
// 碰
val pon = if (tilesCount[chanceTile.code] >= 2) {
val tilesAfterPon = tiles - chanceTile - chanceTile
val shantenAfterPon = regularShanten(
InternalShantenArgs(
tilesAfterPon,
listOf(Pon(chanceTile)),
calcAdvanceNum = calcAdvanceNum,
bestShantenOnly = bestShantenOnly,
allowAnkan = false
)
)
shantenAfterPon.shantenInfo.asWithGot
} else {
null
}
// 吃
val chi = if (allowChi && chanceTile.type != TileType.Z) {
val materials = buildList {
// 边张
if (chanceTile.num == 3 && tilesCount[chanceTile.code - 1] >= 1 && tilesCount[chanceTile.code - 2] >= 1) {
add(Penchan(chanceTile.advance(-2)))
}
if (chanceTile.num == 7 && tilesCount[chanceTile.code + 1] >= 1 && tilesCount[chanceTile.code + 2] >= 1) {
add(Penchan(chanceTile.advance(1)))
}
// 两面
if (chanceTile.num in 4..9 && tilesCount[chanceTile.code - 1] >= 1 && tilesCount[chanceTile.code - 2] >= 1) {
add(Ryanmen(chanceTile.advance(-2)))
}
if (chanceTile.num in 1..6 && tilesCount[chanceTile.code + 1] >= 1 && tilesCount[chanceTile.code + 2] >= 1) {
add(Ryanmen(chanceTile.advance(1)))
}
// 坎张
if (chanceTile.num in 2..8 && tilesCount[chanceTile.code - 1] >= 1 && tilesCount[chanceTile.code + 1] >= 1) {
add(Kanchan(chanceTile.advance(-1)))
}
}
materials.associateWith { tt ->
val tilesAfterChi = tiles - tt.first - tt.second
val shantenAfterChi = regularShanten(
InternalShantenArgs(
tilesAfterChi,
listOf(Chi((tt.withWaiting(chanceTile)).tile)),
calcAdvanceNum = calcAdvanceNum,
bestShantenOnly = bestShantenOnly,
allowAnkan = false
)
)
val shantenInfo = shantenAfterChi.shantenInfo.asWithGot
if (!allowKuikae) {
val discardToAdvance = shantenInfo.discardToAdvance.filterKeys {
it != chanceTile && (tt.type != TatsuType.Ryanmen || (
chanceTile > tt.second && it != tt.first.advance(-1)
|| chanceTile < tt.second && it != tt.second.advance(1))
)
}
shantenInfo.copy(
shantenNum = discardToAdvance.values.minOfOrNull { it.shantenNum } ?: 9999,
discardToAdvance = discardToAdvance
)
} else {
shantenInfo
}
}.filterValues { it.discardToAdvance.isNotEmpty() }
} else {
emptyMap()
}
// minkan
val minkan = if (tilesCount[chanceTile.code] >= 3) {
val tilesAfterMinkan = tiles - chanceTile - chanceTile - chanceTile
val shantenAfterMinkan = regularShanten(
InternalShantenArgs(
tilesAfterMinkan,
listOf(Kan(chanceTile)),
calcAdvanceNum = calcAdvanceNum,
bestShantenOnly = bestShantenOnly,
allowAnkan = false
)
)
shantenAfterMinkan.shantenInfo.asWithoutGot
} else {
null
}
var shantenNum: Int
if (canRon) {
shantenNum = -1
} else {
shantenNum = pass.shantenNum
chi.minOfOrNull { it.value.shantenNum }?.let {
shantenNum = min(shantenNum, it)
}
pon?.shantenNum?.let {
shantenNum = min(shantenNum, it)
}
minkan?.shantenNum?.let {
shantenNum = min(shantenNum, it)
}
}
return if (bestShantenOnly) {
val pass_ = if (pass.shantenNum == shantenNum) pass else null
val chi_ = chi.filterValues { it.shantenNum == shantenNum }
val pon_ = if (pon?.shantenNum == shantenNum) pon else null
val minkan_ = if (minkan?.shantenNum == shantenNum) minkan else null
FuroChanceShantenResult(
hand = passShanten.hand,
shantenInfo = ShantenWithFuroChance(shantenNum, canRon, pass_, chi_, pon_, minkan_)
)
} else {
FuroChanceShantenResult(
hand = passShanten.hand,
shantenInfo = ShantenWithFuroChance(shantenNum, canRon, pass, chi, pon, minkan)
)
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy