せらぴんブログ

サークル「せらぴん」のうのはな透です。やっぱり眼鏡っ娘が好き!!

DXRubyでポーカーを作った

この記事はDXRuby Advent Calender 2014の11日目の記事です……のはずがもう日を跨いでいます。ごめんなさい!!

前回はみれいゆーさんの「DXRubyで成長曲線と父母遺伝の再現、描写」でした。専門的な内容であまりわかってないのですが、これを利用すれば人生シミュレーションが作れそうな気がしました。

さて今回の内容ですが、「DXRubyでポーカーを作った」ということで、過去に自分の作ったゲームと工夫した点を紹介します!! もっとスゴイ読み応えのある記事書きたかったけど用意できませんでした!! ゆるして!!

f:id:yuphiel:20141212021147j:plain

これは今年の1月に作ったテキサス・ホールデムのゲーム、通称「うのはなポーカー」です。

※現在はこちらからダウンロードできます。

プレイヤー表示部が若干オサレになりました。プレイヤー名は某マスの眼鏡ドルになってます。ゆくゆくはアニメーションぬるぬるの眼鏡ドルポーカーにしたいなぁ、と夢想してます。

工夫した点

MVCを意識した

え? 一年前に似たような記事を見たって? 気にするな!!

ゲーム本体はControllerがmain.rb、Viewがview.rb、Modelがその他のソース(game.rb, pot.rb, player.rb, routine.rb, scorer.rb)と分かれています。Controllerはキー入力に応じてModelの各関数を呼び出すことに徹しているし、ViewはModelの内容を描写することに徹しています。

以下が汎用コントローラクラスになります。

# ゲームとビューを操作するコントローラ
class Controller
	# シグナル:コマンドなし
	NO_COMMAND = 0
	# シグナル:ゲーム終了
	GAMEOVER = -1
	# シグナル:ゲームリセット
	RESTART = 1
	
	def initialize
		@timer = Timer.new
		@game = Game.new(@timer)
		@game.startGame
		@drawer = Drawer.new(@game, @timer)
		@keyCommand = {K_RETURN => @game.method(:bet),
                               K_R => Controller.method(:restart),
                               K_ESCAPE => Controller.method(:gameover)}
		Input.setKeyRepeat( K_LCONTROL, 60, 1 )
	end
	
	# コントローラ更新(描写・ゲーム更新・タイマーカウント)
	# サブコントローラがある場合、描写・サブコントローラ更新・タイマーカウントを行う
	def update
		signal = NO_COMMAND
		@timer.update unless @child.kind_of?(TimerStopController)
		if @child
			result = {signal: NO_COMMAND, child: @child}.merge(@child.update)
			@child = result[:child]
			signal = result[:signal]
		else
		  @keyCommand.each_key {|key|
			  if Input.keyPush?(key)
			  	signal = execute(key)
			  	break
			  end
		  }
		end
		result = {child: @child}.merge(@drawer.draw(@game, @child))
		signal = execute(result[:command]) if result[:command]
		@child = result[:child] if result[:child]
		return signal
	end
	
	# キーが押された、あるいはビューからコマンドがあった場合、ゲーム操作メソッドを実行する
	def execute(key)
		result = {signal: NO_COMMAND, command: nil, child: nil}.merge(@keyCommand[key].call)
		@keyCommand[key] = result[:command] if result[:command]
		@child = result[:child]
		return result[:signal]
	end
	
	# ゲームを終了させる
	def self.gameover
		return {signal: GAMEOVER}
	end
	
	# ゲームをリセットする
	def self.restart
		return {signal: RESTART}
	end
end

ゲームのループ内で回し続けるのがupdateメソッド、キーが押された時に実行されるのがexecuteメソッドです。押されたキーと実行するメソッドの対応をkeyCommandフィールドで保持しています。
また、Modelからだけでなく、ViewであるDrawerからもCommandを受け取れるようになっています。これは「Drawerの方でアニメーションを描ききったら、次の処理に自動的に映る」という操作と「アニメーション終わるまで待つの面倒だから、Returnキー押してスキップする」という操作を一つの流れで行えるようにと意図したものです。
実際に、ゲーム内でNPCのベットはキーを押さなくてもポンポン進んでいきます。らくちん!!

Test::Unitによるテストを用意した

class TC_Scorer < Test::Unit::TestCase
	include Scorer
  
	def testHand
		assert_equal(Hand::STRAIGHT_FLUSH, getHand(getCards("SA,SK,SQ,SJ,ST"))[0])
		assert_equal(Hand::FOUR_CARDS, getHand(getCards("SA,CA,DA,HA,SK"))[0])
		assert_equal(Hand::FULL_HOUSE, getHand(getCards("SA,CA,DA,SK,CK"))[0])
		assert_equal(Hand::FLUSH, getHand(getCards("SA,SK,SQ,SJ,S9"))[0])
		assert_equal(Hand::STRAIGHT, getHand(getCards("SA,SK,SQ,SJ,CT"))[0])
		assert_equal(Hand::THREE_CARDS, getHand(getCards("SA,CA,DA,SK,SQ"))[0])
		assert_equal(Hand::TWO_PAIR, getHand(getCards("SA,CA,SK,CK,SQ"))[0])
		assert_equal(Hand::ONE_PAIR, getHand(getCards("SA,CA,SK,SQ,SJ"))[0])
		assert_equal(Hand::NO_PAIR, getHand(getCards("SA,SK,SQ,SJ,C9"))[0])
	end

先日Rspecでテストしよう!的な記事がありましたが、それに先駆けてTest::Unitでのテストを試しておきました。テキサス・ホールデムの中でも複雑で、かつ独立させやすい以下の2点についてテストしています。

  • 手役(特定の7枚からスーテッドを抽出する、ペアを抽出する、役を決定する など)
  • ポッド(誰かがオールインした場合の賞金の分配が正しく行われているか)

これからテストを増やしていくとしたら、AIの部分に取り組みたいです。このAIクッッッッッソ弱いねん……。

強化したい点

オンライン対戦

ここ数日はACに向けてオンライン化を考えており、「ゲーム本体はできてるんだから楽勝でしょ楽勝!」とか恐れ多くも考えていたわけですが、案の定無理でした!!
「対戦ルームを作る」「対戦ルームに参加する」「クライアントの入力を受け付ける」「サーバからクライアントへ情報を投げつける」「ショーダウン後に各プレイヤーの入力を待って同期を図る」など、当初の予想よりボリュームが増大しそうだったので、今回は断念しました。来年までにはできているといいですね。

アニメーション

「アニメがヌルヌル動くゲームを作りたい!!」が私のゲーム作りの原動力なので、アニメーションできるように頑張ります。さしあたってはSpliteともっと友達になりたい。

ということで

拙作うのはなポーカー、お時間に余裕のある方はぜひお試しください。


明日は(というかもう今日だけど)fourxzさんの「DXRuby、IntelliJで入力補完の巻」です。おたのしみに!