あけましておめでとうございます。あんみんどうふです。
トランプを使った「大富豪」というゲームをご存知でしょうか。
私はルールを一切知りません。なのでこれといった説明もできず、多人数でやるトランプゲームということしか知りません。
ということで今回は大富豪のルールを学びつつ、C#でプログラムに起こしてみようと思います。
結構複雑そうなので何回かに分けて投稿します。
開発環境
いつも通りです。
- C#.NET(.NET Core 3.1)
- Visual Studio 2019
作る
大富豪を理解するために、Wikipediaを参考にします。
大富豪(だいふごう)、もしくは大貧民(だいひんみん)は、複数人で遊ぶトランプゲームの1つ。カードを全てのプレイヤーに均等に配り、手持ちのカードを順番に場に出して早く手札を無くすことを競うゲームである。
ここだけ見ると「ババ抜き」を戦略的にしたような感じですね。
ルール確認しながら作っていきます。
1組全てのカードをプレイヤー全員に均等に配る。通常はジョーカーを1枚含めてプレイする。カードは特に人数が多い場合などは2組(あるいはそれ以上)を同時に使うこともある。
カードは全種類使うみたいなので、13(A、2~10、J、Q、K)×4(♤♡♢♧)+1(ジョーカー)の計53枚が必要ですね。
というわけで「カード」を作るために、独自クラスCardを宣言します。
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 71 72 73 74 75 76 77 78 79 80 81 82 | /// <summary> /// カード型クラス /// </summary> class Card { public int Id { get; } // ID public int Num { get; } // 数値 public string Type { get; } // カード種類名 public string ListName { get; } // カード名 (記号+数値) public string MsgName { get; } // カード数字 public int Power { get; } // 強さ /// <summary> /// カード型独自クラス /// </summary> /// <param name="cardId">カードID</param> public Card(int cardId) { // [Id]カードID this.Id = cardId; /** * カードの内部仕様は以下の通り。 * 0 ... なし * 1~13 ... スペードの1~13 * 14~26 ... ハートの1~13 * 27~39 ... ダイヤの1~13 * 40~52 ... クローバーの1~13 * 53 ... ジョーカー * 数値を13で割り、商で種類を判別し、余りから数字を判別する。 * memo: 商と余りを同時に求めるにはMath.DivRem()を使う。 */ // [Type, Name]IDから1引いた数字を13で割って種類名、カード名を割り出す int rem; int quo = Math.DivRem(cardId - 1, 13, out rem); switch(quo) { case 0: this.Type = "スペード"; this.ListName = "♠" + (rem + 1); break; case 1: this.Type = "ハート"; this.ListName = "♥" + (rem + 1); break; case 2: this.Type = "ダイヤ"; this.ListName = "♦" + (rem + 1); break; case 3: this.Type = "クローバー"; this.ListName = "♣" + (rem + 1); break; case 4: this.Type = "ジョーカー"; this.ListName = "JOKER"; break; } // [Val]数値 (JOKERは最強なので99) if (quo == 4) this.Num = 99; else this.Num = rem + 1; // [NameNum]メッセージ出力用のカード数字 if (quo == 4) this.MsgName = "ジョーカー"; else if (rem >= 1 && rem <= 9) this.MsgName = (rem + 1).ToString(); else if (rem == 0) this.MsgName = "A(エース)"; else if (rem == 10) this.MsgName = "J(ジャック)"; else if (rem == 11) this.MsgName = "Q(クイーン)"; else if (rem == 12) this.MsgName = "K(キング)"; // [Power]カードの強さはカードの数字から算出する // 1(エース)~2は13(キング)より強いカードなので、 // 強さは数字に13を足したものとなる if (this.Num > 0 && this.Num < 3) this.Power = this.Num + 13; else this.Power = this.Num; } public override string ToString() { return ListName; } } |
これで「new Card({任意のID})」という感じにインスタンス化すると「1枚のカード」として作成されます。
「{Card型変数}.{プロパティ名}」で以下の情報を取得できます。
プロパティ名 | 型 | 内容 |
---|---|---|
Id | int | カードID(1~53) |
Num | int | カードの番号(1~13) |
Type | string | カードの種類名(スペード、ハート等) |
ListName | string | リスト表示用のカード名(♠3、♥6等) 「黒塗り記号+数字」のフォーマット ジョーカーは「JOKER」 |
MsgName | string | メッセージ表示用のカード名(3、A(エース)、J(ジャック)等) 数字はそのまま A・J・Q・Kは読みが表示される |
Power | int | カードの強さ(3~15、ジョーカーなら99) 3~13はそのまま、1と2はキングより強いので14、15 ジョーカーはいかなるカードにも強いので99 |
例:40(クローバーの1)の場合
1 2 3 4 5 6 7 | Card card = new Card(40); Console.WriteLine(card.Id); // 40 Console.WriteLine(card.Num); // 1 Console.WriteLine(card.Type); // クローバー Console.WriteLine(card.ListName); // ♣1 Console.WriteLine(card.MsgName); // A(エース) Console.WriteLine(card.Power); // 14 |
ちなみにカード作成に必要な情報はインスタンス化時の引数に使うIDだけなので、どのプロパティも読み取り専用にしています。
カードの種類はIDから1引いた数を13で割ってカードを判別する形にしてみました。
IDはそれぞれ、1~13は♤、14~26は♡、27~39は♢、40~52は♧、53はジョーカーに対応しています。
このIDから1を引き13で割った商で種類、余りに1を足して数値を判別します。
ちなみに商はそれぞれ0=♤、1=♡、2=♢、3=♧、4=ジョーカーです。
例えば、IDが「29」だとすると、(29-1)÷13=2余り2。商が2なら「♢」、余りの2に1足して「3」なので、「♢の3」になります。
商を求める時に1を引いて余りに1を足す理由は、例えばIDが「13」の場合だと本来は「♤の13」になりますが、1の足し引きを行わない場合だと商は1、余りは0になるので「♡の0」という結果になってしまいます。明らかにおかしいですね。
これを防ぐために、商はあえて1引くことで割り切れない状態にし、13で割るとどうしても余りが13になることは無いので、1足すことで13になるようにしています。
「カード」を用意したので、次は53枚の「カード」があることを表すカードマスタを作成します。Listを用意して連番で1~53のカードを追加するだけ。
1 2 3 4 5 6 7 8 9 10 11 | // カードマスタ List<Card> cardMst = new List<Card>(53); public MainForm() { InitializeComponent(); // 連番でカードIDをカードマスタに格納 for (int i = 0; i < 53; i++) cardMst.Add(i + 1); } |
初期化
初期化処理を書いていきます。Load時にInitialize関数を呼び出しています。
リトライ機能とか実装する際に融通利くように分けてます。
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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | public int playerCount = 4; // プレイヤー数 public Image[] cardImg; // カード画像 public int playerYou = 0; // 自身のプレイヤーID public string playerYouName = "あなた"; // プレイヤーの名前 int playerParent = -1; // 親プレイヤー int playerNow = -1; // 現在のプレイヤー(番) // 現在場に出ているカード群 Card[] cardsNow = new Card[4]; private void MainForm_Load(object sender, EventArgs e) { Initialize(); } /// <summary> /// 初期化 /// </summary> void Initialize() { for (int i = 0; i < cardsNow.Length; i++) cardsNow[i] = new Card(0); // プレイヤーの人数分List生成 for (int i = 0; i < playerCount; i++) cards.Add(new List<Card>()); // カードをシャッフルしてcardTmpに格納 List<Card> cardTmp = cardMst.OrderBy(i => Guid.NewGuid()).ToList(); // 1プレイヤーにおける初期枚数を計算 int cardCount = (int)Math.Floor((double)(cardTmp.Count / playerCount)); // 初期枚数分順番にカードを渡す for(int p = 0; p < playerCount; p++) { for(int i = 0; i < cardCount; i++) { // ダイヤの3(29)を引いたプレイヤーを先行にする if (cardTmp.First().Id == 29) { playerParent = p; playerNow = p; } cards[p].Add(cardTmp.First()); cardTmp.Remove(cardTmp.First()); } } // 余ったカードがプレイヤー数より多ければエラー // { 53/人数 }枚ずつ渡しているので多くなることは普通無いが念の為… if (cardTmp.Count > playerCount) { ErrorMessage($"カードの枚数が不正です。(1)\nCount: {cardTmp.Count}", true); return; } // プレイヤー数より少ない(0でない)なら余ったカードをランダムに渡す // 0の場合はカードが余っていないのでスキップ else if (cardTmp.Count < playerCount) { List<int> randomIndex = new List<int>(); for (int i = 0; i < playerCount; i++) randomIndex.Add(i); randomIndex = randomIndex.OrderBy(i => Guid.NewGuid()).ToList(); int remain = cardTmp.Count; // 残り枚数 for (int i = 0; i < remain; i++) { cards[randomIndex[i]].Add(cardTmp.First()); cardTmp.Remove(cardTmp.First()); } } // ここまででカードが残る場合エラー if (cardTmp.Count > 0) { ErrorMessage($"カードの枚数が不正です。(2)\nCount: {cardTmp.Count}", true); return; } // 先行プレイヤーが決まっていない場合エラー if (playerNow < 0) { ErrorMessage($"先行になるプレイヤーが決まっていません。\nplayerNow: {playerNow}"); return; } // 各プレイヤーの手持ちのカードを昇順ソートする for (int i = 0; i < playerCount; i++) cards[i] = cards[i].OrderBy(x => x.Power).ThenBy(x => x.Id).ToList(); // Debug for (int i = 0; i < cards.Count; i++) { Debug.Write($"[{i}]({cards[i].Count}) "); foreach (var val in cards[i]) { Debug.Write(val + ","); } if (playerNow == i) Debug.Write("[番]"); Debug.WriteLine("\n--------------------"); } //ReadCardImage(); // カード画像読み込み //WriteMsg($"今回の親プレイヤーは {GetPlayerName(playerParent)} です。"); } |
細かくコメント書いてわかりやすくしました。ブログに書くためにも、備忘録としても・・・。
とは言ってもいきなりドンと出されてもわからないので、分けて解説します。
1 2 3 | // プレイヤーの人数分List生成 for (int i = 0; i < playerCount; i++) cards.Add(new List<int>()); |
cardsという二次元Listを各プレイヤーの持ち手札としています。
手札は数が増減するため、配列だとちょっと取り回しが面倒なのでListを使っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // カードをシャッフルしてcardTmpに格納 List<Card> cardTmp = cardMst.OrderBy(i => Guid.NewGuid()).ToList(); // 1プレイヤーにおける初期枚数を計算 int cardCount = (int)Math.Floor((double)(cardTmp.Count / playerCount)); // 初期枚数分順番にカードを渡す for(int p = 0; p < playerCount; p++) { for(int i = 0; i < cardCount; i++) { // ダイヤの3(29)を引いたプレイヤーを先行にする if (cardTmp.First().Id == 29) { playerParent = p; playerNow = p; } cards[p].Add(cardTmp.First()); cardTmp.Remove(cardTmp.First()); } } |
カードマスタを直接いじるのは怖いので、cardTmpというListに移してシャッフルします。要するにcardTmpは「山札」です。
最初の手札の枚数ですが、Wikipediaによると
1組全てのカードをプレイヤー全員に均等に配る。
とのことなので、まず均等になるように「枚数(53枚)÷人数」で分けます。余りの処理は後回しでとりあえず今はMath.Floorで切り捨てします(余った分は後で配ります)。
カードの配り方は至って単純で、「各プレイヤーの持ち手札に、シャッフルしたcardTmp(山札)の先頭のカードを格納→cardTmp(山札)から削除」を繰り返すことで配っています。
また、こういうルールがあるみたいで。
ゲームはダイヤの3から始める。最初の親が手札から最初のカードを出し、以降順番に次のプレイヤーがカードを出し重ねていく。
とのことなので、カードを配る際に♢の3を引いたプレイヤーはそれを場に出し、その人を基準として順番にカードを出していくことになります。
♢の3を引いたプレイヤーのIDはplayerParent(親プレイヤー)、playerNow(現在のプレイヤー)の変数に格納されます。
場に出す処理は一旦後回しで別の記事に書こうと思います。
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 | // 余ったカードがプレイヤー数より多ければエラー // { 53/人数 }枚ずつ渡しているので多くなることは普通無いが念の為… if (cardTmp.Count > playerCount) { ErrorMessage($"カードの枚数が不正です。(1)\nCount: {cardTmp.Count}", true); return; } // プレイヤー数より少ない(0でない)なら余ったカードをランダムに渡す // 0の場合はカードが余っていないのでスキップ else if (cardTmp.Count < playerCount) { List<int> randomIndex = new List<int>(); for (int i = 0; i < playerCount; i++) randomIndex.Add(i); randomIndex = randomIndex.OrderBy(i => Guid.NewGuid()).ToList(); int remain = cardTmp.Count; // 残り枚数 for (int i = 0; i < remain; i++) { cards[randomIndex[i]].Add(cardTmp.First()); cardTmp.Remove(cardTmp.First()); } } // ここまででカードが残る場合エラー if (cardTmp.Count > 0) { ErrorMessage($"カードの枚数が不正です。(2)\nCount: {cardTmp.Count}", true); return; } // 先行プレイヤーが決まっていない場合エラー if (playerNow < 0) { ErrorMessage($"先行になるプレイヤーが決まっていません。\nplayerNow: {playerNow}"); return; } |
さっきの処理で余ったカードを配ります。
順番に配ると最初のプレイヤーが必ずカードを多く貰うことになり不利になってしまうので、randomIndexというListを作り、プレイヤー数分の連番を格納してシャッフル→先頭から順に対応したIDのプレイヤーに配る方法で公平性を保っています。
ループを回す前にremainという変数でcardTmpの残り枚数を取得していますが、これはfor文の中でListを削る処理(remove)を行っており、条件式にcardTmp.Countを直接入れるとループ回数がおかしくなってしまうのを防ぐためです。ここで一回つまづきました。
また、エラー処理にErrorMessageという関数を使っていますが、これは単にダイアログでエラー出してるだけです。第2引数はtrueだとフォームを閉じます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // 各プレイヤーの手持ちのカードを昇順ソートする for (int i = 0; i < playerCount; i++) cards[i] = cards[i].OrderBy(x => x.Power).ThenBy(x => x.Id).ToList(); // Debug for (int i = 0; i < cards.Count; i++) { Debug.Write($"[{i}]({cards[i].Count}) "); foreach (var val in cards[i]) { Debug.Write(val + ","); } if (playerNow == i) Debug.Write("[番]"); Debug.WriteLine("\n--------------------"); } //ReadCardImage(); // カード画像読み込み //WriteMsg($"今回の親プレイヤーは {GetPlayerName(playerParent)} です。"); |
手札がぐちゃぐちゃなので第1キーをPower(カードの強さ)、第2キーをId(カードID)で昇順ソートしています。これで数字が弱い順に、同じ数字は♠→♥→♦→♣の順に並ぶようになります。
ここまでやったらデバッグとしてコンソールに出力してみます。
手札の内容が出力されます。今回はとりあえず4人に設定しているので4人分出力されればOKです。
(私の環境では何故かConsole.Writeが効かないので、名前空間に「using System.Diagnostics」を追加してDebug.Writeで代用しています・・・。)
1 2 3 4 5 6 7 8 | [0](14) ♥3,♠4,♦5,♣5,♠9,♥9,♦9,♥11,♣11,♠12,♠13,♦13,♥1,♣2, -------------------- [1](13) ♥4,♠6,♣6,♠7,♥7,♦7,♥8,♣8,♦11,♦12,♣12,♣13,♠1, -------------------- [2](13) ♦3,♣3,♦4,♠5,♥6,♦6,♣7,♠8,♣10,♠11,♥12,♦1,♥2,[番] -------------------- [3](13) ♠3,♣4,♥5,♦8,♣9,♠10,♥10,♦10,♥13,♣1,♠2,♦2,JOKER, -------------------- |
プレイヤーID、手札の枚数、手札の内容、番が回ったプレイヤーが一目瞭然です。
(ReadCardImageとWriteMsgの部分は今回やらなかったのでコメントアウトしてます。また今度。)
今回はカードを作り、プレイヤーに配った所まで作りました。続きは次回・・・。