Pythonアドベントカレンダー2020 の12日目です!
2020/12/13追記:OSやブラウザのバージョンによっては絵文字が表示されないようです。出力はgifにしているので、プログラムは心の目で読んでください。
また記事内のコードをブラウザで実行できるようにしました!
Colaboratoryを開く
完成したもの
ターミナルでゲームボーイを動かしました!
フォントサイズが小さくて分かりにくいですが、各ドットが絵文字🟥🟧🟨🟩🟦🟪🟫⬛⬜で表現されています。
いくつかカラーバリエーションをつくってみました。
点字ver.
点字(⠒, ⠛, ⣤, ⣿)は絵文字を描画できないターミナルでも動くのでうれしい。
(完成しなかったもの)
ボタン入力部分を実装できていないので、まだ遊べません…。タイトル画面を無限に眺めることはできます。今後の課題にします。
環境
![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エスケープコード
基本
基本の部分は @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")
(˘ω˘ )
がくるくる回っているのが確認できたら完璧!!
やっていることは単純で
- 描画する
- 少し待つ
- カーソルを左端に戻す
を繰り返しているだけだ。カーソルを左端に戻して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))
上のコードを実行するとマリオが走る!
ここまでまとめ
ターミナル上でドット絵をアニメーションさせることができた。そしてアニメーションさせるには各フレームでのドット絵が必要であることが分かった。つまりゲームボーイの画面情報さえ取得できれば、ターミナルに描画できる状態だ!
2節でエミュレータの概要を説明したのち、3節でエミュレータの画面情報を取得していく。
2.ゲームボーイエミュレータ「PyBoy」
さすがにエミュレータを自作する力はないので、既存のものを借りる。エミュレータの概要をつかむ前に、ゲームボーイについておさらいしておく。
ゲームボーイの画面について
- 解像度:160×144ドット
- 画面:4階調モノクロ
- RGBだと、(0, 0, 0), (85, 85, 85), (153, 153, 153), (255, 255, 255)の4色
- フレームレート:だいたい30fps
実装の際にこの辺の情報が役に立ったり役に立たなかったりするので参考までに。
PyBoyの導入
基本的には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
操作は以下の表を参考にするといい。
●基本的なキーマップ
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が導入できた!
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文字だからかわからないが、毎フレーム描画するとフレームレートが💩だった。各処理の実行時間を測定してみると、ターミナルに絵文字を描画する部分が完全にボトルネックだったので、これを改善しようとした。
工夫:ターミナルを変える(失敗)
描画が速いターミナルを使えばいいやん、と思って、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