日麻折腾笔记Java篇(2)-向听数和牌效率
思考
向听数比较通俗易懂的解释是还差几张牌听牌,用程序能理解的话就是:
1. 将手牌的n张牌替换成任意一张牌,能够满足听牌的n的最小值
2. 此时,手牌就是n向听
向听数可以由下面的公式快速算出(暂时不考虑七对和国士):
向听数=8-2x(顺子数量+刻子数量)-对子数量-搭子数量
所以只需要算出顺子、刻子、对子以及搭子的数量即可
我在这里将对子单独从搭子中列出来,本文以及本系列文章所说的“搭子”均不包含“对子”
但是要注意的是,很多牌形是有多种拆分方式的,诸如下面的示例
1234s
可以认为是123
的顺子加上孤张4
,也可以认为是1
的孤张加上234
的顺子,还可以认为是12
和34
两个搭子……
我完善了上一章中的代码,将对子、刻子、顺子等概念抽象成类,每种拆分情况都由这四种组成,部分示例代码如下
package io.****.mj.util.analyze;
import io.****.mj.util.HandPai;
import java.util.ArrayList;
import java.util.List;
/**
* 手牌分解的每种组合
* 每种组合都由a个顺子、b个刻子、c个对子、d个搭子、e个孤张组合而成
*/
public class SplitCondition {
private List<Duizi> duiziList = new ArrayList<>();
private List<Shunzi> shunziList = new ArrayList<>();
private List<Dazi> daziList = new ArrayList<>();
private List<Kezi> keziList = new ArrayList<>();
private List<HandPai> guzhangList = new ArrayList<>();
public int xiangTing() {
return 8 - 2 * (keziList.size() + shunziList.size()) - daziList.size() - duiziList.size();
}
public void addDuizi(Duizi duizi) {
duiziList.add(duizi);
}
public void addShunzi(Shunzi shunzi) {
shunziList.add(shunzi);
}
public void addDazi(Dazi dazi) {
daziList.add(dazi);
}
public void addKezi(Kezi kezi) {
keziList.add(kezi);
}
public void addGuzhang(HandPai guzhang) {
guzhangList.add(guzhang);
}
public List<Duizi> getDuiziList() {
return duiziList;
}
public List<Shunzi> getShunziList() {
return shunziList;
}
public List<Dazi> getDaziList() {
return daziList;
}
public List<Kezi> getKeziList() {
return keziList;
}
public List<HandPai> getGuzhangList() {
return guzhangList;
}
public void setGuzhangList(List<HandPai> guzhangList) {
this.guzhangList = guzhangList;
}
public SplitCondition copy() {
SplitCondition condition = new SplitCondition();
condition.duiziList = new ArrayList<>(getDuiziList());
condition.daziList = new ArrayList<>(getDaziList());
condition.keziList = new ArrayList<>(getKeziList());
condition.shunziList = new ArrayList<>(getShunziList());
return condition;
}
@Override
public String toString() {
return "[" +
duiziList +
shunziList +
daziList +
keziList +
guzhangList + "]";
}
}
package io.****.mj.util.analyze;
import io.****.mj.util.HandAnalyze;
import io.****.mj.util.HandPai;
import io.****.mj.util.Pai;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class PaiSplitter {
public static List<SplitCondition> split(String str) {
List<HandPai> handPais = HandAnalyze.strToHandPai(str);
List<SplitCondition> conditions = new ArrayList<>();
return split(handPais, conditions, new SplitCondition(), 0);
}
private static List<SplitCondition> split(List<HandPai> handPais, List<SplitCondition> conditions, SplitCondition condition, int depth) {
if (handPais.size() <= 1) {
conditions.add(condition);
return conditions;
}
HandPai current = null;
HandPai next = null;
HandPai nextNext = null;
current = handPais.get(0);
next = handPais.get(1);
if (0 != handPais.size() - 2) {
nextNext = handPais.get(2);
}
// 处理对子
if (current.getPai() == next.getPai()) {
handleDuizi(handPais, conditions, condition.copy(), current, next, depth + 1);
}
// 处理刻子
if (nextNext != null) {
if (current.getPai() == next.getPai() && next.getPai() == nextNext.getPai()) {
handleKezi(handPais, conditions, condition.copy(), current, next, nextNext, depth + 1);
}
}
Pai currentPlusOne = current.getPai().next(false);
if (currentPlusOne != null) {
List<HandPai> filtered = handPais.stream().filter(p -> p.getPai() == currentPlusOne).collect(Collectors.toList());
if (filtered.size() > 0) {
HandPai second = filtered.get(0);
handleDazi(handPais, conditions, condition.copy(), current, second, depth + 1);
Pai currentPlusTwo = currentPlusOne.next(false);
filtered = handPais.stream().filter(p -> p.getPai() == currentPlusTwo).collect(Collectors.toList());
if (filtered.size() > 0) {
HandPai third = filtered.get(0);
handleShunzi(handPais, conditions, condition.copy(), current, second, third, depth + 1);
}
}
}
handleGuzhang(handPais, conditions, condition.copy(), current, depth + 1);
return conditions;
}
private static void handleGuzhang(List<HandPai> currentHandPai, List<SplitCondition> conditions, SplitCondition condition, HandPai first, int depth) {
currentHandPai = new ArrayList<>(currentHandPai);
condition.addGuzhang(first);
currentHandPai.remove(first);
split(currentHandPai, conditions, condition, depth);
}
private static void handleDazi(List<HandPai> currentHandPai, List<SplitCondition> conditions, SplitCondition condition, HandPai first, HandPai second, int depth) {
currentHandPai = new ArrayList<>(currentHandPai);
Dazi dazi = new Dazi(Arrays.asList(first, second));
condition.addDazi(dazi);
currentHandPai.remove(first);
currentHandPai.remove(second);
split(currentHandPai, conditions, condition, depth);
}
private static void handleDuizi(List<HandPai> currentHandPai, List<SplitCondition> conditions, SplitCondition condition, HandPai first, HandPai second, int depth) {
currentHandPai = new ArrayList<>(currentHandPai);
Duizi duizi = new Duizi(Arrays.asList(first, second));
condition.addDuizi(duizi);
currentHandPai.remove(first);
currentHandPai.remove(second);
split(currentHandPai, conditions, condition, depth);
}
private static void handleShunzi(List<HandPai> currentHandPai, List<SplitCondition> conditions, SplitCondition condition, HandPai first, HandPai second,
HandPai third, int depth) {
currentHandPai = new ArrayList<>(currentHandPai);
Shunzi shunzi = new Shunzi(Arrays.asList(first, second, third));
condition.addShunzi(shunzi);
currentHandPai.remove(first);
currentHandPai.remove(second);
currentHandPai.remove(third);
split(currentHandPai, conditions, condition, depth);
}
private static void handleKezi(List<HandPai> currentHandPai, List<SplitCondition> conditions, SplitCondition condition, HandPai first, HandPai second,
HandPai third, int depth) {
currentHandPai = new ArrayList<>(currentHandPai);
Kezi kezi = new Kezi(Arrays.asList(first, second, third));
condition.addKezi(kezi);
currentHandPai.remove(first);
currentHandPai.remove(second);
currentHandPai.remove(third);
split(currentHandPai, conditions, condition, depth);
}
}
相关测试代码
@Test
public void splitDuizi() {
List<SplitCondition> split = PaiSplitter.split("11223344556677s");
Assert.assertTrue(split.stream().anyMatch(c -> c.getDuiziList().size() == 7));
}
@Test
public void testHe() {
List<SplitCondition> split = PaiSplitter.split("11223344556677s");
Assert.assertTrue(split.stream().mapToInt(SplitCondition::xiangTing)
.min().getAsInt() < 1);
}
@Test
public void testKeziAndShunzi() {
List<SplitCondition> split = PaiSplitter.split("11155s134m579p122z");
Assert.assertEquals(3, split.stream().mapToInt(SplitCondition::xiangTing)
.min().getAsInt());
}
牌效率是什么
经营自己的手牌,以达成和牌为目标
我们的任何操作(切牌、碰、吃等)都是为了最终能够和牌服务的,也就是提高和了率。
在只考虑自摸的情况下,我们可以快速的计算出打哪张牌能够使向听数前进、有效牌和改良牌变多。
有效牌
能够减少向听数的牌
通过向听数的计算公式8-2*(面子数)-对子数-搭子数
,我们发现,能够减少向听数的牌有以下几种:
能够与搭子组成一个顺子,此时面子数量+1,搭子数量-1,向听数+1
能够与对子组成一个刻子,此时面子数量+1,对子数量-1,向听数+1
能够与孤张组成一个对子,此时对子数量+1,向听数+1
能够与孤张组成一个搭子,此时搭子数量+1,向听数+1
这样一来就很清晰了,我们来编码,计算以下两种情况
在向听数减少的情况下,打出哪张牌后的有效牌最多
在打出任何牌都不能导致向听数减少的情况下,打出哪张牌后的有效牌最多(也就是指改良)
编码
我们需要有个工具来记录当前玩家视角内牌的数量情况:
/**
* 对局中牌出现数量的计数器
*/
public class PaiCounter {
private Map<Pai, AtomicInteger> paiCount = new HashMap<>();
public PaiCounter() {
Pai.ALL.forEach(p -> {
paiCount.put(p, new AtomicInteger(0));
});
}
public PaiCounter(Map<Pai, AtomicInteger> paiCount) {
this.paiCount = paiCount;
}
/**
* 查询当前玩家视角内某张牌已经出现的数量
* @param pai 需要查询的牌
* @return 数量
*/
public int getCount(Pai pai) {
return paiCount.computeIfAbsent(pai, p -> new AtomicInteger(0)).get();
}
/**
* 玩家新摸到、对手打出、或者吃碰杠等操作,导致当前玩家视角内出现新的牌,请调用此方法计数
*
* @param pai 新出现的牌
*/
public void addCount(Pai pai) {
paiCount.computeIfAbsent(pai, p -> new AtomicInteger(0)).incrementAndGet();
}
}
计算当前手牌打出哪张后的有效进张
/**
* 计算有效牌的工具类
*/
public class EffectSelector {
public static List<EffectCondition> calculate(String pais, PaiCounter counter) {
List<HandPai> handPais = HandAnalyze.strToHandPai(pais);
handPais.forEach(p -> {
counter.addCount(p.getPai());
});
return handPais.stream()
.map(p -> {
EffectCondition condition = new EffectCondition();
condition.setThro(p);
List<HandPai> tmp = new ArrayList<>(handPais);
tmp.remove(p);
List<SplitCondition> split = PaiSplitter.split(tmp, 0);
// 过滤出向听数最少的
int min = split.stream().mapToInt(SplitCondition::xiangTing).min().getAsInt();
split = split.stream()
.filter(d -> d.xiangTing() == min)
.collect(Collectors.toList());
Map<Pai, Integer> collect = calculate(split)
.stream().map(a -> new AbstractMap.SimpleEntry<>(a, 4 - counter.getCount(a)))
.collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue));
condition.setEffectivePais(new TreeMap<>(collect));
return condition;
}).sorted((o1, o2) -> o2.getEffectivePais().values().stream().mapToInt(d -> d).sum()
- o1.getEffectivePais().values().stream().mapToInt(d -> d).sum()).collect(Collectors.toList());
}
public static List<Pai> calculate(List<SplitCondition> conditions) {
return conditions.stream().map(d -> {
List<Pai> daziData = d.getDaziList().stream()
.flatMap(m -> {
List<HandPai> tmp = m.getData();
Collections.sort(tmp);
HandPai first = m.getData().get(0);
HandPai second = m.getData().get(1);
List<Pai> res = new ArrayList<>();
if (first.getPai().getNumber() == second.getPai().getNumber() - 1) {
res.add(first.getPai().prev(false));
res.add(second.getPai().next(false));
} else {
res.add(first.getPai().next(false));
}
return res.stream().filter(Objects::nonNull);
}).collect(Collectors.toList());
List<Pai> duiziData = d.getDuiziList()
.stream()
.map(a -> a.getData().get(0).getPai()).collect(Collectors.toList());
List<Pai> guzhangData = d.getGuzhangList()
.stream()
.flatMap(a -> {
if (a.getPai().getType() == Type.z)
return Stream.of(a.getPai());
else
return Stream.of(a.getPai(), a.getPai().next(false), a.getPai().prev(false));
})
.filter(Objects::nonNull).collect(Collectors.toList());
return Stream.of(daziData, duiziData)
.flatMap(List::stream);
}).flatMap(d -> d).distinct().collect(Collectors.toList());
}
}
测试代码
EffectSelector
.calculate("34677m67p22577s27z", new PaiCounter())
.forEach(System.out::println);
输出如下
打=2z 向听数=3 有效牌=30 {2m=4, 5m=4, 7m=2, 8m=4, 5p=4, 8p=4, 2s=2, 6s=4, 7s=2}
打=7z 向听数=3 有效牌=30 {2m=4, 5m=4, 7m=2, 8m=4, 5p=4, 8p=4, 2s=2, 6s=4, 7s=2}
打=7m 向听数=3 有效牌=28 {2m=4, 5m=4, 8m=4, 5p=4, 8p=4, 2s=2, 6s=4, 7s=2}
打=7m 向听数=3 有效牌=28 {2m=4, 5m=4, 8m=4, 5p=4, 8p=4, 2s=2, 6s=4, 7s=2}
打=7s 向听数=3 有效牌=28 {2m=4, 5m=4, 7m=2, 8m=4, 5p=4, 8p=4, 2s=2, 6s=4}
打=7s 向听数=3 有效牌=28 {2m=4, 5m=4, 7m=2, 8m=4, 5p=4, 8p=4, 2s=2, 6s=4}
打=6m 向听数=3 有效牌=26 {2m=4, 5m=4, 7m=2, 5p=4, 8p=4, 2s=2, 6s=4, 7s=2}
打=5s 向听数=3 有效牌=26 {2m=4, 5m=4, 7m=2, 8m=4, 5p=4, 8p=4, 2s=2, 7s=2}
打=3m 向听数=3 有效牌=22 {5m=4, 7m=2, 5p=4, 8p=4, 2s=2, 6s=4, 7s=2}
天凤牌理
我们用天凤牌理来验证一下我们的程序是否正确:

可以看到是一致的