GitHub Desktopで学ぶブランチ & PR(プルリクエスト)
~WinFormsアプリを題材にしたチーム開発演習~
前提
- Visual Studio で「Form1」を持つ WinForms プロジェクトが用意済み
- GitHub アカウント & GitHub Desktop がセットアップ済み
- ステップ3(ログを ListBox に表示)まで完成している状態
目次
- リポジトリとブランチの準備(リーダー)
- 担当A:防御ボタン追加 → PR作成
- 担当B:毒攻撃追加 → PR作成
- 担当C:ログ強化 → PR作成
- リーダー:レビューとマージ
- 全員:同期と実行テスト
バージョン管理の基本の流れ
推奨順序:担当Aの作業 → マージ → 全員 Pull → その時点の main から 担当Bの作業ブランチ を切る → マージ → Pull → main から 担当Cの作業ブランチ を切る。
途中で分岐を先に切っていた場合は、GitHub Desktop の「Update from main」(あるいは Rebase)で main を取り込む
1. リポジトリとブランチの準備(リーダー)
namespaceは、既存プロジェクトに合わせるか、Form1.Designer.cs/Program.cs 側も合わせてください
操作手順
- GitHub Desktop を起動し、対象リポジトリを選択
- Current Branch > New Branch
- 担当Aさんの作業前は、feature-A
- 担当Bさんの作業前は、feature-B
- 担当Cさんの作業前は、feature-C を作成
- 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操作
- Changes タブで btnDefend の追加を確認
- Summary に「防御ボタンを追加」と記入 → Commit to feature-A
- Push origin
- 右上の Create Pull Request(Preview Pull Request) → GitHubブラウザが開く
- 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. リーダー:レビューとマージ手順まとめ
操作手順
- GitHub Web で各 PR を確認
- 変更内容・動作確認方法をチェック
- 必要ならコメントで修正依頼
- 問題なければ 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;
}
}
}
ディスカッション
コメント一覧
まだ、コメントがありません