見出し画像

Pythonでヌメロン(数当てゲーム)を実装してみる(with discord.py)

今回は、pythonでヌメロン(数当てゲーム)を実装してみようと思います。ヌメロンのルールについては、以下のウィキペディアを確認してください。また、今回は実装にあたって、結果をターミナルに表示してもいいのですが、それでは味気ないのでDiscordのBotに実装します。DiscordのBot用のライブラリのdiscord.pyについては当記事の趣旨とはズレるので解説しませんが、参考になるページは貼っておこうと思います。
Numer0n(Wikipedia) : https://ja.wikipedia.org/wiki/Numer0n
Pythonで実用Discord Bot(Qiita) : https://qiita.com/1ntegrale9/items/9d570ef8175cf178468f

Numer0nの実装の大まかな流れ

①3桁の数字(各桁で数字の重複なし)を生成

②ユーザーが推測した3桁の数字を取得

③生成した数字とユーザーが推測した数字を比較して、EAT・BYTEを出力
※EAT:数字とその桁の位置が一致している場合に+1, 3EATなら数を全て当てたことになる。
※BYTE:数字のみが一致している場合に+1, 場所は問われない。但し、桁の位置も一致している場合はEATが出力される。

④②・③を任意の回数繰り返し、ゲームオーバーやクリア(=3EAT)の条件を作る。

①3桁の数字を生成する

import random

def generate_num():
   """ランダムな3桁の数字をタプルに入れて生成"""
   while True:
       first_num = random.randint(1, 9)
       second_num = random.randint(0, 9)
       third_num = random.randint(0, 9)
       num = (first_num, second_num, third_num)
       if check_duplication(num) is True:
           return num
       else:
           pass

まず、generate_numという関数で一桁ずつ数をrandom.randintで範囲指定して生成しています。最初の桁のみ1~9の範囲で生成するようにして、百の位が0になることを避けています。そして、その生成した各桁の数をタプルにまとめて入れてそのタプルを返り値として返しています。(check_duplicationは後で解説します。)タプルにしている理由は、タプルはリストと違って後から直接書き込みが出来ないためです。要するに生成した数字はゲーム終了まで変化させたくないため、不慮の事故で書き込んだりしないようにする。このNumer0n程度のサイズのコードならリストでも事故が起こる確率は低いですが、コードが長くなるとバグの原因になることがあるので、変化のないデータは必ずタプルに入れるように心がけましょう。

def check_duplication(num):
   """すべての桁で数字が重複していないか判定"""
   if len(num) == len(set(num)) and len(num) == 3:
       return True

上記のcheck_duplicationはその名の通り重複を確認する関数として定義しています。この関数がTrue(重複なし)を返さない限り、generate_numのwhileループは無限に続くことになります。Trueを返した場合、generate_numから生成した数字のタプルが返り値として返ってきます。

len(num) == len(set(num)) and len(num) == 3

上記のコードでは、集合型の重複したデータは一つにまとめられるという性質を利用して、重複をチェックしています。lenは対象のデータの数を返しますが、生成した3桁の数字のタプルを集合に変換して、その数値が3(個)にならないという事はどこかで重複が生じていることになります。そして、3(個)になる(重複なし)の時にTrueを返します。

②ユーザーが推測した3桁の数字を取得する

こちらの内容はdiscord.pyの内容がほとんどになるので解説は控えますが、やっていることは、受け取った3桁の数字をforループで回して、リストに加工しているだけです。ついでにgenerate_numを利用して、gen_num_tupleというのに格納してます。

async def numer0n(self, ctx):
    gen_num_tuple = generate_num()

    def check(m):
        return m.author == ctx.author

    est_num = await self.bot.wait_for("message", check=check, timeout=None)
    est_num_list = []
        for num in est_num.content:
            est_num_list.append(int(num))

今までにやった操作の結果を確認すると...
gen_num_tuple→生成した3桁の数字のタプルが入っている。
est_num_list→ユーザーが予想した3桁の数字のリストが入っている。

次はこの二つの値を関数の引数に入れて比較させて、返り値としてEATとBYTEの値を返してやればいい。

③生成した数字と予想した数字の比較

def check_num(est_num, gen_num):
   """位置が一致のときにEATに加算、数字が一致の時にBYTEに加算"""
   EAT = 0
   BYTE = 0
   i = 0
   while i < 3:
       if est_num[i] == gen_num[i]:
           EAT += 1
       elif est_num[i] in gen_num:
           BYTE += 1
       else:
           pass
       i += 1
   return EAT, BYTE

上のコードはチェックの関数をwhileループで簡略化したものなので、切り分けて解説していく。

まず、このcheck_numという関数にest_numgen_numという引数を渡す。この引数には先程のest_num_listとgen_num_tupleを入れることになる。つまり、引数はリストもしくはタプルの形で渡されるので、インデックスで中身の値の場所を指定して抽出できる。

if est_num[0] == gen_num[0]: #インデックスは0から始まる事に注意
    EAT += 1

ループの一回目では、est_num[0](ユーザーが予想した数字の1桁目)とgen_num[0](生成した数字の1桁目)を比較して、一致している場合はEATに+1をする。

elif est_num[0] in gen_num:
    BYTE += 1
else:
    pass

次にその条件に該当しない場合は、est_num[0](ユーザーが予想した数字の1桁目)がgen_num(生成した数字のタプル)に含まれているかを確認して、該当する場合にはBYTEに+1する。そして、どれにも該当しない場合は何も処理を行わない。
これと同様の比較作業を2、3桁にも実行(自分はwhileループで3回回している)して、最終的なEATとBYTEの値を返り値として返している。

これをメインのNumer0nコードに足してみると...

async def numer0n(self, ctx):
    gen_num_tuple = generate_num()

    def check(m):
        return m.author == ctx.author

    est_num = await self.bot.wait_for("message", check=check, timeout=None)
    est_num_list = []
        for num in est_num.content:
            est_num_list.append(int(num))
    eat_byte = check_num(gen_num_tuple, est_num_list)
    await ctx.send(f'{eat_byte[0]}EAT, {eat_byte[1]}BYTE')

これで1回だけEATとBYTEを判定してくれるので、あとはest_num = await self.bot.wait_for...以下の行をwhileループで回してやれば、任意の回数だけEATかBYTEかを判定してくれる。

④ゲームオーバー、クリアの判定を追加して完成!

あとはEATの値が3の時にクリア、任意の回数までに当てる事が出来なければゲームオーバー(今回は10回とした)というのを実装して完成。

async def numer0n(self, ctx):
    gen_num_tuple = generate_num()

    def check(m):
        return m.author == ctx.author

   i = 0
    while True:
        est_num = await self.bot.wait_for("message", check=check, timeout=None)
        est_num_list = []
            for num in est_num.content:
                est_num_list.append(int(num))
        eat_byte = check_num(gen_num_tuple, est_num_list)
        i += 1

        if eat_byte[0] == 3:
            await ctx.send('ゲームクリア!')
        break #クリアしたのでwhileループを抜ける

     elif i >= 10:
         await ctx.send('ゲームオーバー!10回以内に当てることができませんでした!')
            break #ゲームオーバーなのでwhileループを抜ける
     

        await ctx.send(f'{i}回目: {est_num.content} | {eat_byte[0]}EAT, {eat_byte[1]}BYTE')

自分はUIの見た目にこだわってるのでdiscord.Embedとか色々使って、もっと複雑にしてますが、上記のコードは最低限これだけあれば動くという最小構成です。クリア、ゲームオーバーといった条件の時にbreakでwhileループを抜けて、ゲームの処理を終了しています。

最後に

いかがでしたでしょうか?少しでもpythonの基礎学習に活かしていただければと思います。あとで考えてみると、ズルができるのでユーザーが予想した数の方も重複判定を通した方がいいですね...それでは、また次の記事でお会いしましょう。

この記事が気に入ったらサポートをしてみませんか?