工学じじいの縁側日記

工学じじいの縁側日記

引退間際の工学じじいがきままに、プログラミングやデバイス、工学について呟きます。

【簡単?】初心者がpython3とpygameでヒット&ブローゲーム作ってみる #01【習作?】

初心者がpython3とpygameでヒット&ブローゲーム作ってみる  #01

f:id:gomta777:20191115111505p:plain

前回から

ko-gaku-jiji.hatenablog.jp

前回こんなこと言ってましたが、いわゆるヒット&ブローゲームを作ってみます。今回はプロトタイプとして、テキストベースでコンソール画面で実行できるバージョンを作ってみます。
そして次回以降で、グラフィックを追加してゆき、最終的にビデオゲーム風に仕上げます。(テトリス完成させろよ、という突っ込みお待ちしております)

ヒット&ブローゲーム

ヒット&ブローゲームを御存知でしょうか?
結構有名なので、どこかでは見たことあると思います。
知らない方のためにちょっとだけ説明します。
簡単に言うと、数字当てゲームです。
出題者側から4桁(が多分ノーマルルール)のかぶらない数字が出題されます。
3124、9876、1526など。
解答者側は、この数字を当てます。

解答者の予想した数字の、

位置があっていた場合はHIT(ヒット)
数字のみあっていた場合はBLOW(ブロー)

となります。

例えば、「1526」が出題されたとしましょう。
解答者は、4桁の数字とその並びを予想して答えます。
「1234」と答えた場合

出題 1526
解答 34
結果 1HIT 1BLOW

次に「1256」と答えた場合

出題 1526
解答 25
結果 2HIT 2BLOW

これを、なるべく少ない回数で4HIT獲得しよう!というゲームです。

ヒット&ブローの仲間とゲームの実装

ヒットアンドブローゲームは、スターマインドという名前でボードゲームとして発売されてヒットしています。

ja.wikipedia.org

スターマインドでは、数字の並びではなく、ボールの色を当てるゲームです。(子供のころ友達の家で遊んだことがあるのは内緒です)
このウィキペデアの「その他」の項にありますが、

「ルールが簡潔であるため、パソコンの黎明期にBASICで組まれたプログラムが数多く発表された。」

とあります。そのため、あちこちに様々な実装が散見され、工学系の学校の授業などでも、プログラミングの例題として課題で出されたりします。
なので、実装そのものに関しては、特に工夫もいらず、あまり書くこともないかなぁなんて思ったりしています。
(なんか、pythonならでは的なものがあったら書こうかな)

実装(設計)

ゲームのメイン部分の設計を考えてみます。
設計といってもやることはいくつかしかなく。
大まかに、

出題、入力、判定、出力

がメインかなと思います。

出題

出題は、C言語などではちょっとした工夫をしないと4桁のかぶらない数の列を作れませんが、python様にかかると2行です。

import random

def make_question():
    nums = list(range(10))
    return(random.sample(nums, 4))

num = list(range(10))

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
のリストができるので、そこから4つの数字をランダムにチョイスします。(なんて命令がpythonにちょろっとあるのが笑えます)

random.sample(list, num)
は、指定されたリスト(list)から、重複なしで(num)個の数字を取り出す命令です。
これだけで、「4桁のランダムな並びの重複しない数字の列」が出来上がります。

判定

次にプレイヤーから4桁の数字の列

yin = [n1, n2, n3, n4]

が入力されたときのヒットとブローの判定を考えてみます。
ヒットの判定は、ループでそのまま一致を調べます。
pythonには、もっといい方法がある気がします)

 for i in range( len(yin) ):
        if yin[i] == ans[i]:
            h += 1 # hがヒットの数

次にブローの数を数えます。
あるリストlistにnという要素が含まれるかどうかは、pythonではその中に入っている型とかを考えずに

    n in list

TrueFalseかで判定できます。
したがって、ブローの数は、
「今判定している数がヒットじゃなかったときに、リストに含まれているかどうか」
を判定すればよいです。

 for i in range( len(yin) ):
        if yin[i] == ans[i]:
            h += 1 # hがヒットの数
        else:
            if yin[i] in ans:
                b += 1 # bがブローの数

これだけでヒットとブローの判定ができます。
これを、関数として書いてみます。
yinがユーザの入力、yin = [n1, n2, n3, n4]
ansが出題された数字列、ans= [a1, a2, a3, a4]
を表しています。

def judgement(yin, ans):
    h=0
    b=0
    for i in range(len(yin)):
        if yin[i] == ans[i]:
            h += 1
        else:
            if yin[i] in ans:
                b += 1
    return([h, b])

入力

次に、プレイヤーの入力処理の部分を考えてみます。
入力をベースに考えた処理の流れは、

  1. ゲーム開始
  2. 出題
  3. プレイヤーの解答入力
  4. 判定の表示
  5. (HIT==4がTrueか?)
    • TRUEの場合 おめでとうと表示
    • FALSEの場合 3へもどる

となります。

プレイヤーの入力

出題される数字列は、

ans= [a1, a2, a3, a4]

のlistの形で保存されています。
先ほど作った関数、

def judgement(yin, ans):

で、ヒットとブローの判定をするには、入力した数字列を

yin = [n1, n2, n3, n4]

の形で関数に渡さなければなりません。
プレイヤーが入力した4桁の数字は、通常inputで取得すると、4文字の文字列として、保存されてしまいます。これ整数一けたずつの数字にばらして要素数4のリストに変換します。

    print("Hit&Blowゲーム!")
    print("4桁の重複しない数字を入力してください。")
    print("->", end="")
    tmp = input()
    your_input = []
    for i in tmp:
        your_input.append(int(i))
    print(your_input)

実行結果

Hit&Blowゲーム!
4桁の重複しない数字を入力してください。
->1234
[1, 2, 3, 4]

簡単にできました。python様々です。

判定結果の表示

判定結果を、表示するためにプレイヤーの入力と生成された出題のリストを判定用関数に渡します。

ans = make_question()

出題は、すでに生成され、ansに保存されているとします。
プレイヤーの入力your_inputとansがあれば、ヒットとブローの判定は、

hit,blow = judgement(your_input, ans)

で、取得できます。ここでもpythonは、値を複数返すのが簡単だったりしてうれしいですね。

ちゃんと動くか確認します。

    ans = make_question()
    print(ans)
    print("Hit&Blowゲーム!")
    print("4桁の重複しない数字を入力してください。")
    print("->", end="")
    tmp = input()
    your_input = []
    for i in tmp:
        your_input.append(int(i))
    hit,blow = judgement(your_input, ans)
    print("hit = "+str(hit)+", blow = " + str(blow))

確認用に、ansを生成した直後に、表示しています。
実際に実行してみます

結果:

[7, 3, 5, 4]
Hit&Blowゲーム!
4桁の重複しない数字を入力してください。
->7345
hit = 2, blow = 2
正解まで繰り返す

この処理を、ヒットが4になるまで繰り返します。
処理自体は簡単で、先ほどの入力の部分をまるっと、無限ループにして、hit==4の判定がTrueになったら、おめでとうメッセージを表示してbreakします。

    ans = make_question()
    print(ans)
    print("Hit&Blowゲーム!")

    while True:
        print("4桁の重複しない数字を入力してください。")
        print("->", end="")

        hit = 0
        blow = 0

        tmp = input()
        your_input = []

        for i in tmp:
            your_input.append(int(i))
        hit,blow = judgement(your_input, ans)
        print("hit = "+str(hit)+", blow = " + str(blow))

        if hit == 4:
            print("おめでとう、正解です :" + str(ans))
            break

実行してみます。
実行結果:

[5, 6, 4, 9]
Hit&Blowゲーム!
4桁の重複しない数字を入力してください。
->4569
hit = 1, blow = 3
4桁の重複しない数字を入力してください。
->5649
hit = 4, blow = 0
おめでとう、正解です :[5, 6, 4, 9]

一応これで、ゲームらしい形はできます。

入力制限

現在は、プレイヤーの入力に特に制限を設けていないので、出題は重複のない4桁の数字になっていますが、プレイヤーは何でもかんでもこたえられてしまいます。

[0, 5, 8, 3]
Hit&Blowゲーム!
4桁の重複しない数字を入力してください。
->0000
hit = 1, blow = 3
4桁の重複しない数字を入力してください。
->5555
hit = 1, blow = 3
4桁の重複しない数字を入力してください。
->8888
hit = 1, blow = 3
4桁の重複しない数字を入力してください。
->0055
hit = 1, blow = 3
4桁の重複しない数字を入力してください。
->

さすがに、入力の制限を付けずにプレイヤーの良心にエラー回避のをゆだねるのは、アルゴリズム的にはだめすぎです(笑)

入力の重複チェック

プレイヤーから入力された数字に、重複があったら再度入力を促すことにします。重複のチェックは、リストの中に同じ数字があるかどうかで判断すればよいです。
python様には、集合(set)というデータ構造があります。集合は、重複のないある要素の集まりを表します。
pythonでは、listからsetの変換が可能です。
つまり、重複の許されているlistを重複のないsetに変換できます。

  lst = [1, 2, 2, 2, 2, 3, 4,]
    setfromlist = set(lst)
    print(lst, setfromlist)

結果

[1, 2, 2, 2, 2, 3, 4] {1, 2, 3, 4}


これを利用して、変換前(リスト)と変換後(集合)の要素数が異なっていれば、重複がある、と判定します。

def has_not_duplicates(seq):
    return len(seq) == len(set(seq))

関数def has_not_duplicates(seq):により、入力に重複があるかどうかがTrueFalseで返ってきます。

プレイヤーの入力に可ぶりがない場合のみ(def has_duplicates(seq)がTrue)判定の処理をして、それ以外はスルーして入力に戻ればいいです。

    while True:
        print("4桁の重複しない数字を入力してください。")
        print("->", end="")
        hit = 0
        blow = 0
        tmp = input()
        your_input = []
        for i in tmp:
            your_input.append(int(i))
        if has_not_duplicates(your_input):
            hit, blow = judgement(your_input, ans)
            print("hit = "+str(hit)+", blow = " + str(blow))
            if hit == 4:
                print("おめでとう、正解です :" + str(ans))
                break

ちょっとしたゲーム要素を追加

最後に、スコア代わりに何手目で正解したかを表示する機能を追加してみます。
入力ミス(重複エラー)を1ターンと数えるかどうかでちょっと変わってきますが、今回は数えないほうでやってみます。

プレイヤーの手数を表す変数turnを追加します。
判定処理の部分で、hit==4(正解の時)以外の時には手数turnを増やします。

  turn = 1
    ans = make_question()
    print(ans)
    print("Hit&Blowゲーム!")

    while True:
        print("第%02d手目" % turn)
        print("4桁の重複しない数字を入力してください。")
        print("->", end="")
        hit = 0
        blow = 0
        tmp = input()
        your_input = []
        for i in tmp:
            your_input.append(int(i))
        if has_not_duplicates(your_input):
            hit, blow = judgement(your_input, ans)
            print("hit = "+str(hit)+", blow = " + str(blow))
            if hit == 4:
                print("おめでとう! %02d手目で正解です :" % turn)
                break
            else:
                turn += 1


実行結果:

Hit&Blowゲーム!
第01手目
4桁の重複しない数字を入力してください。
->1234
hit = 0, blow = 1
第02手目
4桁の重複しない数字を入力してください。
->1249
hit = 0, blow = 1
第03手目
4桁の重複しない数字を入力してください。
->9738
hit = 1, blow = 3
第04手目
4桁の重複しない数字を入力してください。
->3978
hit = 4, blow = 0
おめでとう! 04手目で正解です :

完成したソースコード一覧

今回のプログラムの全体像です。

import sys
import random

def has_not_duplicates(seq):
    return len(seq) == len(set(seq))

def judgement(yin, ans):
    h=0
    b=0
    for i in range(len(yin)):
        if yin[i] == ans[i]:
            h += 1
        else:
            if yin[i] in ans:
                b += 1
    return([h, b])

def make_question():
    nums = list(range(10))
    return(random.sample(nums, 4))

def main():
    turn = 1
    ans = make_question()
    print("Hit&Blowゲーム!")

    while True:
        print("第%02d手目" % turn)
        print("4桁の重複しない数字を入力してください。")
        print("->", end="")
        hit = 0
        blow = 0
        tmp = input()
        your_input = []
        for i in tmp:
            your_input.append(int(i))
        if has_not_duplicates(your_input):
            hit, blow = judgement(your_input, ans)
            print("hit = "+str(hit)+", blow = " + str(blow))
            if hit == 4:
                print("おめでとう! %02d手目で正解です :" % turn)
                break
            else:
                turn += 1



if __name__ == "__main__":
        main()

次回

今回のソースコード一式はgithubにもあります。
https://github.com/gomta777/hitandblow/tree/master/textversion

次回以降は、今回作ったヒットアンドブローゲームにpygameでガワをはっていき、ビデオゲーム風に仕上げていきます(笑)
このままでも、面白いですけどね。

よかったら、ブログ村↓のクリックお願いします。