講座全体の概要
プログラミング基礎講座の総仕上げ、「ハイ&ロー」というトランプゲームを作成する実践演習です。数回に分けてヒントや実際のプログラム例の提示、解説等を行っていきます。今回は演習回答例の内容解説の前回からの続きです。
講座全体の概要は以下の記事ゼロから始めるプログラミング基礎講座 第1回【講座の概要・目的】をご覧ください。
演習の内容、ヒント、回答例は前回までの記事ゼロから始めるプログラミング基礎講座 第9回【実践演習】PART1ゼロから始めるプログラミング基礎講座 第9回【実践演習】PART2 ヒントゼロから始めるプログラミング基礎講座 第9回【実践演習】PART3 回答例にありますので、そちらを先にご覧ください。
今回は前回記事ゼロから始めるプログラミング基礎講座 第9回【実践演習】PART4 回答解説1の続きです。
プログラムコードの解説
前回、アプリケーションの仕様を以下の①~⑥のイベントに分け、①、②についてプログラムのポイント解説をしました。
- ①初期表示時
- ②「ゲーム開始」ボタン押下時
- ③「コール」ボタン押下時
- ④「次のカードを出す」ボタン押下時
- ⑤ゲーム終了時
- ⑥「新たにゲーム開始」ボタン押下時
今回は残りの③~⑥のポイントを解説します。
③「コール」ボタン押下時(OnPostCall)
public class IndexModel : PageModel
{
[TempData]
public bool PlayerIsParent { get; set; }
[TempData]
public bool HasCalled { get; set; }
[TempData]
public Card CurrentPlayerCard { get; set; }
[TempData]
public Card CurrentCpuCard { 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
{
未選択,
ハイ,
ロー
}
private enum Judement
{
当たり,
はずれ,
引き分け
}
public IActionResult OnPostCall()
{
if (!InputIsValid())
{
return BadRequest();
}
HasCalled = true;
Judement judement = Judge();
SetResult(judement);
return Page();
}
private bool InputIsValid()
{
return (HighLowChoice == HighOrLow.ハイ || HighLowChoice == HighOrLow.ロー);
}
private void SetResult(Judement judement)
{
const string RESULT_MESSAGE_POINT_CPU = "CPUが2枚獲得";
const string RESULT_MESSAGE_POINT_PLAYER = "プレイヤーが2枚獲得";
const string RESULT_MESSAGE_POINT_BOTH = "両者が1枚ずつ獲得";
ResultMessage = $"{judement}:";
if (judement == Judement.当たり)
{
if (PlayerIsParent)
{
CpuScore += 2;
ResultMessage += RESULT_MESSAGE_POINT_CPU;
}
else
{
PlayerScore += 2;
ResultMessage += RESULT_MESSAGE_POINT_PLAYER;
}
}
else if (judement == Judement.はずれ)
{
if (PlayerIsParent)
{
PlayerScore += 2;
ResultMessage += RESULT_MESSAGE_POINT_PLAYER;
}
else
{
CpuScore += 2;
ResultMessage += RESULT_MESSAGE_POINT_CPU;
}
}
else
{
CpuScore += 1;
PlayerScore += 1;
ResultMessage += RESULT_MESSAGE_POINT_BOTH;
}
}
private Judement Judge()
{
if (PlayerIsParent)
{
return Judge(HighLowChoice, CurrentPlayerCard, CurrentCpuCard);
}
else
{
return Judge(HighLowChoice, CurrentCpuCard, CurrentPlayerCard);
}
}
private Judement Judge(HighOrLow choice, Card parentCard, Card childCard)
{
int compare = childCard.Number - parentCard.Number;
if (compare == 0)
{
return Judement.引き分け;
}
else if (compare > 0 && choice == HighOrLow.ハイ)
{
return Judement.当たり;
}
else if (compare < 0 && choice == HighOrLow.ロー)
{
return Judement.当たり;
}
else
{
return Judement.はずれ;
}
}
}
処理の流れは以下の様になっています。
まず、InputIsValid()で画面入力が正しく行われているかチェックし、正しくない場合は不正な要求がされたとしてエラー(BadRequest)を返してアプリケーションを強制的に終了させています。
事前にブラウザ側でチェックを行っているので入力は常に正しいはずですが、それが正しくない場合には何らかの不正な操作が行われたと判断してアプリケーションを終了させています。必ずしもエラー終了させる必要はありませんが、Webアプリケーションではサーバー側でも必ず入力内容をチェックすべきです。
(C#のWebアプリケーションでは入力内容を検証するための機構が備わっており、本来はそちらを利用した方がいいのですが、当講座では扱っていない事項なので回答例でも使用していません。)
入力が正しければ「コールされたか否か」の状態を表すHasCalledプロパティをtrueに変更してから、コール結果の判定に移ります。このプロパティはIndex.cshtmlで表示処理を制御するために使用します。
Judge()ではプレイヤーが親の場合とそれ以外で引数を変更して、同名の別メソッドJudge()を呼び出しています。メソッドは引数の型が異なれば同名でもそれぞれを区別することができます。
今回の例ではJudge()メソッドを2つに分けて2段階で処理していますが、1つにまとめてしまうことも可能ではあります。しかし、1つにまとめてしまうと、if文の中が長くなって読みづらくなったり、似たような処理を2箇所に記述したりといったことが起きます。1つのメソッドや条件分岐の中はできるだけすっきりさせることを意識しましょう。
コール結果の判定ができたら、SetResult()で獲得枚数の加算と判定結果のメッセージをプロパティに設定して終わります。
SetResult()メソッドの中身が少し長めで、if文のネストもあるので若干読みづらいですが、自分の中ではギリギリ許せる範囲だと思っています。メソッドの分割等をしてもあまり効果もなさそうだし、逆に複雑になるかもしれないとも考えたので、あえてこのままにしています。
<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>
ここでは特に難しいことはなく、HasCalledプロパティがtrueとなった状態で条件分岐がされますので、そこだけに注目すれば処理内容は理解できるかと思います。わからない部分がある場合はPART2のヒントを見返してください。
④「次のカードを出す」ボタン押下時(OnPostNext)
public class IndexModel : PageModel
{
[TempData]
public bool HasCalled { get; set; }
[BindProperty]
public HighOrLow HighLowChoice { get; set; }
public enum HighOrLow
{
未選択,
ハイ,
ロー
}
public IActionResult OnPostNext()
{
HasCalled = false;
PlayerIsParent = !PlayerIsParent;
SetCurrentCard();
if (PlayerIsParent)
{
HighLowChoice = ChoiceByCpu();
}
else
{
HighLowChoice = HighOrLow.未選択;
}
return Page();
}
private HighOrLow ChoiceByCpu()
{
if (CurrentPlayerCard.Number > 6)
{
return HighOrLow.ロー;
}
else
{
return HighOrLow.ハイ;
}
}
}
HasCalledプロパティをfalseに変更し、更に下記の部分で親を変更しています。
PlayerIsParent = !PlayerIsParent;
このPlayerIsParentプロパティはbool型なので値はtrueかfalseとなりますが、自分自身の否定(NOT)を代入することで、値を反転させることができます。
SetCurrentCard()でプレイヤーとCPUそれぞれの次のカードを取り出します。
プレイヤーが親になった場合、CPUがハイ・ローの選択をする番なので、ChoiceByCpu()でハイ・ローを決定します。あくまでも自分のカードは見えていない前提なので、プレイヤーのカードの数字だけを見て単純に決定しています。既に出たカードを記憶しておけば、もっと高い確率で当てる様に変更することもできます。その場合もほとんどのメソッドは変更する必要がなく、選択ロジックの変更はChoiceByCpu()だけに局所化できます。既に出たカードを記憶するのはSetResult()で獲得したカードをListに保存すれば良いでしょう。
決定したCPUの選択を画面に表示させるため、HighLowChoiceプロパティに設定しておきます。対して、CPUが親の場合はプレイヤーが選択をするので、HighLowChoiceプロパティには未選択を設定しておきます。ここでの設定内容は画面のラジオボタンに自動反映されないので、Index.cshtml.csに記述したJavaScriptで反映させます。
<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>
}
}
HasCalledプロパティがtrueとなった状態で条件分岐がされます。
ラジオボタンの変更は下記の部分で行っています。
@{
<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>
}
}
まず、ハイ・ローのどちらも選択されていない状態にするJavaScriptを出力します。その後にHighLowChoiceプロパティの値により、どちらか一方だけチェックを付けるためのJavaScriptを出力します。なぜこの様な面倒なことをするか説明しておくと、HTMLの仕様上、ラジオボタンのchecked属性は初期状態での選択を指定するものです。選択を変更するための属性ではないので、一旦何も選択されていない状態に初期化してからでないと思い通りに選択ができないためです。
(実際にJavaScriptが実行されるのはブラウザ側となります。)
⑤ゲーム終了時(OnPostNext)
public class IndexModel : PageModel
{
[TempData]
public GameState CurrentGameState { get; set; }
[TempData]
public int PlayerScore { get; set; }
[TempData]
public int CpuScore { get; set; }
public string ResultMessage { get; set; }
public enum GameState
{
BeforePlay,
Playing,
End
}
public IActionResult OnPostNext()
{
HasCalled = false;
PlayerIsParent = !PlayerIsParent;
SetCurrentCard();
if (PlayerIsParent)
{
HighLowChoice = ChoiceByCpu();
}
else
{
HighLowChoice = HighOrLow.未選択;
}
return Page();
}
private void SetCurrentCard()
{
if ((PlayerCards.Count > 0) && (CpuCards.Count > 0))
{
CurrentPlayerCard = GetNextCard(PlayerCards);
CurrentCpuCard = GetNextCard(CpuCards);
}
else
{
CurrentGameState = GameState.End;
ResultMessage = GetResultMessage();
}
}
private string GetResultMessage()
{
if (PlayerScore > CpuScore)
{
return "あなたの勝ちです。";
}
else if (PlayerScore < CpuScore)
{
return "あなたの負けです。";
}
else
{
return "引き分けです。";
}
}
}
「次のカードを出す」ボタン押下時に残りのカードがなければゲームを終了させます。
SetCurrentCard()でListの要素数を確認している条件判定がゲーム終了の判断部分です。CurrentGameStateをEndに設定することで、Index.cshtml.csの表示処理を制御しています。
GetResultMessage()では総獲得枚数によって勝敗のメッセージを決定しているだけなので特に難しいところはありません。
<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;
}
}
CurrentGameStateプロパティがEndとなった状態で条件分岐がされます。
「新たにゲーム開始」ボタンには以下の指定を行っています。
asp-page-handler="GameStart"
これは初期表示時の「ゲーム開始」ボタンと同じ設定です。
⑥「新たにゲーム開始」ボタン押下時(OnPostGameStart)
public class IndexModel : PageModel
{
public IActionResult OnPostGameStart()
{
InitState();
DealCards();
SetCurrentCard();
return Page();
}
}
「新たにゲーム開始」ボタンは初期表示時の「ゲーム開始」ボタンと全く同じ処理となり、②「ゲーム開始」ボタン押下時の処理へ戻ります。
プログラミング基礎講座 終了
以上で解説を終わります。また、ここまで長かったですが、プログラミング基礎講座も全て終了です。いかがでしたでしょうか?
難しいところもあったと思いますが、基礎的な部分はひと通り押さえたつもりです。
講座全体を通して不明な点等がありましたら、コメント欄で質問いただければ回答します。
少しでも皆さんの助けになれたら幸いです。
また、ほとんどのプログラミング言語において、オブジェクト指向は避けて通れません。オブジェクト指向プログラミングについてもっと知りたい方は下の記事をご覧ください。
この記事へのコメントはありません。