名古屋で数学するプログラマ(仮)

@antimon2 が趣味兼一部本職の数学で何かするときのブログ。

CodeIQ の「カードゲームの役判定」問題 by @Nabetani

なんか最近 CodeIQ の話ばっか書いているような気がします(^-^;
ここ最近、少し余裕が出来たときにちょこちょこ目についた問題を解いて投稿したりしています。

その中の一つ。カードゲームの役を判定する
先日フィードバックが来た*1のですが、なんか「antimon2 様の評価は『完璧』」とか「今回のベストアンサーだと思います」とか言われちゃいましたヾ(〃∇〃)ノ

そんなたいそうなものではなく、Ruby のメソッドを駆使して見た目シンプルに(ただし決して効率の良くない)コードを書いただけなのですが。
まぁせっかくなので、公開しちゃいます。

問題のおさらい

すでに締め切られているのでサイトを見ても問題は見えないと思いますので、簡単にどんな問題だったのか、概要を記述しておきます。
使用するのは、通常のトランプのカード52枚です。以下、"S", "H", "D", "C" はそれぞれ「スペード」「ハート」「ダイヤ」「クラブ」を表します。

6枚のカードを使って行うカードゲームがあります。その役を判定するプログラムを書いて下さい。
役は下表のとおりです。

役名 記号 意味
アンサー An 同一ランクの4枚組と、同一ランクの2枚組 S2,H2,D2,C2,SA,HA
シンクダブルトリオ sDT 同一ランクの3枚組が、2組。
各3枚組は、スートの組み合わせが同一。
S10,H10,D10,SQ,HQ,DQ
ダブルトリオ DT 同一ランクの3枚組が、2組。
sDT ではない。
S10,D10,C10,SQ,HQ,DQ
シンクコントトリプルペア scTP 同一ランクの2枚組が、3組。
各2枚組はスートの組み合わせが同一。
3つのランクが連続している。
S9,H9,S10,H10,SJ,HJ
コントトリプルペア cTP 同一ランクの2枚組が、3組。
3つのランクが連続している。
scTP ではない。
S9,C9,S10,H10,HJ,DJ
シンクトリプルペア sTP 同一ランクの2枚組が、3組。
各2枚組はスートの組み合わせが同一。
scTP ではない。
S9,H9,S10,H10,SA,HA
トリプルペア TP 同一ランクの2枚組が、3組。
scTP, sTP, cTP のいずれでもない。
S3,H3,H10,D10,SA,CA

data.txt に含まれている手札に各役が何組あるか数えて下さい。

解答

まず私の解答プログラムを。
data.txt が存在しないと動かないですが、まぁそれはこの手の問題の(さが)なので。

結果出力のところで、RUBY_VERSION とか __FILE__ とか参照していますが、このあたりは、求められた解答テキストの形式に合わせた情報をおまけで出力しているだけ*2なので、気にしないでください。

役判定処理のメインは、Cards#hand メソッドに集約されています。その内容は読み取っていただくものとして。「コント(シーケンス)」の判定のみ少しだけ解説。
「コント」は「ランクの連続」を判定する必要があり、つまり「順序」が関係してきます。判定方法としては、以下のいずれかになると思います。

  • ランクを整数などの自然な順序づけが出来るものに変換して管理し、判定に利用。
  • ランクの順序を別管理し、判定時にその順序と照合。

出題者の(フィードバックに載っていた)解答例は前者の方式となっており、よくあるトランプの考え方で「"A" を 1、"J" を 11、"Q" を 12、"K" を 13」に変換して数値的に判定していました。
ただしルールとして「『Q, K, A』もコントと判定する(ポーカーと同様)」とあり、その部分は「〜 || ranks==[1,12,13]」としていました。
うん、この方法も思いついたんですけれど、あまり美しくないな、と思ってたんですよね。
ポーカーでは許されていないけれどもし「『K, A, 2』という組合せもコントと判定する」というルールが加わっていたら、もう一個条件を加えて「〜 || ranks==[1,12,13] || ranks==[1,2,13]」としなければならなくなるわけで、煩雑になっていきますし。
今回はないですがもっと特殊な順序づけだったならば(例えば、「3,6,9,Q,2,5,8,J,A,4,7,10,K」とか)、どんどん煩雑になっていってしまいます。
だから「カードのランクとしては変換せずに文字列のまま管理する」「役の判定時はそのランクの順序を別管理したデータを参照する」という、後者の方法で実装したわけです。

また「一致するか」の判定ですが、Ruby の配列は「要素数が同じで、各インデックスの要素も同じならば2つの配列は同じ(== で true)」となるので、予めソートされていれば == 演算子で比較が出来ます。
でも私のコードでは、ソートはせず、別の方法で判定しています。
配列の - 演算子が「右オペランドの配列の要素を、左オペランドの配列から取り除いた配列を返す」という仕様なので、2つの配列が順序に関係なく要素数と内容が一致していれば、その結果は空の配列になります。そのことを利用して判定しています。
予め用意した順序づけられたランクの配列から、3つずつの組を取り出し、それと「与えられたランクの3つ組み」を上述の方法で比較することで、一致するものがあれば「コント」と判定、というわけです。

これは決して効率の良い方法ではないのですが、見た目はシンプル(というよりコンパクト)にはなりますし、仕様変更にもコードはほとんど変更せず元データの変更で対応できます。
例えばもし「『K, A, 2』という組合せもコントと判定する」というルールが加わったとしても、私のプログラムなら RANKS 定数の定義を

RANKS = %w[A 2 3 4 5 6 7 8 9 10 J Q K A 2]

と後ろに "2" を追加するだけで対応できます。

テスト

なおこの問題には、サンプルデータが(問題ドキュメント中に表形式で)与えられていました。
『プログラムを作成する際の参考にして下さい。』と説明が付けられていましたが、これは「TDD で解くと良いよ」という示唆だと思ったので。素直に テストコードを先に書いて TDD で解きました。
そのテストコードも掲載しておきます。

Ruby 標準の test/unit ライブラリを使用しています。でも require_relative とか |《ブロック引数, …》;《ローカル変数, …》| という書式を利用しているので Ruby1.8.7 では動作しないのですけれど。
また、TDD といっても Cards#hand メソッド以外はテストしていないのですが、他に定義したのは initialize と attr_reader で指定した属性読み取りメソッドだけなのでそこまで厳密じゃなくても良いかなと。
それよりポイントは、「テストメソッド自体の自動生成」。
テストメソッドの元データは、与えられたサンプルデータを CSV 形式(1行の要素数7つ、前の6つが6枚のカードで最後の1つが役)に変換してプログラムのデータ領域(__END__ 以降)に置いています。
それをテストクラスの定義時に動的に読み込んで、「前6つの文字列配列が与えられたときの役が最後の1つに一致するか」というテストをするメソッドを動的に定義している、という仕組み。
テストの可読性は落ちますが、今回はテストコードの提出までは求められていないので別の良いんじゃないかな、と。それにこの方が「ヒューマンエラー(=テストコード自体の記述ミス)によるテスト失敗」は避けられるし。それを言ったら「自動生成したテストメソッドが正しいかのテスト」も実施するのが本当の TDD になるのかもしれないのですが(^-^; そこまでは勘弁(^-^; 今は80個のテストがすべて success するけれど開発中は未実装(よってテストが失敗するはずのパターン)の部分はちゃんと failure になったし。

*1:3連休中で旅行中だったので確認遅れました。

*2:CodeIQ で解答テキストのひな形が用意されている問題では、私はこれをよくやります。