概要
- よ一ど ♀(@yo_do_53)さんが、診断メーカーで「大石泉すき」診断を作る
- 「大石泉すき」の5種類の文字から重複順列がランダムに取り出される
- 「大石泉すき」と並ぶ確率は
1 / (5 ** 5) = 1 / 3125
- 桜花(@ikmesyk3)さんが、↑の並びをポーカーに見立てた「大石泉すきポーカー」を提案
- 私が、役判定プログラムを書く
- 私が、手役の強さ判定プログラムを書く ←New!
実装の過程
今回もPythonを使用します。Pythonではソート時に比較関数を使うことができるので、次のように仕様を設定します。
|compare_hand(a, b)|判定結果|
|----|----|----|
|-1|aの方が強い手役|
|0|優劣が存在しない|
|1|bの方が強い手役|
例えば、compare_hand('大石泉きす', '大石大石泉')
の場合は-1を返すわけですね。
言うまでもありませんが、 役の強さが異なる場合はそこで決着 しますので、それ以外のケースを考えることになります。
def judge_hand(name: str) -> int:
"""役を判定する
Parameters
----------
name : str
「大石泉すき」から重複を許して5文字取って並べたもの
Returns
-------
int
判定された役(0がロイヤルストレートフラッシュ、ブタが8)
"""
# 各文字をバラす
name_char = list(name)
# 各文字の出現回数を数え、出現回数部分だけ抽出したもの。
# 例:「大大すすき」→[2, 2, 1]
char_count = collections.Counter(name_char).values()
# 手役の判定処理
if name == '大石泉すき':
# 説明不要
return 0
elif len(set(name_char)) == 1:
# uniq処理を施して1つしか残らない=ファイブカード
return 1
elif 4 in char_count:
# ある文字が4つある=フォーカード
return 2
elif len(char_count) == 5:
# 5種類ある=元の文字列が5文字なので各文字ある=ストレートフラッシュ
return 3
elif 2 in char_count and 3 in char_count:
# 出現回数に「2」と「3」がある=フルハウス
return 4
elif 'すき' in name:
# すき
return 5
elif 3 in char_count:
# 出現回数に「3」がある=スリーカード
return 6
elif 2 in char_count and len(char_count) == 3:
# 出現回数が3文字分あって「2」が含まれる=消去法からツーペア
return 7
return 8
def compare_hand(a: str, b: str) -> int:
"""手役を比較する
Parameters
----------
a : str
「大石泉すき」から重複を許して5文字取って並べたもの1
b : str
「大石泉すき」から重複を許して5文字取って並べたもの2
Returns
-------
int
aが強ければ-1、同じなら0、bが強ければ1
"""
# 役が異なれば当然そこで判定できる
hand1 = judge_hand(a)
hand2 = judge_hand(b)
if hand1 != hand2:
if hand1 < hand2:
return -1
if hand1 > hand2:
return 1
# 「同じ強さの場合「すき」が(より後ろで)揃っているものが勝つ。」
# 「同じ強さですきの位置も同じの場合正位置に近い文字数が多いものが勝つ。」
# 「上記2つのいずれも同数の場合、正位置にある文字の画数が多いものが勝つ。」
return 0
ルール1「同じ強さの場合「すき」が(より後ろで)揃っているものが勝つ。」
これを、次のように翻訳します。
- 「すき」の文字列がそれぞれの手役に存在するかを確認する
- 片方しか存在しない場合、存在している方が無条件で勝つ
- 両方存在する場合、その位置がより後ろな方が勝つ
一見複雑そうですが、str#find
メソッドが「引数に指定した文字列の位置を左端0スタートで返す。存在しない場合は-1を返す」仕様なので、「findメソッドの戻り値が異なる場合、大きい方が勝つ」と言い換えられます。
# 「同じ強さの場合「すき」が(より後ろで)揃っているものが勝つ。」
pos1 = a.find('すき')
pos2 = b.find('すき')
if pos1 != pos2:
if pos1 < pos2:
return 1
if pos1 > pos2:
return -1
ルール2「同じ強さですきの位置も同じの場合正位置に近い文字数が多いものが勝つ。」
これを、最初は次のように翻訳しました。
- 手役の各文字について、「大」「石」「泉」「す」「き」とどれだけ離れているかSSDを計算する
- SSDが異なる場合、小さい方が勝つ
しかし、これだとcompare_hand('すき泉きす', 'すき石石き')
の値が「この段階で」-1になってしまいます。テストを通すには、
- 手役の各文字について、「大」「石」「泉」「す」「き」の本来の位置と異なっている場合は0点、同じ場合は1点とする
- 合計点数が大きい方が勝つ
と翻訳する必要がありました。
def calc_pos_score(x: str) -> int:
p1: int = '大石泉すき'.find(x[0:1])
p2: int = max('石泉すき'.find(x[1:2]), '石大'.find(x[1:2]))
p3: int = max('泉すき'.find(x[2:3]), '泉石大'.find(x[2:3]))
p4: int = max('すき'.find(x[3:4]), 'す泉石大'.find(x[3:4]))
p5: int = 'きす泉石大'.find(x[4:5])
sum = 0
sum += 1 if p1 == 0 else 0
sum += 1 if p2 == 0 else 0
sum += 1 if p3 == 0 else 0
sum += 1 if p4 == 0 else 0
sum += 1 if p5 == 0 else 0
return sum
# 「同じ強さですきの位置も同じの場合正位置に近い文字数が多いものが勝つ。」
ssd1 = calc_pos_score(a)
ssd2 = calc_pos_score(b)
if ssd1 != ssd2:
if ssd1 < ssd2:
return 1
if ssd1 > ssd2:
return -1
ルール3「上記2つのいずれも同数の場合、正位置にある文字の画数が多いものが勝つ。」
画数を漢字辞典オンラインとKKJN.JPから採用すると、「大石泉すき」はそれぞれ「3画・5画・9画・2画・4画」です。実は姓名判断だと「す」を3画と数えるそうですが、「どう見たって2画でしょうが」といった理由により2画としました。
def calc_stroke_score(x: str) -> int:
p1: int = '大石泉すき'.find(x[0:1])
p2: int = max('石泉すき'.find(x[1:2]), '石大'.find(x[1:2]))
p3: int = max('泉すき'.find(x[2:3]), '泉石大'.find(x[2:3]))
p4: int = max('すき'.find(x[3:4]), 'す泉石大'.find(x[3:4]))
p5: int = 'きす泉石大'.find(x[4:5])
sum = 0
sum += 3 if p1 == 0 else 0
sum += 5 if p2 == 0 else 0
sum += 9 if p3 == 0 else 0
sum += 2 if p4 == 0 else 0
sum += 4 if p5 == 0 else 0
return sum
# 「上記2つのいずれも同数の場合、正位置にある文字の画数が多いものが勝つ。」
stroke1 = calc_stroke_score(a)
stroke2 = calc_stroke_score(b)
if stroke1!= stroke2:
if stroke1< stroke2:
return 1
if stroke1> stroke2:
return -1
検証
後はこれ全部を組み込めば完成!閉廷!……ではなく、「全く同一の手じゃない場合は勝敗が付けられる」ことを証明する必要があります。というわけで3125×3125-3125=9762500
通りを検証すれば……エラーが大量に出ました。
大大大大石/大大大大泉
大大大大石/大大大大す
大大大大石/大大大石大
大大大大石/大大大泉大
(中略)
287564通り
9762500通りからすると287564通りは3%弱ですが、無視することはできません。そこで第四のルールとして、「正位置にない文字の画数が少ないものが勝つ。」も追加します。
大大大大石/大大大石大
(中略)
24942通り
エラー数が1桁少なくなりましたが、できれば0にしたいところです。単純に文字列を比較する「a < b」的なコードを入れると当然エラーは消えますが、それはそれで面白くないので、上手いルール付けが今後の課題となりました。
最終的なソースコード
import collections
import itertools
def judge_hand(name: str) -> int:
"""役を判定する
Parameters
----------
name : str
「大石泉すき」から重複を許して5文字取って並べたもの
Returns
-------
int
判定された役(0がロイヤルストレートフラッシュ、ブタが8)
"""
# 各文字をバラす
name_char = list(name)
# 各文字の出現回数を数え、出現回数部分だけ抽出したもの。
# 例:「大大すすき」→[2, 2, 1]
char_count = collections.Counter(name_char).values()
# 手役の判定処理
if name == '大石泉すき':
# 説明不要
return 0
elif len(set(name_char)) == 1:
# uniq処理を施して1つしか残らない=ファイブカード
return 1
elif 4 in char_count:
# ある文字が4つある=フォーカード
return 2
elif len(char_count) == 5:
# 5種類ある=元の文字列が5文字なので各文字ある=ストレートフラッシュ
return 3
elif 2 in char_count and 3 in char_count:
# 出現回数に「2」と「3」がある=フルハウス
return 4
elif 'すき' in name:
# すき
return 5
elif 3 in char_count:
# 出現回数に「3」がある=スリーカード
return 6
elif 2 in char_count and len(char_count) == 3:
# 出現回数が3文字分あって「2」が含まれる=消去法からツーペア
return 7
return 8
def calc_pos_score(x: str) -> int:
p1: int = '大石泉すき'.find(x[0:1])
p2: int = max('石泉すき'.find(x[1:2]), '石大'.find(x[1:2]))
p3: int = max('泉すき'.find(x[2:3]), '泉石大'.find(x[2:3]))
p4: int = max('すき'.find(x[3:4]), 'す泉石大'.find(x[3:4]))
p5: int = 'きす泉石大'.find(x[4:5])
sum = 0
sum += 1 if p1 == 0 else 0
sum += 1 if p2 == 0 else 0
sum += 1 if p3 == 0 else 0
sum += 1 if p4 == 0 else 0
sum += 1 if p5 == 0 else 0
return sum
def calc_stroke_score(x: str) -> int:
p1: int = '大石泉すき'.find(x[0:1])
p2: int = max('石泉すき'.find(x[1:2]), '石大'.find(x[1:2]))
p3: int = max('泉すき'.find(x[2:3]), '泉石大'.find(x[2:3]))
p4: int = max('すき'.find(x[3:4]), 'す泉石大'.find(x[3:4]))
p5: int = 'きす泉石大'.find(x[4:5])
sum = 0
sum += 3 if p1 == 0 else 0
sum += 5 if p2 == 0 else 0
sum += 9 if p3 == 0 else 0
sum += 2 if p4 == 0 else 0
sum += 4 if p5 == 0 else 0
return sum
def calc_stroke_not_score(x: str) -> int:
p1: int = '大石泉すき'.find(x[0:1])
p2: int = max('石泉すき'.find(x[1:2]), '石大'.find(x[1:2]))
p3: int = max('泉すき'.find(x[2:3]), '泉石大'.find(x[2:3]))
p4: int = max('すき'.find(x[3:4]), 'す泉石大'.find(x[3:4]))
p5: int = 'きす泉石大'.find(x[4:5])
stroke_dict = {
"大": 3,
"石": 5,
"泉": 9,
"す": 2,
"き": 4
}
sum = 0
sum += stroke_dict[x[0:1]] if p1 != 0 else 0
sum += stroke_dict[x[1:2]] if p2 != 0 else 0
sum += stroke_dict[x[2:3]] if p3 != 0 else 0
sum += stroke_dict[x[3:4]] if p4 != 0 else 0
sum += stroke_dict[x[4:5]] if p5 != 0 else 0
return sum
def compare_hand(a: str, b: str) -> int:
"""手役を比較する
Parameters
----------
a : str
「大石泉すき」から重複を許して5文字取って並べたもの1
b : str
「大石泉すき」から重複を許して5文字取って並べたもの2
Returns
-------
int
aが強ければ-1、同じなら0、bが強ければ1
"""
if a == b:
return 0
# 役が異なれば当然そこで判定できる
hand1 = judge_hand(a)
hand2 = judge_hand(b)
if hand1 != hand2:
if hand1 < hand2:
return -1
if hand1 > hand2:
return 1
# 「同じ強さの場合「すき」が(より後ろで)揃っているものが勝つ。」
pos1 = a.find('すき')
pos2 = b.find('すき')
if pos1 != pos2:
if pos1 < pos2:
return 1
if pos1 > pos2:
return -1
# 「同じ強さですきの位置も同じの場合正位置に近い文字数が多いものが勝つ。」
ssd1 = calc_pos_score(a)
ssd2 = calc_pos_score(b)
if ssd1 != ssd2:
if ssd1 < ssd2:
return 1
if ssd1 > ssd2:
return -1
# 「上記2つのいずれも同数の場合、正位置にある文字の画数が多いものが勝つ。」
stroke1 = calc_stroke_score(a)
stroke2 = calc_stroke_score(b)
if stroke1 != stroke2:
if stroke1 < stroke2:
return 1
if stroke1 > stroke2:
return -1
# 「正位置にない文字の画数が少ないものが勝つ。」
stroke1 = calc_stroke_not_score(a)
stroke2 = calc_stroke_not_score(b)
if stroke1 != stroke2:
if stroke1 < stroke2:
return -1
if stroke1 > stroke2:
return 1
# 単純な比較関数
return -1 if stroke1 < stroke2 else 1
# メインルーチン
if __name__ == '__main__':
# テスト用
print(compare_hand('大石泉きす', '大石大石泉')) # -1
print(compare_hand('泉す泉すき', '大大泉石石')) # -1
print(compare_hand('す泉太すき', '泉泉すきす')) # -1
print(compare_hand('大石すき泉', '泉大すき石')) # -1
print(compare_hand('泉泉石きき', '大大石泉泉')) # -1
print(compare_hand('すき泉きす', 'すき石石き')) # -1
# 以下、全通りをテストするためのコード
# 使用する文字の種類
chars = '大石泉すき'
# itertools.productを利用して、重複順列を作り出し、1セットづつname_charに取り出す
name_list = []
for name_char in itertools.product(chars, repeat=5):
# name_charの全文字を結合したものをnameとする
name = ''.join(name_char)
name_list.append(name)
# 検証を行う
sum = 0
for a_name in name_list:
for b_name in name_list:
if a_name != b_name:
result = compare_hand(a_name, b_name)
if result == 0:
if sum < 10:
print(a_name + "/" + b_name)
sum += 1
print("{0}通り".format(str(sum)))