paloma blog

NWエンジニアやってます。主に自宅環境のお遊びを書きます。Pythonもちょっと。タイトルは好きなカクテルから。

スリーカードポーカーゲームを作りたい やっとゲームになった編

今月ずっと取り掛かっているスリーカードポーカーゲームですが、やっとゲームっぽいものを完成させることができました。

コード

前回コミットしてからこれだけ追加しました。
長いのでここに乗せるべきではないかもしれませんが備忘として。

小出しにするのもなんだと思い今回の記事まで一気に作成しました。
エラー処理はちょっと甘いです。

--- a/pokerapp.py
+++ b/pokerapp.py
@@ -1,7 +1,5 @@
-
 import random
 
-
 class Deck:
 
    def __init__(self):
@@ -25,7 +23,6 @@ class Deck:
    def shuffle(self):
        random.shuffle(self.deck)
 
-
    def draw(self):
        return self.deck.pop(0)
 
@@ -46,6 +43,37 @@ class Handcheck:
    def result(self):
        return list(self.player[0].values()), self.player[1]
 
+class Payoff:
+
+   # Use "in" to hand return as a list
+
+   def __init__(self, bet, hand):
+       self.bet = bet
+       self.hand = hand
+
+   def anti_bonus(self):
+       if 4 in self.hand:
+           return self.bet * 1
+       elif 5 in self.hand: 
+           return self.bet * 4
+       elif 6 in self.hand:
+           return self.bet * 5
+       else:
+           return 0
+
+   def pairplus_bonus(self):
+       if 2 in self.hand:
+           return self.bet * 1
+       elif 3 in self.hand:
+           return self.bet * 4
+       elif 4 in self.hand:
+           return self.bet * 6
+       elif 5 in self.hand: 
+           return self.bet * 30
+       elif 6 in self.hand: 
+           return self.bet * 40
+       else:
+           return 0
 
 def Handout(card):
    hand = []
@@ -59,7 +87,7 @@ def Judge(hands, dealer=False):
 
    # Devide hands into suits and numbers
    suit = [ hands[0][0] , hands[1][0] , hands[2][0] ]
-   rank = [int(x[1:]) for x in hands]
+   rank = [ int(x[1:]) for x in hands ]
 
    # Sort of numbers for role evaluation
    rank.sort()
@@ -88,21 +116,23 @@ def Judge(hands, dealer=False):
 def Match(p1, p2):
 
    if p1[0] > p2[0]:
-       print('Player WIN!')
+       return 0
    elif p1[0] < p2[0]:
-       print('Dealer WIN!')
+       return 1
    # Compare number in case draw rank hand
    elif p1[0] == p2[0]:
        if p1[1] > p2[1]:
-           print('Player WIN!')
+           return 0
        elif p1[1] < p2[1]:
-           print('Dealer WIN!')
+           return 1
        # Player wins in case of draw
        else:
-           print('Player WIN!')
+           return 0
 
 def Shaping(hand):
+
    h = []
+   
    for i in hand:
        if '11' in i:
            h.append(i.replace('11', 'J'))
@@ -117,9 +147,51 @@ def Shaping(hand):
    return h
 
 # main
-def main():
+def main(chip):
+
+   print('Your chips are {}'.format(chip))
 
    # Ante
+   while True:
+       i = input('Do you want to bet ante?: [y/n]')
+       if i == 'y':
+           ante = input('How much do you bet?: ')
+           try:
+               ante = int(ante)
+               bet_ante = ante
+
+           except:
+               pass
+
+           break 
+
+       elif i == 'n':
+           bet_ante = 0
+           break 
+
+       else:
+           pass
+
+   # Pair plus
+   while True:
+       i = input('Do you want to bet Pair plus?: [y/n]')
+       if i == 'y':
+           pp = input('How much do you bet?: ')
+           try:
+               pp = int(pp)
+               bet_pairplus =  pp
+
+           except:
+               pass
+
+           break
+
+       elif i == 'n':
+           bet_pairplus = 0
+           break
+
+       else:
+           pass
 
    # Game start
    card = Deck()
@@ -137,15 +209,96 @@ def main():
    p_show = Shaping(player)
    d_show = Shaping(dealer)
 
-   print("Player Hand: {} {}\nDealer Hand: {} {}\n".format( \
-       p_show, p_role[0], \
-       d_show, d_role[0] ))
-
-   Match(p_hand, d_hand)
+   print("\n===== Open Your Hand =====\n")
+   print("Your Hand: {} {}\n".format( p_show, p_role[0] ))
 
    # bet, fold
 
+   print('The same chips as Ante is required to match the dealer.')
+   while True:
+
+       if bet_ante == 0 and bet_pairplus == 0:
+           refund_bet = 0
+           refund_ante = 0
+           break
+
+       else:
+
+           i = input('Do you play in your hand?: [y/n]')
+
+           # Open dealer hand
+
+           print("\n===== Open Dealer Hand =====\n")
+           print("Dealer Hand: {} {}\n".format( d_show, d_role[0] ))
+
+           if i == 'y':
+               try:
+                   # b = int(b)
+                   bet_play = bet_ante
+
+                   # Win / Lose Judge
+                   
+                   match = Match(p_hand, d_hand)
+                   if match == 0:
+                       print('You WIN!')
+
+                       refund_bet = bet_play * 2
+                       refund_ante = bet_play * 2
+
+                   elif match == 1:
+                       print('Dealer WIN!')
+
+                       # Forfelt the bet
+                       refund_bet = 0
+
+                       # Forfelt the ante ig there is no bonus
+                       # Make int type with angle brakets to use inequality sign
+                       if p_hand[0][0] < 3:
+                           refund_ante = 0
+                       else:
+                           pass
+
+                       chip = chip - (bet_play + bet_ante + bet_pairplus)
+
+               except:
+                   pass
+
+           elif i == 'n':
+               print('You are fold.')
+
+               # bet confiscation
+               chip = chip - (bet_ante + bet_pairplus)
+
+               refund_bet = 0
+               refund_ante = 0
+
+           break
+
    # pay off
+   # Win refund
+   all_refund = refund_ante + refund_bet
+
+   # Hand bonus
+   pay_ante = Payoff(bet_ante, p_hand[0]).anti_bonus()
+   pay_pairplus = Payoff(bet_pairplus, p_hand[0]).pairplus_bonus()
+
+   # To reuse the chips for main loop
+   global total
+
+   total = chip + (all_refund + pay_ante + pay_pairplus)
+
+   print('Your chips are {}'.format(total))
 
 if __name__ == '__main__':
-   main()
+
+   # Initial chips
+   chip = 1000
+
+   while True:
+       main(chip)
+       i = input('Continue?: [y/n]')
+       if i == 'y':
+           chip = total
+       else:
+           break
+

以前の整形からベット処理を丸々追加したのでコードが倍くらいになってしまいました。
ゲームとして作成するとどうしても対話型になるので分岐処理がややこしいですね。

簡単な解説

すべての解説はしきれないのですが、
追記したところは

  • アンティ、ペアプラスベットの選択
  • カードドロー # 作成済み
  • 手役判定 # 作成済み
  • 手札勝負ベット (アンティと同額)
  • 払い戻し

です。

アンティ、ペアプラスは金額を賭けるまでループさせるようにしました。
払い戻しは前回の通り手役判定のリターンからベット額をn倍するだけです。

各ベット、勝負、払い戻しはすべてメイン処理に書いてしまいました。
アンティ、ペアプラスベット時のループも関数にできそうですが、
とりあえず早く動くものを作りたかったので今回はメイン内にべた書きで仕上げてしまいました。
(当初は手札引いて判定するところまでしか構想になかったので関数化が間に合いませんでした。)

ゲームプレイはこんな感じ

ゲーム開始時に1000$もらえます。(単位つけ忘れました)
アプリを終了させるまでゲーム終了時のチップを持ち越すことができます。

$ python3 pokerapp.py 
Your chips are 1000
Do you want to bet ante?: [y/n]y
How much do you bet?: 10
Do you want to bet Pair plus?: [y/n]y
How much do you bet?: 10

===== Open Your Hand =====

Your Hand: ['♢2', '♡10', '♡3'] ['High card!']

The same chips as Ante is required to match the dealer.
Do you play in your hand?: [y/n]y

===== Open Dealer Hand =====

Dealer Hand: ['♠6', '♣K', '♡J'] ['High card!']

Dealer WIN!
Your chips are 970
Continue?: [y/n]y
Your chips are 970
Do you want to bet ante?: [y/n]y
How much do you bet?: 100
Do you want to bet Pair plus?: [y/n]y
How much do you bet?: 100

===== Open Your Hand =====

Your Hand: ['♢7', '♣A', '♢A'] ['One Pair!']

The same chips as Ante is required to match the dealer.
Do you play in your hand?: [y/n]y

===== Open Dealer Hand =====

Dealer Hand: ['♡9', '♡6', '♠9'] ['One Pair!']

You WIN!
Your chips are 1470
Continue?: [y/n]y
Your chips are 1470
Do you want to bet ante?: [y/n]y
How much do you bet?: 100
Do you want to bet Pair plus?: [y/n]y
How much do you bet?: 100

===== Open Your Hand =====

Your Hand: ['♡5', '♣J', '♡A'] ['High card!']

The same chips as Ante is required to match the dealer.
Do you play in your hand?: [y/n]y

===== Open Dealer Hand =====

Dealer Hand: ['♢8', '♢3', '♠7'] ["Less than Queen-high. Dealer can't play"]

You WIN!
Your chips are 1870
Continue?: [y/n]n

対話型になってるし、終了させるまで続けられるのでゲームになってますよね!
いい感じじゃないでしょうか。
英文の正しい文法等はいったん置いておきます。

何もしない判定

私しかプレイしないので、アンティもペアプラスも賭けないというパターンは発生しませんが、
どちらも賭けない場合の処理も作っておきます。
ゲームとして成立しないのですが、何もせず次のゲームに行くという処理にします。

アンティもペアプラスも賭けないと次のゲームに移ります。
余計な文も出てるしもう少しいい処理にしたかったのですが、今回は断念してエラーを無くすことに集中しました。

$ python3 pokerapp.py 
Your chips are 1000
Do you want to bet ante?: [y/n]n
Do you want to bet Pair plus?: [y/n]n

===== Open Your Hand =====

Your Hand: ['♣8', '♡2', '♠6'] ['High card!']

The same chips as Ante is required to match the dealer.
Your chips are 1000
Continue?: [y/n]n

むすび

テキストオンリーではありますが、初めてゲームと呼べるものを作れたと思います。
一から自分で作ると動いたときに感慨深いものがありますね。

ゲームだと似た処理の繰り返しになるので、自作クラス・関数の有効性も理解が進みました。

今月は丸々これの作成に費やしてしまいましたが、プログラミングについて色々勉強にもなりました。
ファイル構成・書き方の作法等はまだまだですが、クラス系は割と使えるようになったんじゃないかな。

これをリファクタリングしつつ、シミュレータの作成、テストコードの作成などやっていきたいと思います。

直したいところ
  • チップ不足時の終了処理
    • これは早めに
  • 役判定時に数字を出力する
    • 「○○のワンぺア」的な
  • メイン処理内を関数化する
  • 払い戻しのレートを出力する
    • 初めてやる人用
  • クラスとかは別ファイル出し?
  • 英語の文法を直す

タスクがどんどん増えていきますねえ。


本シリーズ

リポジトリ

github.com

スリーカードポーカーゲームを作りたい 払い戻し機能を作る編

作成中のスリーカードポーカーゲームですがゲームのメインであるベット、払い出しの作成が残っています。

賭け系の処理は

  • 賭ける
  • 勝負
  • 払い戻し

の順となりますが、ゲームを続ける限りチップの増減が発生します。
実装するにはこれらの流れをループさせないと行けません。

自分のレベルだと気が遠くなりそうな処理が残ってますが、遠くなるのはまだ早いです。
出来るところからやりましょう。

という訳で今回は払い戻し機能を作ります。

スリーカードポーカーの払い戻し

スリーカードポーカーは3つの払い戻しがあって

  • 手札で勝つ
    • アンティを賭けないと勝負できない
    • 払い戻しはベット額の2倍
  • アンティボーナス
    • 手札役ボーナス
    • 手札で負けてももらえる
  • アプラスボーナス
    • アンティとは別にベットする
    • 手札役ボーナス
    • 手札で負けてももらえる

があります。

細かい配当

  • アンティボーナス
    • ストレート 1:1
    • スリーカード 5:1
    • ストレートフラッシュ 6:1
  • アプラスボーナス
    • ワンペア 1:1
    • フラッシュ 4:1
    • ストレート 6:1
    • スリーカード 30:1
    • ストレートフラッシュ 40:1

カジノによってレートが違うそうですが、GTAオンラインのレートを採用しています。

作成済みの役判定の戻り値が使えそうなので、今回は試しにペアプラスボーナスを作ってみます。
チップを賭ける部分は作ってないですが、役ごとにn倍でいいですね。

手札判定の戻り値

手札判定後は役名と役ランク値が辞書型で返ってきます。
この役ランク値を流用しましょう。 強い役順に6~1のランクが返ってくるのでこの値を払い戻しの役判定に使います。

  • 役判定の関数
def Judge(hands, dealer=False):

    # Devide hands into suits and numbers
    suit = [ hands[0][0] , hands[1][0] , hands[2][0] ]
    rank = [int(x[1:]) for x in hands]

    # Sort of numbers for role evaluation
    rank.sort()

    # Evaluation of hand
    if len(set(suit)) == 1 and rank[1] == rank[0] + 1 and rank[2] == rank[1] + 1:
        return {'Straight Flash': 6}, max(rank)
    elif len(set(suit)) == 1 and rank[0] == 14 and rank[1] == 2 and rank[2] == 1:
        return {'Straight Flash': 6}, max(rank)
    elif len(set(rank)) == 1:
        return {'Three of a kind!': 5}, max(rank)
    elif rank[1] == rank[0] + 1 and rank[2] == rank[1] + 1:
        return {'Straight!': 4}, max(rank)
    elif rank[0] == 14 and rank[1] == 2 and rank[2] == 1:
        return {'Straight!': 4}, max(rank)
    elif len(set(suit)) == 1:
        return {'Flash!': 3}, max(rank)
    elif len(set(rank)) == 2:
        return {'One Pair!': 2}, max(rank)
    elif max(rank) < 12 and dealer == True:
        return {'Less than Queen-high. Dealer can\'t play': 0}, max(rank)
    elif max(rank):
        return {'High card!': 1}, max(rank)
        

アプラスの払い戻しをコードにするとこんな感じです。
handrankの部分にJudgeで帰ってきた役ランク値を入れます。

def Pair_plus(handrank, chip):
     if 2 in handrank:
         return chip * 1
     elif 3 in handrank:
         return chip * 4
     elif 4 in handrank:
         return chip * 6
     elif 5 in handrank:
         return chip * 30
     elif 6 in handrank:
         return chip * 40
     else:
         return None

動かしてみる

肝心のチップを賭ける処理が作れていないので、試しにこの部分だけターミナルで動かしてみます。
本当はpython使いとしてpdbとか使って動かしてみるのがいいんでしょうけど難しいので手動でやります。

>>> import pokerapp
>>> 
>>> d = pokerapp.Deck()
>>> d.shuffle()
>>> 
>>> 
>>> player = pokerapp.Handout(d)
>>> player
['♢7', '♢13', '♢2']
>>> 
>>> h = pokerapp.Judge(player)
>>> h
({'Flash!': 3}, 13)
>>> 
>>> def Pair_plus(handrank, chip):
...     if 2 in handrank:
...         return chip * 1
...     elif 3 in handrank:
...         return chip * 4
...     elif 4 in handrank:
...         return chip * 6
...     elif 5 in handrank:
...         return chip * 30
...     elif 6 in handrank:
...         return chip * 40
...     else:
...         return None
... 
>>> 
>>> Pair_plus(h[0].values(),100)
400

手札はダイヤのフラッシュです。
チップを100賭けていたとして、ちゃんと4倍の400が返ってきますね。

ここで思ったのですが、アンティボーナスも同じ仕組みなので
払い戻しは同じ機能を持たせる様にクラス化しちゃった方が良さそうですね。
(クラスを作るってのはこういう事か…?)

払い戻しの疑似コード

クラス化はさっき思い付いたばかりなのでとりあえず疑似コードで残しておきます。

class payoff (bet, hand)

    def anti

      if hand == straight 
        bet * 1
      elif hand == three of a kind 
        bet * 4
      elif hand == straignt flash 
        bet * 5
      else 
        none

    def pairplus

      if hand == pair 
        bet * 1
      elif hand == flush 
        bet * 4
      elif hand == straight 
        bet * 6
      elif hand == three of a kind 
        bet * 30
      elif hand == straight flush 
        bet * 40
      else
        none

    def check
      bet + anti + pairplus

これでいけそうな気がします。
あとはゲーム開始~終了までの賭け金処理をどうするかですね。

スリーカードポーカーのややこしいところは勝負に負けても役ボーナスで配当が発生するところなんですよね。
ベット額、アンティ、ペアプラスのタプルとかにしてそれぞれの値を入れ換えていくのがいいのかな?
そして勝負後に各払い戻し分を足して返ってくると。

これが出来てから一連の処理をループさせればゲームになりそうですね。
先がちょっと見えてきました。


本シリーズ

スリーカードポーカーゲームを作りたい 手札を整形する編

着手し始めたスリーカードポーカーゲームですが、今の出力だとまだポーカーの体を成していません。

スリーカードポーカーゲームを作りたい なんとか動く物作れたよ編 - paloma blog

手札の勝敗判定の関係でエースが14、絵柄がそのまま数字になっちゃってます。

masashi@DESKTOP-986MNSO:~/game$ python3 pokerapp.py
Player Hand: ['D9', 'S14', 'C4'] ['High card!']
Dealer Hand: ['H3', 'C2', 'H10'] ["Dealer can't play"]

Player WIN!
masashi@DESKTOP-986MNSO:~/game$ python3 pokerapp.py
Player Hand: ['D3', 'C14', 'C11'] ['High card!']
Dealer Hand: ['H13', 'S8', 'H2'] ['High card!']

Player WIN!
masashi@DESKTOP-986MNSO:~/game$ python3 pokerapp.py
Player Hand: ['H14', 'C13', 'S6'] ['High card!']
Dealer Hand: ['H9', 'D8', 'C10'] ['Straight!']

Dealer WIN!

これでは人様に遊んでいただくときに混乱させてしまいます。
(もう公開しちゃったけど)

そんなわけで今回は手札を整形します。

やること

  • スートの整形
  • エース、絵札の整形

処理の整理

ポーカー系プログラムの流れは大まかに

  1. デッキ作成
  2. ドロー
  3. 手札評価
  4. 勝負

となっていますが、私のプログラムでは数字をそのまま③のランク判定に使用していますので③終了後に変換されるほうが良さそうです。

スートの整形

私のデッキ作成ですが、スートは

self.suit = ['S', 'H', 'D', 'C']

で表現してます。
それぞれの頭文字です。

これをこのまま記号で書き直したら上手いこと動きました。
フラッシュ系のスート判定も問題ありませんでした。

self.suit = ['♠', '♡', '♢', '♣']

スートのUnicodeに対応していないターミナルだとうまく動かないかもです。
(今どきあるか不明ですが)

エース、絵札の整形

デッキ作成してから数字のままの11、12、13、14をJ、Q、K、Aに変換します。

数字はすべての役の判定に使われるので評価処理の前では変えたくありません。
どうせ最後に出力される見た目上の問題なだけなので、大人しく手札オープン時に変更することにしました。

置換は簡単に組み込みのreplaceを使います。

手札はリストで帰ってきますが、リストだと中身を置換できないので一度ループで展開してから別リストに入れなおしました。
下記の関数を作って手札オープン時に組み込んでます。

def Shaping(hand):
    h = []
    for i in hand:
        if '11' in i:
            h.append(i.replace('11', 'J'))
        elif '12' in i:
            h.append(i.replace('12', 'Q'))
        elif '13' in i:
            h.append(i.replace('13', 'K'))
        elif '14' in i:
            h.append(i.replace('14', 'A'))
        else:
            h.append(i)
    return h

差分

差分だけ出してもイマイチかもしれませんが、こんな感じです。
個人的にはgit diffの練習です。

--- a/pokerapp.py
+++ b/pokerapp.py
@@ -10,7 +10,7 @@ class Deck:
 
    def build(self):
        self.value = [x for x in range(2,15)] # Ace is 14
-       self.suit = ['S', 'H', 'D', 'C']
+       self.suit = ['', '', '', '']
 
        for s in self.suit:
            for v in self.value:
@@ -101,6 +101,21 @@ def Match(p1, p2):
        else:
            print('Player WIN!')
 
+def Shaping(hand):
+   h = []
+   for i in hand:
+       if '11' in i:
+           h.append(i.replace('11', 'J'))
+       elif '12' in i:
+           h.append(i.replace('12', 'Q'))
+       elif '13' in i:
+           h.append(i.replace('13', 'K'))
+       elif '14' in i:
+           h.append(i.replace('14', 'A'))
+       else:
+           h.append(i)
+   return h
+
 # main
 def main():
 
@@ -119,9 +134,12 @@ def main():
    p_role = Handcheck(player).get_role()
    d_role = Handcheck(dealer, flag=True).get_role()
 
+   p_show = Shaping(player)
+   d_show = Shaping(dealer)
+
    print("Player Hand: {} {}\nDealer Hand: {} {}\n".format( \
-       player, p_role[0], \
-       dealer, d_role[0] ))
+       p_show, p_role[0], \
+       d_show, d_role[0] ))
 
    Match(p_hand, d_hand)
 

動作

動かすとこんな感じ。

masashi@DESKTOP-986MNSO:~/game$ python3 pokerapp.py
Player Hand: ['♠4', '♠3', '♠Q'] ['Flash!']
Dealer Hand: ['♣7', '♢4', '♣10'] ["Less than Queen-high. Dealer can't play"]

Player WIN!
masashi@DESKTOP-986MNSO:~/game$ python3 pokerapp.py
Player Hand: ['♣Q', '♢5', '♠J'] ['High card!']
Dealer Hand: ['♡4', '♡2', '♢K'] ['High card!']

Dealer WIN!
masashi@DESKTOP-986MNSO:~/game$ python3 pokerapp.py
Player Hand: ['♠9', '♣A', '♠A'] ['One Pair!']
Dealer Hand: ['♡5', '♣8', '♣Q'] ['High card!']

Player WIN!

もうトランプのゲームと分かりますね。
手札と役がリストで出ちゃうのは直したいです。


というわけで簡単な修正ですが、見た目はポーカーっぽくなりました。
整形により手札判定の処理に跳ねるかと思ってましたが、意外とすんなり出来てよかったです。
しかし本命であるBET処理が残ってます。
完成はいつになることやら…。

リポジトリはこちら

github.com

スリーカードポーカーゲームを作りたい なんとか動く物作れたよ編

ここ数年はGWはキャンプに行っていたのですが、今年の予定地が休場になってしまい家で過ごしました。

家にいるなら勉強しなきゃってことで、考えていたポーカーゲームがなんとか動くものになりましたのでおさらいをします。

ルール

  • デッキはジョーカーなしの52枚
  • 3枚のカードでポーカーを行う
  • 勝負はディーラーと1対1
  • カードの交換はできない
  • 手札を見て勝負か降りるかを決める
    • 今回は実装していません
  • ディーラーはQ以上のハイカードじゃないと勝負できない
    • 強制的にプレーヤーの勝ち
  • 勝敗とは別の配当(ペアプラス)があります
    • 今回は実装していません

手役

3枚なので少ないです。 通常のポーカーと異なり、ストレートのほうがフラッシュより強いです。

  1. ストレートフラッシュ
  2. スリーカード
  3. ストレート
  4. フラッシュ
  5. ワンペア
  6. イカード

の順となります。
同役なら数字が高いほうが勝ちです。

ゲーム作成の方針

  • GW中に動くものを作成する
  • 最低限ゲームの勝敗判別ができること

というわけで最小機能で何とか間に合いました。

環境

メイン機のWSL上で動かしてます。

masashi@DESKTOP-986MNSO:~/game$ lsb_release -a ;uname -a ; python3 --version
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.4 LTS
Release:        16.04
Codename:       xenial
Linux DESKTOP-986MNSO 4.4.0-18362-Microsoft #476-Microsoft Fri Nov 01 16:53:00 PST 2019 x86_64 x86_64 x86_64 GNU/Linux
Python 3.5.2

ゲーム画像

画像というかコンソールでプレイしますのでログになりますが…

$ python3 pokerapp.py

上記ファイルを実行すると、カードが配られプレーヤーとディーラーが勝手に勝負します。
掛け金、ベット、フォールドの選択はできませんw

こんな感じです。

masashi@DESKTOP-986MNSO:~/game$ python3 pokerapp.py
Player Hand: ['D12', 'C2', 'D10'] ['High card!']
Dealer Hand: ['C7', 'C13', 'H5'] ['High card!']

Dealer WIN!
masashi@DESKTOP-986MNSO:~/game$
masashi@DESKTOP-986MNSO:~/game$ python3 pokerapp.py
Player Hand: ['S3', 'H4', 'D13'] ['High card!']
Dealer Hand: ['D10', 'D4', 'H6'] ["Dealer can't play"]

Player WIN!
masashi@DESKTOP-986MNSO:~/game$
masashi@DESKTOP-986MNSO:~/game$ python3 pokerapp.py
Player Hand: ['H10', 'H2', 'D14'] ['High card!']
Dealer Hand: ['H6', 'H3', 'S3'] ['One Pair!']

Dealer WIN!

選択の余地が全くないのでゲームというかシミュレーターっぽくなってしまいましたね。

デッキ作成、シャッフル

トランプゲームのデッキ作成、シャッフルの記事はたくさんありますが、
コピペだけだと面白くないので独自に作ってみました。

各13枚 * 4種類を掛ければ1組作れますね。

ポーカーではエースが一番強いのですが、シンプルに14番に割り振りました。
ストレートの判別が簡単になります。
スートも記号で表現したかったのですが、とりあえず今回は諦めます。

class Deck:

    def __init__(self):
        self.deck = []
        self.build()

    def build(self):
        self.value = [x for x in range(2,15)] # Ace is 14
        self.suit = ['S', 'H', 'D', 'C']

        for s in self.suit:
            for v in self.value:
                self.deck.append(''.join([s, str(v)]))

        return self

    def show(self):
        for c in self.deck:
            print(c)

    def shuffle(self):
        random.shuffle(self.deck)


    def draw(self):
        return self.deck.pop(0)

本件で初めて自作クラスを作成しました。
作ってみて何となく慣れてきましたが、難しいですね。

勉強と思って作りましたが関数だけでも作れたかもしれません。
(まだクラス作成のメリットがあんまり…)

動かしてみるとこんな感じ。

>>> import pokerapp
>>>
>>> card = pokerapp.Deck()
>>> card.show()
S2
S3
S4
S5

…

シャッフル。ちゃんと混ざります。

>>> card.shuffle()
>>> card.show()
D4
C13
H14
C5
S6

…

シャッフル後のデッキのリストを頭から引いていきます。

カードを引く関数は別で作成しました。

def Handout(card):
    hand = []
    for i in range(3):
        hand.append(card.draw())

    return hand

手役判定

本当はビット演算とかして判定したかったのですが、
3枚しかないので大人しくバラして判定することにしました。

対戦型なので、役にランク値を持たせて上下を決める流れになります。 役判定の1つ目の戻り値から値だけ抜いて、判定のためにリスト化してます。

valueを抜いただけだと辞書型で返ってしまうので上下の判定ができません。
リスト型は万能ですねえ。

ディーラーはQハイカードより下の役は負けになるので、そのフラグ建てが苦労しました。

  • 手札チェック

同役の場合はカードの数での勝負になるので戻り値がハンドランクと手役の最大値の2つを返しています。

class Handcheck:

    def __init__(self, hand, flag=False):
        self.hand = hand
        self.flag = flag

        if self.flag == True:
            self.player = Judge(self.hand, dealer=True)
        else:
            self.player = Judge(self.hand)

    def get_role(self):
        return list(self.player[0].keys()), self.player[1]

    def result(self):
        return list(self.player[0].values()), self.player[1]
  • 役判定

手札の値をバラしてから役の判定をしています。
1つの関数で完結できました。
3枚で行うゲームだからできる技ですね。

ストレートの処理が大変そうだと思ってましたが、ソートすれば簡単ですね。

def Judge(hands, dealer=False):

    # Devide hands into suits and numbers
    suit = [ hands[0][0] , hands[1][0] , hands[2][0] ]
    rank = [int(x[1:]) for x in hands]

    # Sort of numbers for role evaluation
    rank.sort()

    # Evaluation of hand
    if len(set(suit)) == 1 and rank[1] == rank[0] + 1 and rank[2] == rank[1] + 1:
        return {'Straight Flash': 6}, max(rank)
    elif len(set(rank)) == 1:
        return {'Three of a kind!': 5}, max(rank)
    elif rank[1] == rank[0] + 1 and rank[2] == rank[1] + 1:
        return {'Straight!': 4}, max(rank)
    elif len(set(suit)) == 1:
        return {'Flash!': 3}, max(rank)
    elif len(set(rank)) == 2:
        return {'One Pair!': 2}, max(rank)
    elif max(rank) < 12 and dealer == True:
        return {'Dealer can\'t play': 0}, max(rank)
    elif max(rank):
        return {'High card!': 1}, max(rank)

勝負判定

戻り値の値から上下関係を決めるだけです。 同役同士の引き分けはプレイヤーの勝ちにしました。 GTAオンライン内のルールに則って作成しています。公式ルールはどうかわかりません。

def Match(p1, p2):

    if p1[0] > p2[0]:
        print('Player WIN!')
    elif p1[0] < p2[0]:
        print('Dealer WIN!')
    # Compare number in case draw rank hand
    elif p1[0] == p2[0]:
        if p1[1] > p2[1]:
            print('Player WIN!')
        elif p1[1] < p2[1]:
            print('Dealer WIN!')
        # Player wins in case of draw
        else:
            print('Player WIN!')

メイン処理

ポーカーゲームの流れに沿って関数を呼び出すだけです。
処理が簡潔に書ける分、クラスや関数を作るメリットはありますね。

チップを掛ける処理を作っていないので、今時点ではカードを引くだけのゲームになってます…。

def main():

    # Ante

    # Game start
    card = Deck()
    card.shuffle()

    player = Handout(card)
    dealer = Handout(card)

    p_hand = Handcheck(player).result()
    d_hand = Handcheck(dealer, flag=True).result()

    p_role = Handcheck(player).get_role()
    d_role = Handcheck(dealer, flag=True).get_role()

    print("Player Hand: {} {}\nDealer Hand: {} {}\n".format( \
        player, p_role[0], \
        dealer, d_role[0] ))

    Match(p_hand, d_hand)

    # bet, fold

    # pay off

今後の展開

一回手を付けたからにはもう少しゲームらしくしたいですよね。 というわけでタスクです。

  • 手札の清書
    • スートを記号(♠,♥,♦,♣)に変換する
    • エース、絵柄を文字(A,K,Q,J)に変換する
  • チップの処理追加
    • アンティ(場代)
    • アプラス
    • ベット or フォールド
    • 払い戻し
  • シミュレータの作成
    • 出やすい役の統計、勝率とか
    • 作れたらGTAオンラインで実践します
  • テストコード作成
    • 今回は自分で何回も動かしました
  • GUIゲーム化

あとは適宜コードのリファクタリングですね。
どう書くのがいいコードなのかはまだまだ経験積まないとわからないです。

完走した感想

いや、完走はしてないのですがとりあえずポーカーゲームっぽい動くものが作れました。

これにかかったのは30時間くらいですかね。
GWの4~6まで3日まるまる費やすつもりでしましたが、ゲームやったりお酒飲んだりで思ったより手を付けられませんでした。
家には誘惑がいっぱいです。

関連書籍を読みだけはしていた自作クラスにも初挑戦でしたが、どこの定義がどの変数に当たってるのか初めのうちは苦労しました。 自作クラス作れるとステップアップした気がしますねw

でも関数とクラスの使い分けもまだよくわからないのでこの子を育てて身に着けていこうと思います。

本件のリポジトリです。

github.com

参考サイト

スリーカードポーカーゲームを作りたい 思考編

プログラミングのステップの1つでゲームを作ってみようというのがありますよね。
私も本業はインフラエンジニアの身ですが、 pythonは2年ほど触っていますのでそろそろこちらにも挑戦しようと思います。

これまで一枚岩のコードはいくつか書いてきましたが、
自身初となる自作関数、インタラクティブ処理の練習にもなりますからね。

何を作るか

入門かどうかは置いておいて「python ゲーム」とかで検索すると以下が多く出てきます。

この中で以下の条件で進められるように考えます。

  • CLIから始められるもの
    • コンソールで理解できるトランプ系かテキスト系になりますね
  • ルールが簡単なもの
    • これは単純に複雑さがコーディング量に比例します(と思います)

そんなわけで私の挑戦するゲームを決めました。
スリーカードポーカーです!

スリーカードポーカーとは

ルールは普通のポーカーと同じように52枚のカードから手役を構築しますが、

  • 3枚のカードでポーカーを行う
  • 対戦はディーラーと1対1

というものになります。

スリーカードポーカーにした理由ですが、

  • 誰も作成していない
    • 需要が無いからかも
  • 5枚ポーカーよりは作成が簡単そう
    • 手役のバリエーションは当然少ないです
  • GTAオンラインでハマっている

からです。

もともとはGTAオンラインのミニゲームで出てきて興味を持ったんですが、
検索しても公式ルールとかが出てこないんですよね。

オンラインカジノ系のサイトではちゃんとルール記載があってプレイできるようなので、
ポーカーの一種として存在はしているのでしょうが謎ですね。

とりあえずこちらの作成を目標にしたいと思います。

ブラックジャックは意外と作成記が出てくるし、
ホールデムやビデオポーカーの類のものは処理がすごく大変そうなので
スリーカードポーカーを初のゲームに選定しました。

GW中の課題としてある程度のものはできたらいいですが…
役判定のところが鬼門そうですね。

ちなみにGTAオンライン

f:id:paloma69:20200430222336p:plain

GTAオンラインのスリーカードポーカーはカジノアップデートからできるようになりました。
ポーカーの醍醐味である他プレイヤーとの駆け引きの要素は無いのですが、シンプルながらなかなかどうしてハマります。

一人称視点だと本当にカジノでやっている気分が味わえます。

f:id:paloma69:20200430222723p:plain

GTAオンラインでやればいいじゃんて話ですが、ゲーム開始時のロードがすごく長いのでお手軽に遊べる環境が欲しいのも作成理由の1つです。

GTAオンラインマネーのキャッシュフローを可視化したい グラフ化する編

前回

paloma69.hatenablog.com

溜まったデータはこんな感じです。

PS C:\Users\masashi\tools\gta_assets> cat .\data\gtacashflow.csv
2020/04/12 17:15,40000,1081924
2020/04/12 17:30,40000,1081924
2020/04/12 17:45,0,1121924
2020/04/12 18:00,0,1095781
2020/04/12 18:15,0,1060181
2020/04/12 18:30,0,1236818
2020/04/12 18:45,2000,1236818
2020/04/12 19:00,2000,1136818
2020/04/12 19:15,0,1122693
2020/04/12 19:30,0,1239993
2020/04/12 19:45,0,1248918
2020/04/12 20:00,0,1248918
2020/04/12 20:15,0,1248918

グラフ化

私含めたSIer業界()ならExcelでグラフ化するところですが、
私も脱Excelをしていきたいのでコードを書いてグラフに起こします。

pythonのサイトでよく出てくるmatplotlibを使います。
numpyもセットでよく使われますが、今回は使わず前回タンキングしたCSVを使います。

コード

取得したデータから起こします。
現金と銀行預金のデータがありますので、併せられるよう棒グラフにします。

グラフ化はpyplotというメソッドを使います。
凡例もつけられて凄い便利です。

matplotlibの使用例はpyplotがほとんどなんですが、
他の使い方って出てきませんね。
(今度調べてみます)

とりあえずpyplotはデータをリスト化して使うと覚えておけば何とかなりますw

  • view_assets.py
import csv
import matplotlib.pyplot as plt

with open('.\\data\\gtacashflow.csv', 'r') as f:
    Assets = csv.reader(f)
    DATA = list(Assets)

x = list() 
y_cash = list()
y_bank = list()

for a in DATA:
    x.append(a[0])
    y_cash.append(int(a[1]))
    y_bank.append(int(a[2]))

# Create graph as bar, add legend
p1 = plt.bar(x, y_bank, color='blue')
p2 = plt.bar(x, y_cash, bottom=y_bank, color='green')
plt.legend((p1[0],p2[0]),('Bank','Cash'))

# Fine-tune labels
plt.title('GTA Online Asset Flow')
plt.xticks(rotation=45)
plt.ylabel('Assets ($)')
plt.ylim([0,1500000])

plt.show()

実行

PS C:\Users\masashi\tools\gta_assets> python .\view_assets.py

f:id:paloma69:20200413221209p:plain

OK!

なんかそれっぽくないですか?
思ったより増減の幅が少ないので映えませんがいい感じ!

ミッションで稼いではいますが、持っている拠点の日時の固定費の支出もあるので意外とお金貯まりません。

cashが無いのは死ぬと減ってしまうのですぐ銀行に預けるためです。
預けておくと死んでも減りません。
買い物は預金から使うこともできるのでなるべく現金を持たないのがコツですw

で、これって何の役に立つん?

ただのゲーム内通貨のフローをグラフ化しただけなので役には立ちません。

しかし、自キャラのキャッシュフロー見える化できました。

ゲーム内の犯罪会社の出来事ですが、
自分の稼ぐさまを見ることで一応経営者気分が味わえますw

まあ、メインはコードでのグラフ化なのでそれが実装できて嬉しいです。
GUI系の出力ができるとプログラミングの面白さが加速しますね。

結び

役に立たないと言いながらこれを使って自作の支払い集計ツールの可視化もできそうですね。

また、Rockstar社のゲームなら似たようなHPになってると思うのでこの方法で資金系の可視化ができそうです。
Red Dead Redemption 2は未購入ですが、買ったらこれと合わせてやってみます。

本件のネタは1年くらい前に思いついたのですが、やっと実現できました。

APIが動かない、seleniumでこれ以上進めない、で何回も挫折したのですが、
今どき調べれば何かヒットしますね。(ヒントになり得るものでも)
先人の知恵に感謝です。

データタンキング用のブラウザ操作の過程が一番難しくて
今回のグラフ化はどちらかというと簡単でした。

ブラウザの仕組み、認証の仕組みなどインターネットは奥が深くて楽しいです。
いや、深すぎか。


これもGithubリポジトリ作りました。

github.com

まだリポジトリのみですが、随時更新していきます。
グラフの上限値の自動設定や棒グラフのカラーなんかは調整したいですね。

それでは最後に私の愛車、インテヴェーロ コケットのテールを見ながらお別れしましょう。

f:id:paloma69:20200413225850p:plain

参考サイト

GTAオンラインマネーのキャッシュフローを可視化したい データ溜める編

GTAオンライン面白いですよね。
2013年発売のゲームなんですが、未だにアップデートが続いてます。

私も頻度は減りましたがまだログインしてます。

今は運悪く新型コロナウイルスによる非常事態宣言期間内なのでますますログインが増えそうです。

最近のゲームはメーカーのアカウントを作ってオンラインでステータスを確認することができます。
何でも記録に残っちゃいますね。

しかし基本的に現時点のステータスしか確認できません。

GTAオンラインの重要な要素であるお金の推移が分かるようにしたかったので
いっちょ作ってみようと思います。

自アカウントのステータスを取る

Rockster Social Clubで自キャラのステータスを確認できます。

APIが用意されていた様なのですがしばらくメンテされていないようで動きませんでした。
(日本Social Clubにログインすると日本のドメインにリダイレクトされるのでそれがダメなのかも)

rockstar-api.readthedocs.io

となればseleniumの出番ですね。

環境

ノートWindows10でやります。
WSLも入ってますがGUIも見れた方がいいと思いWindowspythonで動かします。

PS C:\Users\masashi\tools\gta_assets> $psversiontable

Name                           Value
----                           -----
PSVersion                      5.1.18362.628
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.18362.628
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1


PS C:\Users\masashi\tools\gta_assets> python --version
Python 3.7.2

準備

pythonseleniumはだいぶなれたので今回もこれで行きます。

主な処理

  1. SocialClubにアクセス
  2. ログイン処理
  3. GTAマネーの値をゲット
  4. ファイルに保存

seleniumの使用ブラウザはChromeです。

認証が突破できない、困った

seleniumで起動するとまっさらなChromeが立ち上がります。
当然ブラウザにもアカウントのデータがないのでSoclal Clubサインイン時に画像選択の認証がでます。
ロボット対策のreCAPTCHA認証というらしいです。

これね。

f:id:paloma69:20200412215517p:plain

認証を手動でやるのは本末転倒なので他の方法を考えます。

  1. reCAPTCHAを突破する機能を作る
  2. pyautoguiでブラウザ操作する
  3. seleniumGoogleアカウントを読み込ませる

1を実装が出来たらハッカー級です、今の私には無理…
2は座標の取得が大変です。
無難に3でやることにしました。

さすがというかこの辺のナレッジがたくさんありました。

ブラウザセッションの関係で本当は専用ユーザープロファイルを作った方がいいのですが
このためにわざわざ作るのもなんなのでいつも使ってる自分のプロファイルを使います。

保存ディレクトリはURLにchrome://veisionで確認出来ます。

Chromeはバックエンドでプロセスが動いていてこの状態でselenium起動するとセッション重複でエラーになります。

プロセス終了すればseleniumは動くので今回はChromeが動いていない状態での挙動になります。

コード

  • getgtamoney.py
from selenium import webdriver
import configure
import datetime
import time
import csv
import os

os.chdir(configure.my_path)

URL = 'https://ja.socialclub.rockstargames.com'

options = webdriver.ChromeOptions()
options.add_argument('--user-data-dir=' + configure.chrome_path)
browser = webdriver.Chrome(options=options)

browser.get(URL)
time.sleep(10)

# For Singin Function at Firsttime
def Sitelogin():
    signin_btn = browser.find_element_by_xpath('//*[@id="app-page"]/div[1]/div/div/div/div/div/div[1]/a[1]')
    signin_btn.click()

    email = browser.find_element_by_xpath('//*[@id="app-page"]/div[2]/div[1]/div/div/form/fieldset[1]/span/input')
    email.send_keys(configure.email)

    password = browser.find_element_by_xpath('//*[@id="app-page"]/div[2]/div[1]/div/div/form/fieldset[2]/span[1]/span[1]/input')
    password.send_keys(configure.password)

    login_btn = browser.find_element_by_xpath('//*[@id="app-page"]/div[2]/div[1]/div/div/form/fieldset[3]/div/button')
    login_btn.click()

browser.get('/'.join([URL, 'games/gtav/pc/career/overview/gtaonline']))
time.sleep(3)

getcash = browser.find_element_by_xpath('//*[@id="cash-value"]').text
getbank = browser.find_element_by_xpath('//*[@id="bank-value"]').text

# Change datatype integer
cash = int(getcash[1:].replace(',', ''))
bank = int(getbank[1:].replace(',', ''))

gettime = datetime.datetime.today().strftime('%Y/%m/%d %H:%M')

with open('.\\data\\gtacashflow.csv', 'a', newline='') as f:
    writer = csv.writer(f)
    writer.writerow([gettime, cash, bank])


browser.close()
browser.quit()

コードの解説

自アカウント、環境系はconfigureファイルに外だししてます。

ページ遷移の時にseleniumのwaitを使いたかったのですが、今回はsleepで凌いでます。
エラー処理も特にやってないやっつけです。

--user-data-dirで自分のChromeプロファイルのフォルダを指定してRockster Social Clubにログインしておくことで認証をスキップできます。
def Sitelogin()はログイン用に用意したのですが、今回の方法なら不要でした。
なのでコード内で使っていません。
(いつか消します)

このコードで現在の自キャラの現金と銀行の預金が取れます。

値を取ってきた状態はstr型なので計算出来るようにint型に直してます。

実行はこれだけですね。

PS C:\Users\masashi\tools\gta_assets> python .\getgtamoney.py

取得データのタンキング

これで取得出来るようになったので定刻おきに取得してタンキングします。

取るデータは取得時間とその時の所持金だけなのでcsvに溜めます。

2~3時間ほどプレイします。
効率の良いカーディーラーをメインに稼ぎます。

今回は上記のコードをタスクスケジューラで起動しました。
15分おきに起動しています。
タスクスケジューラ起動の際にファイル保存できるようにosモジュールもインポートしてます。

数時間後

お気に入りマイカーのインテヴェーロ コケット。
シボレー コルベットがモデルですね。

f:id:paloma69:20200412220334p:plain

データはこんな感じで溜まってます。
取得時間、現金、銀行預金のデータです。

PS C:\Users\masashi\tools\gta_assets> cat '.\data\gtacashflow.csv'
2020/04/12 17:15,40000,1081924
2020/04/12 17:30,40000,1081924
2020/04/12 17:45,0,1121924
2020/04/12 18:00,0,1095781
2020/04/12 18:15,0,1060181
2020/04/12 18:30,0,1236818
2020/04/12 18:45,2000,1236818
2020/04/12 19:00,2000,1136818
2020/04/12 19:15,0,1122693
2020/04/12 19:30,0,1239993
2020/04/12 19:45,0,1248918
2020/04/12 20:00,0,1248918
2020/04/12 20:15,0,1248918

いよいよ可視化します!

表計算ソフトを使っても面白くないのでここもpythonを使ってグラフ化してみます。

pythonはライブラリが沢山あるのでグラフ化パッケージもちゃんとあります。

matplotlibというライブラリがメジャーで色々情報があり使いやすそうだったのでこれを使ってみます。

と言うわけで次回に続きます。

参考サイト