チーム開発で学ぶ!WinForm ハイローゲーム構築【制作編】

2025年2月19日

以下は、インターフェースの説明とそのコード例を含め、全体の設計・実装サンプルとドキュメントを統合した、チーム開発向けのサンプル資料です。この資料は、既存の基本機能(Card、Deck など)はそのまま利用し、拡張機能として以下を追加更新する前提です。

  • HiLowGame クラス: ゲームの状態管理とユーザー入力による判定ロジック
  • GameResult クラス: ユーザー入力に対する判定結果とメッセージのまとめ
  • HiLowForm (WinForm UI): PictureBox、Label、Button などを用いた操作・表示
  • README.md: プロジェクト概要、ビルド手順、担当タスクなどを記載したドキュメント

また、システム全体の拡張性を高めるため、インターフェースを導入し、依存性注入や非同期処理、エラーハンドリング、ログ出力を取り入れた設計サンプルも含まれています。

GitHub Desktop を活用した 4 人チームのステップバイステップ実践ガイド

※基本機能は既存コードをそのまま利用し、以下の拡張機能の追加更新を中心に作業を進めます。


1. プロジェクト概要とゲーム内容、チーム開発のポイント

プロジェクト概要

目的:
WinForm を用いてハイローゲームの基盤を実装する。
基本機能として、既存の Card クラス、Deck クラスなどによりカード生成や管理を実現しています。

今回の拡張機能:

  • HiLowGame クラス
    ゲームの状態管理(現在のカード、スコア、終了状態)とユーザー入力(ハイ/ロー)の判定ロジックを実装します。
  • GameResult クラス
    ユーザー入力に対する判定結果と、その結果に伴うメッセージをまとめます。
  • HiLowForm (WinForm UI)
    PictureBox、Label、Button などの UI コントロールを用いて、ゲームの操作および表示を実現します。
  • README.md
    プロジェクト概要、ビルド手順、各担当タスク、使用技術、今後の拡張計画などを記載したドキュメントの作成。

ゲーム内容

ルール:

  1. 初期カードの表示:
    ゲーム開始時にランダムなカードが表示されます。
  2. ユーザーの予想:
    ユーザーは、次に引くカードが現在のカードより「高い(ハイ)」か「低い(ロー)」かを予想します。
  3. カードの比較:
    次のカードをデッキから引き、ユーザーの予想と比較します。
  4. 判定とスコア更新:
    予想が正解の場合はスコアが加算され、誤っている場合はスコアは変わりません。
    ゲームはカードがなくなるか、ユーザーが終了するまで続きます。

目的:
ユーザーは連続して正しい予想を行い、高スコアを目指します。

チーム開発のポイント

  • バージョン管理:
    GitHub Desktop を用い、各担当タスク毎にブランチを作成し、プルリクエストでコードレビューを実施します。
  • 役割分担(例):
    • メンバーA(基本設計・ドキュメント担当): README.md の作成、基本機能の設計確認
    • メンバーB(デッキ機能担当): Deck クラスの非同期処理、インターフェースの実装などパフォーマンス最適化
    • メンバーC(UI/拡張担当): HiLowGame クラス、GameResult クラスの実装
    • メンバーD(UI担当・応用担当): HiLowForm (WinForm UI) の実装および Unity での再現例(応用は別途対応)
  • コミュニケーション:
    定例ミーティング、GitHub Issues、Wiki などを利用し、進捗や課題を共有します。

2. 基本機能の確認(既存部分)

以下のクラスは既に基本機能として完成しており、今回の拡張では変更を加えません。

  • Card.cs
  • CardControl.cs
  • Player.cs
  • Hand.cs
  • Suit.cs

※基本機能の詳細なコードは、別途チーム内で共有済みです。


3. 拡張機能:追加更新タスク

3.1. HiLowGame クラスの実装

目的:
現在のカード、スコア、ゲーム状態の管理およびユーザー入力に対する判定を実装します。

担当例(メンバーC):
既存ロジックの拡張または整理し、クラスを作成・更新します。

【サンプルコード:HiLowGame.cs】

/// <summary>
/// ハイローゲームの状態管理と判定ロジックを実装するクラスです。
/// このクラスは、外部から IDeck を注入することで、デッキの実装を柔軟に切り替えられるようにしています。
/// また、非同期のゲーム初期化メソッドにより、内部で非同期シャッフル処理を統合しています。
/// </summary>
public class HiLowGame
{
    /// <summary>
    /// ゲームで使用するカードのデッキを取得します。
    /// </summary>
    public IDeck Deck { get; private set; }

    /// <summary>
    /// 現在表示されているカードを取得します。
    /// </summary>
    public Card CurrentCard { get; private set; }

    /// <summary>
    /// 現在のスコアを取得します。
    /// </summary>
    public int Score { get; private set; }

    /// <summary>
    /// ゲームが終了しているかどうかを示す値を取得します。
    /// </summary>
    public bool IsGameOver { get; private set; }

    /// <summary>
    /// カード表示用のユーザーコントロールです。設定されている場合、カードの更新時に自動で表示が更新されます。
    /// </summary>
    public CardControl CardDisplay { get; set; }

    /// <summary>
    /// 新しい HiLowGame インスタンスを初期化し、外部から IDeck を注入します。
    /// </summary>
    /// <param name="deck">ゲームで使用するデッキの実装。</param>
    public HiLowGame(IDeck deck)
    {
        Deck = deck;
    }

    /// <summary>
    /// 非同期に新しいゲームを開始します。
    /// デッキを非同期でシャッフルし、最初のカードを設定します。
    /// また、スコアをリセットし、ゲーム終了フラグを false に設定します。
    /// カード表示用のコントロールが設定されている場合、初期カードの表示も更新します。
    /// </summary>
    /// <returns>非同期処理のタスク。</returns>
    public async Task StartNewGameAsync()
    {
        await Deck.ShuffleAsync();
        CurrentCard = Deck.DrawCard();
        Score = 0;
        IsGameOver = false;
        UpdateCardDisplay();
    }

    /// <summary>
    /// ユーザーの予想に基づいて次のカードを引き、判定結果を返します。
    /// カード表示用のコントロールが設定されている場合、新しいカードの表示を更新します。
    /// </summary>
    /// <param name="guessHigh">
    /// ユーザーの予想を示します。true の場合は「ハイ」、false の場合は「ロー」となります。
    /// </param>
    /// <returns>
    /// ゲームの判定結果を含む <see cref="GameResult"/> オブジェクトを返します。
    /// </returns>
    public GameResult MakeGuess(bool guessHigh)
    {
        if (IsGameOver)
            return new GameResult(CurrentCard, null, false, "ゲームは終了しています。");

        Card nextCard = Deck.DrawCard();
        if (nextCard == null)
        {
            IsGameOver = true;
            return new GameResult(CurrentCard, nextCard, false, "デッキが空です。ゲーム終了!");
        }

        bool isCorrect = false;
        if (nextCard.Value == CurrentCard.Value)
            isCorrect = false;
        else if (guessHigh && nextCard.Value > CurrentCard.Value)
            isCorrect = true;
        else if (!guessHigh && nextCard.Value < CurrentCard.Value)
            isCorrect = true;

        if (isCorrect)
            Score++;

        Card previousCard = CurrentCard;
        CurrentCard = nextCard;

        UpdateCardDisplay();

        string guessStr = guessHigh ? "ハイ" : "ロー";
        string message = $"{guessStr} を選択しました。\n次のカードは {nextCard} です。\n" +
                            (isCorrect ? "正解!" : "不正解!");

        return new GameResult(previousCard, nextCard, isCorrect, message);
    }

    /// <summary>
    /// CardDisplay プロパティに設定されている CardControl に現在のカードを表示します。
    /// </summary>
    private void UpdateCardDisplay()
    {
        if (CardDisplay != null)
        {
            CardDisplay.SetCard(CurrentCard);
        }
    }
}

3.2. GameResult クラスの実装

目的:
ユーザー入力に対する判定結果とメッセージをまとめ、UI に返すためのデータクラスです。

担当例(メンバーC):
基本設計に沿って作成します。

【サンプルコード:GameResult.cs】

/// <summary>
/// ユーザー入力に対する判定結果とメッセージをまとめるクラスです。
/// このクラスは、ユーザーの選択に基づくカードの比較結果と、それに伴うメッセージを格納します。
/// </summary>
public class GameResult
{
    /// <summary>
    /// 判定前のカードを取得します。
    /// </summary>
    public Card PreviousCard { get; private set; }

    /// <summary>
    /// 判定後に引いた新しいカードを取得します。
    /// </summary>
    public Card NewCard { get; private set; }

    /// <summary>
    /// ユーザーの予想が正解だったかどうかを示す値を取得します。
    /// </summary>
    public bool IsCorrect { get; private set; }

    /// <summary>
    /// 判定結果に基づくメッセージを取得します。
    /// </summary>
    public string Message { get; private set; }

    /// <summary>
    /// 新しい <see cref="GameResult"/> インスタンスを初期化します。
    /// </summary>
    /// <param name="previousCard">判定前のカード。</param>
    /// <param name="newCard">判定後に引いた新しいカード。</param>
    /// <param name="isCorrect">ユーザーの予想が正解であるかどうか。</param>
    /// <param name="message">判定結果に基づくメッセージ。</param>
    public GameResult(Card previousCard, Card newCard, bool isCorrect, string message)
    {
        PreviousCard = previousCard;
        NewCard = newCard;
        IsCorrect = isCorrect;
        Message = message;
    }
}

3.3. HiLowForm (WinForm UI) の実装

目的:
PictureBox、Label、Button などを用いたユーザー操作および表示の実装。

担当例(メンバーD):
既存の UI 基盤に基づいて、操作ロジックの追加や細部の調整を行います。

【サンプルコード:HiLowForm.cs】

/// <summary>
/// WinForm UI でハイローゲームの操作・表示を実現するクラスです。
/// このクラスは、ユーザーが「ハイ」または「ロー」ボタンをクリックした際に、
/// ゲームロジック (HiLowGame) を呼び出し、その結果を画面に反映させます。
/// </summary>
public partial class HiLowForm : Form
{
    /// <summary>
    /// ゲームのロジックおよび状態を管理する <see cref="HiLowGame"/> のインスタンスです。
    /// </summary>
    private HiLowGame game;

    /// <summary>
    /// 新しい <see cref="HiLowForm"/> インスタンスを初期化し、ゲームの初期設定を行います。
    /// </summary>
    public HiLowForm()
    {
        InitializeComponent();
        // IDeck の実装として Deck クラスを利用
        game = new HiLowGame(new Deck());
        // ここでカード表示用のコントロールを HiLowGame に渡す
        game.CardDisplay = cardControl;
    }

    /// <summary>
    /// フォームのロード時に呼び出され、非同期で新しいゲームを開始し、初期表示を更新します。
    /// </summary>
    /// <param name="sender">イベントの送信元オブジェクト。</param>
    /// <param name="e">イベントデータ。</param>
    private async void HiLowForm_Load(object sender, EventArgs e)
    {
        await game.StartNewGameAsync();
        UpdateDisplay(); // 必要なら初期表示も更新
    }

    /// <summary>
    /// 「ハイ」ボタンがクリックされた際のイベントハンドラです。
    /// ユーザーの予想が「ハイ」であるとし、ゲームロジックに処理を委ねます。
    /// </summary>
    /// <param name="sender">イベントの送信元オブジェクト。</param>
    /// <param name="e">イベントデータ。</param>
    private void btnHigh_Click(object sender, EventArgs e)
    {
        ProcessGuess(true);
    }

    /// <summary>
    /// 「ロー」ボタンがクリックされた際のイベントハンドラです。
    /// ユーザーの予想が「ロー」であるとし、ゲームロジックに処理を委ねます。
    /// </summary>
    /// <param name="sender">イベントの送信元オブジェクト。</param>
    /// <param name="e">イベントデータ。</param>
    private void btnLow_Click(object sender, EventArgs e)
    {
        ProcessGuess(false);
    }

    /// <summary>
    /// ユーザーの予想に基づいて、次のカードを引く処理を実行し、結果を表示します。
    /// </summary>
    /// <param name="guessHigh">
    /// ユーザーの予想を示します。true の場合は「ハイ」、false の場合は「ロー」となります。
    /// </param>
    private void ProcessGuess(bool guessHigh)
    {
        GameResult result = game.MakeGuess(guessHigh);
        MessageBox.Show(result.Message);

        // ゲーム終了の場合、ボタンを無効化
        if (game.IsGameOver)
        {
            btnHigh.Enabled = false;
            btnLow.Enabled = false;
        }
        UpdateDisplay();
    }

    /// <summary>
    /// 現在のゲーム状態に基づいて UI を更新します。
    /// 表示内容には、現在のカードの画像、カード情報、およびスコアが含まれます。
    /// </summary>
    private void UpdateDisplay()
    {
        if (game.CurrentCard != null)
        {
            pictureBoxCard.Image = game.CurrentCard.CardImage;
            labelCard.Text = game.CurrentCard.ToString();
        }
        labelScore.Text = $"スコア: {game.Score}";
    }
}

3.4. README.md の作成

目的:
プロジェクト概要、ビルド手順、各担当タスク、使用技術、今後の拡張計画などをまとめたドキュメントを作成します。

担当例(メンバーA またはプロジェクトリーダー):
以下の内容を参考に README.md を作成してください。

【README.md の例】

# WinForm ハイローゲーム構築プロジェクト

## プロジェクト概要
WinForm を使用してシンプルなハイローゲームの基盤を実装します。  
基本機能(Card、Deck など)は既存のコードを利用し、今回の拡張機能として以下を追加します:
- **HiLowGame クラス**: ゲームの状態管理と判定ロジック
- **GameResult クラス**: 判定結果とメッセージのまとめ
- **HiLowForm (WinForm UI)**: ユーザー操作(ハイ/ロー選択)および表示の実装
- **README.md**: このドキュメント

## ゲーム内容
**ルール:**
1. ゲーム開始時にランダムなカードが表示される。
2. ユーザーは次のカードが「高い(ハイ)」か「低い(ロー)」かを予想する。
3. 次のカードと比較し、正解の場合はスコアが加算される。
4. デッキが尽きるか、ユーザーが終了するまでゲームが進行する。

## ビルド手順
1. Visual Studio でソリューションを開きます。
2. 必要な画像ファイルを `images` フォルダに配置します。
3. ソリューションをビルド&実行します。

## 各担当メンバーとタスク
- **メンバーA(基本設計・ドキュメント担当):** README.md の作成、基本機能の設計確認
- **メンバーB(デッキ機能担当):** Deck クラスの非同期シャッフル処理の導入
- **メンバーC(UI/拡張担当):** HiLowGame クラス、GameResult クラスの実装
- **メンバーD(UI担当・応用担当):** HiLowForm の UI 実装、Unity での再現例作成

## 今後の拡張案
- クラス間のリファクタリング(責務分離)
- UI 改善(アニメーション、レスポンシブ対応、エラーハンドリング)
- オンライン対戦、ランキング機能の追加

4. インターフェースの導入とそのサンプルコード

インターフェースの説明

インターフェースは、クラスが実装すべきメソッドやプロパティの契約を定義するためのものです。
これにより、以下のメリットがあります。

  • 拡張性:
    実装を簡単に差し替えることができ、将来的な仕様変更に柔軟に対応できます。
  • テスト容易性:
    モック実装を作成し、ユニットテストが容易になります。
  • 依存性注入:
    コンストラクタでインターフェース型を受け取ることで、依存関係が明示され、保守性の高い設計になります。

今回のサンプルでは、カードデッキ機能を抽象化するために IDeck インターフェースを導入し、Deck クラスがその実装を担います。

IDeck インターフェースのコード例

/// <summary>
/// カードデッキの基本機能(シャッフル、カードのドロー、残りカード数の取得)を定義するインターフェースです。
/// </summary>
public interface IDeck
{
    /// <summary>
    /// 非同期にカードをシャッフルします。
    /// </summary>
    /// <returns>非同期処理のタスク。</returns>
    Task ShuffleAsync();

    /// <summary>
    /// デッキから1枚カードを引き、そのカードを返します。
    /// カードがない場合は null を返します。
    /// </summary>
    /// <returns>引かれたカード。</returns>
    Card DrawCard();

    /// <summary>
    /// 残っているカードの枚数を取得します。
    /// </summary>
    int CardsRemaining { get; }
}

以下は、インターフェース IDeck を実装するようにリファクタリングした新しい Deck クラスのサンプルコードです。

  • 主な変更点:
    • クラス宣言に : IDeck を追加して、インターフェースを実装するようにしました。
    • CardsRemaining() メソッドをプロパティに変更し、IDeck の仕様に合わせています。
    • 同期版の Shuffle() メソッドを実装し、ShuffleAsync() では内部的に Shuffle() を呼び出すようにしました。

同期版の Shuffle() を別途実装するかどうかは、プロジェクト全体の設計方針や利用状況に依存します。以下の点を考慮してください。

  • コードの再利用性:
    同期版の Shuffle() を実装しておけば、非同期版 ShuffleAsync() はそのロジックを単にラップするだけで済みます。これにより、コードの重複を避け、同期処理が必要な場合にも対応できます。
  • 柔軟性:
    場合によっては、UI以外のシナリオで同期的なシャッフル処理が求められることもあります。たとえば、初期化時やテスト時に同期的な実行が有用な場合があります。
  • シンプルな設計:
    もし非同期処理のみで十分であり、同期処理の呼び出しが全く発生しないなら、直接非同期版のロジックを実装しても構いません。ただし、その場合でも内部で共通のロジックを分離しておくと、テストや保守性が向上する可能性があります。

/// <summary>
/// 複数のカードを管理するクラスです。
/// カードの生成、シャッフル、カードを引く機能を提供します。
/// このクラスは IDeck インターフェースを実装しています。
/// </summary>
public class Deck : IDeck
{
    private List<Card> cards;

    /// <summary>
    /// Deck を初期化し、全カード(例:4スート × 13枚)を生成します。
    /// </summary>
    public Deck()
    {
        cards = new List<Card>();

        // Enum からすべてのスートを取得
        foreach (Suit suit in Enum.GetValues(typeof(Suit)))
        {
            for (int i = 1; i <= 13; i++)
            {
                // ※画像ファイルはプロジェクト内の「images」フォルダに配置してください.
                string imagePath = $"images/{suit.ToString().ToLower()}_{i}.png";
                Image cardImage = Image.FromFile(imagePath);
                cards.Add(new Card(suit, i, cardImage));
            }
        }
    }

    /// <summary>
    /// カードをシャッフルします。(同期処理)
    /// </summary>
    public void Shuffle()
    {
        Random rand = new Random();
        for (int i = cards.Count - 1; i > 0; i--)
        {
            int j = rand.Next(i + 1);
            Card temp = cards[i];
            cards[i] = cards[j];
            cards[j] = temp;
        }
    }

    /// <summary>
    /// 非同期にカードをシャッフルします。
    /// UI スレッドをブロックしないために、Task.Run を利用してバックグラウンドでシャッフルを実行します。
    /// </summary>
    /// <returns>非同期処理のタスク。</returns>
    public async Task ShuffleAsync()
    {
        await Task.Run(() => Shuffle());
    }

    /// <summary>
    /// デッキから1枚カードを引き、そのカードをデッキから削除します。
    /// カードがない場合は null を返します。
    /// </summary>
    /// <returns>引かれたカード。</returns>
    public Card DrawCard()
    {
        if (cards.Count == 0)
            return null;
        Card card = cards[0];
        cards.RemoveAt(0);
        return card;
    }

    /// <summary>
    /// 残っているカードの枚数を取得します。
    /// </summary>
    public int CardsRemaining
    {
        get { return cards.Count; }
    }
}

補足

  • この変更により、Deck クラスは IDeck インターフェースの契約(ShuffleAsync()DrawCard()CardsRemaining)を正しく実装しています。
  • HiLowGame クラスでは、コンストラクタで IDeck 型を受け取ることで、依存性注入が可能になり、テストや将来的な実装差し替えが容易になります。

このサンプルコードを基に、チーム開発の一環として各担当メンバーでリファクタリングやテストを進めてください。


5. チーム開発:GitHub Desktop 活用とタスク担当

5.1. ブランチ運用

  • リポジトリ作成&クローン:
    GitHub 上でリポジトリを作成し、各メンバーがクローンします。
  • ブランチの切り方:
    • feature/hilowgame(メンバーC担当)
    • feature/gameresult(メンバーC担当)
    • feature/hilowform(メンバーD担当)
    • feature/readme(メンバーA担当)
    • feature/deckinterface(メンバーB担当)
  • 基本機能(Card、Deck 等)は master ブランチに統合済み
  • コミット&プルリクエスト:
    定期的にコミットし、プルリクエストを発行。コードレビュー後に統合します。

5.2. 役割分担例

  • メンバーA: 基本設計・ドキュメント担当(README.md の作成など)
  • メンバーB: デッキ機能担当(Deck の非同期シャッフル処理の導入、インターフェースの導入)
  • メンバーC: UI/拡張担当(HiLowGame、GameResult の実装)
  • メンバーD: UI担当・応用担当(HiLowForm の UI 実装、Unity での再現例作成)

5.3. タスク管理と進捗共有

  • タスク管理ツール: Trello や GitHub Projects を活用
  • 定例ミーティング: 定期的に進捗会議を開催
  • Issue トラッキング: GitHub Issues を利用して課題を管理

6. 設計上のポイントとリファクタリングのサンプルコード

このサンプルコードでは、以下のリファクタリング手法を採用しています。

  1. 責務の分離とモジュール化
    • UI (HiLowForm) とゲームロジック (HiLowGame, Deck, Card) を明確に分離。
    • 各クラスは単一の責務を持つように設計。
  2. インターフェースと依存性注入
    • IDeck インターフェースを導入し、Deck クラスが実装。
    • HiLowGame はコンストラクタで IDeck を受け取り、柔軟な実装変更やテストが可能。
  3. 非同期処理
    • Deck クラスの ShuffleAsync メソッドにより、UI のブロッキングを防止。

7. まとめと 今後の課題

7.1. これまでの成果

  • 基本機能実装:
    Card、Deck などの既存の基本機能により、WinForm 上でハイローゲームの基盤が完成。
  • 拡張機能の追加更新:
    • HiLowGame、GameResult、HiLowForm の実装
    • README.md の作成
    • 今後、Deck の非同期処理や Unity 再現例の追加も検討

7.2. 今後の課題と拡張案

  • 各クラス間のリファクタリング(責務分離のさらなる改善)
  • UI 改善(アニメーション、エラーハンドリング、レスポンシブデザイン)
  • オンライン対戦、ランキング機能の追加拡張
  • Unity での実装を基に、他プラットフォームへの展開を検討

この資料を基に、チーム内でのコードレビューや実装ガイドラインとして活用してください。
各担当メンバーは、自身のタスクに沿って実装・統合を進め、定期的なミーティングやプルリクエストで進捗を共有してください。
ご不明点や追加の相談事項があれば、チーム内で積極的にディスカッションしましょう。

参考

以下は、コード全体の構造を表すクラス図と、ハイローゲームでユーザーが「ハイ」または「ロー」を選択したときの流れを示すシーケンス図の例です。


クラス図

主要なクラス(CardCardControlDeckIDeckGameResultHandHiLowGameHiLowFormPlayer、および列挙型 Suit)の関係性を表現しています。

補足:

  • DeckIDeck インターフェースを実装しています。
  • HiLowGame は、外部から注入された IDeck を利用し、さらに UI 更新のために CardControl(カード表示用ユーザーコントロール)を参照する場合があります。
  • PlayerHand を持ち、その中に複数の Card が含まれます。

シーケンス図

次に、ユーザーがボタンをクリックして「ハイ」または「ロー」の予想を行い、ゲームロジックが処理を進めるシーケンスを示す図の例です。

シーケンス図の流れのポイント:

  1. ユーザー操作:
    • ユーザーが「ハイ」または「ロー」ボタンをクリックする。
  2. フォーム側の処理:
    • HiLowForm のイベントハンドラ (btnHigh_Click/btnLow_Click) が呼ばれ、内部で ProcessGuess が実行される。
  3. ゲームロジック:
    • ProcessGuess メソッドが HiLowGame.MakeGuess を呼び出す。
    • HiLowGameDeck.DrawCard() を呼び出して次のカードを取得。
    • 取得したカードと現在のカードを比較し、スコア更新および状態の更新を行う。
    • カード表示用コントロール (CardControl) が設定されている場合は、SetCard により UI の表示を更新。
  4. 結果表示:
    • HiLowGame から返された GameResult をもとに、HiLowForm がメッセージボックスを表示し、さらに UI 上の表示を更新する。

これらの図は、コード全体の構造と動作の流れを把握するための一例です。プロジェクトに合わせて詳細を調整してください。