日麻折騰筆記Java篇(2)-向聽數(shù)和牌效率
思考
向聽數(shù)比較通俗易懂的解釋是還差幾張牌聽牌,用程序能理解的話就是:
1.?將手牌的n張牌替換成任意一張牌,能夠滿足聽牌的n的最小值
2.?此時,手牌就是n向聽
向聽數(shù)可以由下面的公式快速算出(暫時不考慮七對和國士):
向聽數(shù)=8-2x(順子數(shù)量+刻子數(shù)量)-對子數(shù)量-搭子數(shù)量
所以只需要算出順子、刻子、對子以及搭子的數(shù)量即可
我在這里將對子單獨從搭子中列出來,本文以及本系列文章所說的“搭子”均不包含“對子”
但是要注意的是,很多牌形是有多種拆分方式的,諸如下面的示例
1234s
可以認(rèn)為是123
的順子加上孤張4
,也可以認(rèn)為是1
的孤張加上234
的順子,還可以認(rèn)為是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);
? ? }
}
相關(guān)測試代碼
@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());
}
牌效率是什么
經(jīng)營自己的手牌,以達(dá)成和牌為目標(biāo)
我們的任何操作(切牌、碰、吃等)都是為了最終能夠和牌服務(wù)的,也就是提高和了率。
在只考慮自摸的情況下,我們可以快速的計算出打哪張牌能夠使向聽數(shù)前進(jìn)、有效牌和改良牌變多。
有效牌
能夠減少向聽數(shù)的牌
通過向聽數(shù)的計算公式8-2*(面子數(shù))-對子數(shù)-搭子數(shù)
,我們發(fā)現(xiàn),能夠減少向聽數(shù)的牌有以下幾種:
能夠與搭子組成一個順子,此時面子數(shù)量+1,搭子數(shù)量-1,向聽數(shù)+1
能夠與對子組成一個刻子,此時面子數(shù)量+1,對子數(shù)量-1,向聽數(shù)+1
能夠與孤張組成一個對子,此時對子數(shù)量+1,向聽數(shù)+1
能夠與孤張組成一個搭子,此時搭子數(shù)量+1,向聽數(shù)+1
這樣一來就很清晰了,我們來編碼,計算以下兩種情況
在向聽數(shù)減少的情況下,打出哪張牌后的有效牌最多
在打出任何牌都不能導(dǎo)致向聽數(shù)減少的情況下,打出哪張牌后的有效牌最多(也就是指改良)
編碼
我們需要有個工具來記錄當(dāng)前玩家視角內(nèi)牌的數(shù)量情況:
/**
?* 對局中牌出現(xiàn)數(shù)量的計數(shù)器
?*/
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;
? ? }
? ? /**
? ? ?* 查詢當(dāng)前玩家視角內(nèi)某張牌已經(jīng)出現(xiàn)的數(shù)量
? ? ?* @param pai 需要查詢的牌
? ? ?* @return 數(shù)量
? ? ?*/
? ? public int getCount(Pai pai) {
? ? ? ? return paiCount.computeIfAbsent(pai, p -> new AtomicInteger(0)).get();
? ? }
? ? /**
? ? ?* 玩家新摸到、對手打出、或者吃碰杠等操作,導(dǎo)致當(dāng)前玩家視角內(nèi)出現(xiàn)新的牌,請調(diào)用此方法計數(shù)
? ? ?*
? ? ?* @param pai 新出現(xiàn)的牌
? ? ?*/
? ? public void addCount(Pai pai) {
? ? ? ? paiCount.computeIfAbsent(pai, p -> new AtomicInteger(0)).incrementAndGet();
? ? }
}
計算當(dāng)前手牌打出哪張后的有效進(jìn)張
/**
* 計算有效牌的工具類
*/
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);
? ? ? ? ? ? ? ? ? ?// 過濾出向聽數(shù)最少的
? ? ? ? ? ? ? ? ? ?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 向聽數(shù)=3 有效牌=30 {2m=4, 5m=4, 7m=2, 8m=4, 5p=4, 8p=4, 2s=2, 6s=4, 7s=2}
打=7z 向聽數(shù)=3 有效牌=30 {2m=4, 5m=4, 7m=2, 8m=4, 5p=4, 8p=4, 2s=2, 6s=4, 7s=2}
打=7m 向聽數(shù)=3 有效牌=28 {2m=4, 5m=4, 8m=4, 5p=4, 8p=4, 2s=2, 6s=4, 7s=2}
打=7m 向聽數(shù)=3 有效牌=28 {2m=4, 5m=4, 8m=4, 5p=4, 8p=4, 2s=2, 6s=4, 7s=2}
打=7s 向聽數(shù)=3 有效牌=28 {2m=4, 5m=4, 7m=2, 8m=4, 5p=4, 8p=4, 2s=2, 6s=4}
打=7s 向聽數(shù)=3 有效牌=28 {2m=4, 5m=4, 7m=2, 8m=4, 5p=4, 8p=4, 2s=2, 6s=4}
打=6m 向聽數(shù)=3 有效牌=26 {2m=4, 5m=4, 7m=2, 5p=4, 8p=4, 2s=2, 6s=4, 7s=2}
打=5s 向聽數(shù)=3 有效牌=26 {2m=4, 5m=4, 7m=2, 8m=4, 5p=4, 8p=4, 2s=2, 7s=2}
打=3m 向聽數(shù)=3 有效牌=22 {5m=4, 7m=2, 5p=4, 8p=4, 2s=2, 6s=4, 7s=2}
天鳳牌理
我們用天鳳牌理來驗證一下我們的程序是否正確:

可以看到是一致的