ロゴ ロゴ

【Python】簡単!ミニゲーム製作!【CUI】

ゲームを作るためのライブラリ色々ありすぎてよくわかんないんだけど!?ってなったとき、だったらライブラリなしでコマンドプロンプト上で動くやつを作ればいいんじゃね?っていうことで簡単なゲームを作ってみました。

コマンドプロンプトミニゲーム

私も色々ゲームライブラリ触ってきましたが、ライブラリごとに書き方が色々あって勉強してる間に、めんどくさくなってきたりしますよね。だったら初心に戻ってコマンドプロンプトで動くゲームでよくね?って意味不明な発想になった方にオススメのゲームを紹介しますね。ゲーム操作画面は↓のようなものです。今回はプログラミング言語初心者でもわかるように、オブジェクト指向で書かないように工夫はしたんですが、難易度設定とかちょっとだけ本格的にしようと努力してしまった結果、一部わかりにくいかもしれないです…。

実際のプレイ画面↓(★(プレイヤー)を動かして、◎(コイン)を手に入れる)

ソースコードと解説

最初に言っておくと、winsoundというものを使っている都合上多分windows以外では動かないと思うのでご注意ください。最初はモジュール関係のimportと、グローバルで使う変数の定義です。

import os
import msvcrt
import random
import time
import copy
import winsound
from enum import IntEnum

"""
ゲームモードを定義
"""
class Mode(IntEnum):
    MENU = 0
    PLAY = 1
    SETTING = 2
    BYE = 3

difficulty = 0  # ゲームの難易度(0:easy, 1:normal, 2:hard)
save_map = []  # マップ保存用
cursor = 0  # カーソルの位置
hero = [1, 1]  # 主人公(プレイヤー)の位置
start_time = 0  # 時間計測用
coin = 0  # とったコインの数

ゲームモードはEnumっていう列挙型を使いました。続いて表示するマップの自動生成部分を最初に定義しておきます。

"""
マップの初期化
@return : 生成されたマップ
"""
def map_init():
    addition = difficulty * 5   # 難易度によってマップの大きさを変える
    map = []
    for i in range(10 + addition):
        map.append([])
        for j in range(10 + addition):
            if i == 0 or i == 9 + addition or j == 0 or j == 9 + addition:
                map[i].append("■")  # 一番端はブロックで埋める
            else:
                map[i].append(" ")  # それ以外の場所は空欄(移動できるスペース)
    map[random.randint(1, 8 + addition)][random.randint(1, 8 + addition)] = "◎"
    # ブロックのないランダムな場所にコインを置く

    return map

外周は■で囲んで、プレイヤーが移動できないようにします。そうじゃないと無限遠まで遠くまでいけてしまうので笑 難易度の設定(後述)に応じて、マップのサイズを5マスずつ増やしています。コインの位置はランダムで置くようにしています。ゲームはランダムがないとつまんなくなりますからね…。次はキーボード入力受付に関するものです。

"""
キーボード入力受付
@return : 受け付けたキーボードの指示
"""
def control():
    key = ord(msvcrt.getch())
    if key == 27: #エスケープ
        return 'exit'
    elif key == 13: #エンター
        return "select"
    elif key == 224: #スペシャルキー
        key = ord(msvcrt.getch())
        if key == 72: #上
            return 'up'
        elif key == 80: #下
            return 'down'
        elif key == 77: #右
            return 'right'
        elif key == 75: #左
            return 'left'

    return None

pythonではinputを使ってもキーボード入力ができるんですが、それを使うと入力が終わったらエンターを押さなくちゃならないので、操作性が悪くなるため、キーが押されたら即座に何のキーが押されたか判別できるmsvcrtのgetch()を使いました。これにより、↑キーを押したらエンターを待たずにすぐにreturn ‘up’が返される仕組みです。これから先はゲームの各画面(メニュー、設定、ゲーム、終了)を表示するための関数が続きます。返り値としてすべて、次に遷移するモードを返しています(この方が書きやすかったため)。まずはゲームのメインの部分になります。

"""
ゲーム初期化
"""
def init_game():
    global start_time
    global save_map
    global coin
    global hero
    start_time = time.time()
    save_map = map_init()
    coin = 0
    hero = [1, 1]

"""
ゲームメイン画面
@return : 次表示するゲーム画面
"""
def play():
    global save_map
    global coin
    map = copy.deepcopy(save_map) # 今の状態のマップをコピーしてくる
    # deepcopyは参照渡しにならないように書いている

    # 次移動する先がブロックでなければ移動
    if con == 'up': #上
        if map[hero[0] - 1][hero[1]] != "■":
            hero[0] -= 1
    elif con == 'down': #下
        if map[hero[0] + 1][hero[1]] != "■":
            hero[0] += 1
    elif con == 'right': #右
        if map[hero[0]][hero[1] + 1] != "■":
            hero[1] += 1
    elif con == 'left': #左
        if map[hero[0]][hero[1] - 1] != "■":
            hero[1] -= 1

    if map[hero[0]][hero[1]] == '◎': # 主人公とコインが同じ位置ならば
        coin += 1
        save_map = map_init()  # 新しいマップにする
        winsound.Beep(1000, 50)  # コイン取得の効果音

    map[hero[0]][hero[1]] = '★'  # 主人公を★で表示

    print("==============   play   ==============")
    remaining_time = 20 - int((time.time() - start_time))  # 残り時間
    print("TIME : ", remaining_time)
    print("coin : ", coin)
    for m in map:
        print(*m, sep='')  # マップを表示
    
    if remaining_time <= 0:  # 終了時間を過ぎたら
        winsound.PlaySound('SystemAsterisk', winsound.SND_ASYNC)
        print("Finish!!")
        input("please press the enter key ...")
        return Mode.MENU

    return Mode.PLAY

ゲーム初期化は、ゲームを始めるときにコインが10個もった状態でスタートしたりしないように書いたものです。メインの方は、まずプレイヤーの操作(conは、control関数からの返り値を受け取ってある変数です、この関数の外で定義してあります(後述))を受け付けて、それに応じてhero変数の位置を変更するものです。コインを取ったら、マップがリセットされた新たな場所にコインが生成されるという仕組みです。最後は終了時間を計って、今回は20秒経過したら、ゲームを終了するという仕組みです。winsound.PlaySoundはwindowsの標準に入っている音を出します。なのでこの音を出すためにmp3といった外部ファイルは必要ありません。ちなみにdeepcopyの参照渡しとは、そのままコピーするとそこで値を変更すると、コピー元の値も変えちゃうので、プレイヤーの軌跡が全部表示されてしまうので、新たなリストを生成してコピー元の値に影響を与えないようにするっていうイメージです。次は難易度設定についてです。

"""
セッティング画面
@return : 次表示するゲーム画面
"""
def setting():
    global cursor
    global difficulty

    if con == "select":  # エンターキー
        if cursor == 0:
            difficulty = 0
        elif cursor == 1:
            difficulty = 1
        elif cursor == 2:
            difficulty = 2
        cursor = 0
        winsound.PlaySound('SystemAsterisk', winsound.SND_ASYNC)
        return Mode.MENU
    elif con == "up":  # 上
        if cursor != 0:
            cursor -= 1
    elif con == "down":  # 下
        if cursor != 2:
            cursor +=1

    print("==============  setting  ==============")
    menu = ["easy","normal","hard"]
    for i, m in enumerate(menu):
        if i == difficulty:
            menu[i] = m + ' <-now'  # 現在の難易度をわかりやすく表示
    for i, str in enumerate(menu):
        if cursor == i:
            print("              *",str)  # 現在のカーソルの位置を表示
        else:
            print("              ",str)

    return Mode.SETTING

ゲームの醍醐味として難易度がありますよね。簡単なものからだんだん難しくなるとより面白くなると思います。それを簡単に実現するために今回はマップの大きさを変えるといったことで難易度を変更します。ここではその難易度を0(easy),1(normal),2(hard)の3段階で変えることができます。それぞれのどこが今選択されているかを可視化するためのifが後ろのほうにめちゃくちゃあります。次は終了処理です。

"""
終了画面
@return : 次表示するゲーム画面
"""
def bye():
    winsound.PlaySound('SystemHand',winsound.SND_ASYNC)
    print("good bye!")
    time.sleep(2)
    exit()  # ここで終了する

    return Mode.BYE  # 関数の書き方を統一するための返り値なため無意味

ゲームを終了するのは単純にexit()を呼べば終了します。他のゲーム関数と書き方を合わせるためにreturnを記述していますが、ここまでは通りません。次はメニュー画面です。

"""
メニュー画面
@return : 次表示するゲーム画面
"""
def menu():
    global cursor
    if con == "select":  # エンターキー
        if cursor == 0:
            cursor = 0
            init_game()
            winsound.PlaySound('SystemAsterisk', winsound.SND_ASYNC)
            return Mode.PLAY
        elif cursor == 1:
            cursor = 0
            return Mode.SETTING
        elif cursor == 2:
            return Mode.BYE
    elif con == "up":  # 上
        if cursor != 0:
            cursor -= 1
    elif con == "down":  # 下
        if cursor != 2:
            cursor +=1

    print("==============   menu   ==============")
    menu = ["play", "setting", "bye"]
    for i, str in enumerate(menu):
        if cursor == i:
            print("              *",str)  # 現在のカーソルの位置を表示
        else:
            print("              ",str)
    
    return Mode.MENU

いたってシンプルですね。最後にこれらをすべて連結するためのコードを記述しておしまいです。

if __name__ == "__main__":
    mode_list = {Mode.MENU:menu, Mode.PLAY:play, Mode.SETTING:setting, Mode.BYE:bye}
    mode = Mode.MENU  # 起動時のゲーム画面はメニュー
    while True:
        os.system('cls')  # 画面を綺麗にする
        # 一番上の画面デザインを表示する
        print(" ∧_∧  Command prompt mini game!")
        print("(。・ω・。)つ━☆・*。")
        print("⊂   ノ    ・゜+.")
        print(" しーJ   °。+ *´¨)")
        print("         .· ´¸.·*´¨) ¸.·*¨)")
        print("          (¸.·´ (¸.·'* ☆")
        print("")
        con = ''
        if msvcrt.kbhit():
            con = control()  # キーボード操作を受け付ける
        if con == "exit":
            exit()  # Escが押されたらすぐ終了
        mode = mode_list[mode]()  # 各ゲームモード(関数)へ入る

コマンドプロンプトでプログラミングしたことがある方はわかると思いますが、printの画面表示はどんどん下につらなっていくと思います。それではループする度に画面が流れて非常に見難いものなってしまうので、osのsystem(‘cls’)を使って、画面を一度綺麗にしてからprintすることで、カーソル以外は毎回同じprintをするので変わらなく表示することができ、あたかもカーソルだけが動いているように見せることができます。ここで定義したconがすべてのゲーム画面で使うことができる、キー操作の変数です。

おわりに

いかがでしたか?ちなみにこのゲームの最高得点はどれくらいになりましたか?私はだいたい18ぐらいですね…。

ぶっちゃけ、実はこれに主人公のイラストつけて、背景のイラストつければそのままよくあるゲームになるので労力はあんまり変わらない気が…。まあ、コマンドプロンプト上でもそこそこのゲームが作れるんだ~くらいに思ってもらえれば嬉しいです!

コメント入力

関連サイト