オブジェクト指向の習作～非同期処理を添えて～


# アドベントカレンダー
これは[農工大アドベントカレンダー Advent Calendar 2024](https://qiita.com/advent-calendar/2024/tuat)の5日目の記事です！


作成したプログラムはこちらから
https://github.com/Delphyilia/pokemon_roguelike_web

webでゲームが遊べます
https://delphyilia.github.io/pokemon_roguelike_web/


# この記事について
今年の春ごろ、pythonの記法を勉強する中でオブジェクト指向・クラスの概念を知りました。
使いこなすために、習作としてポケモン風ローグライクゲームを作成したので、紹介したいと思います。
…
だけの予定だったのですが、途中で`PyScript`の存在を知り、webにアップすることでpythonの環境がない人やプログラマー以外の方でも遊べることに気が付きました。その途中で非同期処理に躓いたので、少し触れていきます。

# ポケモンはオブジェクト
オブジェクト指向の詳細な説明は他の記事やサイトを参考にしてください。
ここではざっくりと「ある共通の概念や動作をまとめたもの」という認識でとらえています。

では、全ポケモンが共通して持っている性質をまとめ、おおもとのクラスを作成していきましょう。
全てのポケモンは、以下の要素を持っています（本当はもっとあるはずですが、プログラムに起こしやすいのがこのあたりでした）。

- 名前
- タイプ
- 捕獲する難しさ
- 倒した時の経験値
- 4つの技
- 最大3種類の特性
- 能力値
- 自分のポケモンか、敵のポケモンか、野生か

列挙できました。これをもとに、おおもとの`Pokemon`クラスを作ります。

```python
class Pokemon:
    def __init__(self, level:int, position:POSITION) -> None:
        self.monster_name :str
        self.level :int = level
        self.type1 :str
        self.type2 :str = ""
        self.capture_difficulty :int = 255
        self.baseEXP :int = 255
        self.move1 :Move
        self.move2 :Move
        self.move3 :Move
        self.move4 :Move
        self.ability1 :Ability
        self.ability2 :Ability = None
        self.ability3 :Ability = None
        self.set_individual_value()
        self.set_effort_value()
        self.set_rank_modifiers()
        self.position :POSITION = position
```
`__init__`関数はインスタンスが作成されたときに実行されます。
インスタンスとは、このクラスが実態を持つ時です。
ポケモンにおいて「実態を持つ」とは、どの場面を指すのか？
草むらから飛び出してきたときや、敵のトレーナーが出てきたタイミングです。
であるならば、ポケモンのレベルが固定だとちょっとまずいわけです。
現在訪れている場所によって、ポケモンの強さを変更したいからです。

この点を意識して、もう一度見てみましょう。
```python
class Pokemon:
    def __init__(self, level:int, position:POSITION) -> None:
        self.level :int = level
        self.position :POSITION = position
```

`level`と`position`以外の情報は固定（だったり別の関数で作成）していますが、この2つは引数を代入しています。これにより、遭遇する場所によってレベルを変更したり、敵トレーナーの手持ちとして出現することが可能になります。

# 継承
おおもとのクラスを作成したので、次は継承を行います。
継承とは、さきほど作成したクラスを再利用し、派生させてゆくことです。

ここでは、サイドンを例にとり説明します。まずはコードを見てみましょう。

```python
class Rhydon(Pokemon):
    def __init__(self,newlevel,position):
        super().__init__(newlevel,position)
        self.level = newlevel
        self.monster_name = "サイドン"
        self.move1 = earthquake()
        self.move2 = Thunderbolt()
        self.move3 = HydroPump()
        self.move4 = HornAttack()
        self.type1 = "じめん"
        self.type2 = "いわ"
        self.ability1 = LightningRod()
        self.capture_difficulty = 100
        super().setBase(105, 130, 120, 45, 45, 40)
```

先程作成したクラスと違い、今回は特定のポケモンを指します。設定していなかった`monster_name`や`move1`、`type1`などを制定していきます。

このように、1つおおもとのクラスを作成してしまえば、あとは流用するだけで様々なポケモンを作成することができます！

# ポケモン以外…例えば技も継承しよう
「共通の性質」さえあればクラスにまとめられます！
ポケモンの使用する技も同様です。
技のおおもとのクラスを作成しましょう。

まず、技のもつ性質を列挙します。

- 威力
- 分類
- PP（技の使用回数のこと）
- 最大PP
- 命中率
- 名前
- タイプ

性質を列挙することができましたね！
では、コードに起こしましょう。

```python
class Move():
    def __init__(self):
        self.power :int
        self.category :MOVE_CATEGORY
        self.PP :int = 0
        self.maxPP :int = 0
        self.accuracy :int
        self.move_name :str
        self.type :str
```

例えば、これを継承してできたのが`Thunderbolt`、10万ボルトという技です。
```python
class Thunderbolt(Move):
    def __init__(self):
        super().__init__()
        self.power = 90
        self.category = MOVE_CATEGORY.SPECIAL
        self.PP = 15
        self.maxPP = 15
        self.accuracy = 100
        self.move_name = "10万ボルト"
        self.type = "でんき"
```

このように、元となる親クラスが存在するとさまざまな派生を作りやすくなります！
どのような事項を設定したらよいか明確になり、コードを書く効率が向上できます。

# プログラムの完成
こうしてできたものがこちらになります（唐突）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/80923450-43a0-9062-7184-ec2e312f0982.png)

[こちらのリポジトリ](https://github.com/Delphyilia/pokemon_roguelike_web)からプログラムをコピーなりforkなりしていただき、`python main.py`を実行すれば遊ぶことができます！



# webに載せたい
それでは、今回も最後まで読んでいただき、ありがとうございましたー！

<br>
<br>
<br>
<br>

…せっかく（パクリとはいえ）ゲームを作成したのだから、遊んでもらいたい！
でも、非プログラマにpythonをinstallだのgitを使えだの、障壁がいろいろあるんじゃないか？
`pyinstaller`を用いてexeファイルにしたものを配布するのはセキュリティ面であまりよろしくないらしい…

<strong>全コードをpythonで書いてしまったが、どうにかしてwebにあげたい！（天啓）</strong>
そこで見つけたのが、[`PyScript`](https://pyscript.net/)でした。

# PyScriptを使ってみよう
Pythonコードをwebサイトで動作させることが可能なプラットフォームです。
試しに使用してみましょう。

作成したhtmlファイルです。

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
    <script defer src="https://pyscript.net/latest/pyscript.js"></script>
    <style>
        py-terminal[docked] {
            position: fixed;
            top: 0;
            width: 100vw;
            max-height: 80vh;
            overflow: auto;
            z-index: 1000;
        }
        .py-terminal {
            margin: 0;
        }
        body {
            padding-top: 40vh;
        }
    </style>
</head>
<body>
    <h1>ポケモンゲーム</h1>
    <div id="py-output">

    </div> <!-- printの出力先 -->
    <py-config>
        output = "py-output"
    </py-config>
<py-script src = "all.py" worker>
</py-script>
</body>

</html>
```

`<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />`
`<script defer src="https://pyscript.net/latest/pyscript.js"></script>`

この2種により`PyScript`が使用可能になります。
`<body>`要素に`<py-script src="使用するpythonファイル" worker>`と記述します。これで準備完了です。
`worker`というのは、非同期処理を行ってくれるものです。

以下は[公式ページ](https://docs.pyscript.net/2024.5.1/user-guide/workers/)より引用
> Workers run code that won't block the "main thread" controlling the user interface. 
>>ワーカーは、ユーザー インターフェイスを制御する「メイン スレッド」をブロックしないコードを実行します。

# 非同期処理…？初めてなんですけど！
ところで、作成したゲームは標準入出力で完結しています。
起動したら、最初の1匹を選ぶよう聞かれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/dbf950e2-1fa6-5499-9ddb-404eeffbd72a.png)

半角の数字を用いて回答すると、次に進みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/b0c3b2d0-9870-df9d-3d0e-47a8239b869d.png)

これは`input()`関数が数字の入力を受け付けています。
`input()`関数はどうやらメインスレッドをブロックする？ようです。
web版で適切な対処をしないとこのようになります。


![Videotogif (2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/124af89b-c80d-5b0c-99a9-174446dcd95b.gif)


「ポケットモンスター(?)の世界へようこそ！」
「まずは、最初の仲間を選んでね！」
の文言が表示されるより先に入力欄が出てしまいます。

おそらく原因は、文言は`print()`関数を用いて表示しますが、直後の`input()`関数に処理を防がれています。なので、プログラム内で使用した`input()`の直前に`await`をし、printしたい文章が先に出るようにしなければなりません。

プログラム全容は1000行を超えてしまうので、主要な部分を抜き出して紹介します。

```python
import asyncio

async def int_input(prompt,min,max):
    while True:
        print(prompt)
        await asyncio.sleep(0.1)
        i = input()

async def main():
    print("ポケットモンスター(?)の世界へようこそ！")
    print("まずは、最初の仲間を選んでね！")
    print("1: LV.5 ランターン 2: LV.5 アーボック 3: LV.5 サイドン\n数字を入力:")

    n = await int_input("",1,6)　　# awaitを付けて呼び出す


    (mainの処理が続く…)


asyncio.ensure_future(main())　　# 非同期タスクでスケジューリング
```

`async def int_input()`のように文頭に`async`を付けることにより関数内で`await asyncio.sleep()`を使用することができます。
`input()`関数が実行される前に`await asyncio.sleep(0.1)`とすることで、先に`print()`が作動することが約束されます。
この自作関数は`main()`内で呼び出して使用しています。呼び出すときにも`await`を付けなければなりません。（関数自体の終了を約束したいから、のように理解していますがどうでしょうか…）
最後に`asyncio.ensure_future(main())`とすることで、並列して実行できます。PyScript自体が常にループさせているようなので、`asyncio.tun()`ではうまくいきませんでした。


# 終わりに
当初はオブジェクト指向とその利用について深堀りするつもりでしたが、天啓がおりてきたため中断し、初めての非同期処理をやってみました！

オブジェクト指向はもっと有効な利用法があるはずだと模索中です。
非同期処理は1度`TypeScript`で必要になりかけた場面がありましたが、自力でなんとかするのは今回が初めてでした。

ただ、どちらもゲーム作成を通して微々たるものながら理解することができました！
今後はもっと効率よく時間をかけずにコーディングしたいですね。

PyScriptは本当に画期的なものです。ぜひ皆さんも試してみてください！