138
100

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PythonAdvent Calendar 2020

Day 12

ターミナル上でゲームボーイを動かす【Python】

Last updated at Posted at 2020-12-12

Pythonアドベントカレンダー2020 の12日目です!

2020/12/13追記:OSやブラウザのバージョンによっては絵文字が表示されないようです。出力はgifにしているので、プログラムは心の目で読んでください。
また記事内のコードをブラウザで実行できる
ようにしました!
Colaboratoryを開く

完成したもの

ターミナルでゲームボーイを動かしました!
フォントサイズが小さくて分かりにくいですが、各ドットが絵文字🟥🟧🟨🟩🟦🟪🟫⬛⬜で表現されています。

color_cool.gif

いくつかカラーバリエーションをつくってみました。

目に優しいver.
color_hot-min.gif

点字ver.
点字(⠒, ⠛, ⣤, ⣿)は絵文字を描画できないターミナルでも動くのでうれしい。
color_mono_Trim.gif

(完成しなかったもの)

ボタン入力部分を実装できていないので、まだ遊べません…。タイトル画面を無限に眺めることはできます。今後の課題にします。

環境

Windows10-Education ![Windows terminal](https://img.shields.io/badge/Terminal-Windows terminal-brightgreen) ![python](https://img.shields.io/badge/Pythonl-3.6, 3.7-brightgreen)

ゲームボーイエミュレータのPyBoyのWindows version がPython 3.6 and 3.7しか対応していないので注意です(2020//12/12現在)。OSとターミナルは何でも良いはずですが、絵文字を描画できるターミナルでないとカラーでは実行できません。ただしオプション引数を指定すれば、絵文字が描画できないターミナルでも点字を使ってゲームボーイができるようにしました!あとゲームボーイのROMファイルhoge.gbも必要です。がんばってカセットから吸い出しましょう。

実装はこちら→GitHub: dannyso16/Gameboy-on-terminal

以下丁寧語やめます。

記事の流れ

以下の順に説明していく。

  1. ANSIエスケープコードとは?
  2. ゲームボーイエミュレータ「PyBoy」の概要
  3. pyboyから画面情報を抜き出す
  4. 実装

1.ANSIエスケープコード

基本

基本の部分は @kuroituさんの記事「Pythonで進捗表示したい Qiita」が分かりやすいので、「発展ansiエスケープコード」の部分を見てほしい。

要はANSIエスケープコードを使えばカーソルの制御ができるもので、「カーソルを上に3行動かす」ようなことができる。

ANSIエスケープコード 効果
\033[nA カーソルを上にn行移動
\033[nB カーソルを下にn行移動
\033[nC カーソルを右にn移動
\033[nD カーソルを左にn移動
\033[nK n=0のときはカーソルより後ろの文字列を削除、
n=1のときはカーソルより前の文字列を削除、
n=2のときは行全体を削除

これを応用すると、さきに紹介した記事のようにプログレスバーを自由に実装出来たりする。まずは簡単な例から動かして理解を深めよう。適当な顔文字をアニメーションさせてみる。

2020/12/13追記:Colaboratoryで試せるようにしました!→Colaboratoryを開く

import time

spin_characters = ["( ˘ω˘  )",
                   "(   ˘ω )",
                   "(     ˘)",
                   "(˘     )",
                   "(ω˘    )",
                   "(˘ω˘   )"
                   ]
cycle = len(spin_characters)

for i in range(cycle*5):
    i %= cycle
    s = spin_characters[i]
    print(s, end="")
    time.sleep(0.2)
    print("\033[1A")

# 後処理
print("\033[2K")

(˘ω˘ )がくるくる回っているのが確認できたら完璧!!

kao_qiita_Trim.gif

やっていることは単純で

  1. 描画する
  2. 少し待つ
  3. カーソルを左端に戻す

を繰り返しているだけだ。カーソルを左端に戻してprintすると上書きされるのがポイントだ。

絵文字でドット絵をつくる

ドット絵を表すために、色つきの四角形の絵文字🟥🟧🟨🟩🟦🟪🟫⬛⬜を使う。ちょっと意外だが、🟥🟧🟨🟩🟦🟪🟫の7つは、2019年の Unicode 12.0 で新しく追加されたものらしい。

とりあえずマリオのドット絵を書いてみた

# マリオ:ドット絵は右を参考に書いたhttps://www.spriters-resource.com/nes/supermariobros/sheet/50365/
stand = """
⬜⬜⬜⬜⬜⬜🟥🟥🟥🟥🟥⬜⬜⬜⬜⬜
⬜⬜⬜⬜🟥🟥🟥🟥🟥🟥🟥🟥🟥⬜⬜⬜
⬜⬜⬜⬜🟫🟫🟫🟨🟨🟫🟨⬜⬜⬜⬜⬜
⬜⬜⬜🟫🟨🟫🟨🟨🟨🟫🟨🟨🟨⬜⬜⬜
⬜⬜⬜🟫🟨🟫🟫🟨🟨🟨🟫🟨🟨🟨⬜⬜
⬜⬜⬜🟫🟫🟨🟨🟨🟨🟫🟫🟫🟫⬜⬜⬜
⬜⬜⬜⬜⬜🟨🟨🟨🟨🟨🟨🟨⬜⬜⬜⬜
⬜⬜⬜⬜🟥🟥🟦🟥🟥🟥⬜⬜⬜⬜⬜⬜
⬜⬜⬜🟥🟥🟥🟦🟥🟥🟦🟥🟥🟥⬜⬜⬜
⬜⬜🟥🟥🟥🟥🟦🟦🟦🟦🟥🟥🟥🟥⬜⬜
⬜⬜🟨🟨🟥🟦🟨🟦🟦🟨🟦🟥🟨🟨⬜⬜
⬜⬜🟨🟨🟨🟦🟦🟦🟦🟦🟦🟨🟨🟨⬜⬜
⬜⬜🟨🟨🟦🟦🟦🟦🟦🟦🟦🟦🟨🟨⬜⬜
⬜⬜⬜⬜🟦🟦🟦⬜⬜🟦🟦🟦⬜⬜⬜⬜
⬜⬜⬜🟫🟫🟫⬜⬜⬜⬜🟫🟫🟫⬜⬜⬜
⬜⬜🟫🟫🟫🟫⬜⬜⬜⬜🟫🟫🟫🟫⬜⬜"""

なかなかの出来栄えだ!

ドット絵アニメーション

まずは描画したいデータを頑張ってつくる。パラパラ漫画のやり方だ。

spin_characters = [
    """
⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜⬜🟥🟥🟥🟥🟥⬜⬜⬜⬜⬜
⬜⬜⬜⬜🟥🟥🟥🟥🟥🟥🟥🟥🟥⬜⬜⬜
⬜⬜⬜⬜🟫🟫🟫🟨🟨🟫🟨⬜⬜⬜⬜⬜
⬜⬜⬜🟫🟨🟫🟨🟨🟨🟫🟨🟨🟨⬜⬜⬜
⬜⬜⬜🟫🟨🟫🟫🟨🟨🟨🟫🟨🟨🟨⬜⬜
⬜⬜⬜🟫🟫🟨🟨🟨🟨🟫🟫🟫🟫⬜⬜⬜
⬜⬜⬜⬜⬜🟨🟨🟨🟨🟨🟨🟨⬜⬜⬜⬜
⬜⬜⬜⬜🟥🟥🟥🟦🟥🟥⬜⬜⬜⬜⬜⬜
⬜⬜⬜🟥🟥🟥🟥🟦🟦🟥🟥⬜⬜⬜⬜⬜
⬜⬜⬜🟥🟥🟥🟦🟦🟨🟦🟦⬜⬜⬜⬜⬜
⬜⬜⬜🟥🟥🟥🟥🟦🟦🟦🟦⬜⬜⬜⬜⬜
⬜⬜⬜🟦🟥🟥🟨🟨🟦🟦🟦⬜⬜⬜⬜⬜
⬜⬜⬜⬜🟦🟥🟨🟨🟦🟦⬜⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜🟦🟦🟦🟫🟫🟫⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜🟫🟫🟫🟫⬜⬜⬜⬜⬜⬜⬜""",

    """
⬜⬜⬜⬜⬜⬜🟥🟥🟥🟥🟥⬜⬜⬜⬜⬜
⬜⬜⬜⬜🟥🟥🟥🟥🟥🟥🟥🟥🟥⬜⬜⬜
⬜⬜⬜⬜🟫🟫🟫🟨🟨🟫🟨⬜⬜⬜⬜⬜
⬜⬜⬜🟫🟨🟫🟨🟨🟨🟫🟨🟨🟨⬜⬜⬜
⬜⬜⬜🟫🟨🟫🟫🟨🟨🟨🟫🟨🟨🟨⬜⬜
⬜⬜⬜🟫🟫🟨🟨🟨🟨🟫🟫🟫🟫⬜⬜⬜
⬜⬜⬜⬜⬜🟨🟨🟨🟨🟨🟨🟨⬜⬜⬜⬜
⬜⬜⬜⬜⬜⬜⬜🟥🟦🟦🟥⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜🟥🟥🟥🟥🟦🟥🟨🟨⬜⬜⬜
⬜⬜⬜🟨🟨🟥🟥🟥🟥🟥🟥🟨🟨🟨⬜⬜
⬜⬜🟨🟨🟨🟦🟥🟥🟥🟥🟥🟨🟨⬜⬜⬜
⬜⬜⬜🟫🟫🟦🟦🟦🟦🟦🟦🟦⬜⬜⬜⬜
⬜⬜⬜🟫🟦🟦🟦🟦🟦🟦🟦🟦⬜⬜⬜⬜
⬜⬜🟫🟫🟦🟦⬜⬜🟦🟦🟦⬜⬜⬜⬜⬜
⬜⬜🟫⬜⬜⬜⬜🟫🟫🟫⬜⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜⬜⬜⬜🟫🟫🟫⬜⬜⬜⬜⬜""",

    """
⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜⬜🟥🟥🟥🟥🟥⬜⬜⬜⬜⬜
⬜⬜⬜⬜🟥🟥🟥🟥🟥🟥🟥🟥🟥⬜⬜⬜
⬜⬜⬜⬜🟫🟫🟫🟨🟨🟫🟨⬜⬜⬜⬜⬜
⬜⬜⬜🟫🟨🟫🟨🟨🟨🟫🟨🟨🟨⬜⬜⬜
⬜⬜⬜🟫🟨🟫🟫🟨🟨🟨🟫🟨🟨🟨⬜⬜
⬜⬜⬜🟫🟫🟨🟨🟨🟨🟫🟫🟫🟫⬜⬜⬜
⬜⬜⬜⬜⬜🟨🟨🟨🟨🟨🟨🟨⬜⬜⬜⬜
⬜⬜⬜⬜🟥🟥🟥🟦🟥🟥⬜⬜⬜⬜⬜⬜
⬜⬜⬜🟥🟥🟥🟥🟦🟦🟥🟥⬜⬜⬜⬜⬜
⬜⬜⬜🟥🟥🟥🟦🟦🟨🟦🟦⬜⬜⬜⬜⬜
⬜⬜⬜🟥🟥🟥🟥🟦🟦🟦🟦⬜⬜⬜⬜⬜
⬜⬜⬜🟦🟥🟥🟨🟨🟦🟦🟦⬜⬜⬜⬜⬜
⬜⬜⬜⬜🟦🟥🟨🟨🟦🟦⬜⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜🟦🟦🟦🟫🟫🟫⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜🟫🟫🟫🟫⬜⬜⬜⬜⬜⬜⬜""",

    """
⬜⬜⬜⬜⬜⬜🟥🟥🟥🟥🟥⬜⬜⬜⬜⬜
⬜⬜⬜⬜🟥🟥🟥🟥🟥🟥🟥🟥🟥⬜⬜⬜
⬜⬜⬜⬜🟫🟫🟫🟨🟨🟫🟨⬜⬜⬜⬜⬜
⬜⬜⬜🟫🟨🟫🟨🟨🟨🟫🟨🟨🟨⬜⬜⬜
⬜⬜⬜🟫🟨🟫🟫🟨🟨🟨🟫🟨🟨🟨⬜⬜
⬜⬜⬜🟫🟫🟨🟨🟨🟨🟫🟫🟫🟫⬜⬜⬜
⬜⬜⬜⬜⬜🟨🟨🟨🟨🟨🟨🟨⬜⬜⬜⬜
⬜⬜🟥🟥🟥🟥🟦🟥🟥🟥🟦⬜⬜⬜⬜⬜
🟨🟨🟥🟥🟥🟥🟦🟦🟥🟥🟥🟦🟥🟨🟨🟨
🟨🟨🟨⬜🟥🟥🟦🟦🟦🟦🟦🟦🟥🟥🟨🟨
🟨🟨⬜⬜🟦🟦🟦🟨🟦🟦🟦🟨⬜⬜🟫⬜
⬜⬜⬜🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦🟫🟫⬜
⬜⬜🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦🟫🟫⬜
⬜🟫🟫🟦🟦🟦⬜⬜⬜⬜🟦🟦🟦🟫🟫⬜
⬜🟫🟫🟫⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜
⬜⬜🟫🟫🟫⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜
""",

    """
⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜⬜🟥🟥🟥🟥🟥⬜⬜⬜⬜⬜
⬜⬜⬜⬜🟥🟥🟥🟥🟥🟥🟥🟥🟥⬜⬜⬜
⬜⬜⬜⬜🟫🟫🟫🟨🟨🟫🟨⬜⬜⬜⬜⬜
⬜⬜⬜🟫🟨🟫🟨🟨🟨🟫🟨🟨🟨⬜⬜⬜
⬜⬜⬜🟫🟨🟫🟫🟨🟨🟨🟫🟨🟨🟨⬜⬜
⬜⬜⬜🟫🟫🟨🟨🟨🟨🟫🟫🟫🟫⬜⬜⬜
⬜⬜⬜⬜⬜🟨🟨🟨🟨🟨🟨🟨⬜⬜⬜⬜
⬜⬜⬜⬜🟥🟥🟥🟦🟥🟥⬜⬜⬜⬜⬜⬜
⬜⬜⬜🟥🟥🟥🟥🟦🟦🟥🟥⬜⬜⬜⬜⬜
⬜⬜⬜🟥🟥🟥🟦🟦🟨🟦🟦⬜⬜⬜⬜⬜
⬜⬜⬜🟥🟥🟥🟥🟦🟦🟦🟦⬜⬜⬜⬜⬜
⬜⬜⬜🟦🟥🟥🟨🟨🟦🟦🟦⬜⬜⬜⬜⬜
⬜⬜⬜⬜🟦🟥🟨🟨🟦🟦⬜⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜🟦🟦🟦🟫🟫🟫⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜🟫🟫🟫🟫⬜⬜⬜⬜⬜⬜⬜""",
]

あとはさっきの要領で、描画→カーソルを左上に戻す→描画… とくり返せばいい。

import time

FPS = 20

rows = spin_characters[0].split("\n")
rows = [r for r in rows if r]
height = len(rows)

during = 50
cycle = len(spin_characters)

for i in range(during):
    rows = spin_characters[i % cycle].split("\n")
    rows = [r for r in rows if r]

    for row in rows:
        # row = "⬜"*i + row
        print(row)
    time.sleep(1/FPS)
    print("\033[{}A".format(height+1))
else:
    # 全部終わったら"立ち"状態を表示
    rows = stand.split("\n")
    rows = [r for r in rows if r]

    for row in rows:
        # row = "⬜"*i + row
        print(row)
    time.sleep(0.05)
    print("\033[{}A".format(height+1))

# 後処理
print("\033[{}B".format(height+1))

上のコードを実行するとマリオが走る!

mario_qiita_Trim.gif

ここまでまとめ

ターミナル上でドット絵をアニメーションさせることができた。そしてアニメーションさせるには各フレームでのドット絵が必要であることが分かった。つまりゲームボーイの画面情報さえ取得できれば、ターミナルに描画できる状態だ!

2節でエミュレータの概要を説明したのち、3節でエミュレータの画面情報を取得していく。

2.ゲームボーイエミュレータ「PyBoy」

さすがにエミュレータを自作する力はないので、既存のものを借りる。エミュレータの概要をつかむ前に、ゲームボーイについておさらいしておく。

ゲームボーイの画面について

  • 解像度:160×144ドット
  • 画面:4階調モノクロ
    • RGBだと、(0, 0, 0), (85, 85, 85), (153, 153, 153), (255, 255, 255)の4色
  • フレームレート:だいたい30fps

実装の際にこの辺の情報が役に立ったり役に立たなかったりするので参考までに。

PyBoyの導入

pyboy.jpg

基本的にはPyBoy installation (GitHub)に丁寧に書いてあるのでこれに従えばいい。LinuxやMacはコマンド一発で入るが、Windowsはちょっとめんどくさい。Windows10の場合を以下に乗せておく(興味ない方はここまでスキップ)。

$ python --version # 3.6 か 3.7 であることを確認する

# SDLの導入
$ (New-Object Net.WebClient).DownloadFile('https://www.libsdl.org/release/SDL2-devel-2.0.10-VC.zip', 'SDL2-devel-2.0.10-VC.zip')
$ Expand-Archive -Force 'SDL2-devel-2.0.10-VC.zip' C:\SDL2\

自分のPCだとExpand-Archive -Force 'SDL2-devel-2.0.10-VC.zip' C:\SDL2\がうまく動作しなかったので、GUIでzipを解凍したのちC:\SDL2\配下に置いた。

# パスを通す
$ setx PYSDL2_DLL_PATH C:\SDL2\SDL2-2.0.10\lib\x64
$ setx PATH "%PATH%;C:\SDL2\SDL2-2.0.10\lib\x64"> 

$ exit # (ターミナルを再起動しとく)

$ python -m pip install --upgrade --user pip
$ python -m pip install --user pyboy

ここまで来たら、実際にエミュレータを起動できる。

python -m pyboy path/to/rom.gb

pyboy_installation_demo.png
図:自分はポケモン青で試した、起動すると嬉しい。


操作は以下の表を参考にするといい。

●基本的なキーマップ

Keyboard key GameBoy equivalant
Up Up
Down Down
Left Left
Right Right
A A
S B
Return Start
Backspace Select

●特殊操作

Keyboard key Emulator function
Escape Quit
D Debug
Space Unlimited FPS
Z Save state
X Load state
I Toggle screen recording
, Rewind backwards
. Rewind forward

なお手元にROMファイルがない場合は、pyboy/pyboy/default_rom.gbにテスト用のROMがあるのでそれで試すといい。

pyboy_installation_demo2.png

これで PyBoyが導入できた!

3.PyBoyから画面情報を抜き出す

スクリーンショット機能もあるので、なんか良いのないかとソースコードを眺めてたら、AI用のbotsupportなるものがあった。

pyboy.botsupport documentation

この中のscreen()が使えそうである。

def screen(self)

Use this method to get a Screen object. This can be used to get the screen buffer in a variety of formats.

It's also here you can find the screen position (SCX, SCY, WX, WY) for each scan line in the screen buffer. See Screen.tilemap_position() for more information.

Returns

Screen: A Screen object with helper functions for reading the screen buff

さらにラッキーなことに return されるScreen object には np.array で値を返すメソッドがある。

def screen_ndarray(self)

Provides the screen data in NumPy format. The dataformat is always RGB.

Returns

numpy.ndarray:
Screendata in ndarray of bytes with shape (160, 144, 3)

ありがてえ。さらにソースを眺めていると、OpenAIGymのためのクラスも作られている。観測として画面情報が与えられているので、observationに画面情報を代入する部分を流用して実装できそうだ。

# Pyboy/pyboy/openai_gym.py より
def _get_observation(self):
        if self.observation_type == "raw":
            observation = np.asarray(self.pyboy.botsupport_manager().screen().screen_ndarray(), dtype=np.uint8)
        elif self.observation_type in ["tiles", "compressed", "minimal"]:
            observation = self.game_wrapper._game_area_np(self.observation_type)
        else:
            raise NotImplementedError(f"observation_type {self.observation_type} is invalid")
        return observation

4.実装

GitHubを見てください→GitHub: dannyso16/Gameboy-on-terminal

絵文字は4byte文字だからかわからないが、毎フレーム描画するとフレームレートが💩だった。各処理の実行時間を測定してみると、ターミナルに絵文字を描画する部分が完全にボトルネックだったので、これを改善しようとした。

tooooo_slow.gif

工夫:ターミナルを変える(失敗)

描画が速いターミナルを使えばいいやん、と思って、Alacrittyを試した。残念ながら絵文字は表示できなかった。断念。

工夫:描画をさぼる

仕方がないので、数フレームに一回描画をすることでフレームレートを30あたりに保つようにした。ただしスキップする割合は完全にハードコーディングしてしまったので、実行するPCによって調整してください…。

まとめ

ターミナルの描画速度なんて考えたこともなかったのですが、ゲームをするにはターミナルの描画の遅さが課題となりました。**ターミナルはゲームを遊ぶのに適していないようです!**ターミナルで1万文字以上の描画を高速で行いたい需要はなさそうなので仕方がないですが、いつかターミナルで遊べるようになったらいいですね。

余談

実は11月くらいからプログレスバーの代わりにマリオを躍らせる記事をこっそり書いていましたが、先を越されました。(Pythonで進捗表示したい!@kuroitu on Qiita)。自分のやつより面白くて完全に敗北したので、趣向を変えてゲームの記事にした経緯があったりします。

あと点字で描画すると絵文字で描画するより明らかに frame rate が高くなる現象は、原因がわからずじまいでした。(点字は3byte文字で絵文字が4byte文字だからっぽい?)。

参考

Qiita記事にさりげなく(けど、わかりやすく)環境を記載する Qiita
いろいろな色の四角形 絵文字 Lets emoji
Pythonで進捗表示したい Qiita
ゲームボーイ wikipedia
spriters-resource: mario-bros

Pyboy関係
Baekalfen/PyBoy GitHub
PyBoy installation GitHub
PyBoy documentation
pyboy.botsupport documentation

138
100
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
138
100

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?