C#でつくる素数大富豪、完成品はこんな感じです。
これまでコンピュータの次の一手を考えることだけを考えてきましたが、今回は自分(=プレイヤー)と対戦できるものをつくります。実験的なアプリなのでカードを表示させてカードゲームっぽく見せることはしません。数字が表示されるだけです。
まずコンピュータとプレイヤーの2人で対戦します。そこで両者が持っているカードを格納しているリストを示します。トランプを使うのでスート(スペードとかダイヤとか)があるのですが、いまは考えないことにします。
それから親が出せるカードの枚数を決めるとともに、次のプレイヤーは前のプレイヤーよりも大きな数を出さなければなりません。そこで出すことができる枚数とそれよりも大きな数を出さなければならない数をフィールド変数で管理します。もし-1であれば自分が親であり、好きな枚数で好きな数を出すことができます。
1 2 3 4 5 6 7 8 |
public partial class Form1 : Form { List<int> CompCardsDealt = new List<int>(); List<int> PlayerCardsDealt = new List<int>(); int CountCardsPutMust = -1; long MinValueCards = -1; } |
プレイヤーの動作に関する処理
ゲームが開始されたらコンピュータとプレイヤーにカードが配られ、ゲームが開始されます。その処理を示します。最初はプレイヤーが親で始まります。
出すカードを選ぶ
出したいカードをボタンをクリックすることで選択します。存在しないカードは選択できないのでその場合は灰色表示します。これは後述するEnableNumberButtonメソッドでおこないます。そのときキャンセルされることもあるのでプレイヤーのカードを別のリストにコピーしておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
public partial class Form1 : Form { List<int> PlayerCardsDealtCopy = new List<int>(); private void ButtonGameStart_Click(object sender, EventArgs e) { // 最初は好きな枚数で好きな数を出せる CountCardsPutMust = -1; MinValueCards = -1; // カードをシャッフルする shuffledCards = ShuffleCards(); // コンピュータに13枚カードを配る // 当然のことながらシャッフルされたカードは13枚だけなくなる // TextBoxにコンピュータのカードを表示させる CompCardsDealt = shuffledCards.Take(13).ToList(); shuffledCards = shuffledCards.Skip(13).ToList(); CompCardsDealt = CompCardsDealt.OrderBy(x => x).ToList(); TextBoxCompCards.Text = String.Join(", ", CompCardsDealt); // 同様にプレイヤーにも13枚カードを配る PlayerCardsDealt = shuffledCards.Take(13).ToList(); shuffledCards = shuffledCards.Skip(13).ToList(); PlayerCardsDealt = PlayerCardsDealt.OrderBy(x => x).ToList(); TextBoxPlayerCards.Text = String.Join(", ", PlayerCardsDealt); // TextBoxになにか書かれている場合、すべてクリアする TextBoxCompPutCards.Text = ""; TextBoxCompNumberMadeWithCards.Text = ""; TextBoxCompPutCardsAsPrimeFactor.Text = ""; TextBoxCompResultsPrimeFactorization.Text = ""; // プレイヤーのカードを別のリストにコピーする // コピーされたリストは後述するEnableNumberButtonメソッドで使う PlayerCardsDealtCopy = new List<int>(PlayerCardsDealt); InputDataClear(); // 進行しているゲームはどのような状態になっているか表示させる labelMessage.Text = "あなたが親です。"; } } |
プレイヤーがカードを出すためには数字が書かれたボタンを押します。どのボタンが押されたらその番号をもつカードが出されるかがわかるようにしておかなければなりません。その役割をしているのがGetNumberFromButtonメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public partial class Form1 : Form { int GetNumberFromButton(Button button) { int number = 0; if (button == ButtonA) number = 1; else if (button == Button2) number = 2; else if (button == Button3) number = 3; else if (button == Button4) number = 4; else if (button == Button5) number = 5; else if (button == Button6) number = 6; else if (button == Button7) number = 7; else if (button == Button8) number = 8; else if (button == Button9) number = 9; else if (button == Button10) number = 10; else if (button == ButtonJ) number = 11; else if (button == ButtonQ) number = 12; else if (button == ButtonK) number = 13; return number; } } |
数字が書かれたボタンをクリックしたときの処理を示します。ボタンがクリックされるときは素数としてカードを出すときにクリックされるときと、合成数出しで素因数を指定する場合があります。これはCheckBoxで区別しています。
またカードが出されたら、そのカードはなくなってしまうのでボタンをクリックすることができなくなるかもしれません。そこでEnableNumberButtonメソッドを呼び出して、ボタンをクリックできるできないをセットしなおしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public partial class Form1 : Form { // プレイヤーによって場に出されたカード List<int> PlayerPutCards1 = new List<int>(); // プレイヤーによって素因数場に出されたカード List<int> PlayerPutCards2 = new List<int>(); // プレイヤーによって素因数場に出されたカードでつくられる「素因数」 // 「素因数」にカギ括弧がついているのは // プレイヤーの勘違いで本当に素数かどうかは現時点ではわからないため List<int> Primefactors = new List<int>(); // プレイヤーによって素因数場に出されたが「素因数」としては確定していないカード List<int> TempPrimefactors = new List<int>(); private void ButtonNumber_Click(object sender, EventArgs e) { int number = GetNumberFromButton((Button)sender); if (!CheckBoxPrimeFactor.Checked) { PlayerPutCards1.Add(number); TextBoxPlayerPutCards.Text += number.ToString() + ", "; TextBoxPlayerNumberMadeWithCards.Text += number.ToString(); } else { PlayerPutCards2.Add(number); TextBoxPlayerPutCardsAsPrimeFactor.Text += number.ToString() + ", "; TextBoxPlayerResultsPrimeFactorization.Text += number.ToString(); TempPrimefactors.Add(number); } PlayerCardsDealtCopy.Remove(number); EnableNumberButton(); } } |
ボタンをクリックすることでプレイヤーが持っているカードを出すことができるのですが、もしプレイヤーがそのカードを持っていないのであればそのボタンはクリックできないようにしなければなりません。GetNumberFromButtonメソッドはそのためのメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public partial class Form1 : Form { void EnableNumberButton() { ButtonA.Enabled = PlayerCardsDealtCopy.Any(x => x == 1); Button2.Enabled = PlayerCardsDealtCopy.Any(x => x == 2); Button3.Enabled = PlayerCardsDealtCopy.Any(x => x == 3); Button4.Enabled = PlayerCardsDealtCopy.Any(x => x == 4); Button5.Enabled = PlayerCardsDealtCopy.Any(x => x == 5); Button6.Enabled = PlayerCardsDealtCopy.Any(x => x == 6); Button7.Enabled = PlayerCardsDealtCopy.Any(x => x == 7); Button8.Enabled = PlayerCardsDealtCopy.Any(x => x == 8); Button9.Enabled = PlayerCardsDealtCopy.Any(x => x == 9); Button10.Enabled = PlayerCardsDealtCopy.Any(x => x == 10); ButtonJ.Enabled = PlayerCardsDealtCopy.Any(x => x == 11); ButtonQ.Enabled = PlayerCardsDealtCopy.Any(x => x == 12); ButtonK.Enabled = PlayerCardsDealtCopy.Any(x => x == 13); } } |
合成数出しをする場合、素因数を複数指定することになります。かけ算の記号である×をクリックすると素因数として確定され、これをPrimefactors内に格納します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public partial class Form1 : Form { private void ButtonX_Click(object sender, EventArgs e) { if (!CheckBoxPrimeFactor.Checked) return; Primefactors.Add(GetNumber(TempPrimefactors)); TempPrimefactors.Clear(); TextBoxPlayerResultsPrimeFactorization.Text += " × "; } } |
出されたカードの正当性のチェック
プレイヤーによって出すカードが確定し、決定ボタンがクリックされたときの処理を示します。
ここでは最初に本当に出せるカードなのかを調べます。場に出せるカードはそのとき親でなければ制約があります。もし条件を満たしていないのであればカードを出せない旨を表示します。
条件を満たしていれば、素数として出された場合は本当に素数なのか、合成数として出された場合は素因数の計算はあっているのかを調べます。もし合っているのであればカードを出したことにして、PlayerCardsDealtのなかから取り除きます。この処理はCheckPlayerCards1メソッド、またはCheckPlayerCards2メソッドのなかでおこなわれます。
この処理によってプレイヤーのカードがすべてなくなっていればプレイヤーの勝利です。そうでなければコンピュータに手番が移ります。そしてコンピュータの勝利が確定しないのであれば再びプレイヤーの手番となります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
public partial class Form1 : Form { private void ButtonDecision_Click(object sender, EventArgs e) { if (CountCardsPutMust != -1) { string str = ""; if (CountCardsPutMust != PlayerPutCards1.Count) str += $"場に出せるカードは{CountCardsPutMust}枚限定です。\n"; if (MinValueCards > GetNumber(PlayerPutCards1)) str += $"場に出せるカードは{MinValueCards}以上になるようなカードを出してください"; if (str != "") { MessageBox.Show(str); return; } } int cardCount = 0; long minValue = 0; if (Primefactors.Count == 0) CheckPlayerCards1(ref cardCount, ref minValue); // 素数として出された else CheckPlayerCards2(ref cardCount, ref minValue); // 合成数として出された if (PlayerCardsDealt.Count == 0) { labelMessage.Text = "あなたの勝ちです。"; ButtonDecision.Enabled = false; return; } // コンピュータの手番では「決定」「パス」ボタンは押せなくする ButtonDecision.Enabled = false; ButtonPass.Enabled = false; // コンピュータの手番 CompPutCardsメソッドは後述 CompPutCards(cardCount, minValue, ref CountCardsPutMust, ref MinValueCards); // コンピュータの手番が終わったとき勝負がついているかもしれない if (CompCardsDealt.Count == 0) { labelMessage.Text = "コンピュータの勝ちです。"; ButtonDecision.Enabled = false; return; } // コンピュータの手番が終わったら自分の手番であることと、 // どのようなカードであれば出せるかを表示する if (CountCardsPutMust != -1) labelMessage.Text = $"{CountCardsPutMust}枚のカードで{MinValueCards}以上の数を出してください"; else labelMessage.Text = $"あなたが親です。"; ButtonDecision.Enabled = true; ButtonPass.Enabled = true; } } |
CheckPlayerCards1メソッドとCheckPlayerCards2メソッドはプレイヤーによって出されたカードが正当なものであるかどうかをチェックするためのものです。素数として出された場合はCheckPlayerCards1メソッドで、合成数として出された場合はCheckPlayerCards2メソッドでチェックがおこなわれます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public partial class Form1 : Form { void CheckPlayerCards1(ref int cardCount, ref long minValue) { cardCount = PlayerPutCards1.Count; long number = GetNumber(PlayerPutCards1); if (IsPrimeNumber(number)) { foreach (int card in PlayerPutCards1) PlayerCardsDealt.Remove(card); TextBoxPlayerCards.Text = String.Join(", ", PlayerCardsDealt); minValue = number + 1; } else { string mes = $"{number}は素数ではありません"; MessageBox.Show(mes); minValue = -1; cardCount = -1; } InputDataClear(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
public partial class Form1 : Form { void CheckPlayerCards2(ref int cardCount, ref long minValue) { cardCount = PlayerPutCards1.Count; minValue = GetNumber(PlayerPutCards1) + 1; if (TempPrimefactors.Count > 0) { Primefactors.Add(GetNumber(TempPrimefactors)); TempPrimefactors.Clear(); } int value = 1; foreach (int i in Primefactors) value *= i; string mes = ""; if (value != GetNumber(PlayerPutCards1)) { string mes1 = String.Join(" × ", Primefactors.Select(x => x.ToString()).ToArray()); int compositeNumber = GetNumber(PlayerPutCards1); mes += $"{mes1}は{compositeNumber}にはなりません\n"; cardCount = -1; minValue = -1; } if (Primefactors.Any(x => !IsPrimeNumber(x))) { mes += "素因数のなかに素数ではないものがあります。"; cardCount = -1; minValue = -1; } if (cardCount != -1) { foreach (int card in PlayerPutCards1) PlayerCardsDealt.Remove(card); foreach (int card in PlayerPutCards2) PlayerCardsDealt.Remove(card); TextBoxPlayerCards.Text = String.Join(", ", PlayerCardsDealt); } else MessageBox.Show(mes); InputDataClear(); } } |
キャンセルやパスに関する処理
カードが出されたときやゲーム開始のとき、いったん選択されたカードがキャンセルされたときはPlayerPutCards1やPlayerPutCards2などに格納されているデータをすべてクリアしなければなりません。InputDataClearメソッドはそのためのものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public partial class Form1 : Form { void InputDataClear() { PlayerPutCards1.Clear(); PlayerPutCards2.Clear(); Primefactors.Clear(); TempPrimefactors.Clear(); TextBoxPlayerPutCards.Text = ""; TextBoxPlayerNumberMadeWithCards.Text = ""; TextBoxPlayerPutCardsAsPrimeFactor.Text = ""; TextBoxPlayerResultsPrimeFactorization.Text = ""; PlayerCardsDealtCopy = new List<int>(PlayerCardsDealt); EnableNumberButton(); } } |
うっかりミスで出せないカードを選択してしまう場合もあるかもしれないので、この場合は選択したカードをすべてクリアできる手段を用意しました。
1 2 3 4 5 6 7 |
public partial class Form1 : Form { private void ButtonClear_Click(object sender, EventArgs e) { InputDataClear(); } } |
プレイヤーがカードを一枚山札から取るための処理です。1ターンにつきカードを取れるのは1回だけです。そのためこれを実行するとボタンが灰色表示になります。
1 2 3 4 5 6 7 8 9 10 11 |
public partial class Form1 : Form { private void ButtonDraw_Click(object sender, EventArgs e) { ButtonDraw.Enabled = false; PlayerCardsDealt.Add(this.shuffledCards[0]); shuffledCards = shuffledCards.Skip(1).ToList(); TextBoxPlayerCards.Text = String.Join(", ", PlayerCardsDealt); } } |
出せるカードがない場合、戦略的にパスをしたい場合に対応するためのパスの処理です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public partial class Form1 : Form { private void ButtonPass_Click(object sender, EventArgs e) { InputDataClear(); ButtonDecision.Enabled = false; ButtonPass.Enabled = false; // パスをしたらコンピュータの手番となる CompPutCards(-1, -1, ref CountCardsPutMust, ref MinValueCards); if (CompCardsDealt.Count == 0) { // ゲームセットなのでプレイヤーがカードを出すためのボタンを使用不可にする labelMessage.Text = "コンピュータの勝ちです。"; ButtonDecision.Enabled = false; return; } // コンピュータもパスするかもしれない if (CountCardsPutMust != -1) labelMessage.Text = $"{CountCardsPutMust}枚のカードで{MinValueCards}以上の数を出してください"; else labelMessage.Text = $"あなたが親です。"; ButtonDecision.Enabled = true; ButtonPass.Enabled = true; } } |
コンピュータが手番のときの処理
コンピュータが手番のときの処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public partial class Form1 : Form { void CompPutCards(int cardCount, long minValue, ref int newCardCount, ref long newMinValue) { // 必要であればプレイヤーが山札から1枚取れるようにする ButtonDraw.Enabled = true; ResultCards resultCards = SearchNumber(false, cardCount, minValue); if (resultCards == null) resultCards = SearchNumber(true, cardCount, minValue); ShowResult(resultCards); // コンピュータがパスしたらプレイヤーが親となる if (resultCards == null) { newCardCount = -1; newMinValue = -1; } else { newCardCount = resultCards.Cards1.Count; newMinValue = resultCards.Number + 1; } } } |
コンピュータの手番のときに出せるカードを決めるための処理を示します。SearchNumberメソッドの引数が増えています。カードはなんでも出せるわけでなく、前のプレイヤーと同じ枚数のカードを出すとともにそれよりも大きな数でなければならないからです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
public partial class Form1 : Form { ResultCards SearchNumber(bool isPrimeNumber, int cardCount, long minValue) { // cardCount が -1 のときは親なので好きな枚数を出せる List<ResultCards> resultCardsList = new List<ResultCards>(); if(cardCount == 2 || cardCount == -1) resultCardsList.AddRange(GetCardsCanPut(CompCardsDealt.ToArray(), 2)); if (cardCount == 3 || cardCount == -1) resultCardsList.AddRange(GetCardsCanPut(CompCardsDealt.ToArray(), 3)); if (cardCount == 4 || cardCount == -1) resultCardsList.AddRange(GetCardsCanPut(CompCardsDealt.ToArray(), 4)); if (cardCount == 1 || cardCount == -1) resultCardsList.AddRange(GetCardsCanPut(CompCardsDealt.ToArray(), 1)); List<ResultCards> rets = new List<ResultCards>(); ResultCards NextBest = null; foreach (ResultCards resultCards in resultCardsList) { if(resultCards.Number < minValue) continue; if (resultCards.IsPrimeNumber != isPrimeNumber) continue; // 次に出せるカードがあるか調べておく List<int> copyCardsDealt = new List<int>(CompCardsDealt); foreach (int card in resultCards.Cards1) copyCardsDealt.Remove(card); foreach (int card in resultCards.Cards2) copyCardsDealt.Remove(card); List<ResultCards> nextResultCardsList = new List<ResultCards>(); nextResultCardsList.AddRange(GetCardsCanPut(copyCardsDealt.ToArray(), 2)); nextResultCardsList.AddRange(GetCardsCanPut(copyCardsDealt.ToArray(), 3)); nextResultCardsList.AddRange(GetCardsCanPut(copyCardsDealt.ToArray(), 4)); nextResultCardsList.AddRange(GetCardsCanPut(copyCardsDealt.ToArray(), 1)); // 次に出せるカードがあるならそれをストックしておく if (nextResultCardsList.Count > 0) rets.Add(resultCards); // ないなら次善策として保存しておく else if (NextBest == null) NextBest = resultCards; // コンピュータの手持ちのカードをすべて消費できるならそのまま出す if (copyCardsDealt.Count == 0) return resultCards; } // 次の一手になりうるものが見つからない場合は次善策として保存していたものを使う if (rets.Count == 0 && NextBest != null) return NextBest; // 本当に次の一手が存在しないならnullを返す if (rets.Count == 0 && NextBest == null) return null; // 偶数のカードばかり残るとやりにくいので偶数のカードを使う手を優先する // 消費する偶数のカードがもっとも大きいものを次の一手とする List<ResultCards> rets2 = rets.Where(x => x.Cards1.Where(card => card % 2 == 0 && card != 2).ToList().Count > 0).ToList(); if (rets2.Count > 0) { int max = rets2.Max(x => x.Cards1.Count(card => card % 2 == 0 && card != 2)); ResultCards ret = rets.First(x => x.Cards1.Where(card => card % 2 == 0 && card != 2).ToList().Count == max); return ret; } else return rets[0]; } } |
これはコンピュータがカードを出したとき(出せなかったとき)にその結果を表示するためのメソッドです。コンピュータの手持ちのカードがすべてなくなっていればゲームは終了です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public partial class Form1 : Form { void ShowResult(ResultCards resultCards) { if (resultCards != null) { foreach (int card in resultCards.Cards1) CompCardsDealt.Remove(card); foreach (int card in resultCards.Cards2) CompCardsDealt.Remove(card); TextBoxCompCards.Text = String.Join(", ", CompCardsDealt); TextBoxCompPutCards.Text = String.Join(", ", resultCards.Cards1.ToArray()); TextBoxCompPutCardsAsPrimeFactor.Text = String.Join(", ", resultCards.Cards2.ToArray()); TextBoxCompResultsPrimeFactorization.Text = String.Join(" × ", resultCards.PrimeNumbers.ToArray()); string isPrimeNumber = resultCards.IsPrimeNumber ? "素数:" : "合成数:"; TextBoxCompNumberMadeWithCards.Text = isPrimeNumber + String.Join("", resultCards.Cards1.ToArray()); } else { TextBoxCompPutCards.Text = ""; TextBoxCompPutCardsAsPrimeFactor.Text = ""; TextBoxCompResultsPrimeFactorization.Text = ""; TextBoxCompNumberMadeWithCards.Text = ""; if (CompCardsDealt.Count > 0) { CompCardsDealt.Add(shuffledCards[0]); shuffledCards = shuffledCards.Skip(1).ToList(); TextBoxCompNumberMadeWithCards.Text = "カードが出せない"; TextBoxCompCards.Text = "残りのカード:" + String.Join(", ", CompCardsDealt); } else TextBoxCompNumberMadeWithCards.Text = "すべてのカードが出された"; } } } |
実際に対戦してみたら散々な結果になりました。