GitHub Desktopで学ぶブランチ & PR(プルリクエスト)

~WinFormsアプリを題材にしたチーム開発演習~


前提


目次

  1. リポジトリとブランチの準備(リーダー)
  2. 担当A:防御ボタン追加 → PR作成
  3. 担当B:毒攻撃追加 → PR作成
  4. 担当C:ログ強化 → PR作成
  5. リーダー:レビューとマージ
  6. 全員:同期と実行テスト

バージョン管理の基本の流れ

推奨順序:担当Aの作業 → マージ → 全員 Pull → その時点の main から 担当Bの作業ブランチ を切る → マージ → Pull → main から 担当Cの作業ブランチ を切る。

途中で分岐を先に切っていた場合は、GitHub Desktop の「Update from main」(あるいは Rebase)で main を取り込む


1. リポジトリとブランチの準備(リーダー)

namespaceは、既存プロジェクトに合わせるか、Form1.Designer.cs/Program.cs 側も合わせてください

操作手順

  1. GitHub Desktop を起動し、対象リポジトリを選択
  2. Current Branch > New Branch
    • 担当Aさんの作業前は、feature-A
    • 担当Bさんの作業前は、feature-B
    • 担当Cさんの作業前は、feature-C を作成
  3. Push origin で GitHub にアップロード

2. STEP3準拠+担当A(防御)を追加した Form1.cs

「Current Branch > feature-A を選択してから Designer 編集とコード変更を行う」

1) Designer 変更(担当A)

  • Button を1つ追加
    • Name: btnDefend
    • Text: 「防御」
  • btnDefend の Click に btnDefend_Click を接続(既存の lblPlayer, lblEnemy, btnAttack, btnHeal, lstLog はそのまま)

コード(Form1.cs 全体置き換え)

STEP3 版(AppendLog や EnemyTurnWithCritical を使用)に、防御機能だけを最小限で足した形です。

Form1.cs を全置換する場合、namespace を既存のものに合わせてください。一致しないと Designer の partial が分離してビルドできません。

using System;
using System.Windows.Forms;

namespace BattleApp
{
    public partial class Form1 : Form
    {
        private int _playerHp = 30;
        private int _enemyHp = 20;
        private int _healLeft = 3;
        private readonly Random _rand = new Random();

        // ▼ A: 防御フラグ(次の敵攻撃のみ軽減)
        private bool _isDefending = false;

        public Form1()
        {
            InitializeComponent();
            UpdateStatus();
            UpdateHealButtonText();
            AppendLog("バトルスタート!");
        }

        private void btnAttack_Click(object sender, EventArgs e)
        {
            if (!BattleOngoing())
            {
                return;
            }

            int damage = _rand.Next(5, 10);
            _enemyHp = Math.Max(0, _enemyHp - damage);
            AppendLog($"プレイヤーの攻撃! {damage} のダメージ!");
            UpdateStatus();

            if (_enemyHp <= 0)
            {
                AppendLog("敵を倒した! 勝利!");
                SetCommandsEnabled(false);
                return;
            }

            EnemyTurnWithCritical();
        }

        private void btnHeal_Click(object sender, EventArgs e)
        {
            if (!BattleOngoing() || _healLeft <= 0)
            {
                return;
            }

            int heal = _rand.Next(6, 12);
            _playerHp += heal;
            _healLeft--;
            AppendLog($"プレイヤーは {heal} かいふくした!\nプレイヤーHP: {_playerHp}");
            UpdateStatus();
            UpdateHealButtonText();
            SetCommandsEnabled(true); // ← 残回数に応じて Heal を自動無効化

            if (BattleOngoing())
            {
                EnemyTurnWithCritical();
            }
        }

        // ▼ A: 防御ボタン(次の敵攻撃のみ半減)
        private void btnDefend_Click(object sender, EventArgs e)
        {
            if (!BattleOngoing())
            {
                return;
            }

            _isDefending = true;
            AppendLog("プレイヤーは防御の体勢を取った(次の被ダメ半減)");
            // 防御も行動の一つとして、敵ターンへ
            EnemyTurnWithCritical();
        }

        // (STEP2準拠)20%クリティカル+ A:防御の適用を同メソッド内で処理
        private void EnemyTurnWithCritical()
        {
            bool isCritical = (_rand.Next(0, 5) == 0); // 20%
            int baseDamage = isCritical ? _rand.Next(10, 15) : _rand.Next(3, 8);

            // ▼ A: 防御の適用(1回限り)
            int enemyDamage = baseDamage;
            if (_isDefending)
            {
                enemyDamage = Math.Max(1, baseDamage / 2);
                _isDefending = false; // 一度適用したら解除
                AppendLog("防御により被ダメージが軽減される!");
            }

            _playerHp = Math.Max(0, _playerHp - enemyDamage);

            if (isCritical)
            {
                AppendLog($"敵のクリティカルヒット! {enemyDamage} の大ダメージ!\nプレイヤーHP: {_playerHp}");
            }
            else
            {
                AppendLog($"敵の攻撃! {enemyDamage} のダメージ!\nプレイヤーHP: {_playerHp}");
            }

            UpdateStatus();

            if (_playerHp <= 0)
            {
                AppendLog("プレイヤーは倒れた… 敗北…");
                SetCommandsEnabled(false);
            }
        }

        private bool BattleOngoing()
        {
            return _playerHp > 0 && _enemyHp > 0;
        }

        private void UpdateStatus()
        {
            lblPlayer.Text = $"プレイヤーHP: {_playerHp}";
            lblEnemy.Text  = $"敵HP: {_enemyHp}";
        }

        private void SetCommandsEnabled(bool enabled)
        {
            btnAttack.Enabled = enabled;
            btnHeal.Enabled   = enabled && _healLeft > 0;
            btnDefend.Enabled = enabled; // A: 防御ボタンの有効/無効も同期
        }

        private void UpdateHealButtonText()
        {
            btnHeal.Text = $"かいふく({_healLeft})";
        }

        // (STEP3準拠)ListBox版のログ出力
        private void AppendLog(string line)
        {
            lstLog.Items.Add(line);
            lstLog.TopIndex = lstLog.Items.Count - 1; // 常に最新へスクロール
        }
    }
}

GitHub Desktop操作

  1. Changes タブで btnDefend の追加を確認
  2. Summary に「防御ボタンを追加」と記入 → Commit to feature-A
  3. Push origin
  4. 右上の Create Pull Request(Preview Pull Request) → GitHubブラウザが開く
  5. Base: main, Compare: feature-A で PR 作成

スクリーンショット例

R/マージ時の注意(担当A→リーダー)

リーダー(レビュー)

  • 防御が正しく織り込まれているか(ダメージ計算順・ログ表記)。
  • 回復ボタンが残回数 0 で無効化されるか。
  • namespace が既存と一致しているか(partial 分離なし)。
  • Designer 差分が最小か(btnDefend 追加のみ)。
  • OKなら Merge Pull Request

3. 担当B:毒攻撃追加 → PR作成

PRマージ後は main を Pull → そこから新規に feature-Bを作成

仕様(学習向けの最小構成)

  • 敵の攻撃時に30% の確率でプレイヤーに毒付与
  • 毒は 3ターン 継続、ターン終了時(敵行動の後)に2ダメージ
  • 再付与されたら 残りターンをリセット(重ね掛けはしない)

追加・変更点(差分)

1) フィールドを追加(Form1 の先頭のフィールド群へ)

// ▼ B: 毒(プレイヤー側)
private int _playerPoisonTurns = 0; // 残りターン数。0なら未毒

乱数は既存の _rand を使い回します(毎回 new Random() しない)。

2) EnemyTurnWithCritical を拡張

  • 「敵の攻撃 → (30%で毒付与)→ 毒の継続ダメージ → 終了判定」の順にします。
  • 既に担当Aの防御を組み込んでいる場合は、そのロジックを尊重したうえで毒を追加します。
private void EnemyTurnWithCritical()
{
    bool isCritical = (_rand.Next(0, 5) == 0); // 20%
    int baseDamage = isCritical ? _rand.Next(10, 15) : _rand.Next(3, 8);

    // ▼ A: 防御の適用(担当Aが導入済みの場合)
    int enemyDamage = baseDamage;
    if (_isDefending) // 担当Aのフラグ
    {
        enemyDamage = Math.Max(1, baseDamage / 2);
        _isDefending = false; // 1回で解除
        AppendLog("防御により被ダメージが軽減される!");
    }

    _playerHp = Math.Max(0, _playerHp - enemyDamage);

    if (isCritical)
        AppendLog($"敵のクリティカルヒット! {enemyDamage} の大ダメージ!\nプレイヤーHP: {_playerHp}");
    else
        AppendLog($"敵の攻撃! {enemyDamage} のダメージ!\nプレイヤーHP: {_playerHp}");

    // ▼ B: 30%で毒を付与(残り3ターン、重ね掛けせずリセット)
    if (_playerHp > 0 && _rand.NextDouble() < 0.3)
    {
        _playerPoisonTurns = 3;
        AppendLog("敵の毒攻撃! プレイヤーは毒に侵された(3ターン)");
    }

    // ▼ B: ターン終了時の継続ダメージ(DOT)
    if (_playerPoisonTurns > 0 && _playerHp > 0)
    {
        int dot = 2;
        _playerHp = Math.Max(0, _playerHp - dot);
        _playerPoisonTurns--;
        AppendLog($"毒の継続ダメージ! {dot} のダメージ(残り {_playerPoisonTurns} ターン)\nプレイヤーHP: {_playerHp}");
    }

    UpdateStatus();

    if (_playerHp <= 0)
    {
        AppendLog("プレイヤーは倒れた… 敗北…");
        SetCommandsEnabled(false);
    }
}

これで、担当Bのブランチ feature-B にコミット → Push → Create Pull Request(Base: main / Compare: feature-B)できます。

GitHub Desktop操作

  • Commit to feature-B(Summary:「毒状態を追加」)
  • Push origin
  • Create Pull Request(Base: main / Compare: feature-B)

PR/マージ時の注意(担当B→リーダー)

リーダー(レビュー)

  • 毒付与の確率処理が正しいか(30% の確率で発動)。
  • 毒の継続ダメージ処理が正しいタイミング(敵行動の後、ターン終了時)で呼ばれているか。
  • 残りターン数の管理が正しくできているか(3ターンで減少、0で終了)。
  • HP が負の値にならないよう Math.Max で処理しているか。
  • 既存のログ出力フォーマットと統一感があるか(ダメージや残りターン数の表示)。
  • 乱数生成が適切に _rand を再利用しているか(毎回 new Random() していないか)。
  • Designer差分が最小か(UIファイルに不要な変更が混ざっていないか)。
  • OKなら Merge Pull Request

4. 担当C:ログ強化 → PR作成

PRマージ後は main を Pull → そこから新規に feature-C を作成

仕様(既存APIは維持)

  • 重要イベントの強調表示(勝敗など)
  • ターン番号の導入(既存の AppendLog を置き換えず、そのまま使えるように内部で付番)

追加・変更点(差分)

1) フィールドを追加(Form1 の先頭のフィールド群へ)

// ▼ C: ログ用ターンカウンタ
private int _turn = 1;

2) 重要ログ用ヘルパーを追加

// ▼ C: 重要イベントを強調表示するログ
private void AppendLogImportant(string line)
{
    lstLog.Items.Add($"=== Turn {_turn}: {line} ===");
    lstLog.TopIndex = lstLog.Items.Count - 1;
}

3) 既存の AppendLog を内側でターン番号付きに変更

呼び出し側のコード(既存の AppendLog(“…"))はそのままでOK。
すべての通常ログにターン番号が自動で付きます。

// (STEP3準拠の置き換え版)ListBox版のログ出力+ターン番号付与
private void AppendLog(string line)
{
    lstLog.Items.Add($"Turn {_turn}: {line}");
    lstLog.TopIndex = lstLog.Items.Count - 1;
}

4) 「1ターンの終わり」で _turn を進める

STEP3のフローではプレイヤー行動→敵行動で1ターンとみなせるので、
敵処理の最後(=EnemyTurnWithCritical の最後)でターンを increment します。

private void EnemyTurnWithCritical()
{
    // …(攻撃/防御/毒/継続ダメージまで、前掲のBの処理)

    UpdateStatus();

    if (_playerHp <= 0)
    {
        AppendLogImportant("プレイヤーは倒れた… 敗北…");
        SetCommandsEnabled(false);
    }
    else if (_enemyHp <= 0)
    {
        AppendLogImportant("敵を倒した! 勝利!");
        SetCommandsEnabled(false);
    }

    // ▼ C: 1ターン終了(プレイヤー→敵まで終えたらカウントアップ)
    _turn++;
}

先に提示した担当A/Bのログや勝敗ログは、そのまま AppendLog を AppendLogImportant に一部差し替えるとより見やすくなります(任意)。

例(勝敗時の置き換え):

// プレイヤー勝利(どこかの勝利判定箇所で)
AppendLogImportant("敵を倒した! 勝利!");

// プレイヤー敗北
AppendLogImportant("プレイヤーは倒れた… 敗北…");

GitHub Desktop操作

  • Commit to feature-C(Summary:「ログを強調表示に変更」)
  • Push origin
  • Create Pull Request(Base: main / Compare: feature-C)

R/マージ時の注意(担当B→リーダー)

リーダー(レビュー)

  • ログ強化が正しく機能しているか(ターン番号が一貫して付与されているか、重要イベントで AppendLogImportant が呼ばれているか)
  • 既存のメソッドとの互換性が維持されているか(既存の AppendLog 呼び出しはそのまま動作し、エラーが出ていないか)
  • ターンカウンタの更新タイミングが適切か(プレイヤー行動+敵行動で1ターン進むようになっているか)
  • ログの表記が統一されているか(通常ログ・重要ログのフォーマットが混在せず、一目で区別できるか)
  • Designer差分が最小か(UIの追加・削除がなく、コード上の変更のみで済んでいるか)
  • OKなら Merge Pull Request

5. リーダー:レビューとマージ手順まとめ

操作手順

  1. GitHub Web で各 PR を確認
  2. 変更内容・動作確認方法をチェック
  3. 必要ならコメントで修正依頼
  4. 問題なければ Merge Pull Request

6. 全員:同期と実行テスト

  • 各自 GitHub Desktop で Pull origin → main を最新化
  • Visual Studio で実行して、新機能が統合されていることを確認

PR 作成メモ(担当B/C 用)

  • A(防御)
    • Title: feat: 防御アクションを追加(次回被ダメ半減)
    • 説明要点: 適用タイミング(次の敵攻撃のみ)、ログ文言、UI差分(btnDefend のみ)。
  • B(毒)
    • Title: feat: 毒ステータスを追加(30%付与/3ターン/各2ダメ)
    • 説明要点: 付与タイミング(敵攻撃後)、DOTタイミング(ターン終了時)、重ね掛け仕様(リセット)
  • C(ログ強化)
    • Title: feat: ログ強化(ターン番号付与/重要イベント強調)
    • 説明要点: 既存 AppendLog の呼び出し互換、AppendLogImportant の追加、カウントアップのタイミング

参考:A+B+C を統合した最終版(STEP3準拠)

既存の AppendLog / EnemyTurnWithCritical を使い、A(防御)、B(毒)、C(ログ強化)を全部入れた Form1.cs 完成例です。(UI 名:lblPlayer, lblEnemy, btnAttack, btnHeal, btnDefend, lstLog)

using System;
using System.Windows.Forms;

namespace BattleApp
{
    public partial class Form1 : Form
    {
        private int _playerHp = 30;
        private int _enemyHp = 20;
        private int _healLeft = 3;
        private readonly Random _rand = new Random();

        // A: 防御
        private bool _isDefending = false;

        // B: 毒(プレイヤー)
        private int _playerPoisonTurns = 0;

        // C: ログ用ターン番号
        private int _turn = 1;

        public Form1()
        {
            InitializeComponent();
            UpdateStatus();
            UpdateHealButtonText();
            AppendLog("バトルスタート!");
        }

        private void btnAttack_Click(object sender, EventArgs e)
        {
            if (!BattleOngoing()) return;

            int damage = _rand.Next(5, 10);
            _enemyHp = Math.Max(0, _enemyHp - damage);
            AppendLog($"プレイヤーの攻撃! {damage} のダメージ!");
            UpdateStatus();

            if (_enemyHp <= 0)
            {
                AppendLogImportant("敵を倒した! 勝利!");
                SetCommandsEnabled(false);
                return;
            }

            EnemyTurnWithCritical();
        }

        private void btnHeal_Click(object sender, EventArgs e)
        {
            if (!BattleOngoing() || _healLeft <= 0) return;

            int heal = _rand.Next(6, 12);
            _playerHp += heal;
            _healLeft--;
            AppendLog($"プレイヤーは {heal} かいふくした!\nプレイヤーHP: {_playerHp}");
            UpdateStatus();
            UpdateHealButtonText();

            // ★ ここで残回数を反映させて Heal ボタンを自動無効化
            SetCommandsEnabled(true);

            if (BattleOngoing())
            {
                EnemyTurnWithCritical();
            }
        }

        // A: 防御ボタン(次の敵攻撃のみ半減)
        private void btnDefend_Click(object sender, EventArgs e)
        {
            if (!BattleOngoing()) return;

            _isDefending = true;
            AppendLog("プレイヤーは防御の体勢を取った(次の被ダメ半減)");
            EnemyTurnWithCritical();
        }

        // STEP2準拠のクリティカルに A・B・C を統合
        private void EnemyTurnWithCritical()
        {
            bool isCritical = (_rand.Next(0, 5) == 0); // 20%
            int baseDamage = isCritical ? _rand.Next(10, 15) : _rand.Next(3, 8);

            // A: 防御適用
            int enemyDamage = baseDamage;
            if (_isDefending)
            {
                enemyDamage = Math.Max(1, baseDamage / 2);
                _isDefending = false;
                AppendLog("防御により被ダメージが軽減される!");
            }

            _playerHp = Math.Max(0, _playerHp - enemyDamage);

            if (isCritical)
                AppendLog($"敵のクリティカルヒット! {enemyDamage} の大ダメージ!\nプレイヤーHP: {_playerHp}");
            else
                AppendLog($"敵の攻撃! {enemyDamage} のダメージ!\nプレイヤーHP: {_playerHp}");

            // B: 30%で毒付与(残り3ターン/重ね掛けなし)
            if (_playerHp > 0 && _rand.NextDouble() < 0.3)
            {
                _playerPoisonTurns = 3;
                AppendLog("敵の毒攻撃! プレイヤーは毒に侵された(3ターン)");
            }

            // B: ターン終了時の継続ダメージ
            if (_playerPoisonTurns > 0 && _playerHp > 0)
            {
                int dot = 2;
                _playerHp = Math.Max(0, _playerHp - dot);
                _playerPoisonTurns--;
                AppendLog($"毒の継続ダメージ! {dot} のダメージ(残り {_playerPoisonTurns} ターン)\nプレイヤーHP: {_playerHp}");
            }

            UpdateStatus();

            if (_playerHp <= 0)
            {
                AppendLogImportant("プレイヤーは倒れた… 敗北…");
                SetCommandsEnabled(false);
            }
            else if (_enemyHp <= 0)
            {
                AppendLogImportant("敵を倒した! 勝利!");
                SetCommandsEnabled(false);
            }

            // C: 1ターン終了
            _turn++;
        }

        private bool BattleOngoing() => _playerHp > 0 && _enemyHp > 0;

        private void UpdateStatus()
        {
            lblPlayer.Text = $"プレイヤーHP: {_playerHp}";
            lblEnemy.Text  = $"敵HP: {_enemyHp}";
        }

        private void SetCommandsEnabled(bool enabled)
        {
            btnAttack.Enabled = enabled;
            btnHeal.Enabled   = enabled && _healLeft > 0;
            btnDefend.Enabled = enabled; // A: 防御ボタンも同期
        }

        private void UpdateHealButtonText()
        {
            btnHeal.Text = $"かいふく({_healLeft})";
        }

        // C: ターン番号を自動付与する通常ログ
        private void AppendLog(string line)
        {
            lstLog.Items.Add($"Turn {_turn}: {line}");
            lstLog.TopIndex = lstLog.Items.Count - 1;
        }

        // C: 重要イベント用の強調ログ
        private void AppendLogImportant(string line)
        {
            lstLog.Items.Add($"=== Turn {_turn}: {line} ===");
            lstLog.TopIndex = lstLog.Items.Count - 1;
        }
    }
}
訪問数 15 回, 今日の訪問数 15回

C#

Posted by hidepon