あんみんどうふです。
前回は手札を配る所まで作りました。
今回はカードを場に出して次の番へ進められる所まで作ります。
前回の記事はこちら。
UI
まず画面が無いと始まらないので適当にコントロールを配置します。
とりあえずこんな感じになりました。
各種コントロールは以下の通りです。これからもう少し増える予定ですが、暫定としてとりあえず。
Label
labelRemainCards・・・自身の手札残り枚数。
labelMsg・・・現在の状況をテキストとして表示する領域。
Button
buttonStart・・・ゲーム開始ボタン。ゲーム中は表示されない。
buttonConfirm・・・手札を選んで確定するボタン。
buttonPassMe・・・カードを出せない時にパスをして次の番へ進めるボタン。
PictureBox
pictureBoxCardNow1・・・現在場に出ているカードの1枚目。
pictureBoxCardNow2・・・現在場に出ているカードの2枚目。
pictureBoxCardNow3・・・現在場に出ているカードの3枚目。
pictureBoxCardNow4・・・現在場に出ているカードの4枚目。
CheckedListBox
checkedListBoxMyCards・・・自身の手札。出すカードにチェックを入れる。
フォームのBackColor(背景色)はテーブルゲームっぽい色にしています。SeaGreen(#2e8b57)です。
関数もろもろ
コントロールを用意したら次に細かい関数をいろいろ用意していきます。
メッセージ出力
前回コメントアウトしていた関数その1、「WriteMsg」です。labelMsgにテキストを表示させる関数を作ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /// <summary> /// メッセージを書き込む(改行を含む)。 /// </summary> /// <param name="message">ログ内容</param> /// <param name="postscript">Trueなら改行して追記</param> void WriteMsg(string message, bool postscript = false) { if (postscript) { string msg = ""; if (labelMsg.Text != "") msg = Environment.NewLine; labelMsg.Text += msg + message; } else labelMsg.Text = message; } |
WriteMsg(“メッセージの内容”)と打つだけでlabelMsgに反映されます。
第2引数のboolはTrueを入れると改行して追記するようにしてます(デフォルトはFalse)。
関数化すると”いざコントロールがまるっと変わることになった時”に関数内3箇所の「labelMsg.Text」を変えるだけでよくなります。
最初はこの部分をLabelではなくTextBoxで作っていたので一度助けられました。
プレイヤーの名前を取得する
プレイヤーIDから名前を取得して返します。
自身のIDと一致する場合「あなた」を返し、それ以外は「CPU {ID}」の文字列を返しています。
1 2 3 4 5 6 7 8 9 10 11 12 | /// <summary> /// プレイヤー名を返す。 /// </summary> /// <param name="playerId">プレイヤーID</param> /// <returns>プレイヤー名</returns> string GetPlayerName(int playerId) { string result; if (playerId == playerYou) result = "あなた"; else result = "CPU" + playerId; return result; } |
カード画像一括読込
前回の記事でコメントアウトしていた関数その2、「ReadCardImage」です。
実行ファイルと同フォルダ内の「img」フォルダから「{種類ID}-{カード番号}.png」(例:♥の6→「1-6.png」)を読み込んで配列cardImgに格納しています。
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 | public Image[] cardImg; // カード画像 /// <summary> /// カード画像を読み込む。 /// </summary> void ReadCardImage() { // 画像配列を初期化 cardImg = new Image[53]; // imgフォルダの(マークID)-(数字).pngを順に読み込む for (int i = 0; i < 5; i++) { for (int j = 0; (i < 4 && j < 13) || (i == 4 && j == 0); j++) { string location = $@"img\{i}-{j + 1}.png"; if (File.Exists(location) && cardImg[i * 13 + j] == null) { //Debug.WriteLine($"Reading {location}"); cardImg[i * 13 + j] = Image.FromFile(location); } else { ErrorMessage($@"画像の読み込みに失敗しました。{"\n"}参照したソース: {location}"); return; } } } } |
ちなみにカードの画像はチコデザ様より拝借してます。
トランプのイラスト – オリジナル全53枚のカード無料素材
場に出ているカードを表示
カードを出したり消したりした時に呼び出す関数「UpdateViewCardNow」です。
cardsNow(場に出ているカード)が1枚でもあれば先程のReadCardImageで読み込んだ画像を各PictureBoxに表示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | /// <summary> /// 現在場に出ているカードを表示(更新)する。 /// </summary> void UpdateViewCardNow() { PictureBox[] picCardsNow = { pictureBoxCardsNow1, pictureBoxCardsNow2, pictureBoxCardsNow3, pictureBoxCardsNow4 }; for(int i = 0; i < cardsNow.Length; i++) { if (cardsNow[i] != null && cardsNow[i].Id > 0) picCardsNow[i].Image = cardImg[cardsNow[i].Id - 1]; else picCardsNow[i].Image = null; } } |
場に出ているカードをすべて消す
カードをリセットします。1巡分終わる時に使います。
コードは至って単純で、cardsNow内全てにID0のカード(空)を入れてるだけ。
1 2 3 4 5 6 7 8 | /// <summary> /// 場に出ているカードを全て消す(リセットする)。 /// </summary> void CardsReset() { for(int i = 0; i < cardsNow.Length; i++) cardsNow[i] = new Card(0); } |
自身の所持するカードのリストを更新
checkedListBoxMyCardsの項目を更新します。
Listから自身の手札を取得してソートし、データソースとして格納しています。
ついでに残り枚数のテキストも更新しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /// <summary> /// 自身の所持カード更新 /// </summary> void UpdateCardList() { // 全ての項目のチェックを外す for (int i = 0; i < checkedListBoxMyCards.Items.Count; i++) checkedListBoxMyCards.SetItemChecked(i, false); // 独自クラス型のListに所持カードを入れる List<Card> cmb = new List<Card>(); foreach(Card card in cards[playerYou]) cmb.Add(card); // 強さで昇順ソートし、CheckedListBoxに格納 cmb = cmb.OrderBy(x => x.Power).ThenBy(x => x.Id).ToList(); checkedListBoxMyCards.DataSource = cmb; labelRemainCards.Text = $"残り枚数: {checkedListBoxMyCards.Items.Count}"; } |
カードを場に出す
プレイヤーとカードIDを指定してカードを場に出します。
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 | /// <summary> /// 指定したカードを場に出す。 /// </summary> /// <param name="targetPlayer">対象プレイヤー</param> /// <param name="cardId1">カード(1枚目) ※必須</param> /// <param name="cardId2">カード(2枚目)</param> /// <param name="cardId3">カード(3枚目)</param> /// <param name="cardId4">カード(4枚目)</param> void CardsPlay(int targetPlayer, int cardId1, int cardId2 = 0, int cardId3 = 0, int cardId4 = 0) { bool[] judge = new bool[4]; int[] cardIds = { cardId1, cardId2, cardId3, cardId4 }; Debug.WriteLine($"CardsPlay: 場に出す枚数・・・ {cardIds.Count(x => x != 0)}枚"); // 各カードがプレイヤーの手札に存在するかチェック // 存在する => True, 存在しない => False for (int i = 0; i < cardIds.Length; i++) { Debug.WriteLine(cards[targetPlayer].Any(x => x.Id == cardIds[i])); if (cards[targetPlayer].Any(x => x.Id == cardIds[i]) || cardIds[i] == 0) judge[i] = true; } // 全てTrueならカードを場に出す if(judge[0] && judge[1] && judge[2] && judge[3]) { for(int i = 0; i < cardIds.Length && cardIds[i] != 0; i++) { // カードを場に出す cardsNow[i] = new Card(cardIds[i]); // 手札から削除する cards[targetPlayer].RemoveAll(x => x.Id== cardIds[i]); } Debug.WriteLine($"CardsPlay[成功]: {cardId1}-{cardId2}-{cardId3}-{cardId4}"); } else { Debug.WriteLine($"CardsPlay[失敗]: {cardId1}:{judge[0]}, {cardId2}:{judge[1]}, {cardId3}:{judge[2]}, {cardId4}:{judge[3]}"); } } |
引数で対象プレイヤー、対象カード1~4枚を指定し、そのプレイヤーがカードを所持しているかチェックします。
カードを持っている or 指定無し(IDが0)ならフラグをTrueにし、4枚分すべてのフラグがTrueならカードを場に出し、手札から削除するようにしています。
Debug~から始まる部分は各値をコンソールに出力していますが、デバッグ用なので無視していいです。
次の番へ進める
ここまで紹介した関数を使い、次の番へ進める関数「NextPlayer」を書いていきます。
playerNow(現在のプレイヤーID)を増やして番を進める
↓
必要ならリセット処理をし、メッセージ表示
↓
次のプレイヤーへ
みたいな流れです。
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 | /// <summary> /// 番を進める。 /// </summary> async void NextPlayer() { // 現在の番のプレイヤーを進める playerNow++; // プレイヤー数を超えたら0に戻す if (playerNow >= playerCount) playerNow = 0; // 1周したら場に出ているカードをリセットする if (playerParent == playerNow) { WriteMsg("1周したので場のカードは回収されます。"); await Task.Delay(speed); CardsReset(); UpdateViewCardNow(); WriteMsg($"親プレイヤー {GetPlayerName(playerParent)} の番です。", true); } else { WriteMsg($"現在のカードは {cardsNow[0].MsgName} です。"); WriteMsg($"{GetPlayerName(playerNow)} の番です。", true); } // 現在のプレイヤーが自身以外ならCPUのAI処理実行 if (DEBUG_AUTO || playerNow != playerYou) { checkedListBoxMyCards.Enabled = false; buttonConfirm.Enabled = false; buttonPassMe.Enabled = false; //CpuAI(); } else { checkedListBoxMyCards.Enabled = true; buttonConfirm.Enabled = true; buttonPassMe.Enabled = true; } } |
32行目の関数「CpuAI」をコメントアウトしていますが、これはCPUのAIに関するものです。長くなるのでこれは次回に回します。
ゲーム開始ボタン
buttonStartのClickイベントを作ります。これが無いと始まりません。
1 2 3 4 5 6 7 8 9 | private void buttonStart_Click(object sender, EventArgs e) { NextPlayer(); // 番を進める UpdateCardList(); // 手持ちカードリスト更新 UpdateViewCardNow(); // 場に出ているカード表示 buttonStart.Visible = false; buttonConfirm.Visible = true; buttonPassMe.Visible = true; } |
初期化処理ちょっと修正
ReadCardImageとWriteMsgを作ったので、前回作った関数「Initialize」の97、98行目のコメントアウトを削除します。後ろから2行分です。
1 2 3 4 5 | ... ReadCardImage(); // カード画像読み込み WriteMsg($"今回の親プレイヤーは {GetPlayerName(playerParent)} です。"); } |
また、こんなルールがありましたね。(Wikipedia引用)
ゲームはダイヤの3から始める。最初の親が手札から最初のカードを出し、以降順番に次のプレイヤーがカードを出し重ねていく。
前回はカードを配る部分しか作っていなかったので、♢の3を持つプレイヤーが親となる所までは出来ていましたが、場に出す処理は入れていないので昇順ソートの次の行に関数CardsPlayを追記します。
前回の記事で言うと84行目辺り。
1 2 3 4 5 6 7 | // 各プレイヤーの手持ちのカードを昇順ソートする for (int i = 0; i < playerCount; i++) cards[i] = cards[i].OrderBy(x => x.Power).ThenBy(x => x.Id).ToList(); // ダイヤの3(29)を場に出して次のプレイヤーの番へ CardsPlay(playerNow, 29); ... |
実行してみる
ここまで書いたら実行します。
今回は私が♢の3を持っているようなので親プレイヤーになりました。
Startをクリックします。
♢の3が場に出され、手札が表示され、メッセージも表示されました。
番がCPU1へと進んでいます。
また、自身の手札の内容がリストに表示されていることも確認できます。
ごちゃごちゃと書いてますが、まだこのくらいしかできていません。次回はプレイヤーが選んだカードを場に出したり、CPUに選ばせたりできるようにします。
つづく、かも。