美文网首页
编程kata,抽丝剥茧PokerHands

编程kata,抽丝剥茧PokerHands

作者: hxfirefox | 来源:发表于2018-06-24 22:39 被阅读41次

我喜欢玩游戏,更喜欢搜集和练习游戏类的编程kata,因为这种kata会让我在练习过程中体会到十足的乐趣,前面的编程道场介绍了一个经典kata——FizzBuzz游戏,这次我们要认识另一个kata——PokerHands游戏。

背景介绍

来自赌神的凝视

PokerHands取材自德州扑克,52张扑克分为4个花色(用C、D、H、S表示),扑克牌面最小为2,最大为Ace,表示为2、3、4、5、6、7、8、9、T、J、Q、K、A。一轮游戏中,玩家会持有5张牌,按照规则(同花顺、四只、三带二、同花、顺子、三只、两对、一对、散牌)进行比较,其中同花顺最大散牌最小,而当遇到相同牌面值时,则比较剩余牌的大小。

有了之前FizzBuzz的经历,我们知道对这种多规则并且规则与规则间存在优先级的kata,可以采用策略模式或者责任链模式来实现,有了这一基本思路就可以开始练习了。

实现

上面提到PokerHands的练习目标是使用设计模式完成游戏规则的编码,先从优先级最低的散牌开始,散牌的规则比较容易理解,简单的说就是比大小,但别看这一句比大小,其实包含了几个概念:

  • 两张牌相等如何判断
  • 两张牌大小如何判断

显然我们要实现的代码需要具备这两个功能,结合牌的描述,可以设计出下面的Poker类。

public class Poker implements Comparable<Poker> {
    private final Suit suit;
    private final int value;

    public Poker(Suit suit, int value) {
        this.suit = suit;
        this.value = value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Poker poker = (Poker) o;

        return value == poker.value;

    }

    ...

    @Override
    public int compareTo(Poker o) {
        return Integer.compare(this.value, o.value);
    }
}

散牌只是在Poker的基础上实现多个Poker的比较,由于是比较一手牌中最大,所以排序就可以快速帮助我们完成散牌规则。

public class HighCard implements Rule {
    @Override
    public int compare(List<Poker> white, List<Poker> black) {
        final List<Integer> sortedWhite = collect(white);
        final List<Integer> sortedBlack = collect(black);

        return IntStream.range(0, 5)
                .map(i -> sortedWhite.get(i).compareTo(sortedBlack.get(i)))
                .filter(c -> c != 0).findFirst().orElse(0);
    }

    @Override
    public List<Integer> collect(List<Poker> pokers) {
        return pokers.stream().map(Poker::getValue)
                .sorted(Comparator.reverseOrder())
                .collect(Collectors.toList());
    }
}

一对与两对规则非常相似,都是从一手牌中找出重复并比较重复牌的大小,因此可以引入模板来实现对相同功能的编制,唯一需要区分的是对子的个数。

public abstract class Pair implements Rule {
    @Override
    public int compare(List<Poker> white, List<Poker> black) {
        return compareWithPair(white, black, getPairCount());
    }

    protected abstract int getPairCount();

    private int compareWithPair(List<Poker> white, List<Poker> black, int pairCount) {
        final List<Integer> pairWhite = collect(white);
        final List<Integer> pairBlack = collect(black);
        final int pairWhiteSize = pairWhite.size();
        final int pairBlackSize = pairBlack.size();
        if (pairWhite.size() < pairCount && pairBlackSize < pairCount)
            return 0;
        if (pairWhiteSize == pairCount && pairBlackSize < pairCount)
            return 1;
        if (pairWhiteSize < pairCount && pairBlackSize == pairCount)
            return -1;

        return IntStream.range(0, pairCount)
                .map(i -> pairWhite.get(i).compareTo(pairBlack.get(i)))
                .filter(c -> c != 0).findFirst().orElse(0);
    }

    @Override
    public List<Integer> collect(List<Poker> pokers) {
        Set<Integer> set = new HashSet<>();
        return pokers.stream().map(Poker::getValue).filter(v -> !set.add(v))
                .sorted(Comparator.reverseOrder()).collect(Collectors.toList());
    }
}

沿这个思路继续,很快地就能得到三只、顺子和同花的规则代码,加上前面的散牌和对子,便可以组合和推导出四只、三拖二以及同花顺,例如三拖二就是三只和对子的组合。

public class FullHouse implements Rule {
    private ThreeofaKind three = new ThreeofaKind();
    private OnePair pair = new OnePair();

    @Override
    public int compare(List<Poker> white, List<Poker> black) {
        final List<Integer> whiteFullHouse = collect(white);
        final List<Integer> blackFullHouse = collect(black);
        final int whiteSize = whiteFullHouse.size();
        final int blackSize = blackFullHouse.size();
        if (whiteSize < 2 && blackSize < 2)
            return 0;
        if (whiteSize == 2 && blackSize < 2)
            return 1;
        if (whiteSize < 2 && blackSize == 2)
            return -1;
        return three.compare(white, black);
    }

    @Override
    public List<Integer> collect(List<Poker> pokers) {
        final List<Integer> threePokers = three.collect(pokers);
        final List<Integer> pairPokers = pair.collect(pokers);
        threePokers.addAll(pairPokers);
        return threePokers;
    }
}

思考

在规则实现的过程中,尽管使用了设计模式,但是仍然会有不少看上去十分相似的代码,如何去消除这些重复呢?

另外每增加一个规则就会增加一个类,在规则较少时这些类看上去非常规整和漂亮,但当达到一定数量时,如此多的类就会让代码结构看上去非常“庞大”,有没有什么办法能够控制一下类的数量呢?

要回答这两个问题,需要重新考察分析一下游戏中的规则,将规则分组归类之后得到下面的表达式,这些表达式用于描述玩家手中的牌可以归属哪种规则。从表达式可以看到不少规则是在计算值出现频率,例如两对按这个表达式的解读是出现2组值出现2次的牌,而三只就是出现1组值出现3次的牌;此外还有计算花色和牌面差值的规则,唯一有点特殊的是散牌,似乎是其他规则的反例,但实际上散牌可以理解为5组值出现1次的牌。

high card       -> high card
pair            -> 1*(值出现*2)
two pair        -> 2*(值出现*2)
three of a kind -> 1*(值出现*3)
flush           -> 花色相同
straight        -> 值间差值=1
four of a kind  -> 1*(值出现*4)
full house      -> 1*(值出现*3)+1*(值出现*2)
straight flush  -> 花色相同+值间差值=1

经过上面的分析,规则对于牌面的描述可以总结为三条规则:

  • 差值,即是否满足等差的牌面
  • 频率,即是否存在按指定次数分布的牌面
  • 花色,即是否花色一致
@FunctionalInterface
public interface Matcher {
    boolean match(List<Poker> pokers);

    static Matcher duplicate(int frequency, int groupCount) {
        return pokers -> {
            final HashSet<Poker> set = new HashSet<>(pokers);
            return set.stream().filter(p -> Collections.frequency(pokers, p) == frequency)
                    .collect(Collectors.toList()).size() == groupCount;
        };
    }

    static Matcher dvalue(int value) {
        return pokers -> {
            final List<Poker> orderPokers = pokers.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
            return orderPokers.get(0).getValue() - orderPokers.get(orderPokers.size() - 1).getValue() == value;
        };
    }

    static Matcher samesuit() {
        return pokers -> {
            final HashSet<Suit> set = new HashSet<>(pokers.stream().map(Poker::getSuit).collect(Collectors.toList()));
            return set.size() == 1;
        };
    }

    static Matcher select(Matcher... matchers) {
        return pokers -> Arrays.stream(matchers).allMatch(m -> m.match(pokers));
    }
}

不同的牌面描述对应了不同特征牌搜集方式和比较方式,归纳特征牌的搜集和比较方法,得到下面的表达式。

high card       -> 排序
pair            -> 对子值 + 余牌排序
two pair        -> 2组对子值(排序) + 余牌排序
three of a kind -> 3只值 + 余牌排序
flush           -> 排序
straight        -> 排序
four of a kind  -> 4只值 + 余牌排序
full house      -> 3只值 + 对子值
straight flush  -> 排序

可见牌面搜集主要依靠排序和特征值提取(按频率提取),而在牌面进行比较时,实际上是一种散牌比较的变形。散牌比较排序后按从大到小进行比较,而非散牌的牌面比较也需要排序,特征值排序结合余牌排序,而特征值又始终位于余牌之前,是一种逻辑上的大值,例如:牌面2H 5S 9C 2D 7H,可以简化为2 9 7 5,这就是一种特征值+余牌排序的表达。

@FunctionalInterface
public interface Assemble {
    List<Integer> collect(List<Poker> pokers);

    static Assemble sortmap() {
        return pokers -> pokers.stream().map(Poker::getValue)
                .sorted(Comparator.reverseOrder()).collect(Collectors.toList());
    }

    static Assemble dupmap(int times) {
        return pokers -> {
            HashSet<Poker> set = new HashSet<>(pokers);
            return set.stream().filter(p -> Collections.frequency(pokers, p) == times)
                    .map(Poker::getValue).sorted(Comparator.reverseOrder()).collect(Collectors.toList());
        };
    }

    static Assemble rest(Assemble assemble) {
        return pokers -> {
            final List<Integer> list = assemble.collect(pokers);
            return pokers.stream().map(Poker::getValue).filter(v->!list.contains(v))
                    .sorted(Comparator.reverseOrder()).collect(Collectors.toList());
        };

    }

    static Assemble combine(Assemble a1, Assemble a2) {
        return pokers -> {
            final List<Integer> l1 = a1.collect(pokers);
            final List<Integer> l2 = a2.collect(pokers);
            l1.addAll(l2);
            return l1;
        };
    }
}

按照上面的分析和推导,最终形成的规则样式如下,它通过一组牌面描述和牌面搜集描述来刻画一条规则。

with(select(dvalue(4), duplicate(1, 5), samesuit()), to(STRAIGHT_FLUSH.getType(), sortmap())),
with(duplicate(4, 1), to(FOUR_OF_A_KIND.getType(), dupmap(4))),
with(select(duplicate(3, 1), duplicate(2, 1)), to(FULL_HOUSE.getType(), combine(dupmap(3), dupmap(2)))),
with(samesuit(), to(FLUSH.getType(), sortmap())),
with(select(dvalue(4), duplicate(1, 1)), to(STRAIGHT.getType(), sortmap())),
with(duplicate(3, 1), to(THREE_OF_A_KIND.getType(), combine(dupmap(3), rest(dupmap(3))))),
with(duplicate(2, 2), to(TWO_PAIRS.getType(), combine(dupmap(2), rest(dupmap(2))))),
with(duplicate(2, 1), to(PAIR.getType(), combine(dupmap(2), rest(dupmap(2))))),
with(duplicate(1, 5), to(HIGH_CARD.getType(), sortmap()))

显然在经历了这一番的变化之后,代码的结构发生的变化,原来看似重复的代码变成了可任意组合的小块,原来庞大的规则类也化成了一条条更加直观的规则描述。

结语

PokerHands的好玩之处在于你明知道它的套路,却仍然能够在练习的过程中发掘出更多有意思的东西,这便是kata的魅力所在,你永远能够从中获取自己的kata感悟。

同时上面这些变化和思考并不是一次修订带来,而是在写代码的过程中逐步思考得到,边练边想边调整,不断地重构我们的代码设计,这就是kata的精妙所在,它总是精心设计的,以期触发更深的思考。

相关文章

网友评论

      本文标题:编程kata,抽丝剥茧PokerHands

      本文链接:https://www.haomeiwen.com/subject/wpjdyftx.html