Skip to content

ポーカーゲーム作成Step5 ~完成~

全体のゲーム構造はここまでですべて完成しています!今回やることはひたすらゲーム進行のロジックを書くだけです。

お詫び

前回まででもかなりエラーが発生してしまいました(ごめんなさい)が、現在の中途半端なMVCモデルでこれ以上ロジックを拡張するとなんかすごいエラー地獄になりそうなので、もう少しだけ厳密にMVCを分けることにしました。

というわけで、前回作ったViewとControllerの内容がかなりめちゃくちゃ大幅に変更されます。ごめん!

あと、「前回の段階でエラーが出てどうしても先に進めなくなってしまった!」という人は、前回までの完成版コードをGitHubに置いておくので、git cloneにて取得してみてください。多分動くはず...

bash
git clone https://github.com/noyu38/szpp_poker.git

さらに、「日本語が文字化けしてうまく表示されない」という人は以下のコマンドをWSLで実行してみてください。

bash
sudo apt update
sudo apt install fonts-ipafont fonts-noto-cjk

ゲーム進行を管理するクラス(PorkerGame.java)の実装

現時点ではゲーム進行をGameControllerクラスに任せていましたが、これはバグったときに意味わかんなくなるのでやめます(ごめん)

GameControllerクラスはあとで内容を書き換えます。ExchangeButtonListener.javaに関してはもう消しちゃってください。

まずはPokerGame.javaの実装をしていきましょう。このクラスが担うのは「ゲームの進行ロジック」です。「ゲームスタート」、「カードの交換処理」、「コインのベット処理」などのゲームのロジックを書きます。PokerGame.javaszpp/model内に作成してください!

java
package model;

import java.util.ArrayList;
import java.util.List;

public class PokerGame {
    private Deck deck;
    private HumanPlayer human;
    private CpuPlayer cpu;

    private int pot; // お互いの賭け金の合計
    private String gameStatusMessage; // 画面に表示するメッセージ

    public PokerGame() {
        this.deck = new Deck();
        this.human = new HumanPlayer("あなた", 1000);
        this.cpu = new CpuPlayer("CPU", 1000);
    }
}

ゲームスタート時の処理の実装

ゲームがスタートしたときの処理は、startNewGame()に実装していきましょう。このメソッドは後々GameControllerクラスのstartNewGame()で呼び出す形にします。

java
public void startNewGame() {
    deck.reset();
    human.clearHand();
    cpu.clearHand();

    // 参加費として10コインずつ払う
    pot = 0;
    human.payCoins(10);
    cpu.payCoins(10);
    pot += 20;

    for (int i = 0; i < 5; i++) {
        human.drawCard(deck);
        cpu.drawCard(deck);
    }

    gameStatusMessage = "ゲーム開始![コール]か[レイズ(+100)]か[フォールド]を選んでください";
}

PokerGameクラスはmodelパッケージのクラスであるため、viewを直接操作することは許されません。そのため、現在のゲーム進行状況はgameStatusMessageに格納しておき、後でGameControllerクラスでそのメッセージを取得し、GameControllerviewにメッセージを表示させるといった処理の流れを踏まなければ表示ができません。

コインを賭ける処理の実装

ベット処理を実装していきます。ギャンブルの知識なんか知らないほうが良いに決まってますが、ポーカーはギャンブルなので残念ながら知らなければいけません。

ポーカーにおける賭け金に関する操作のうち、今回実装するのは以下の3つです。

  • コール

    対戦相手が賭けたコインと同じ額を賭けて勝負する。(今回はゲーム開始のたびに10コインを強制的に支払うため、10コインで勝負するという意味になる)

  • レイズ

    賭け金を増やして勝負する。勝てそうな手札のときにやるやつ(簡単のため、増やす量は100コインで固定)

  • フォールド

    ゲームを降りる。

これらの操作をexecuteBetAction()に実装していきます。また、簡単のため、CPUは必ずコールをする仕様にします。これのせいでコールとフォールドの違いが殆どなくなっちゃった

java
// コインを賭ける処理
public void executeBetAction(String action) {
    if (action.equals("FOLD")) {
        gameStatusMessage = "あなたは勝負を降りました。CPUの勝利";
        cpu.addCoins(pot);
        return;
    }

    int humanBet = 0;
    if (action.equals("RAISE")) {
        humanBet += 100;
        human.payCoins(humanBet);
        pot += humanBet;
        gameStatusMessage = "あなたは100枚レイズした!";
    } else {
        gameStatusMessage = "あなたはコールした";
    }

    // CPUは必ずコールをする仕様にする
    int cpuBet = humanBet;
    cpu.payCoins(cpuBet);
    pot += cpuBet;
    gameStatusMessage += "CPUはコールしました。交換するカードを選んでください";
}

カード交換と役の勝負をする処理の実装

まずはHandEvaluatorクラスを改良する必要があります。現在はこのクラスは役の名前を判定する処理しかできず、勝敗判定ができません。それぞれの役の強さを数値化できれば、役の強さが比べられそうです。HandEvaluatorクラスに以下のメソッドを追加してください。

java
public static int getHandScore(List<Card> hand, String rankName) {
    int baseScore = getBaseScore(rankName);

    // 手札の中で一番強い数字(Aは14に変換)を探す
    int highCard = 0;
    for (Card c : hand) {
        int r = c.getRank();
        if (r == 1)
            r = 14;
        if (r > highCard)
            highCard = r;
    }

    return (baseScore * 100) + highCard;
}

private static int getBaseScore(String rankName) {
    switch (rankName) {
        case "ロイヤルストレートフラッシュ":
            return 10;
        case "ストレートフラッシュ":
            return 9;
        case "フォーカード":
            return 8;
        case "フルハウス":
            return 7;
        case "フラッシュ":
            return 6;
        case "ストレート":
            return 5;
        case "スリーカード":
            return 4;
        case "ツーペア":
            return 3;
        case "ワンペア":
            return 2;
        case "ハイカード":
            return 1;
        default:
            return 0;
    }
}

役の強さが数値化できたので、実際に勝敗判定を行う処理を実装していきましょう。このコードを書くのはPokerGameクラスです。

java
// カード交換と勝敗判定
public void executeExchange(List<Integer> selectedIndices) {
    List<Card> humanHand = human.getHand();
    List<Card> cpuHand = cpu.getHand();
    List<Card> humanTrashs = new ArrayList<>();

    for (int index : selectedIndices) {
        humanTrashs.add(humanHand.get(index));
    }

    human.setTrashCards(humanTrashs);
    List<Card> actualTrashs = human.decideExchangeCards();
    human.trashCards(actualTrashs);
    for (int i = 0; i < actualTrashs.size(); i++) {
        human.drawCard(deck);
    }

    List<Card> cpuTrashs = cpu.decideExchangeCards();
    cpu.trashCards(cpuTrashs);
    for (int i = 0; i < cpuTrashs.size(); i++) {
        cpu.drawCard(deck);
    }

    // 勝敗判定
    String humanRank = HandEvaluator.evaluate(humanHand);
    String cpuRank = HandEvaluator.evaluate(cpuHand);
    int humanScore = HandEvaluator.getHandScore(humanHand, humanRank);
    int cpuScore = HandEvaluator.getHandScore(cpuHand, cpuRank);

    if (humanScore > cpuScore) {
        gameStatusMessage = String.format("勝利!(+%d) あなた: %s | CPU: %s", pot, humanRank, cpuRank);
        human.addCoins(pot);
    } else if (cpuScore > humanScore) {
        gameStatusMessage = String.format("敗北...(-%d) あなた: %s | CPU %s", (pot / 2), humanRank, cpuRank);
        cpu.addCoins(pot);
    } else {
        gameStatusMessage = String.format("引き分け! あなた: %s | CPU: %s", humanRank, cpuRank);
        human.addCoins(pot / 2);
        cpu.addCoins(pot / 2);
    }
}

ついでに各種ユーティリティメソッドもPokerGameクラスに書いちゃいましょう。

java
public List<String> getHumanHandAsStrings() {
    List<String> list = new ArrayList<>();
    for (Card c : human.getHand()) {
        list.add(c.toString());
    }
    return list;
}

public List<String> getCpuHandAsStrings() {
    List<String> list = new ArrayList<>();
    for (Card c : cpu.getHand()) {
        list.add(c.toString());
    }
    return list;
}

public String getGameStatusMessage() {
    return gameStatusMessage;
}

public String getHumanCoins() {
    return "あなたのコイン: " + human.getCoins();
}

public String getCpuCoins() {
    return "CPUのコイン: " + cpu.getCoins();
}

GuiView.javaの改変

今回のMVCモデルの変更に伴って、GuiViewクラスもかなり変更されます。そうでなくともGUIの実装はかなりお作法的な側面が強く、説明しているとそれだけで日が昇ってしまうので割愛します(今後Swingを使う予定はないしね)

なので、GuiView.javaは以下のコードに丸々書き換えてくれれば良いです。

java
package view;

import java.awt.*;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.*;
import javax.swing.border.Border;

public class GuiView {
    private JFrame frame;
    private JPanel cpuPanel, humanPanel;
    private JButton exchangeButton, callButton, raiseButton, foldButton;
    private JLabel statusLabel, coinLabel;
    private List<JToggleButton> humanCardButtons;

    private final Color TABLE_COLOR = new Color(34, 139, 34);
    private final Color CARD_BACK_COLOR = new Color(25, 25, 112);

    public GuiView() {
        frame = new JFrame("SZPP Poker");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Dimension maxScreen = Toolkit.getDefaultToolkit().getScreenSize();
        frame.setSize(maxScreen);
        frame.setLayout(new BorderLayout());

        cpuPanel = new JPanel();
        cpuPanel.setBackground(TABLE_COLOR);
        humanPanel = new JPanel();
        humanPanel.setBackground(TABLE_COLOR);
        humanCardButtons = new ArrayList<>();

        JPanel centerPanel = new JPanel(new GridLayout(2, 1));
        centerPanel.add(cpuPanel);
        centerPanel.add(humanPanel);
        frame.add(centerPanel, BorderLayout.CENTER);

        // 下部のパネル。メッセージとコイン表示、各種ボタンを置く場所
        JPanel bottomPanel = new JPanel(new GridLayout(2, 1)); // 2段構えにする
        bottomPanel.setBackground(Color.DARK_GRAY);

        // 上部分。メッセージとコイン表示
        JPanel infoPanel = new JPanel();
        infoPanel.setBackground(Color.DARK_GRAY);
        statusLabel = new JLabel("ゲーム開始");
        statusLabel.setForeground(Color.YELLOW);
        statusLabel.setFont(new Font("SansSerif", Font.BOLD, 20));

        coinLabel = new JLabel("");
        coinLabel.setForeground(Color.WHITE);
        coinLabel.setFont(new Font("SansSerif", Font.BOLD, 18));
        infoPanel.add(statusLabel);
        infoPanel.add(new JLabel("   |   "));
        infoPanel.add(coinLabel);

        // 下部分。各種ボタン
        JPanel buttonPanel = new JPanel();
        buttonPanel.setBackground(Color.DARK_GRAY);
        foldButton = new JButton("フォールド");
        callButton = new JButton("コール");
        raiseButton = new JButton("レイズ");
        exchangeButton = new JButton("交換");
        exchangeButton.setEnabled(false); // 最初は押せない

        buttonPanel.add(foldButton);
        buttonPanel.add(callButton);
        buttonPanel.add(raiseButton);
        buttonPanel.add(exchangeButton);

        bottomPanel.add(infoPanel);
        bottomPanel.add(buttonPanel);
        frame.add(bottomPanel, BorderLayout.SOUTH);

        frame.setVisible(true);
    }

    private Color getMarkColor(String mark) {
        return (mark.equals("♦") || mark.equals("♥")) ? Color.RED : Color.BLACK;
    }

    private String formatCardHtml(String cardStr) {
        String mark = cardStr.substring(0, 1);
        String rank = cardStr.substring(1);
        return String.format(
                "<html><div style='text-align: center; font-size: 20px;'>%s<br><span style='font-size: 36px;'>%s</span></div></html>",
                mark, rank);
    }

    public void updateCpuHand(List<String> handStrings, boolean hide) {
        cpuPanel.removeAll();
        for (String cardStr : handStrings) {
            JLabel label = new JLabel();
            label.setPreferredSize(new Dimension(140, 200));
            label.setHorizontalAlignment(SwingConstants.CENTER);
            label.setOpaque(true);
            if (hide) {
                label.setBackground(CARD_BACK_COLOR);
                label.setBorder(BorderFactory.createLineBorder(Color.WHITE, 4));
            } else {
                label.setBackground(Color.WHITE);
                label.setBorder(BorderFactory.createLineBorder(Color.BLACK, 2));
                label.setForeground(getMarkColor(cardStr.substring(0, 1)));
                label.setText(formatCardHtml(cardStr));
            }
            cpuPanel.add(label);
        }
        cpuPanel.revalidate();
        cpuPanel.repaint();
    }

    public void updateHumanHand(List<String> handStrings) {
        humanPanel.removeAll();
        humanCardButtons.clear();
        for (String cardStr : handStrings) {
            JToggleButton btn = new JToggleButton();
            btn.setPreferredSize(new Dimension(140, 200));
            btn.setBackground(Color.WHITE);
            btn.setForeground(getMarkColor(cardStr.substring(0, 1)));
            btn.setText(formatCardHtml(cardStr));

            Border unselectedBorder = BorderFactory.createLineBorder(Color.GRAY, 2);
            Border selectedBorder = BorderFactory.createLineBorder(Color.RED, 5);
            btn.setBorder(unselectedBorder);
            btn.addActionListener(e -> btn.setBorder(btn.isSelected() ? selectedBorder : unselectedBorder));

            humanCardButtons.add(btn);
            humanPanel.add(btn);
        }
        humanPanel.revalidate();
        humanPanel.repaint();
    }

    public void updateStatus(String statusText, String coinText) {
        statusLabel.setText(statusText);
        coinLabel.setText(coinText);
    }

    public void setBettingMode(boolean isBetting) {
        foldButton.setEnabled(isBetting);
        callButton.setEnabled(isBetting);
        raiseButton.setEnabled(isBetting);
        exchangeButton.setEnabled(!isBetting);
    }

    public void disableExchangeButton() {
        exchangeButton.setEnabled(false);
    }

    public List<Integer> getSelectedCardIndices() {
        List<Integer> selectedIndices = new ArrayList<>();
        for (int i = 0; i < humanCardButtons.size(); i++) {
            if (humanCardButtons.get(i).isSelected())
                selectedIndices.add(i);
        }
        return selectedIndices;
    }

    public void addActionListeners(ActionListener fold, ActionListener call, ActionListener raise,
            ActionListener exchange) {
        foldButton.addActionListener(fold);
        callButton.addActionListener(call);
        raiseButton.addActionListener(raise);
        exchangeButton.addActionListener(exchange);
    }
}

GameController.javaの改変

これまではこのクラスがゲームの進行役を務めていましたが、PokerGameクラスがその役割をしてくれるようになったので、GameControllerクラスはmodelviewの橋渡し役に注力します。本来のcontrollerの役目に近づきましたね。

java
package controller;

import java.util.List;
import model.PokerGame;
import view.GuiView;

public class GameController {
    private GuiView view;
    private PokerGame game;

    public GameController(GuiView view) {
        this.view = view;
        this.game = new PokerGame();

        this.view.addActionListeners(
                e -> handleBetAction("FOLD"),
                e -> handleBetAction("CALL"),
                e -> handleBetAction("RAISE"),
                e -> handleExchange());

        startNewGame();
    }

    private void startNewGame() {
        game.startNewGame();
        updateScreen(true, true);
    }

    private void handleBetAction(String action) {
        game.executeBetAction(action);

        if (action.equals("FOLD")) {
            updateScreen(false, false);
            view.disableExchangeButton();
        } else {
            updateScreen(true, false);
        }
    }

    private void handleExchange() {
        List<Integer> selectedIndices = view.getSelectedCardIndices();
        game.executeExchange(selectedIndices);
        updateScreen(false, false);
        view.setBettingMode(false);
        view.disableExchangeButton();
    }

    private void updateScreen(boolean hideCpu, boolean isBettingMode) {
        view.updateHumanHand(game.getHumanHandAsStrings());
        view.updateCpuHand(game.getCpuHandAsStrings(), hideCpu);
        view.updateStatus(game.getGameStatusMessage(), game.getHumanCoins() + " / " + game.getCpuCoins());
        view.setBettingMode(isBettingMode);
    }
}

これでゲームが起動できるようになったはずです!試しに再度コンパイル&実行をしてみましょう!(エラーが出る場合は教えてください)

さらに改良したい

現時点では一度勝負したらゲームがそこでストップするようになっています。そこで、どちらかの賭け金が0になるまで何回も勝負できるようなゲームにしてみましょう。

一度勝負してから次の勝負を始める処理は色々あります。「次の勝負へ」みたいなボタンを配置するのも良さそうですし、以下のように書けば、「5秒経ったら勝手に次の勝負が始まる」という処理を実装することができます。

java
new Timer(5000, e -> {
            ((Timer) e.getSource()).stop();
            if (!game.isGameOver()) {
                startNewGame();
            }
        }).start();

ここで使用しているTimerクラスはjavax.swing.Timerパッケージのものです。他のパッケージにも同じ名前のクラスがあるため、間違えないようにしてください。