講座全体の概要
プログラミング基礎講座の総仕上げ、「ハイ&ロー」というトランプゲームを作成する実践演習です。数回に分けてヒントや実際のプログラム例の提示、解説等を行っていきます。今回は演習回答例の内容解説をします。
講座全体の概要は以下の記事ゼロから始めるプログラミング基礎講座 第1回【講座の概要・目的】をご覧ください。
演習の内容、ヒント、回答例は前回までの記事ゼロから始めるプログラミング基礎講座 第9回【実践演習】PART1ゼロから始めるプログラミング基礎講座 第9回【実践演習】PART2 ヒントゼロから始めるプログラミング基礎講座 第9回【実践演習】PART3 回答例にありますので、そちらを先にご覧ください。
今回はPART3で掲載した回答例についての解説です。
アプリケーションの仕様
まず、アプリケーションの仕様をイベントごとに分けて説明します。
①初期表示時
初期表示(OnGet)イベントではゲーム開始画面を表示し、ゲーム開始の待機状態とします。
②「ゲーム開始」ボタン押下時
ゲーム開始(OnPostGameStart)イベントでは以下の様なゲーム画面を表示します。最初の親は常にCPUとし、プレイヤーがハイ・ローのコールを行う状態となります。
ゲーム画面にはプレイヤー及びCPUの現在のカードを表示しますが、親のカードは表向き(数字が見える向き)、子のカードは裏向きとします。
「次のカードを出す」ボタンは押下不可とし、「コール」ボタンがハイライトされるように配色します。
ハイ・ロー選択ラジオボタンは、CPUが親の場合、未選択の状態で表示し、入力を可能かつ必須とします。
③「コール」ボタン押下時
コール(OnPostCall)イベントではゲーム画面に以下の様に結果を表示します。
親と子のカードを両方とも表向きにし、当たり・はずれ・引き分け、プレイヤー・CPUの獲得枚数を結果として表示します。
また、「コール」ボタンは押下不可とし、「次のカードを出す」ボタンを押下可能かつハイライトする様に配色も変更します。
④「次のカードを出す」ボタン押下時
次のカードを出す(OnPostNext)イベントでは親と子を交代し、コールを行う状態に戻します。プレイヤーが親になった場合は以下の様になります。
ハイ・ロー選択ラジオボタンは、プレイヤーが親の場合、どちらかを選択済みの状態で表示し、変更不可とします。
残りのカードがなくなるまで「コール」と「次のカードを出す」を繰り返します。
「次のカードを出す」押下時に残りのカードがない場合はゲーム終了とします。
⑤ゲーム終了時
ゲーム終了時は以下の様にゲーム終了画面を表示します。
ゲーム終了画面にはプレイヤー及びCPUの総獲得枚数、プレイヤーの勝ち・負け・引き分けを表示します。
⑥「新たにゲーム開始」ボタン押下時
ゲーム終了画面で「新たにゲーム開始」を押下されると、②と同じゲーム開始(OnPostGameStart)イベントで再スタートします。
プログラムコードの解説
前述の①~⑥のイベントごとに前回の回答例プログラムのポイントを解説します。
①初期表示時(OnGet)
public class IndexModel : PageModel
{
[TempData]
public GameState CurrentGameState { get; set; }
public enum GameState
{
BeforePlay,
Playing,
End
}
public void OnGet()
{
CurrentGameState = GameState.BeforePlay;
}
}
現在のゲームの進行状況を管理するために、CurrentGameStateプロパティを用意しています。データ型はGameStateというenumを用意して3つの状態を表しています。
enumを使用することで、int型やstring型を使うよりもコードの読みやすさも上がりますし、間違った値を設定するリスクもほとんどなくなります。
OnGetではこのプロパティをBeforePlay(ゲーム開始前)という状態に設定するのみです。
<div class="text-center">
@{
if (Model.CurrentGameState == IndexModel.GameState.BeforePlay)
{
<form method="post">
<div class="row w-100 mt-3 mb-5 justify-content-center">
<input asp-page-handler="GameStart" class="btn btn-primary col-sm-2" type="submit" value="ゲーム開始" />
</div>
</form>
return;
}
else if (Model.CurrentGameState == IndexModel.GameState.End)
{
<div class="container-fluid mb-4">
<div class="row mb-4">
<div class="col-6 d-flex flex-column" id="PlayerCard">
<span>プレイヤーの総獲得枚数</span>
<span>@Model.PlayerScore</span>
</div>
<div class="col-6 d-flex flex-column" id="CpuCard">
<span>CPUのの総獲得枚数</span>
<span>@Model.CpuScore</span>
</div>
</div>
<div class="row mb-4">
<span class="col">@Model.ResultMessage</span>
</div>
</div>
<form method="post">
<div class="row w-100 mt-3 mb-5 justify-content-center">
<input asp-page-handler="GameStart" class="btn btn-primary col-sm-2" type="submit" value="新たにゲーム開始" />
</div>
</form>
return;
}
}
</div>
Index.cshtml.csでは先ほど設定したCurrentGameStateを参照して、BeforePlayの場合はゲーム開始ボタンを表示しています。GameStateというenumはIndexModelというクラスの内部に定義しているので、IndexModel以外で参照する場合にはIndexModel.から指定する必要があります。
ここでは、ゲーム開始ボタン表示の直後にreturn文で処理を終了し、それ以外の表示処理をさせないようにしています。
②「ゲーム開始」ボタン押下時(OnPostGameStart)
public class IndexModel : PageModel
{
[TempData]
public GameState CurrentGameState { get; set; }
[TempData]
public bool PlayerIsParent { get; set; }
[TempData]
public bool HasCalled { get; set; }
[TempData]
public Card CurrentPlayerCard { get; set; }
[TempData]
public Card CurrentCpuCard { get; set; }
[TempData]
public List<Card> PlayerCards { get; set; }
[TempData]
public List<Card> CpuCards { get; set; }
[BindProperty]
public HighOrLow HighLowChoice { get; set; }
[TempData]
public int PlayerScore { get; set; }
[TempData]
public int CpuScore { get; set; }
public string ResultMessage { get; set; }
public enum HighOrLow
{
未選択,
ハイ,
ロー
}
public enum GameState
{
BeforePlay,
Playing,
End
}
public IActionResult OnPostGameStart()
{
InitState();
DealCards();
SetCurrentCard();
return Page();
}
private void InitState()
{
CurrentGameState = GameState.Playing;
PlayerIsParent = false;
PlayerScore = 0;
CpuScore = 0;
HasCalled = false;
ResultMessage = string.Empty;
HighLowChoice = HighOrLow.未選択;
}
private void SetCurrentCard()
{
if ((PlayerCards.Count > 0) && (CpuCards.Count > 0))
{
CurrentPlayerCard = GetNextCard(PlayerCards);
CurrentCpuCard = GetNextCard(CpuCards);
}
else
{
CurrentGameState = GameState.End;
ResultMessage = GetResultMessage();
}
}
private Card GetNextCard(List<Card> cards)
{
Card card = null;
if (cards.Count > 0)
{
card = cards[0];
cards.RemoveAt(0);
}
return card;
}
private void DealCards()
{
PlayerCards = new List<Card>();
CpuCards = new List<Card>();
Cards cards = new Cards();
bool dealToPlayer = true;
foreach (Card card in cards)
{
if (dealToPlayer)
{
PlayerCards.Add(card);
}
else
{
CpuCards.Add(card);
}
dealToPlayer = !dealToPlayer;
}
}
}
CurrentGameStateの他に、PlayerIsParent(プレイヤーが親か否か)、HasCalled(コールがされたか否か)という状態を表すプロパティを用意しました。これらはIndex.cshtml.csで表示処理を分岐するために使用します。
ゲーム中のデータを保持するために、配られたカードの残りを表すList<Card>型、現在のカードを表すCard型および獲得した枚数を表すint型のプロパティをそれぞれプレイヤー用とCPU用に用意しています。
これらはゲームが終了するまで保持している必要があるためTempData属性を付けています。Index.cshtml.csで
TempData.Keep();
を実行しているので、意図的にクリアしない限りはアプリケーション終了まで保持されます。
HighLowChoiceプロパティは画面からのハイ・ローの選択入力を受け付けるために使用するため、BindProperty属性を付けています。
ResultMessageプロパティは一時的に画面に表示したいメッセージを格納するために使用し、1回表示したらデータを再利用することはないので、TempData等の属性は付けていません。(今見るとこのプロパティの使い方は少し適切ではなかったと反省しています。コール時の結果、ゲーム終了時の最終結果の2種類の表示にこのプロパティを利用しており、表示する場所も異なっているので、目的別にプロパティを分けて適切な名前を付けた方が、後で読んだ時にもわかりやすくなると思います。)
実際の処理は以下の部分です。
public IActionResult OnPostGameStart()
{
InitState();
DealCards();
SetCurrentCard();
return Page();
}
ここでは大まかな処理の流れが理解しやすいように抽象的に記述しています。
InitState()ではCurrentGameStateをPlayingに変更し、その他のプロパティを初期化しています。
DealCards()ではメプレイヤーとCPUにカードを配っています。メソッド内部ではそれぞれのリストに52枚のカードを1枚ずつ交互に追加しています。
SetCurrentCard()では現在のカード(画面に表示するカード)を取り出しています。メソッド内部ではリストの先頭からカードを取り出し、取り出したカードはリストから削除しています。
この様に、1つのメソッドの中を簡潔にしておいた方が、後から見た時に注目したい箇所が探しやすく、見る範囲が限定されるので、いきなり具体的なコードを書かず、抽象的に大まかな流れを記述した方がいいです。また、メソッド名もなんとなく処理内容が想像できる様にしておきましょう。
ここまでの講座ではメソッド名や変数名の付け方については言及していませんでした。チームで開発する様な場合は名前付けのルールが決まっていることもありますし、決まっていなくても他人が見てもできるだけわかりやすい様にすべきです。自分だけで開発している場合でも後で見返すことや、仕事であれば他人に引き継ぐこともあるわけですから常に意識しておきましょう。名前付けがいいかげんだとそれだけで実力がないと思われます
場合によっては日本語を使っても構いません。チーム開発の場合はルールで禁止されている場合はそれに従いますが、個人の開発や、日本語が禁止されていない場合は日本語を使用した方がわかりやすい場面も多々あります。チーム開発の場合で明確に禁止も許可もされていない場合は確認してから使用しましょう。
<div class="text-center">
<div class="container-fluid mb-4">
<div class="row">
<div class="col-6 d-flex flex-column" id="PlayerCard">
@{
<span>プレイヤーカード</span>
if (Model.PlayerIsParent || Model.HasCalled)
{
@Model.CurrentPlayerCard.CardImage.FrontImageToHtml();
}
else
{
@Model.CurrentPlayerCard.CardImage.BackImageToHtml();
}
}
</div>
<div class="col-6 d-flex flex-column" id="CpuCard">
@{
<span>CPUカード</span>
if (!(Model.PlayerIsParent) || Model.HasCalled)
{
@Model.CurrentCpuCard.CardImage.FrontImageToHtml();
}
else
{
@Model.CurrentCpuCard.CardImage.BackImageToHtml();
}
}
</div>
</div>
</div>
<form method="post">
@{
string pointerEventStyle = "pointer-events: none";
if (Model.HasCalled)
{
<div class="form-text mb-3">@Model.ResultMessage</div>
}
else if (Model.PlayerIsParent)
{
<div class="form-text mb-3">CPUの番です。CPUの選択は@(Model.HighLowChoice)です。コールしてください。</div>
}
else
{
<div class="form-text mb-3">プレイヤーの番です。ハイかローを選択してコールしてください。</div>
pointerEventStyle = "pointer-events: auto";
}
}
<div class="form-check form-check-inline justify-content-center mb-2" style="@pointerEventStyle">
<div class="mr-3">
<input class="form-check-input" asp-for="HighLowChoice" type="radio" value="@IndexModel.HighOrLow.ハイ" id="radioHigh" required />
<label class="form-check-label" for="radioHigh">@IndexModel.HighOrLow.ハイ</label>
</div>
<div class="mr-3">
<input class="form-check-input" asp-for="HighLowChoice" type="radio" value="@IndexModel.HighOrLow.ロー" id="radioLow" />
<label class="form-check-label" for="radioLow">@IndexModel.HighOrLow.ロー</label>
</div>
</div>
<div class="mt-3">
@{
if (Model.HasCalled)
{
<button asp-page-handler="Call" class="btn btn-dark col-sm-2" type="submit" disabled="disabled">コール</button>
<button asp-page-handler="Next" class="btn btn-primary col-sm-2" type="submit">次のカードを出す</button>
}
else
{
<button asp-page-handler="Call" class="btn btn-primary col-sm-2" type="submit">コール</button>
<button asp-page-handler="Next" class="btn btn-dark col-sm-2" type="submit" disabled="disabled">次のカードを出す</button>
}
}
</div>
</form>
</div>
@{
<script>
document.getElementById('radioHigh').removeAttribute('checked');
document.getElementById('radioLow').removeAttribute('checked');
</script>
if (Model.HighLowChoice == IndexModel.HighOrLow.ハイ)
{
<script>
document.getElementById('radioHigh').setAttribute('checked', 'checked');
</script>
}
else if (Model.HighLowChoice == IndexModel.HighOrLow.ロー)
{
<script>
document.getElementById('radioLow').setAttribute('checked', 'checked');
</script>
}
}
@{
TempData.Keep();
}
CurrentGameStateがPlaying以外ではこの部分の処理がされる前にreturn文で終了しているため、ここではCurrentGameStateがPlayingであることを前提にして条件分岐を少しでも単純にするようにしています。
以下の部分ではHighOrLowというenumを使用してラジオボタンのラベルを設定しています。
<div class="mr-3">
<input class="form-check-input" asp-for="HighLowChoice" type="radio" value="@IndexModel.HighOrLow.ハイ" id="radioHigh" required />
<label class="form-check-label" for="radioHigh">@IndexModel.HighOrLow.ハイ</label>
</div>
<div class="mr-3">
<input class="form-check-input" asp-for="HighLowChoice" type="radio" value="@IndexModel.HighOrLow.ロー" id="radioLow" />
<label class="form-check-label" for="radioLow">@IndexModel.HighOrLow.ロー</label>
</div>
enumをcshtmlで参照すると列挙子(ここでの「ハイ」、「ロー」の様なenumに定義されている定数のこと)の名前がそのまま表示されます。
それ以外はゼロから始めるプログラミング基礎講座 第9回【実践演習】PART2 ヒントで触れた通りです。
なお、HTMLタグに指定しているclassはBootStrapを使用してレイアウトを整えているだけです。BootStrapを使わずに独自にCSSを作成した方が自分の思い通りにできると思いますし、大規模なアプリケーション開発では見た目の統一感やHTMLコーディングを簡便にするためにもアプリケーション全体で使用する独自CSSを用意するべきだと思います。ただ、この講座ではHTML、CSSに主眼を置いてはいませんのでBootStrapの使用で最低限の見た目を整えるにとどめています。
続きは次回PART5で
長くなりましたので今回はここまでとして、続きの解説は次回PART5で行います。以上、お疲れ様でした。
この記事へのコメントはありません。