Skip to content

ポーカーゲーム作成Step4 ~ViewとControllerの実装~

ここまでの実装でModelの実装が一旦全て完了しました。ここからはViewとControllerを作成していきます。まずはViewから作成していきましょう

Viewの実装

今回はJavaにデフォルトで組み込まれているSwingとAWT(Abstract Window Toolkit)いうライブラリを使用してGUIを作成していきます。

SwingでのGUI作成の際にはFrameとPanelの概念を使います。

  • Frame ウィンドウのことです。画面の枠や右上の×(閉じる)ボタン、タイトルバーなどを持ちます。JFrameというクラスで実装できます。

  • Panel フレームの上に配置するエリアのことです。今回の場合は「相手のカードエリア」、「自分のカードエリア」、「操作ボタンエリア」などというようにPanelを役割ごとに分割することで、UI設計がやりやすくなります。JPanelというクラスで実装できます。

Panelの上に様々なテキストやボタンを設置することでUIを作成していきます。これを踏まえて実際にUIを作成していきましょう。szpp_poker/viewGuiView.javaを作成してください。

GuiViewのコンストラクタ

まずは画面の土台となるウィンドウと、各パネルを配置する設定を書いていきます。少し長いですが、一つ一つの部品を組み立てていくイメージで書いてみてください。

java
package view;

import model.Card;
import javax.swing.*;
import javax.swing.border.Border;

import java.awt.*;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;

public class GuiView {
    private JFrame frame; // ウィンドウ
    private JPanel cpuPanel; // CPUのカード表示エリア
    private JPanel humanPanel; // 人間のカード表示エリア
    private JButton exchangeButton; // カード交換用ボタン
    private JLabel statusLabel; // ステータスを表示するラベル
    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 screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        frame.setSize(screenSize);
        frame.setLayout(new BorderLayout());

        // パネルの初期化と背景色の設定
        // CPUのカード表示エリア
        cpuPanel = new JPanel();
        cpuPanel.setBackground(TABLE_COLOR);
        cpuPanel.setBorder(BorderFactory.createTitledBorder(
                BorderFactory.createLineBorder(Color.WHITE), "CPUの手札", 0, 0, new Font("SansSerif", Font.BOLD, 18),
                Color.WHITE));

        // 人間のカード表示エリア
        humanPanel = new JPanel();
        humanPanel.setBackground(TABLE_COLOR);
        humanPanel.setBorder(BorderFactory.createTitledBorder(
                BorderFactory.createLineBorder(Color.WHITE), "あなたの手札(捨てるカードをクリック)", 0, 0,
                new Font("SansSerif", Font.BOLD, 18), Color.WHITE));
        humanCardButtons = new ArrayList<>();

        // 中央に2つのパネルを配置
        JPanel centerPanel = new JPanel(new GridLayout(2, 1));
        centerPanel.add(cpuPanel);
        centerPanel.add(humanPanel);
        frame.add(centerPanel, BorderLayout.CENTER);

        // 下部にボタンとステータスを配置
        JPanel bottomPanel = new JPanel();
        bottomPanel.setBackground(Color.DARK_GRAY);
        bottomPanel.setPreferredSize(new Dimension(0, 80)); // 高さを少し広げる

        exchangeButton = new JButton("選択したカードを交換する");
        exchangeButton.setFont(new Font("SansSerif", Font.BOLD, 20));

        statusLabel = new JLabel("ゲーム開始!");
        statusLabel.setFont(new Font("SansSerif", Font.BOLD, 24));
        statusLabel.setForeground(Color.YELLOW);
        statusLabel.setBorder(BorderFactory.createEmptyBorder(0, 30, 0, 0)); // 左に余白

        bottomPanel.add(exchangeButton);
        bottomPanel.add(statusLabel);
        frame.add(bottomPanel, BorderLayout.SOUTH);

        frame.setVisible(true);
    }
}

レイアウトマネージャー

コードの中にあるBorderLayoutGridLayoutという単語についてです。 Swingに限らず、GUIを作成するときにボタンやテキストの位置を「X座標は〇〇で、Y座標は××で⋯」といったように絶対位置で指定するのはとても大変です。レイアウトマネージャーは位置を自動的に整えてくれます。

BorderLayoutは画面を「東西南北+中央」の5つのエリアに分けて配置します。今回は中央(CENTER)にカード画面、南(SOUTH)にボタンを配置しています。

GridLayoutは画面をマス目状に区切って配置します。今回はGridLayout(2, 1)なので縦2行・横1列のマスを作成し、上にCPU、下に人間の画面を割り当てています。

手札の描画処理

コンストラクタができたので、次は手札を表示・更新する処理を書いていきましょう。

java
// いい感じの見た目にするためのメソッドたち
// マークによって色を変える
private Color getMarkColor(String mark) {
    // マークが♦か♥なら赤色
    if (mark.equals("♦") || mark.equals("♥")) {
        return Color.RED;
    }
    // それ以外なら黒色
    return Color.BLACK;
}

// カードのテキストをトランプらしく表示する
private String formatCardHtml(Card card) {
    String str = card.toString();
    // 最初の一文字(マーク)
    String mark = str.substring(0, 1);
    // 残りの文字(数字)
    String rank = str.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);
}

// 画面更新メソッドたち(ここから重要!!!)
// CPUの手札を描画(hideがtrueなら裏向きにする)
public void updateCpuHand(List<Card> hand, boolean hide) {
    cpuPanel.removeAll();
    for (int i = 0; i < hand.size(); i++) {
        Card card = hand.get(i);
        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));
            label.setText("<html><div style='color: white; font-size: 48px;'>?</div></html>");
        } else {
            // 表向きのカードデザイン
            label.setBackground(Color.WHITE);
            label.setBorder(BorderFactory.createLineBorder(Color.BLACK, 2));
            label.setForeground(getMarkColor(card.getMark()));
            label.setText(formatCardHtml(card));
        }
        cpuPanel.add(label);
    }
    cpuPanel.revalidate();
    cpuPanel.repaint();
}

// 人間の手札をトグルボタンとして描画
public void updateHumanHand(List<Card> hand) {
    humanPanel.removeAll();
    humanCardButtons.clear();
    for (int i = 0; i < hand.size(); i++) {
        Card card = hand.get(i);
        JToggleButton btn = new JToggleButton();
        // カードのサイズ
        btn.setPreferredSize(new Dimension(140, 200));
        btn.setBackground(Color.WHITE);
        btn.setForeground(getMarkColor(card.getMark()));
        btn.setText(formatCardHtml(card));
        // 選択時の見た目を調整する
        // 非選択時はグレーの枠で囲う
        Border unselectedBorder = BorderFactory.createLineBorder(Color.GRAY, 2);
        // 選択時は赤枠で囲う
        Border selectedBorder = BorderFactory.createLineBorder(Color.RED, 5);
        btn.setBorder(unselectedBorder);
        // ボタンがクリックされたときに枠線を変えるイベント
        btn.addActionListener(e -> {
            if (btn.isSelected()) {
                btn.setBorder(selectedBorder);
            } else {
                btn.setBorder(unselectedBorder);
            }
        });
        humanCardButtons.add(btn);
        humanPanel.add(btn);
    }
    humanPanel.revalidate();
    humanPanel.repaint();
}

CPUの手札はラベルで実装しているのに対し、人間の手札はトグルボタンで実装しているのが特徴です。ラベルはJLabelクラス、トグルボタンはJToggleButtonクラスを使用します。

  • cpuPanel.removeAll(), humanPanel.removeAll(), humanCardButtons.clear(): メソッド名のとおりです。PanelやToggleButtonを空っぽにします。
  • setPreferredSize(): ラベルやボタンの大きさを設定します。引数はDimension型です。
  • setBackGround(): 背景色を設定します。引数はColor型です。
  • setBorder(): ラベルやボタンを囲う枠の設定をしています。引数はBorder型です。
  • setText(): メソッド名のとおりです。
  • btn.addActionListener(): ボタンがクリックされたときにどんな処理をするかを決めることができます。今回はクリックされたら枠の色を変えるようにしています。

Controllerに操作を委ねる

最後に、Controllerからの指示を受け取るためのメソッドを作っていきます。

ここで重要なのが、Viewはゲームの進行ルールを一切知ってはいけないということです。ゲームの進行はすべて進行役であるControllerに任せる必要があります。例えば、「カード交換ボタンを押したら自分とCPUの手札を捨てて、捨てた分だけ新しく引き直す」といったような処理は絶対にViewに書いてはいけません。

java
public void setStatus(String text) {
        statusLabel.setText(text);
    }

// カード交換ボタンを押せないようにする(交換直後にもう一度交換したりするのを防ぐ)
public void disableExchangeButton() {
    exchangeButton.setEnabled(false);
}

// Controllerからボタンのクリックイベントを登録する
public void addExchangeButtonListener(ActionListener listener) {
    exchangeButton.addActionListener(listener);
}

// 画面上で「選択状態」になっているボタンのインデックスを取得
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;
}

Controllerの実装

最後にゲームの進行役であるControllerを作成します。 Controllerは以下のような処理を担当します。

  1. 「Viewで発生したイベントを監視」
  2. 「イベントをもとにModelのデータを更新」
  3. 「更新されたModelのデータを使ってViewを更新」

このように、ControllerはViewとModelの橋渡しのような役割を担います。 まずはszpp_poker/controllerの中にGameController.javaを作成してください。

GameControllerクラスの実装

今回のステップではとりあえずゲームが起動し、カード交換ボタンを押したときにちゃんとカードが交換されるようにするところまで実装します。 GameControllerクラスにはひとまずコンストラクタと、ゲーム起動時に初期画面を表示する処理を書きます。

java
package controller;

import model.CpuPlayer;
import model.Deck;
import model.HumanPlayer;
import view.GuiView;

public class GameController {
    private GuiView view;
    private Deck deck;
    private HumanPlayer human;
    private CpuPlayer cpu;

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

        ExchangeButtonListener listener = new ExchangeButtonListener(this.view, this.deck, this.human, this.cpu);

        this.view.addExchangeButtonListener(listener);

        startNewGame();
    }

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

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

        view.updateHumanHand(human.getHand());
        view.updateCpuHand(cpu.getHand(), true);
        view.setStatus("交換するカードを選択してボタンを押してください");
    }
}

これまでいろんなプログラムを見てきたみなさんなら何をしているのかはもうわかりますね?

ExchangeButtonListenerクラスの実装

ExchangeButtonListenerActionListenerインターフェースの実装クラスです。インターフェースについては詳しい説明は割愛しますが、大体継承と同じように扱うことができます。ただし、継承するための修飾子はextendsではなくimplementsを使用します。(インターフェースを継承することを、実装するといいます。)

ExchangeButtonListenerには、交換ボタンがクリックされたときの処理のみを書きます。ActionListenerインターフェースを実装することで、クリックされたときの処理をactionPerformed()メソッドに書けば勝手に実行してくれるようになります。

今回交換ボタンをクリックされたときに行いたい処理は以下の通りです。

  • 人間の手札を交換
  • CPUの手札を交換
  • 交換後の手札から役を判定
  • Viewを更新し、お互いの手札を公開する
  • Viewを更新し、お互いの役をテキストで表示する

この処理をactionPerformed()メソッド内に頑張って書いてみましょう。

java
package controller;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;

import model.CpuPlayer;
import model.Deck;
import model.HandEvaluator;
import model.HumanPlayer;
import model.Card;
import view.GuiView;

public class ExchangeButtonListener implements ActionListener {
    private GuiView view;
    private Deck deck;
    private HumanPlayer human;
    private CpuPlayer cpu;

    public ExchangeButtonListener(GuiView view, Deck deck, HumanPlayer human, CpuPlayer cpu) {
        this.view = view;
        this.deck = deck;
        this.human = human;
        this.cpu = cpu;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        // 人間のカード交換処理
        // 現在の人間プレイヤーの手札
        List<Card> humanHand = human.getHand();
        // 捨てるカードのリスト
        List<Card> humanTrashs = new ArrayList<>();
        // 自分の手札のうち、捨てるカードのインデックスのみをまとめたリスト
        List<Integer> selectedIndices = view.getSelectedCardIndices();

        for (int i = 0; i < selectedIndices.size(); i++) {
            int trashIndex = selectedIndices.get(i);
            humanTrashs.add(humanHand.get(trashIndex));
        }

        // 人間が捨てるカードをセット
        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);
        }

        // 今のhumanHandは更新前の状態であるため、更新する
        humanHand = human.getHand();
        // CPUの手札も取得しておく
        List<Card> cpuHand = cpu.getHand();

        // 役の判定
        String humanRank = HandEvaluator.evaluate(humanHand);
        String cpuRank = HandEvaluator.evaluate(cpuHand);

        // Viewの更新(CPUの手札を公開する)
        view.updateHumanHand(humanHand);
        view.updateCpuHand(cpuHand, false);
        view.disableExchangeButton();

        view.setStatus("勝負! " + "あなた: " + humanRank + " | " + "CPU: " + cpuRank);
    }
}

実際にゲームを起動してみる

ここまで実装できたら、Main.javaを以下のように書き換えて再度コンパイル・実行してみてください。

java
import view.GuiView;
import controller.GameController;
import javax.swing.SwingUtilities;

public class Main {
    public static void main(String[] args) {
        // Swing GUIの起動方法(おまじない)
        SwingUtilities.invokeLater(() -> {
            GuiView view = new GuiView();
            GameController controller = new GameController(view);
        });
    }
}