このページでは、Python での「ババ抜き」ゲームの作り方を解説していきたいと思います。
今回は標準入出力(コンソール)のみを利用したババ抜きの作り方を解説していきたいと思います。
最終的には、下記のように “各プレイヤーの手持ちのカード”・”引かれたカード”・”捨てられたカード” などを表示しながら自動でゲームが進む「ババ抜き」を作成していきます(自動進行ではなくユーザーがプレイ可能なスクリプトも紹介しています)。
プレイヤー0のカード:[J:0][H:8][H:10][H:K][S:J][H:7][C:4][S:6] プレイヤー1のカード:[C:2][H:6][D:4] プレイヤー2のカード:[S:10][S:2][H:5][D:6][C:7] プレイヤー3のカード:[C:6][S:8][H:J][C:K][S:5] プレイヤー1 -> プレイヤー0:[H:6] プレイヤー0が捨てたカード:[S:6][H:6] 〜略〜 プレイヤー0 -> プレイヤー2:[H:7] プレイヤー2が捨てたカード:[C:7][H:7] プレイヤー0のカード:[J:0] プレイヤー1のカード: プレイヤー2のカード: プレイヤー3のカード: プレイヤー0の負けです...
また、今回は “クラス構成” や各クラスに必要になる “データ属性” や “メソッド” の洗い出し方の考え方を中心にして「ババ抜き」の作り方を解説していきたいと思います。
ババ抜きなどの身近なゲームは、必要になるクラスやメソッドもイメージしやすいので、オブジェクト指向でのプログラミングのトレーニングに向いていると思います。
ただ、クラス構成等はどういった観点に基づいて構成するかによって大きく変わるので、今回紹介する構成や考え方もあくまでもその一例であると考えて参考にしていただければと思います。
必要なクラス
では、まずはババ抜きを作成する上で “必要になるクラス” について考えていきたいと思います。
おそらくこのページを見てくれている方は、実際にババ抜きをプレイしたことのある人が多いのではないかと思います。
こういったよく知っているゲームの場合は、そのゲームの中に “登場するもの” を抽出することで、最低限必要なクラスを洗い出すことができます。あとは、それらのクラスの他に必要になりそうなクラス(各クラスに指示を出すクラスや UI を担当するクラスなど)を追加していけば良いと思います。
ということでババ抜きがどんなゲームであるかを思い出してみましょう!
ゲームの流れをざっと箇条書きで書くと下記のようになると思います。
ディーラーが各プレイヤーに対してカードを配るプレイヤーがペアになったカードを捨てるプレイヤーが他のプレイヤーからカードを引くプレイヤーがペアになったカードを捨てる- 次にカードを引く
プレイヤーを決める - 手札にカードが残っている
プレイヤーが2人以上なら3.に戻る - 手札にカードが残っている
プレイヤーが1人なら結果を表示する- 手札が残っている
プレイヤーが負け
- 手札が残っている
で、ここで登場するものは カード・ディーラー・プレイヤー の3つなので、下記の3つはクラスとして用意してよさそうですね!
Card:カードPlayer:プレイヤーDealer:ディーラー
あとは、上記の流れに沿ってゲームが進行するように、プレイヤーやディーラーに指示を出したり、カードを引くプレイヤーを決めたりするようなクラスもあった方が良いので、このクラスを OldMaid として用意したいと思います。
上記4つのクラスがあればババ抜きとしては成立すると思います。が、今回は各プレイヤーの手札の表示や各プレイヤーが捨てたカードの表示などの画面表示を行う機能もクラスとして切り出し、UI クラスとして用意したいと思います(UI に対しても OldMaid が指示を出すものとします)。
ということで、今回は下図で表す5つのクラスを用意してババ抜きを作成していきたいと思います。

この5つのクラスそれぞれの役割は下記のようになります。
Card:カードを実現する(カードのマークと数字を管理する)Player:プレイヤーを実現する(手札のカードを管理する)Dealer:ディーラーを実現する(カードを配る)UI:ユーザーへの表示・ユーザーからの入力受付を行うOldMaid:ババ抜きのゲームが成立するように各オブジェクトに指示を出す- 各種クラスのオブジェクトの生成も行う
- 指示を出すオブジェクトも決める(カードを引くプレイヤーを決める等)
Player の役割の “手札のカードを管理する” とは、要は Player の手札のカードへの変更(カードを追加したりカードを取り出したり)は Player 自身が行うということを意味しています。
また、今回 UI をわざわざ用意したのは、UI を別のクラスとして切り出すことで、標準入出力以外への表示や入力受付を行う際の変更を局所化できるかなぁと思ったからです。
例えばですが、tkinter を利用すれば画面にカードを描画することで手札のカードを表示したり、そのカードへのマウスクリック操作により相手から引くカードを指定したりすることも可能です。
こういったユーザーインタフェース部分の拡張を行う際のことを考慮して UI をクラス化しています。
また、Dealer をわざわざ用意しなくても、カードを配る処理を Player のうちの1人に持たせるようなクラス構成も考えられると思います。
こんな感じで、もっと異なったクラス構成を思いついた方もいらっしゃると思いますが、今回はこの5つのクラスを用意する前提で、ババ抜きの作り方を解説していきたいと思います。
各クラスに必要なデータ属性
続いて各クラスに必要なデータ属性(プロパティ)を考えていきたいと思います。
これもババ抜きのゲームの流れを思い浮かべ、そのゲームの流れを実現する上で、各オブジェクトが “知っていなければいけない情報” や “持っていなければいけないデータを” 考えていくと洗い出ししやすいと思います。
スポンサーリンク
Card クラスに必要なデータ属性
ババ抜きでは、同じ数字のペアのカードを捨てることでゲームを進めていくことになるので、数字は Card クラスに絶対に持たせておかなければならないデータ属性になります。
マークは別になくてもゲームとしては支障がないのですが、カードを画面に表示する際にはマークもあった方が見栄えは良い(トランプらしさが出る)ので、今回はマークもデータ属性として持たせておきたいと思います。
ということで、Card クラスには、少なくとも下記の2つのデータ属性があった方が良さそうと考えることができます。
suit:カードのマークnumber:カードの数字
Player クラスに必要なデータ属性
Player は配られたカードを使ってゲームをするわけですから、ババ抜きを行う上でカードを持っていなければいけません。また、カードは Card クラスのオブジェクトで、さらに手札にカードは複数存在するわけですから、Card クラスのオブジェクトのリストをデータ属性で持っておいた方が良さそうです。
また、Player は複数存在するわけですから、それらの Player を区別するために名前や ID などの識別子を持たせておいた方が便利です(特に画面に手札のカードを表示する時など)。今回は ID を識別子として持たせたいと思います。
ということで Player クラスには、少なくとも下記の2つのデータ属性があった方が良さそうと考えることができます。
cards:手札のカード(Cardクラスのオブジェクトのリスト)id:識別子
cards をリストとして持っておくことで、手札へのカードの追加や手札からのカードの取り出しは、リストに用意されたメソッドを実行することで実現することができます(カードを追加するときは cards.append(x)、カードを取り出すときは cards.pop(x) など)。
Dealer クラスに必要なデータ属性
まず Dealer はカードを配るのですから、カードを持っていなければいけないですね!また、カードは Card クラスのオブジェクトで、さらに配るカードは複数存在するわけですから、Card クラスのオブジェクトのリストをデータ属性で持っておいた方が良さそうです。
さらに、プレイヤーに対してカードを配るわけですから、カードを配る対象のプレイヤーが誰なのかを知っていなければいけません。プレイヤーは Player クラスのオブジェクトで、配る相手のプレイヤーは複数存在するわけですから、Player クラスのオブジェクトのリストをデータ属性で持っておいた方が良さそうです。
ということで Dealer クラスには、少なくとも下記の2つのデータ属性があった方が良さそうと考えることができます。
cards:配るカード(Cardクラスのオブジェクトのリスト)players:配る対象のプレイヤー(Playerクラスのオブジェクトのリスト)
プレイヤーもカードのリスト cards を持っていますので、プレイヤーに配る処理は、ディーラーの持つ cards のリストからカードを1枚取り出して、それをプレイヤーの持つ cards のリストに移動するイメージのものになると思います。
スポンサーリンク
UI クラスに必要なデータ属性
今回のババ抜きでは標準入出力使用しないので、必要なデータ属性は特にないかなぁと思います。
例えば標準入出力以外を利用するのであれば、表示先を示す情報等をデータ属性として保持しておいた方が良いと思います。が、今回は標準入出力しか使用しないので、そういった情報がなくても print 関数や input 関数を実行するだけで、UI クラスの役割(ユーザーへの表示・ユーザーからの入力受付を行う)を果たすことはできます。
データ属性を持たない代わりに、表示を行う際には、表示するためのデータは全て、他のクラスのオブジェクトから渡してもらう(引数に指定してもらう)ようにしたいと思います。
OldMaid クラスに必要なデータ属性
OldMaid はディーラーやプレイヤー、UI に指示を出すわけですから、指示を出す対象であるディーラーやプレイヤー、UI を知っておく必要があります。
なので、OldMaid クラスには、少なくとも下記の3つのデータ属性があった方が良さそうと考えることができます。
dealer:指示を出す対象のディーラー(Dealerクラスのオブジェクト)players:指示を出す対象のプレイヤー(Playerクラスのオブジェクトのリスト)ui:指示を出す対象の UI(UIクラスのオブジェクト)
ババ抜きではプレイヤーは複数人いるのでリストにしていますが、ディーラーは一人、UI も一つあれば良いので、これらは単なるオブジェクトとしています。
また、OldMaid はカードを引くプレイヤーの順番を制御したりもするので、現在カードを引いているプレイヤーを示す情報などをデータ属性に持たせても良いと思います。
各クラスに必要なメソッド
続いて、各クラスに必要なメソッドを考えていきたいと思います。
この必要なメソッドについても、ここまでと同様に、実際に自身がババ抜きをプレイする時のことを思い浮かべると考えやすいと思います。
各メソッドを洗い出す上では、まずはゲームの流れの中でどのようなことが行われているのかに注目するのが良いと思います。
- ディーラーが各プレイヤーに対して
カードを配る - プレイヤーが
ペアになったカードを捨てる - プレイヤーが他のプレイヤーから
カードを引く - プレイヤーが
ペアになったカードを捨てる - 次に
カードを引くプレイヤーを決める - 手札にカードが残っているプレイヤーが2人以上なら
3.に戻る - 手札にカードが残っているプレイヤーが1人なら
結果を表示する- 手札が残っているプレイヤーが負け
上記のようなババ抜きのゲームの中で行われることをざっと箇条書きで書き出すと次のようになると思います。
- カードを配る
- ペアになったカードを捨てる
- カード引くプレイヤーを決める
- プレイヤーを次の人に回す
- 結果を表示する
つまり、ババ抜きを作成するためには、上記の機能を実現してやる必要があります。
で、その機能を実現する上で、各クラスのオブジェクトがどのように相互作用しながら動作する必要があるのかを考えることで、各クラスに最低限必要になるメソッドを洗い出すことができます。
相互作用すると聞くと難しいかもしれませんが、要は、どのクラスのオブジェクトがどのクラスのオブジェクトに “処理の依頼” を行う必要があるかを考えていけば良いです。
これは、「メソッドの実行 = 処理の依頼」であると考えることができるからです。
# objectAがobjectBに"XXXして"と依頼する
objectA.objectB.doXXX() # doXXX="XXXして"という依頼に応える処理
そして、依頼を受ける側は、その依頼に応えるための処理を “メソッド” として持っておく必要があると考えると分かりやすいかなぁと思います。
また、どんな処理の依頼が必要になるかを考える上で重要なのが、“どのクラス(どのオブジェクト)がその処理を行うべきか” を考えることだと思います。
例えば「カードを画面に表示する」処理であれば、カードのマークと数字を print 関数で表示すれば良いだけなので、どのクラスでのオブジェクトでも実行できてしまいます。
ただ、今回は表示を行う役割を持った UI クラスが存在しますので、「カードを画面に表示する」処理はやっぱり UI クラスが実行すべきであろうと考えることができます。
逆にいうと、他のクラスのオブジェクトは「カードを画面に表示する」処理は実行しない方が良いことになります。
そうなると、他のクラスのオブジェクトが「カードを画面に表示する」処理を行いたい時には UI クラスに “カードを画面に表示して” と依頼を行う必要があることになります。
つまり、”どのクラス(どのオブジェクト)がその処理を行うべきか” を考えることで、自身のオブジェクトでは “実行しない方がよい処理” が他のクラスのオブジェクトへの “依頼” として自然と浮かび上がってくることになります。
さらに、依頼を受けた側のオブジェクトはその依頼に応える必要があるため、依頼を受けるオブジェクトのクラスにはその依頼に応えるための処理をメソッドとして用意しておく必要があることになります。
例えば上記の例であれば、UI クラスには、”カードを画面に表示して” という依頼に応えるための “カードを画面に表示する” という処理を行うメソッドを用意しておく必要があることになります。
こんな感じで、機能を実現する上でオブジェクト間で発生する依頼を考えることで、必要になるメソッドを洗い出すことができます。
もちろん各クラスの役割から想定される必要そうなメソッドを用意しても良いですが、上記のように依頼ベースで考えた方が、本当に必要なメソッド(公開すべきメソッド)を漏れなく抽出しやすいかなぁと思います。
また、”どのクラス(どのオブジェクト)がその処理を行うべきか” を考慮せずに、各クラスに好き勝手メソッドを実装してしまうと、最初に決めたクラス構成(クラスの役割)から大きく離れた出来上がりになる可能性もあるので注意してください。逆に “どのクラス(どのオブジェクト)がその処理を行うべきか” を考えてメソッドを用意することで、最初に決めたクラス構成を維持しやすくなります。
この辺りを踏まえて、下記の機能を実現する上で、どのような処理の依頼が発生するのかについて考えていきたいと思います。
- カードを配る
- ペアになったカードを捨てる
- カードを引く
- プレイヤーを次の人に回す
- 結果を表示する
スポンサーリンク
「カードを配る」に必要なメソッド
ということで、まずはババ抜きの最初に行われる「カードを配る」を実現するために、各クラスのオブジェクトがどのように他のクラスのオブジェクトに処理を依頼する必要があるかを考えてみましょう。
各クラスの役割を再掲しておくと下記のようになります。
Card:カードを実現する(カードのマークと数字を管理する)Player:プレイヤーを実現する(手札のカードを管理する)Dealer:ディーラーを実現する(カードを配る)UI:ユーザーへの表示・ユーザーからの入力受付を行うOldMaid:ババ抜きのゲームが成立するように各オブジェクトに指示を出す- 各種クラスのオブジェクトの生成も行う
- 指示を出すオブジェクトも決める(カードを引くプレイヤーを決める等)
今回は各クラスに指示を出す OldMaid クラスがありますので、このクラスのオブジェクトから各種機能が開始されるものとして考えていきたいと思います。
では「カードを配る」を実現するために、まず OldMaid のオブジェクトは何をすべきでしょうか?
「カードを配る」を実現するだけであれば、OldMaid のオブジェクトのみの処理で実現するようなことも可能です。
例えば OldMaid が Dealer の持っているカード(cards)をそのまま Player の手札のカード(cards)に追加すれば「カードを配る」が実現できます。
ただ、カードを配ることを役割としている Dealer クラスがあるので、やっぱりカードを配るのは Dealer クラスが行うべきですよね!
逆に OldMaid はカードを配る処理は行わない方が良いので、Dealer に対して “カードを配って” と依頼のみを行う方が良いです。

ここで Dealer クラスのオブジェクトに対して依頼が発生するので、Dealer クラスはその依頼に応えるための処理、つまり “カードを配る” 処理をメソッド(公開メソッド)として用意しておく必要があります。
Dealerクラスに必要なメソッドdealCards(カードを配る)
では、Dealer クラスの dealCards ではどのような処理を行う必要があるでしょうか?
まずトランプゲームなので、ゲームにランダム性を持たせるためにカードをシャッフルする方が良いですね!さらに、シャッフル後、カードを1枚取り出します。
この辺りの処理は他のクラスの役割には存在しませんし、ディーラーの役割として考えても違和感はないですね!なので、この辺りの処理は Dealer クラスで行うものとします。
次に行うのは、取り出したカードの Player への受け渡しです。
これに関しても、直接 Dealer が Player の手札のカード(cards)にカードを加えることでも実現することができるのですが、Player は手札のカードを管理する役割を持っているので、手札のカードの変更は Player が行うべきであろうと考えることができます(トランプゲームしている最中に他のプレイヤーなどから手札のカードを勝手に変更されると嫌ですよね…)。
なので、Dealer がカードを直接 Player の手札に加えるのではなく、Player に対して “カードを手札に加えて” という依頼を行うようにします。

ここで Player クラスのオブジェクトに対して依頼が発生するので、Player には “手札にカードを加えて” という依頼に応えるための “手札にカードを加える” メソッドが必要であることが分かります。
Playerクラスに必要なメソッドaddCard(手札にカードを加える)
こんな感じで、各機能を実現する上で、どのような処理の依頼が発生するのかを考えれば、必要になるメソッドが浮かび上がってきます。
今回は説明を省略していますが、本当は依頼するときにどのようなデータを渡す必要があるかも一緒に考えた方が良いです
そのデータがメソッドの引数になりますので、必要になるメソッドだけでなく、そのメソッドに必要な引数も一緒に洗い出すことができます
今回のクラス構成の場合、単に「カードを配る」機能を実現するのであれば、発生する処理の依頼は上記の2つだけだと思います。
ただ、今回はカードを配り終わった後に、各プレイヤーの手札のカードの表示も行うようようにしたいので、ここまでの考え方通り、カードを配り終わった後に UI クラスのオブジェクトに対して “プレイヤーの手札のカードを表示して” と依頼するようにしたいと思います。

なので UI クラスにはその “プレイヤーの手札のカードを表示して” という依頼に応えるためのメソッドが必要になります。
UIクラスに必要なメソッドshowCards(プレイヤーの手札のカードを表示する)
つまり、「カードを配る」を実現するために必要なメソッドは下記のようになります(手札のカードの表示込み)。
Dealerクラスに必要なメソッドdealCards(カードを配る)
Playerクラスに必要なメソッドaddCard(手札にカードを加える)
UIクラスに必要なメソッドshowCards(プレイヤーの手札のカードを表示する)
最終的な各オブジェクト間での依頼の流れは下の図のようになります。

洗い出したメソッドが、上の図のそれぞれの依頼に対応していることが確認できると思います。
各クラスで必要になるメソッドを洗い出すためには、各機能を実現する上で発生する処理の依頼を洗い出すことが重要で、その処理の依頼は上の図のように絵で描きながら考えると洗い出ししやすいと思います(上の図は UML のコミュニケーション図を簡略したものです)。
各クラスに必要になるメソッドの洗い出しする時の考え方がなんとなく伝わったでしょうか…?
ちなみに、今回はオブジェクトの持つデータ属性の取得はゲッターなどは用意せずに、直接オブジェクトのデータ属性を参照する形で実現しようとしているので、必要な依頼は上記の3つのみになります。
もしゲッターなどを用意するのであれば、当然ですが必要な依頼はもっと多くなります(例えばカードを表示する際には、UI から Card に対して “カードの数字を教えて”・”カードのマークを教えて” と依頼する必要があります)。
「ペアになったカードを捨てる」に必要なメソッド
先程の「カードを配る」で必要になるクラスのメソッドの洗い出し方は詳細に解説したつもりなので、ここからは簡単に各機能に必要なクラスのメソッドを洗い出していきたいと思います。
まず「ペアになったカードを捨てる」を実現する上で発生する処理の依頼を図で表したものが下記になります。

捨てられたカードを表示するために UI への依頼も行っています。
これらの依頼に答えるためのメソッドが必要になりますので、「ペアになったカードを捨てる」を実現するために必要になるメソッドは下記のようになります。
Playerクラスに必要なメソッドdiscardPair(ペアになったカードを捨てる)
UIクラスに必要なメソッドshowDiscardCards(捨てられたカードを表示する)
discardPair では Player 自身の手札のカードのみで処理を行う(ペアを探してペアになったカードを手札から捨てる)ので、他のクラスへの依頼は発生しません。
また、捨てられたカードを表示するためには、捨てられたカードの情報を UI に渡す必要があるので、そのカードを discardPair の戻り値として OldMaid が受け取るようにする必要があります(そしてそれを UI に渡す)
「カードを引く」に必要なメソッド
「カードを引く」を実現する上で発生する処理の依頼を図で表したものが下記になります。

プレイヤーが引いたカードを表示するために UI への依頼も行っています。
これらの依頼に答えるためのメソッドが必要になりますので、「カードを引く」を実現するために必要になるメソッドは下記のようになります。
Playerクラスに必要なメソッドdrawCard(カードを引く)releaseCard(カードを手札から手放す)
UIクラスに必要なメソッドshowDrawCard(引かれたカードを表示する)
Player のオブジェクトが行うカードを引く処理は、相手の Player のオブジェクトのカードを引いてから、そのカードを自身の手札に加える処理と考えることができます。
Player は自身の手札を管理する(オブジェクトの持つ手札のカードへの変更は、そのオブジェクト自身が行う)ことを役割としていますので、直接相手の Player の手札からカードを取り出すのではなく、相手の Player に “カード手放して” と依頼することで処理を実現するようにしています。
そのため、その依頼に応えるための “カードを手放す” メソッド releaseCard を用意する必要があります。
また、引かれたカードを表示するためには、引かれたカードの情報を UI に渡す必要があるので、そのカードを drawCard の戻り値として OldMaid が受け取るようにする必要があります(そしてそれを UI に渡す)
スポンサーリンク
「カードを引くプレイヤーを決める」に必要なメソッド
「カードを引くプレイヤーを決める」を実現する上で発生する処理の依頼を図で表したものが下記になります。

カードを引くプレイヤーを決めるのは OldMaid クラスの役割にしていますので、他のクラスに依頼を出すことなく OldMaid のオブジェクトのみで機能を実現することができます。
カードを引くプレイヤーを決めるためには、プレイヤーの手持ちのカードの枚数を知る必要がありますので(カードの枚数が 0 のプレイヤーはスキップする必要がある)、Player に “手札にカードあるか教えて” と依頼することも考えましたが、OldMaid 自身が Player の手札のカードであるデータ属性 cards の要素数をカウントするようにしました。
「結果を表示する」に必要なメソッド
「結果を表示する」を実現する上で発生する処理の依頼を図で表したものが下記になります。

この依頼に答えるためのメソッドが必要になりますので、「結果を表示する」を実現するために必要になるメソッドは下記のようになります。
UIクラスに必要なメソッドshowReuslt(結果を表示する)
今回は結果として “負けたプレイヤー” を表示するようにしたいと思います。そのため、showReuslt 実行時には、負けたプレイヤー、すなわち手札にカードが残っているプレイヤーの ID を引数として渡すようにします。
各クラスの実装例
クラスに必要なメソッドでは依頼を元に必要なメソッドを洗い出しましたので、必ず実装しなければいけないメソッド(公開メソッド)は、その洗い出ししたメソッドのみとなります。なぜなら、それ以外のメソッドは他のクラスのオブジェクトからは呼び出されない(依頼されない)からです。
ですので、OldMaid は以外のクラスは、後は各クラスにその洗い出ししたメソッドを実装していけばいいだけです(ただし、それ以外のメソッドを実装してはいけないというわけではないです。可読性などを考慮して他のメソッドを用意しても OK です)。
OldMaid は他のクラスに指示を出す(依頼する)必要があるので、指示を出す順番等を考慮しながら実装する必要があります(例えばカードを配布した後にペアになったカードを捨てるようにプレイヤーに指示するなど)。
ここでは、実際に各クラスの実装例を紹介していきたいと思います。
スポンサーリンク
Card クラスの実装例
Card クラスの実装例は下記のようになります。
class Card():
# 数値からマーク文字列への変換
SUIT_STR = {
0: "D", 1: "H", 2: "S", 3: "C", 4: "J"
}
# 数値から数字文字列への変換
NUMBER_STR = {
0: "A", 1: "2", 2: "3", 3: "4", 4: "5", 5: "6", 6: "7",
7: "8", 8: "9", 9: "10", 10: "J", 11: "Q", 12: "K", 13: "0"
}
def __init__(self, suit, number):
self.suit = Card.SUIT_STR[suit] # マーク
self.number = Card.NUMBER_STR[number] # 数字
各クラスに必要なメソッドでメソッドの洗い出しを行いましたが、Card クラスに必要なメソッドはありませんでしたので、コンストラクタ __init__ のみを用意しています。
Player クラスの実装例
各クラスに必要なメソッドで洗い出ししたように、Player クラスに必要になるメソッドは下記のようになります。
addCard(手札にカードを加える)discardPair(ペアになったカードを捨てる)drawCard(カードを引く)releaseCard(カードを手札から手放す)
上記4つのメソッドを用意した Player クラスの実装例は下記のようになります。
class Player():
def __init__(self, id):
self.id = id # プレイヤーの名前
self.cards = [] # プレイヤーの手札
def addCard(self, card):
'''カードを手札に加える'''
self.cards.append(card)
def releaseCard(self, num):
'''カードを手放す(他の人に渡す)'''
return self.cards.pop(num)
def drawCard(self, player2):
'''player2からカードを引く'''
# 引くカードを決める
num = self.selectCard(len(player2.cards))
# num枚目のカードを手放すことをplayer2に依頼
card = player2.releaseCard(num)
# 手放されたカードを手札に加えることをplayer1に依頼
self.addCard(card)
return card
def discardPair(self):
'''ペアとなるカードを捨てる'''
discarded = []
# ペアとなるカードを探す
pair = self.getPair()
# ペアとなるカードがなくなるまでループ
while pair:
card1, card2 = pair
# 捨てるカードをリストに追加
discarded.append(card1)
discarded.append(card2)
# カードを捨てる
self.discardCard(card1)
self.discardCard(card2)
# 再びペアとなるカードを探す
pair = self.getPair()
return discarded
def getPair(self):
'''ペアとなるカードを探す'''
for i, card1 in enumerate(self.cards):
# 1枚目のカードの数字を取得
number1 = card1.number
for card2 in self.cards[i + 1:]:
# 2枚目のカードの数字を取得
number2 = card2.number
if number1 == number2:
# 数字が同じならこの2つのカードはペア
return (card1, card2)
def discardCard(self, card):
'''カードを捨てる'''
self.cards.remove(card)
def selectCard(self, num):
'''num枚のカードから引くカードを決める'''
return random.randrange(num)
コンストラクタ以外にも、上記で挙げた4つのメソッド “以外のメソッド” もありますが、これらは上記の4つのメソッドを実装するために用意したメソッドで、他のクラスのオブジェクトからの依頼を受け付けるためのメソッドではありません(要はプライベートメソッドです。__ 付けていませんが…)。
ですので、わざわざメソッドにしなくても上記4つのメソッドの中で同等の処理さえ実装してやれば、ババ抜きを実現することは可能です。
以降のクラスにおいても各クラスに必要なメソッドで挙げたメソッド以外のものを用意していますが、これらはババ抜きを実現するためには必然ではないものの、可読性などを考慮して用意したメソッドになります。
また、discardPair メソッドでは、getPair メソッドで見つけ出したペア(同じ数字の2枚のカード)をリスト cards から削除するようにしています。そして、削除した cards から再び getPair を実行し、同様の処理をペアが見つからなくなるまで実行するようにしています。
getPair は毎回リストの先頭からペアを探すので処理速度は遅いです…。
Dealer クラスの実装例
各クラスに必要なメソッドで洗い出ししたように、Dealer クラスに必要になるメソッドは下記のようになります。
dealCards(カードを配る)
上記1つのメソッドを用意した Dealer クラスの実装例は下記のようになります。
class Dealer():
def __init__(self, cards, players):
self.cards = cards # 配るカード
self.players = players # 配る相手
def dealCards(self):
'''カードを配る'''
# カードをシャッフルする
random.shuffle(self.cards)
player_num = len(self.players)
i = 0
while self.cards:
player = self.players[i % player_num]
# 先頭のカードを取り出し、そのカードをプレイヤーの手札に加えてもらう
card = self.cards.pop(0)
player.addCard(card)
i += 1
ポイントは、Player クラスのオブジェクト player に “手札にカードを加えて” と依頼するために player.addCard を実行しているところだと思います。
後から思ったのですが、Dealer が動作するのは dealCards が呼び出された時の一回のみなので、わざわざデータ属性を用意しなくても dealCards の引数で cards と players を受け取ってしまった方が良かったかもしれないです…。
スポンサーリンク
UI クラスの実装例
各クラスに必要なメソッドで洗い出ししたように、UI クラスに必要になるメソッドは下記のようになります。
showCards(プレイヤーの手札のカードを表示する)showDiscardCards(捨てられたカードを表示する)showDrawCard(引かれたカードを表示する)showReuslt(結果を表示する)
上記4つのメソッドを用意した UI クラスの実装例は下記のようになります。
class UI():
def showCards(self, id, cards):
'''手持ちのカードを表示する'''
# Cardオブジェクトのリストを文字列に変換して表示
cards_str = self.getCardsStr(cards)
print("プレイヤー" + str(id) + "のカード:" + cards_str)
def showDiscardCards(self, id, cards):
'''捨てられたカードを表示する'''
if len(cards) != 0:
# Cardオブジェクトのリストを文字列に変換して表示
cards_str = self.getCardsStr(cards)
print("プレイヤー" + str(id) + "が" + "捨てたカード:" + cards_str)
def showDrawCard(self, id1, id2, card):
'''カードの交換状況を表示する'''
# Cardオブジェクトを文字列に変換して表示
card_str = self.getCardStr(card)
print("プレイヤー" + str(id2) + " -> プレイヤー" + str(id1) + ":" + card_str)
def showResult(self, id):
'''ゲームの結果を表示する'''
print("プレイヤー" + str(id) + "の負けです...")
def getCardsStr(self, cards):
'''Cardオブジェクトのリストを文字列に変換'''
cards_str = ""
for card in cards:
cards_str += self.getCardStr(card)
return cards_str
def getCardStr(self, card):
'''Cardオブジェクトを文字列に変換'''
return "[" + card.suit + ":" + card.number + "]"
データ属性を用意していない代わりに、表示するために必要な情報(手札のカードやプレイヤーの ID など)は全て引数で受け取るようにしています。
OldMaid クラスの実装例
OldMaid クラスは各クラスに指示を出すクラスなので、必須になるメソッドはありません。
ただし、ババ抜きとしてゲームが進むように、各クラスのオブジェクトに指示を出すように実装する必要があります。より具体的には、下記のようにゲームが進むように各クラスのオブジェクトに対して指示を出します。
- ディーラーが各プレイヤーに対してカードを配る
- プレイヤーがペアになったカードを捨てる
- プレイヤーが他のプレイヤーからカードを引く
- プレイヤーがペアになったカードを捨てる
- 次にカードを引くプレイヤーを決める
- 手札にカードが残っているプレイヤーが2人以上なら 3. に戻る
- 手札にカードが残っているプレイヤーが1人なら結果を表示する(そのプレイヤーが負け)
OldMaid クラスの実装例は下記のようになります。
class OldMaid():
def __init__(self):
# 各種オブジェクトを生成
cards = self.newCards()
self.players = self.newPlayers()
self.dealer = self.newDealer(cards)
self.ui = self.newUI()
# ゲームを開始する
self.start()
def start(self):
'''ゲーム開始'''
# カードを配布する
self.dealCards()
# 各プレイヤーにカードのペアを捨ててもらう
self.discardPairs()
for player1, player2 in self.nextPlayer():
# nextPlayerで決まったplayer1がplayer2からカードを引く
self.draw(player1, player2)
# 最終的な手札のカードと結果を表示
self.showCards()
self.showResult()
def draw(self, player1, player2):
# 現在のカードを表示
self.showCards()
# カードを引く
self.drawCard(player1, player2)
# ペアになったカードを捨てる
self.discardPairs()
def newCards(self):
'''トランプのカードを作成する'''
cards = []
for suit in range(4): # マークの種類は4
for number in range(13): # 数字の種類は13
# マークがsuit、数字がnumberのカードを作成
card = Card(suit, number)
cards.append(card)
# ジョーカーのカードを作成
joker = Card(4, 13)
cards.append(joker)
return cards
def newPlayers(self):
'''プレイヤーを作成する'''
players = []
for id in range(NUM_PLAYER):
# プレイヤーを作成
player = Player(id)
players.append(player)
return players
def newDealer(self, cards):
'''ディーラー作成する'''
return Dealer(cards, self.players)
def newUI(self):
'''UIを作成する'''
return UI()
def dealCards(self):
'''カードを配布する'''
# dealerにカードを配ることを依頼
self.dealer.dealCards()
# 配布後のカードを表示
self.showCards()
def showCards(self):
'''カードを表示する'''
for player in self.players:
# プレイヤーごとにカードの表示をUIに依頼
self.ui.showCards(player.id, player.cards)
time.sleep(INTERVAL / 1000)
def discardPairs(self):
'''各プレイヤーにペアを捨てさせる'''
for player in self.players:
# playerにペアになったカードを捨てるよう依頼
discarded = player.discardPair()
# 捨てられたカードの情報の表示をuiに依頼
self.ui.showDiscardCards(player.id, discarded)
time.sleep(INTERVAL / 1000)
def drawCard(self, player1, player2):
'''カードを交換する'''
# player1にplayer2からカードを引くことを依頼
card = player1.drawCard(player2)
# カードのやり取りの表示をuiに依頼
self.ui.showDrawCard(player1.id, player2.id, card)
time.sleep(INTERVAL / 1000)
def nextPlayer(self):
'''次にカードを引く人と引かれる人を気前る'''
player_num = len(self.players)
# ゲーム開始直後は末尾のプレイヤーのプレイ後と考えてスタート
player1_id = player_num - 1
while True:
'''まずはカードを引く人を決める'''
# 次にカードを引く人を暫定で決める
next_player_id = (player1_id + 1) % player_num
# 人が手持ちのカードが0枚なら他のプレイヤーを探す
while not len(self.players[next_player_id].cards):
# 次にカードを引く人を暫定で決める
next_player_id = (next_player_id + 1) % player_num
# 次にカードを引く人が決まる前に1週回ったらもうカードを引く人はいない
if next_player_id == player1_id:
return None
# 手持ちのカードがあるプレイヤーが見つかったのでカードを引く人確定
player1_id = next_player_id
player1 = self.players[next_player_id]
'''次にカードを引かれる人を決める'''
# 以降は"次にカードを引く人を決める処理"と同様の処理なのでコメント省略
next_player_id = (player1_id + 1) % player_num
while not len(self.players[next_player_id].cards):
next_player_id = (next_player_id + 1) % player_num
if next_player_id == player1_id:
player2 = None
return None
player2 = self.players[next_player_id]
# 決定したプレイヤーを返却
yield (player1, player2)
def showResult(self):
'''結果を表示する'''
for player in self.players:
if len(player.cards):
# まだカードがある人が負け
self.ui.showResult(player.id)
各クラスのオブジェクトを生成した後(newXxxx を実行した後)、上記のような流れでゲームが進むように各クラスのオブジェクトに指示を出すようにしています。
カードを引く処理は、次にカードを引くプレイヤーを決めながら何回も実行する必要があるため、start メソッドの下記部分でループを行うようにしています。
for player1, player2 in self.nextPlayer():
# nextPlayerで決まったplayer1がplayer2からカードを引く
self.draw(player1, player2)
nextPlayer メソッドがちょっと複雑ですが、基本的には事前にカードを引いたプレイヤーの次のプレイヤーを “次にカードを引くプレイヤー(player1)” に、その次のプレイヤーを “次にカードを引かれるプレイヤー(player2)” として選んでいるだけです(player1 と player2 という変数名がイマイチですが、他にいい名前が思い浮かびませんでした…)。
ただ、カードを持っていないプレイヤーはスキップする必要があるので、そのスキップを行うために内側の while ループを実行しています。
カードを持っているプレイヤーが1人の場合は nextPlayer メソッドは return を実行してカードを引くループを終了するようにしています。
また、ところどころで time.sleep を実行しているのは、各種の表示が行われる間隔を空けるためです。time.sleep を入れないと一気に処理が進んでババ抜きゲームがどのように進行しているのかが分かりにくいため、このような間隔を空けるための処理を入れています。
ババ抜きのサンプルスクリプト
最後にババ抜きのサンプルスクリトを紹介したいと思います。
スポンサーリンク
自動で進行するババ抜き
ここまで解説してきた内容を踏まえて作成したサンプルスクリプトは下記のようになります。といっても、各クラスの実装例で紹介した各クラスをまとめただけのものになります。
スクリプト先頭付近の NUM_PLAYER によってプレイヤーの人数を、INTERVAL によって各表示実行後のスリープ時間を設定することができます。
# -*- coding: utf-8 -*-
import random
import time
# プレイ人数
NUM_PLAYER = 4
# ゲームを進める時間の間隔(ms)
INTERVAL = 100
class Card():
# 数値からマーク文字列への変換
SUIT_STR = {
0: "D", 1: "H", 2: "S", 3: "C", 4: "J"
}
# 数値から数字文字列への変換
NUMBER_STR = {
0: "A", 1: "2", 2: "3", 3: "4", 4: "5", 5: "6", 6: "7",
7: "8", 8: "9", 9: "10", 10: "J", 11: "Q", 12: "K", 13: "0"
}
def __init__(self, suit, number):
self.suit = Card.SUIT_STR[suit] # マーク
self.number = Card.NUMBER_STR[number] # 数字
class Player():
def __init__(self, id):
self.id = id # プレイヤーの名前
self.cards = [] # プレイヤーの手札
def addCard(self, card):
'''カードを手札に加える'''
self.cards.append(card)
def releaseCard(self, num):
'''カードを手放す(他の人に渡す)'''
return self.cards.pop(num)
def drawCard(self, player2):
'''player2からカードを引く'''
# 引くカードを決める
num = self.selectCard(len(player2.cards))
# num枚目のカードを手放すことをplayer2に依頼
card = player2.releaseCard(num)
# 手放されたカードを手札に加えることをplayer1に依頼
self.addCard(card)
return card
def discardPair(self):
'''ペアとなるカードを捨てる'''
discarded = []
# ペアとなるカードを探す
pair = self.getPair()
# ペアとなるカードがなくなるまでループ
while pair:
card1, card2 = pair
# 捨てるカードをリストに追加
discarded.append(card1)
discarded.append(card2)
# カードを捨てる
self.discardCard(card1)
self.discardCard(card2)
# 再びペアとなるカードを探す
pair = self.getPair()
return discarded
def getPair(self):
'''ペアとなるカードを探す'''
for i, card1 in enumerate(self.cards):
# 1枚目のカードの数字を取得
number1 = card1.number
for card2 in self.cards[i + 1:]:
# 2枚目のカードの数字を取得
number2 = card2.number
if number1 == number2:
# 数字が同じならこの2つのカードはペア
return (card1, card2)
def discardCard(self, card):
'''カードを捨てる'''
self.cards.remove(card)
def selectCard(self, num):
'''num枚のカードから引くカードを決める'''
return random.randrange(num)
class Dealer():
def __init__(self, cards, players):
self.cards = cards # 配るカード
self.players = players # 配る相手
def dealCards(self):
'''カードを配る'''
# カードをシャッフルする
random.shuffle(self.cards)
player_num = len(self.players)
i = 0
while self.cards:
player = self.players[i % player_num]
# 先頭のカードを取り出し、そのカードをプレイヤーの手札に加えてもらう
card = self.cards.pop(0)
player.addCard(card)
i += 1
class UI():
def showCards(self, id, cards):
'''手持ちのカードを表示する'''
# Cardオブジェクトのリストを文字列に変換して表示
cards_str = self.getCardsStr(cards)
print("プレイヤー" + str(id) + "のカード:" + cards_str)
def showDiscardCards(self, id, cards):
'''捨てられたカードを表示する'''
if len(cards) != 0:
# Cardオブジェクトのリストを文字列に変換して表示
cards_str = self.getCardsStr(cards)
print("プレイヤー" + str(id) + "が" + "捨てたカード:" + cards_str)
def showDrawCard(self, id1, id2, card):
'''カードの交換状況を表示する'''
# Cardオブジェクトを文字列に変換して表示
card_str = self.getCardStr(card)
print("プレイヤー" + str(id2) + " -> プレイヤー" + str(id1) + ":" + card_str)
def showResult(self, id):
'''ゲームの結果を表示する'''
print("プレイヤー" + str(id) + "の負けです...")
def getCardsStr(self, cards):
'''Cardオブジェクトのリストを文字列に変換'''
cards_str = ""
for card in cards:
cards_str += self.getCardStr(card)
return cards_str
def getCardStr(self, card):
'''Cardオブジェクトを文字列に変換'''
return "[" + card.suit + ":" + card.number + "]"
class OldMaid():
def __init__(self):
# 各種オブジェクトを生成
cards = self.newCards()
self.players = self.newPlayers()
self.dealer = self.newDealer(cards)
self.ui = self.newUI()
# ゲームを開始する
self.start()
def start(self):
'''ゲーム開始'''
# カードを配布する
self.dealCards()
# 各プレイヤーにカードのペアを捨ててもらう
self.discardPairs()
for player1, player2 in self.nextPlayer():
# nextPlayerで決まったplayer1がplayer2からカードを引く
self.draw(player1, player2)
# 最終的な手札のカードと結果を表示
self.showCards()
self.showResult()
def draw(self, player1, player2):
# 現在のカードを表示
self.showCards()
# カードを引く
self.drawCard(player1, player2)
# ペアになったカードを捨てる
self.discardPairs()
def newCards(self):
'''トランプのカードを作成する'''
cards = []
for suit in range(4): # マークの種類は4
for number in range(13): # 数字の種類は13
# マークがsuit、数字がnumberのカードを作成
card = Card(suit, number)
cards.append(card)
# ジョーカーのカードを作成
joker = Card(4, 13)
cards.append(joker)
return cards
def newPlayers(self):
'''プレイヤーを作成する'''
players = []
for id in range(NUM_PLAYER):
# プレイヤーを作成
player = Player(id)
players.append(player)
return players
def newDealer(self, cards):
'''ディーラー作成する'''
return Dealer(cards, self.players)
def newUI(self):
'''UIを作成する'''
return UI()
def dealCards(self):
'''カードを配布する'''
# dealerにカードを配ることを依頼
self.dealer.dealCards()
# 配布後のカードを表示
self.showCards()
def showCards(self):
'''カードを表示する'''
for player in self.players:
# プレイヤーごとにカードの表示をUIに依頼
self.ui.showCards(player.id, player.cards)
time.sleep(INTERVAL / 1000)
def discardPairs(self):
'''各プレイヤーにペアを捨てさせる'''
for player in self.players:
# playerにペアになったカードを捨てるよう依頼
discarded = player.discardPair()
# 捨てられたカードの情報の表示をuiに依頼
self.ui.showDiscardCards(player.id, discarded)
time.sleep(INTERVAL / 1000)
def drawCard(self, player1, player2):
'''カードを交換する'''
# player1にplayer2からカードを引くことを依頼
card = player1.drawCard(player2)
# カードのやり取りの表示をuiに依頼
self.ui.showDrawCard(player1.id, player2.id, card)
time.sleep(INTERVAL / 1000)
def nextPlayer(self):
'''次にカードを引く人と引かれる人を気前る'''
player_num = len(self.players)
# ゲーム開始直後は末尾のプレイヤーのプレイ後と考えてスタート
player1_id = player_num - 1
while True:
'''まずはカードを引く人を決める'''
# 次にカードを引く人を暫定で決める
next_player_id = (player1_id + 1) % player_num
# 人が手持ちのカードが0枚なら他のプレイヤーを探す
while not len(self.players[next_player_id].cards):
# 次にカードを引く人を暫定で決める
next_player_id = (next_player_id + 1) % player_num
# 次にカードを引く人が決まる前に1週回ったらもうカードを引く人はいない
if next_player_id == player1_id:
return None
# 手持ちのカードがあるプレイヤーが見つかったのでカードを引く人確定
player1_id = next_player_id
player1 = self.players[next_player_id]
'''次にカードを引かれる人を決める'''
# 以降は"次にカードを引く人を決める処理"と同様の処理なのでコメント省略
next_player_id = (player1_id + 1) % player_num
while not len(self.players[next_player_id].cards):
next_player_id = (next_player_id + 1) % player_num
if next_player_id == player1_id:
player2 = None
return None
player2 = self.players[next_player_id]
# 決定したプレイヤーを返却
yield (player1, player2)
def showResult(self):
'''結果を表示する'''
for player in self.players:
if len(player.cards):
# まだカードがある人が負け
self.ui.showResult(player.id)
if __name__ == "__main__":
OldMaid()
プレイ可能なババ抜き
上記のスクリプトではババ抜きが自動的に進行しますが、サブクラスを用意することでユーザーがプレイできるようにすることもできます(各種サブクラスでユーザーがプレイできるようにメソッドの追加とオーバーライドを行う)。
この場合は、上記スクリプトの下記部分を、
if __name__ == "__main__":
OldMaid()
下記で上書きして実行してください。
class PlayingPlayer(Player):
def drawCard(self, player2, num):
'''player2からカードを引く'''
# num枚目のカードを手放すことをplayer2に依頼
card = player2.releaseCard(num)
# 手放されたカードを手札に加えることをplayer1に依頼
self.addCard(card)
return card
class PlayingUI(UI):
def inputSelectCard(self, id, num):
'''引くカードの入力を受け付ける'''
print("何枚目のカードを引きますか?(0 〜 " + str(num - 1) + ")")
return int(input())
def showNumCard(self, id, num):
'''カードの枚数を表示する'''
print("プレイヤー" + str(id) + "のカード:" + str(num) + "枚")
class PlayingOldMaid(OldMaid):
def newPlayers(self):
'''プレイヤーを作成する'''
players = []
# プレイヤーの一人はプレイ可能なプレイヤーとする
players.append(PlayingPlayer(0))
for id in range(1, NUM_PLAYER):
# プレイヤーを作成
player = Player(id)
players.append(player)
return players
def newUI(self):
'''UIを作成する'''
# プレイ専用のUIを作成する
return PlayingUI()
def drawCard(self, player1, player2):
if type(player1) != PlayingPlayer:
super().drawCard(player1, player2)
return
# player2から引くカードをユーザーに入力してもらう
num = self.ui.inputSelectCard(player1.id, len(player2.cards))
# player1にplayer2からカードを引くことを依頼
card = player1.drawCard(player2, num)
# カードのやり取りの表示をuiに依頼
self.ui.showDrawCard(player1.id, player2.id, card)
time.sleep(INTERVAL / 1000)
def showCards(self):
'''カードを表示する'''
for player in self.players:
if type(player) == PlayingPlayer:
# ユーザーのカードの表示をUIに依頼
self.ui.showCards(player.id, player.cards)
else:
# ユーザー以外のカードは表示せず、枚数のみ表示するように依頼
self.ui.showNumCard(player.id, len(player.cards))
time.sleep(INTERVAL / 1000)
if __name__ == "__main__":
PlayingOldMaid()
これにより、プレイヤーの一人をユーザーが操作することができるようになります(といっても、引くカードの番号を決めれるようになるだけですが…)。
また、他のプレイヤーのカードが伏せられるようにもしています。
プレイヤー0のカード:[D:2][S:7][S:A][D:J][D:5][C:6][C:2][S:J][D:8][S:9][C:7][J:0][S:4][H:5] プレイヤー1のカード:13枚 プレイヤー2のカード:13枚 プレイヤー3のカード:13枚 プレイヤー0が捨てたカード:[D:2][C:2][S:7][C:7][D:J][S:J][D:5][H:5] プレイヤー1が捨てたカード:[C:9][D:9][S:8][H:8][S:5][C:5] プレイヤー2が捨てたカード:[C:3][D:3][C:K][D:K][S:2][H:2][H:10][S:10] プレイヤー3が捨てたカード:[H:4][D:4][S:Q][C:Q][C:A][H:A] プレイヤー0のカード:[S:A][C:6][D:8][S:9][J:0][S:4] プレイヤー1のカード:7枚 プレイヤー2のカード:5枚 プレイヤー3のカード:7枚 何枚目のカードを引きますか?(0 〜 6)
まとめ
このページでは、Python での「ババ抜き」の作り方を解説しました!
なんとなくかもしれませんが、私がどういったことを考えてクラスを作成しているかが伝わったのではないかと思います。クラス構成に正解はないと思いますし、今回紹介したのもクラス構成を考える上での一例に過ぎないですが、クラス構成を考える上での1つの考え方として参考になるのではないかと思います。
今回紹介した「ババ抜き」のように、プレイしたことのあるゲームであれば、登場するものをオブジェクトとして捉えることで割と簡単にクラス構成を考えることができます。
なので、オブジェクト指向を取り入れた設計やプログラミングのトレーニングには結構向いてるんじゃないかなぁと思います。
実践してみることでオブジェクト指向に慣れることができると思いますので、是非いろんなゲームのプログラミングや設計に挑戦してみてください!

