概要
仕事で2進数を使った判定処理について学んだので共有したくて執筆しました。
仕事上で設計したロジックは本記事で紹介するものより複雑でしたが、本質的な部分は変わらないので使って頂けると思います。
説明用にじゃんけんの判定ロジックを実装例としました。
テストコードも作成しましたので、併せてご覧頂ければと思います。
2進数についてはご存じである前提で進めさせていただきます。
もし理解に不安がありましたら、以下のサイトがわかり易いので参照ください。
ゆるゆるプログラミング – ビット演算
仕様
- じゃんけんの判定は3人以上に対応できること。
- 勝ったプレイヤーは勝ち数を保持すること。
実装
Handクラス
じゃんけんの手を保持するクラスです。
1 2 3 4 5 6 7 |
package main; public class Hand { public static final byte GU = 0b001; public static final byte CHOKI = 0b010; public static final byte PA = 0b100; } |
- じゃんけんの手
2進数で表現しています。
Playerクラス
じゃんけんのプレイヤークラスです。
じゃんけんの手と勝ち数を保持します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package main; public class Player { private byte _hand; private int _winCount; public Player setHand(byte hand) { _hand = hand; return this; } // じゃんけんの手を出すときの掛け声(ポン) public byte pon() { return _hand; } public int winCount() { return _winCount; } public void notifyResult(boolean result) { if(result == GameMaster.WIN) _winCount++; } } |
- notifyResultメソッド
進行役から勝ちを宣言された場合、自身の勝ち数を1つ増やします。
GameMasterクラス
じゃんけんゲームの進行役です。
じゃんけんの結果を判定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
package main; import java.util.Set; public class GameMaster { // 勝ち判定パターン private final byte GU_WIN = 0b011; private final byte CHOKI_WIN = 0b110; private final byte PA_WIN = 0b101; // 勝った場合に通知する値 public static final boolean WIN = true; public void judge(Set<Player> playerSet) { // 全プレイヤーが出した手を和集合として保持する // (ex)3人でプレイし、全員が別々の手を出した場合 // 1人目(GU :0b001) -> result(0b001) // 2人目(CHOKI:0b010) -> result(0b011) // 3人目(PA :0b100) -> result(0b111) byte result = 0b000; for (Player player: playerSet) { result = (byte) (result | player.pon()); } // 誰かが勝った場合、勝ち判定パターンに一致する手を出したプレイヤーに勝ちを通知する // (ex)グーとチョキだと、グーを出したプレイヤーが勝つ // GU(0b001) or CHOKI(0b010) -> result(0b011) // GU_WIN(0b011)に一致するので、GUを出したのプレイヤーに勝ちを通知する final byte finalResult = result; playerSet.stream() .filter(player -> player.pon() == winHand(finalResult)) .forEach(player -> player.notifyResult(WIN)); } private byte winHand(byte result) { if(result == GU_WIN) return Hand.GU; if(result == CHOKI_WIN) return Hand.CHOKI; if(result == PA_WIN) return Hand.PA; return 0; } } |
- judgeメソッド
- winHandメソッド
プレイヤー全員の手をビットフラグでresult変数に保持します。
– 全員が違う手を出せば、0b111になります。
– 勝敗の決まる手の組み合わせであれば、いずれかが0となります(※勝ち判定パターンのいずれかに該当)
– 勝った手を出したプレイヤーには、勝ち数を増やすように勝ちを通知します。
じゃんけんした結果がいずれかの勝ちパターンになる場合、勝ちになる手を返します。
誰も勝っていない場合、0を返すことで誰も勝ち判定になりません。
プレイヤーの人数がどれだけ増えても、result変数の結果が和集合で集約されるだけなので、判定するプレイヤーの手をequalsで判定したりする必要が無く、非常にシンプルになります!
テストコード
TestGameMaster
主に進行役(GameMasterクラス)のじゃんけん判定(judgeメソッド)のテストコードです。
プレイヤーが2人の場合と3人の場合でテストケースを作成しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
package test; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.*; import java.util.HashSet; import java.util.Set; import java.util.stream.IntStream; import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import main.GameMaster; import main.Hand; import main.Player; class TestGameMaster { @ParameterizedTest @MethodSource("handProvider") void 自分の手を決める(byte hand, byte expected) { Player self = new Player(); self.setHand(hand); assertThat(self.pon(), is(expected)); } private static Stream<Arguments> handProvider() { return Stream.of( Arguments.of(Hand.GU, (byte)0b001), Arguments.of(Hand.CHOKI, (byte)0b010), Arguments.of(Hand.PA, (byte)0b100) ); } @ParameterizedTest @MethodSource("judge2HandsProvider") void じゃんけんの結果を判定する_2人(byte winSide, byte loseSide) { GameMaster gm = new GameMaster(); Player winSidePlayer = new Player(); Player loseSidePlayer = new Player(); winSidePlayer.setHand(winSide); loseSidePlayer.setHand(loseSide); Set<Player> playerSet = new HashSet<>(); playerSet.add(winSidePlayer); playerSet.add(loseSidePlayer); gm.judge(playerSet); assertThat(winSidePlayer.winCount(), is(1)); } private static Stream<Arguments> judge2HandsProvider() { return Stream.of( Arguments.of(Hand.GU, Hand.CHOKI), Arguments.of(Hand.CHOKI, Hand.PA), Arguments.of(Hand.PA, Hand.GU) ); } @ParameterizedTest @MethodSource("judge3HandsProvider") void じゃんけんの結果を判定する_3人(int expected, byte... hands) { GameMaster gm = new GameMaster(); Set<Player> playerSet = new HashSet<>(); IntStream.range(0, hands.length) .forEach(index -> playerSet.add(new Player().setHand((byte)hands[index]))); gm.judge(playerSet); assertThat(playerSet.stream() .mapToInt(player -> player.winCount()) .sum(), is(expected)); } private static Stream<Arguments> judge3HandsProvider() { byte[] drow_all = {Hand.GU, Hand.GU, Hand.GU}; byte[] win_1player = {Hand.GU, Hand.CHOKI, Hand.CHOKI}; byte[] win_2player = {Hand.CHOKI, Hand.CHOKI, Hand.PA}; byte[] drow_all_mismatch = {Hand.GU, Hand.CHOKI, Hand.PA}; return Stream.of( Arguments.of(0, drow_all ), Arguments.of(1, win_1player), Arguments.of(2, win_2player), Arguments.of(0, drow_all_mismatch) ); } } |
- 自分の手を決める
- じゃんけんの結果を判定する_2人
- じゃんけんの結果を判定する_3人
Handクラスの手がプレイヤーに正しくセットされたかをテストしています。
テストデータとして、勝つ方と負ける方のプレイヤーがいます。
勝つ方のプレイヤーの勝ち数が増えていることで動作の正当性をテストしています。
3人いる場合、引き分けが2パターンと勝ちが2パターンあります。
– 引き分け:全員が違う手、または全員が同じ手
– 勝ち :1人だけ勝つ、または2人が勝つ
これらも、プレイヤーの勝ち数を数えることで動作の正当性をテストしています。
※プレイヤーが4人、5人と増えても試験のパターンは3人の場合と基本的に変わらないので実施していません。
まとめ
- 2進数を使うと最終的な結果である和集合(論理和)で判定が容易にできる。
- テストコードを書くことで、ロジックの正当性を試験できる。
2進数の判定を使うシーンはあまりないかもしれませんが、複雑なドメインや判定処理で利用できそうなシーンがありましたら、ぜひ活用してみて下さい!
コメント