buto > /dev/null

だいたい急に挑戦してゴールにたどり着かずに飽きる日々です

pyxel ゲーム画面遷移+動きをつける

前回はpyxelでゲーム画面表示+キャラの移動までできた pyxelであつ森を知ったかぶりする

ソースコードはこちら

家に着いたらローディング画面を表示

キャラが家に着いたらこのローディング画面に遷移させたい

f:id:butorisa:20201020173318p:plain

こちらの記事をお手本にしました たぶんどんな環境でも使えるであろう、ゲーム画面遷移の作り方になります~

class GameMode(Enum):
    """ 画面一覧 """
    # 町
    Town = auto()
    # 家の中
    Home = auto()

画面遷移を制御するEnumを定義

def switch_active(self, obj, target):
    """ オブジェクトが重なったら一方を非表示にする """
    if abs(obj.position[0] - target.position[0]) < 10\
            and abs(obj.position[1] - target.position[1]) < 10:
        obj.active = False
        # ロード画面
        self.game_mode = GameMode.Home
    else:
        obj.active = True
        # ゲーム画面
        self.game_mode = GameMode.Town

元々作っていたキャラが家に着いたらキャラを非表示にする処理 → ゲームモードのEnumを切り替える

def draw(self):
    """ 画面描画 """
    if self.game_mode == GameMode.Town:
        self.draw_town()
    elif self.game_mode == GameMode.Home:
        self.draw_loading()
        # self.draw_home()

def draw_town(self):
    """ 町を描画 """
    # 背景
    pyxel.cls(7)
    pyxel.bltm(0, 0, 0, 0, 0, 16, 16)
    # 家
    pyxel.blt(self.home.position[0], self.home.position[1], 0, 16, 0, 16, 16, 5)
    # うさぎ
    pyxel.blt(40, 80, 0, 48, 0, 16, 16, 5)
    # プレイヤー
    if self.player.active:
        pyxel.blt(self.player.position[0], self.player.position[1], 0, 0, 0, 16, 16, 5)

def draw_home(self):
    """ 家の中を描画 """
    pass

def draw_loading(self):
    """ ロード画面を描画 """
    pyxel.cls(7)
    pyxel.bltm(0, 0, 0, 16, 0, 16, 16)

ゲームモードによって描画する画面を制御

家に着いたらうさぎが表示されるようになった!! レイアウト変更した^^

f:id:butorisa:20201020173340g:plain

ローディング画面のうさちゃんをジャンプさせる

こちらの記事をお手本にしました Python:pyxelで作ったレトロ風タイトル画面

まずうさちゃんジャンプ画像を用意

f:id:butorisa:20201020173403p:plain

def draw_loading(self):
    """ ロード画面を描画 """
    # ウサギがジャンプする画像を表示
    if self.flip:
        tm_x = 32
        tm_y = 0
    else:
        tm_x = 16
        tm_y = 0

    pyxel.cls(7)
    pyxel.bltm(0, 0, 0, tm_x, tm_y, 16, 16)

    if pyxel.frame_count % 15 == 0:
        self.flip = not self.flip

ゲームフレームが15の倍数の時に通常⇔ジャンプ画像を切り替える タイルマップも座標を指定して表示できる ※ self.flip__init__()関数で初期化しています

ローディング画面でうさちゃんがジャンプ!

f:id:butorisa:20201020173419g:plain

pyxelであつ森を知ったかぶりする

最近Twitterではあつ森が流行ってますね ゲームキューブ版はやってたなあ ゲーム始めちゃうと一日中やってしまうのであつ森は買わない!でも正直やりたい… 自分でゲーム作るならOKだよね?プログラミングだし!

ドットでかわいいゲームが作れるpyxel

pyxel公式ドキュメントが丁寧に書かれているので、つまずくことなくインストール完了 (pyenvを入れ直してpython3.8.2の環境にpyxelをインストール)

pyxelで使えるショートカットキーがあるのが親切

ゲームキャラを作る

pyxelに搭載されているPyxel Editorでドット絵が描ける! ターミナルでpyxeleditor ファイル名を実行するとエディタが起動 (ファイル名は拡張子なしでコマンド実行しても自動でつけてくれてた)

f:id:butorisa:20201020172908p:plain

このエディタでもスクリーンショットのショートカットキー使えた

背景を作る

背景はエディタの左上の□が4つある「タイルマップエディタ」で作れる 右下から背景にしたい画像を選択、左側でドラッグすると画像が貼り付けられる

f:id:butorisa:20201020173025p:plain

ゲーム画面を表示する

こちらの記事をお手本にコーディングしました 【Pythonでゲームを作ろう!】レトロな2Dゲームを作ってみた!

コード一式はこちら

draw関数に画面に表示する処理を書く blt()でエディタで作成した画像を表示、bltm()でタイルマップを表示する エディタで作成した.pyxresファイルは1枚の画像なのでblt()の第4、5引数に座標を指定して プレイヤー、家など個々の画像を切り出して画面に表示する ※ 座標は2枚目のエディタ画面のように右側のサムネイルにカーソルを合わせると上部に表示される

def draw(self):
    """ 画面描画 """
    # 背景
    pyxel.cls(7)
    # bltm(x, y, tm, u, v, w, h, [colkey]) colkey指定なし
    pyxel.bltm(0, 0, 0, 0, 0, 16, 16)
    # プレイヤー描画
    if self.player.active:
        # blt(x, y, img, u, v, w, h, [colkey]) colkey=5で藍色を透過
        pyxel.blt(self.player.position[0], self.player.position[1], 0, 0, 0, 16, 16, 5)
    # 家描画
    pyxel.blt(self.home.position[0], self.home.position[1], 0, 16, 0, 16, 16, 5)
    # うさぎ
    pyxel.blt(16, 24, 0, 48, 0, 16, 16, 5)

ゲームキャラを動かす

矢印キーで動くようにしました 「>0」とかは画面からはみ出ない制御です

class Player:
    """ プレイヤーのコントロール """
    def __init__(self):
        self.active = True
        self.position = [48, 47]

    def action(self):
        """ プレイヤー移動 """
        if pyxel.btnp(pyxel.KEY_LEFT) and self.position[0] > 0:
            self.position[0] -= 10
        if pyxel.btnp(pyxel.KEY_RIGHT) and self.position[0] < 110:
            self.position[0] += 10
        if pyxel.btnp(pyxel.KEY_UP) and self.position[1] > 0:
            self.position[1] -= 10
        if pyxel.btnp(pyxel.KEY_DOWN) and self.position[1] < 110:
            self.position[1] += 10

超ロークオリティあつ森の完成

全然あつ森じゃない… 女の子が原っぱ駆け巡ってるだけだわ…

f:id:butorisa:20201020173046g:plain

パッケージ管理ライブラリ pyflowを使えなかった話

パッケージ管理も流行に乗りたい!!

仮想環境自体いらないレベルだけど「新しいでしょ」って言いたいからpyenv卒業! 2020 年の Python パッケージ管理ベストプラクティスを読んで pyflowを推してるっぽかったから早速インストール!!

pyflowインストール

インストールはこちらにアクセス 私はMacなのでzipped Mac binaryをクリックして実行ファイルをダウンロード undefined.jpg 「pyflow」っていう実行ファイルがダウンロードされるのでそれを(とりあえず)ホームディレクトリに配置しておく パス通せば場所はどこでも良さそう

仮想環境を作る

ターミナルを開いて、pyflow new 環境名するとPythonバージョンを聞かれる

~ % pyflow new pretty-hopper
Please enter the Python version for this project: (eg: 3.8)
3.8
Created a new Python project named pretty-hopper

pyxelでゲーム作りたいので3.8と入力してEnter

作成された環境(プロジェクト)はこれ

pretty_hopper
┝ .git
┝ .gitignore
┝ LICENSE
┝ README.md
┝ pretty_hopper
    ┝ __init__.py
┝ pyproject.toml

ライブラリインストール

pyflow install ライブラリでインストールできる

pretty-hopper % pyflow install pyxel      
Automatic installation of Python 3.8 on Mac is currently unsupported. If you'd like to use this version of Python, please install it.

なんと…python3.8はNGだとのこと pyflow resetでプロジェクトを削除してからプロジェクトディレクトリを消して3.7で再作成 そしてリトライ!あれ?エラーが

pretty-hopper % pyflow install pyxel
Found lockfile
⬇ Installing pyxel 1.3.7 ...
Added a console script: install_pyxel_examples
Added a console script: pyxeleditor
Added a console script: pyxelpackager
⬇ Installing pyinstaller 3.6.0 ...
EX PAR: "__pypackages__/3.7/lib/PyInstaller-3.6" bin: "__pypackages__/3.7/.venv/bin"
thread 'main' panicked at 'Problem running setup.py bdist_wheel: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/libcore/result.rs:1188:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

1回エラーが出るとpyflow listとかもエラーになってしまう…闇が深すぎる… ちなみにPython3.7環境を作ってすぐにpyflow install numpyはインストール成功した pip使わない弊害かな

とりあえず、pyxelでゲーム作りたいのでpipenvかpoetryにしよう

Python DB登録

SQLAlchemyを使ってMySQLにデータ登録

画面の入力フォームの値をDBに登録する(レイアウトは後回し)

f:id:butorisa:20201020172623p:plain

データを登録するテーブル定義

# カラム名 データ型 制約
1 no int primary_key, auto_increment
2 title varchar(100) not null
3 content varchar(1000) -

入力フォームを作成

入力フォームはいつもの書き方で <input name="xxx">で項目名を設定

        <form action="/post" method="post">
            <div class="input-group mb-3">
              <div class="input-group-prepend">
                <span class="input-group-text" id="basic-addon1">Title</span>
              </div>
              <input type="text" class="form-control" name="title" aria-label="Title" aria-describedby="basic-addon1">
            </div>
            <div class="input-group">
              <div class="input-group-prepend">
                <span class="input-group-text">content</span>
              </div>
              <textarea class="form-control" name="content" aria-label="With textarea"></textarea>
            </div>
            <button type="submit" class="btn btn-outline-info">submit</button>
        </form>

DB登録処理を作成

引数に登録するオブジェクトを取りsession.add()で登録する

    def register_item(self, item=None):
        """ itemテーブル登録 """
        session = db.create_session()
        session.add(item)
        session.commit()

Controllerを定義

POSTでリクエストが来たらDB登録処理を呼んで、画面表示処理を実行 request.form['xxx'](フォームのname属性)で入力値が受け取れる auto_incrementのカラムは値を設定しなくてOK

@app.route('/post', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        # DB登録するオブジェクトを生成
        target = Item(title=request.form['title'], content=request.form['content'])
        Item.register_item(app, item=target)

        # 画面再表示
        return show_mindmap()

ソースコード一式はgithubに置きました

SQL弱者としてはSELECTとINSERTができればかなり満足

Python DBデータを取得する

Flask+jinja2+MySQLでちょっとした画面を作る

お手軽WebフレームワークFlaskでMySQLに登録したデータを表示する画面を作ってみる!

環境構築

  1. MySQLをインストール
    • brew install mysql
    • brew services start mysql
    • mysql -uroot
    • クライアントツールはMySQL Workbenchを利用
    • クライアントツールからスキーマ、テーブル、データを作成
  2. Pythonライブラリをインストール ※PyPI最新版を利用
    • Flask
    • jinja2
    • pymysql データ取得だけでは使わなかった
    • sqlalchemy

MySQLに接続する

ソースコードGitHub

ファイル構成 - config.py:DB接続情報を記述 - database.py:セッションを取得 - models/item.py:itemテーブルのモデルクラス データ取得メソッドを記述 - main.py:item.pyのデータ取得メソッドを呼び、HTMLを返す

f:id:butorisa:20201020172428p:plain

database.py SQLAlchemyを使ってMySQLの接続セッションを確立

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from config import SystemConfig


def create_session():
    # create_engine()の引数はconfig.pyに記述したDB接続情報
    engine = create_engine(SystemConfig.SQLALCHEMY_DATABASE_URI)
    session = sessionmaker(bind=engine)
    return session()

item.py database.pyからセッションを取得し、データを取得

import database as db
from sqlalchemy import (Column, Integer, String)
from sqlalchemy.ext.declarative import declarative_base


base = declarative_base()


class Item(base):
    # テーブル情報
    __tablename__ = 'item'
    no = Column(Integer, primary_key=True, autoincrement=True)
    title = Column(String(100), nullable=False)
    content = Column(String(1000), nullable=False)

    def get_item(self):
        """ itemテーブルの全データを取得 """
        session = db.create_session()
        item_list = session.query(Item).all()
        if item_list is None:
            return []
        return item_list

session.query(モデルクラス)でデータ取得できる!!

自作ゲームで強化学習 #2

前回の記事からgymにもっとゲームをあそんでもらうべく、報酬の条件を調整してみる!

スコアに応じた報酬を与えてみる

  • スコア0:-10
  • それ以上:スコア÷100(スコア100だと報酬+1)

結果:カップケーキを1つ取ったら動かなくなった コードはこちら

f:id:butorisa:20201020172252p:plain

5秒以内にカップケーキを取ったらボーナスを与える

上記のスコア+(前回ケーキを取ってから)5秒以内だったら更に10加算

結果:カップケーキを1つ取ったら動かなくなった

f:id:butorisa:20201020172307p:plain

5秒以内にプレイヤーが移動したらボーナスを与える

上記2つに加えて移動したら報酬を増やすようにした

結果:移動は増えたが、ケーキを取りに行く感じではなかった

ここまでのコードはこちら

わかったこと

  • 「5秒以内に動かなかったら報酬をマイナス」という減点法は効果なかった
    • プレイヤーが動かないままになってしまった
  • 「プレイヤー移動で報酬プラス」は効果あり
    • 移動アクションは増えた
    • ケーキにたどり着かない場所を行ったり来たりする移動が多かった

自作ゲームで強化学習 #1

AIにもゲームをあそんでもらった

pygameで作ったゲーム強化学習して上達するのか試してみた!!

カップケーキ取ったらスコア+100、20秒後にスコアを表示

f:id:butorisa:20201020171924g:plain

強化学習実行までの道のり

こちらの記事を写経しました 【AI + pygame】pygameで作るインベーダー風ゲーム 1. ゲームコードを Open AI Gym が実行できるよう修正 1. 強化学習モデル+学習実行コードを記事からぱくる 1. 実行コードを動かすとエラーが出るので対応 - モデルへの入力データ次元が違うとか - tensorflowはこのメソッドを実装してないとか(tensorflowのバージョン不適) - こんな変数はないとか(ただのコードミス)

強化学習スタート

  • 環境(Anaconda) AnacondaとPyPIがごちゃ混ぜになっております…
    • Python:3.7.6
    • pygame:1.9.6 ※pip
    • gym:0.17.1
    • keras:2.3.1 ※pip
    • keras-rl:0.4.2 ※pip
    • pillow:7.1.1
    • tensorflow:1.13.1 ※pip
  • ソースコード
  • パラメータ
    • 学習ステップ数:20,000 1エピソードで行ったキー操作の回数(多分)
    • テストエピソード数:10 ゲーム開始~終了で1エピソード
  • 報酬の設定 ゲーム終了時のスコアで報酬を決める
    •  0 ~ 100 : -10
    • 101 ~ 500 : +5
    • 501 ~ 1000: +10
    • 1001 ~   : +15

10,000ステップくらいから↓のような感じ スコアは100~300で全然上がらない

f:id:butorisa:20201020172001g:plain

ゲーム開始時は右下にカップケーキがあって、開始直後に右下のケーキを取るようになった (ここは400ステップくらいで学習してくれた)

報酬=スコアにすればよかった!

テスト結果 reward(報酬)はずっと5のまま

Testing for 10 episodes ...

Episode 1: reward: 5.000, steps: 173
Episode 2: reward: 5.000, steps: 173
Episode 3: reward: 5.000, steps: 174
Episode 4: reward: 5.000, steps: 173
Episode 5: reward: 5.000, steps: 173
Episode 6: reward: 5.000, steps: 173
Episode 7: reward: 5.000, steps: 172
Episode 8: reward: 5.000, steps: 172
Episode 9: reward: 5.000, steps: 173
Episode 10: reward: 5.000, steps: 173

上記の報酬設定だと、ケーキを1~5回取った時は同じ報酬だから プレイヤーがたくさん動いてケーキを取りまくるスタイルにならなかった

ケーキを取った回数に比例して報酬をプラスすればもっと動いてくれると期待