Kivyからの挑戦状．Pongを改良しよう！

# はじめに
最近，スマホアプリを作りたい欲が出てきたので，Pythonで書けるKivyのチュートリアルをやってました．
この記事は，[Kivyの最初のチュートリアル，Pong](https://kivy.org/doc/stable/tutorials/pong.html)の一番最後の文章の "few ideas of things you could do" を<b>挑戦状</b>と意訳して，挑戦状に挑んだ結果？をまとめたものです．

以下文章
> Where To Go Now?¶
> Have some fun
> Well, the pong game is pretty much complete. If you understood all of the things that are covered in this tutorial, give yourself a pat on the back and think about how you could improve the game. Here are a <b>few ideas of things you could do</b>:

読める程度に少し整理したのですが，元はコードを書いてる最終のメモ書きなので，読みづらかったら教えてください...

# 1. いい感じの図形・画像を入れてみよう（Add some nicer graphics）

```kv
<PongBall>:
    size: 50, 50 
    canvas:
        Ellipse:
            source: "./img/ナエトル.png"
            pos: self.pos
            size: self.size 
```
上みたいな感じで，```source```に画像のパスを設定してあげると良い
ナエトルは，写真フォルダに唯一あったいい感じの大きさのやつです．ヒコザルが好きです．

[sourceについて](https://kivy.org/doc/stable/api-kivy.graphics.instructions.html#kivy.graphics.instructions.VertexInstruction.source)

# 2. 特定の点数になったら，「playerXの勝利！」と表示しよう！
一方のプレイヤーの点数が10点になったら大きく「playerXの勝利！」と表示
```kv
<PongGame>:
  # もともとのコードに追加する
  message: message
  Message:
      id: message
      text: ""
  Label:
      font_size: 70  
      center_x: root.width / 2
      color:"red"
      top: root.top / 2 + 50
      text: str(root.message.text)
```
お気持ちとしては，
- ```Message```クラスを新しく作る
  - この時，```id```を指定して，上で宣言してPythonのコード内で使えるようにする
- ```Label```を作って，```Message```の文字を表示する

```python
from kivy.properties import StringProperty

class Message(Widget):
    text = StringProperty()

    def show_message(self, player):
        self.text = f"player{player} won !!!"

class PongGame(Widget):
  # 諸々最初にある
  message = ObjectProperty(None) # これ重要！
  max_score = 3
  # もろもろ

  def update(self):
    # もろもろ
    temp = self.is_game_end()
    if temp:
        self.message.show_message(temp)

  def is_game_end(self):
    if self.player1.score == self.max_score:
        return "1"
    elif self.player2.score == self.max_score:
        return "2"
```
- <b>```Message```クラス内の```StringProperty```が結構重要</b>
  - 普通に```text = ""```って書くと，表示されない

# 3. 「スタート，停止，リセット」メニューを追加しよう！
- ボタンは普通に追加できる
## Click
  - KVファイルにon_press: temp()みたいに書いても動かない
```python
class PongGame(Widget):

  def callback(self, instance):
          instance.text = "some text"

  def init_manual(self):
    self.reset_btn.bind(on_press=self.callback)
```
 - 下のように```bind```を使ったら動いた（重要だったので2回）
```python
self.reset_btn.bind(on_press=self.reset_game)
```
## リセット
  - ```PongPaddle```クラスに```init_manual```を追加して，初期状態のy座標を保存し，ボタンが押された際に戻せばできそう
```python
class PongPaddle(Widget):
    def init_manual(self):
        self.org = [self.score, int(self.center_y)]

    def clear(self):
        self.score, self.center_y = self.org
```
  -  色々と試したが，できない
  -  座標系が原因なのかな？
     -  https://qiita.com/gotta_dive_into_python/items/abe433ae3b716d4fb8a7
     -  ~~体力があればこれをやりたい~~ 無かった
## 停止/スタート
- ボタンが押されたら，表示を変更＋ボールの速度を0/前の状態に戻す，をすれば良さそう
  - ボタンをテキストを別にしてるが，一緒にしたい...
    - （記憶が正しければ，）以前ボタンのみでやっても文字が変わらなかった...
- コードを色々と追加する
```kv
<PongGame>:
    play_control_btn: play_control_btn
    control_text: control_text
    Button:
        id: play_control_btn
        size: 100, 100
        pos: (root.width - self.width) / 2, (root.height - self.height) / 4 * 3
        text: str(root.control_text.text)
    
    ControlText:
        id: control_text
        text: "Pause"
```
```python
class ControlText(Widget):
    text = StringProperty()

    def change_text(self, is_playing):
        if is_playing:
            self.text = "Play"
        else:
            self.text = "Pause"

class PongBall(Widget):
    pre = [velocity_x, velocity_y]

    def init_manual(self):
        self.org = [self.velocity, self.pos]

    def clear(self):
        self.velocity, self.pos = self.org

    def stop(self):
        self.pre = [self.velocity_x, self.velocity_y]
        self.velocity_x, self.velocity_y = 0, 0

    def start(self):
        self.velocity_x, self.velocity_y = self.pre

class PongGame(Widget):
    control_text = ObjectProperty(None)

    def control_game(self, instance):
        self.control_text.change_text(self.is_playing)
        if self.is_playing:
            self.ball.stop()
        else:
            self.ball.start()
        self.is_playing ^= True
    def init_manual(self):
        self.ball.init_manual()
        self.is_playing = True
        self.play_control_btn.bind(on_press=self.control_game)

class PongApp(App):
    def build(self):
        game.init_manual()
```

# 4. 4人プレイのPongを作ってみよう！
- 上と下のプレイヤークラスを作成する
```kv
<PongPaddleVer>:
    size: 25, 200
    canvas:
        Rectangle:
            pos: self.pos
            size: self.size

<PongPaddleHor>:
    size: 200, 25
    canvas:
        Rectangle:
            pos: self.pos
            size: self.size

<PongGame>:
    player3: player_top
    player4: player_bottom

    PongPaddleHor:
        id: player_top
        y: root.height - self.height
        center_x: root.center_x
    
    PongPaddleHor:
        id: player_bottom
        y: root.y
        center_x: root.center_x
```
- Pythonの```PongPaddle```には色々関数が詰まってるから，```PongPaddleHor```, ```PongPaddleVer```にコピペするのは無駄だなぁ～
  - 2つのクラス，```PongPaddle```を継承すれば良さそう！
  - ```bounce_ball```の処理，上下と左右で違うから，継承する必要は無かったな...
```python
class PongPaddleVer(PongPaddle):
    score = NumericProperty(0)

    def move(self, ball):
        self.center_y = ball.center_y

    def bounce_ball(self, ball):
        if self.collide_widget(ball):
            vx, vy = ball.velocity
            offset = (ball.center_y - self.center_y) / (self.height / 2)
            bounced = Vector(-1 * vx, vy)
            vel = bounced * 1.01
            ball.velocity = vel.x, vel.y + offset


class PongPaddleHor(PongPaddle):
    score = NumericProperty(0)

    def move(self, ball):
        self.center_x = ball.center_x

    def bounce_ball(self, ball):
        if self.collide_widget(ball):
            vx, vy = ball.velocity
            offset = (ball.center_x - self.center_x) / (self.width / 2)
            bounced = Vector(vx, -1 * vy)
            vel = bounced * 1.01
            ball.velocity = vel.x + offset, vel.y

class PongGame(Widget):
  def update(self, dt):
        if self.ball.y < self.y:
        self.player4.score += 1
        self.serve_ball(vel=(4, 0))
        if self.ball.top > self.height:
            self.player3.score += 1
            self.serve_ball(vel=(-4, 0))
```
- 2人プレイ時は，「PlayerXの勝利！」で良かったが，4人だと「勝ち」ではなく「負け」を表示したほうが良さそうなので，そうした．

# 5. 跳ね返る処理をもっと現実的にしよう！
- 現状のシンプルのではなく，端っこに当たったら，いい感じに現実っぽくしたい
    - めんどくさい...
    - また別の機会にやる...

# チュートリアルを終えて

## Playerを自動的に動かしてみたいよね
(これは，<b>挑戦状</b>をやる前だったので，プレイヤーが二人しかいない感じで書かれてます．)

- 理由
  - 今の状態だと，一人で両方動かす必要ある
  - しんどい
- やったこと
  - ```PongPaddle``` クラスに ```move``` 関数を追加
    ```py
    def move(self, ball):
        self.center_y = ball.center_y
    ```
  - ```PongGame ``` クラス内の ```update``` 関数内にコードを追加
    ```py
    self.player2.move(self.ball)
    ```
  - 過程
    - ```PongBall```クラスがボールをコントロールしてるから，```PongPlayer2```クラスを作って，```move```関数でプレイヤーをコントロールすれば良い！
    - エラーが出て動かない
      - ```AttributeError: 'PongPaddle' object has no attribute 'move'```
      - どうやら，```player2```は```PongPaddle```オブジェクトらしい（よく分からず，よりあえずコードを写してた...）
    - ```move```関数を```PongPaddle```クラスに追加したら動いた！
    - このままだと，手動でPlayer2を動かせるので，以下のコードを消す
        ```python
        if touch.x > self.width - self.width / 3:
            self.player2.center_y = touch.y
        ```

# 最終的なコード
<details>
  <summary>Python</summary>
  
  ```py
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import (
    NumericProperty,
    ReferenceListProperty,
    ObjectProperty,
    StringProperty,
)
from kivy.uix.label import Label
from kivy.vector import Vector
from kivy.clock import Clock
from random import randint


class PongPaddle(Widget):
    score = NumericProperty(0)


class PongPaddleVer(PongPaddle):
    score = NumericProperty(0)

    def move(self, ball):
        self.center_y = ball.center_y

    def bounce_ball(self, ball):
        if self.collide_widget(ball):
            vx, vy = ball.velocity
            offset = (ball.center_y - self.center_y) / (self.height / 2)
            bounced = Vector(-1 * vx, vy)
            vel = bounced * 1.01
            ball.velocity = vel.x, vel.y + offset


class PongPaddleHor(PongPaddle):
    score = NumericProperty(0)

    def move(self, ball):
        self.center_x = ball.center_x

    def bounce_ball(self, ball):
        if self.collide_widget(ball):
            vx, vy = ball.velocity
            offset = (ball.center_x - self.center_x) / (self.width / 2)
            bounced = Vector(vx, -1 * vy)
            vel = bounced * 1.01
            ball.velocity = vel.x + offset, vel.y


class PongBall(Widget):
    velocity_x = NumericProperty(0)
    velocity_y = NumericProperty(0)
    velocity = ReferenceListProperty(velocity_x, velocity_y)
    pre = [velocity_x, velocity_y]

    def move(self):
        self.pos = Vector(*self.velocity) + self.pos

    def stop(self):
        self.pre = [self.velocity_x, self.velocity_y]
        self.velocity_x, self.velocity_y = 0, 0

    def start(self):
        self.velocity_x, self.velocity_y = self.pre


class Message(Widget):
    text = StringProperty()

    def show_message(self, message):
        self.text = message


class ControlText(Widget):
    text = StringProperty()

    def change_text(self, is_playing):
        if is_playing:
            self.text = "Play"
        else:
            self.text = "Pause"


class PongGame(Widget):
    ball = ObjectProperty(None)
    player1 = ObjectProperty(None)
    player2 = ObjectProperty(None)
    player3 = ObjectProperty(None)
    player4 = ObjectProperty(None)
    message = ObjectProperty(None)
    play_control_btn = ObjectProperty(None)
    control_text = ObjectProperty(None)
    max_score = 3

    def add_temp(self, pos):
        label_temp = Label(text="a", pos=pos)

        self.add_widget(label_temp)

    def serve_ball(self, vel=None):
        if vel is None:
            vel = (4, randint(-5, 5))
        self.ball.center = self.center
        self.ball.velocity = vel

    def update(self, dt):
        self.ball.move()
        # Player1以外はボールにオート追従する
        # self.player1.move(self.ball)
        self.player2.move(self.ball)
        self.player3.move(self.ball)
        self.player4.move(self.ball)

        # bounce of paddles
        self.player1.bounce_ball(self.ball)
        self.player2.bounce_ball(self.ball)
        self.player3.bounce_ball(self.ball)
        self.player4.bounce_ball(self.ball)

        # went of to a side to score point?
        if self.ball.x < self.x:
            self.player1.score += 1
            self.serve_ball()
        if self.ball.right > self.width:
            self.player2.score += 1
            self.serve_ball()
        if self.ball.y < self.y:
            self.player4.score += 1
            self.serve_ball()
        if self.ball.top > self.height:
            self.player3.score += 1
            self.serve_ball()

        temp = self.is_game_end()
        if temp:
            self.message.show_message(f"player{temp} lose!")

    def on_touch_move(self, touch):
        if touch.x < self.width / 3:
            self.player1.center_y = touch.y
        if touch.x > self.width - self.width / 3:
            self.player2.center_y = touch.y

    def is_game_end(self):
        players = [self.player1, self.player3, self.player3, self.player4]
        for i in range(len(players)):
            if players[i].score == self.max_score:
                return i + 1

    def control_game(self, instance):
        self.control_text.change_text(self.is_playing)
        if self.is_playing:
            self.ball.stop()
        else:
            self.ball.start()
        self.is_playing ^= True

    def init_manual(self):
        self.is_playing = True
        self.play_control_btn.bind(on_press=self.control_game)


class PongApp(App):
    def build(self):
        game = PongGame()
        game.init_manual()
        game.serve_ball()
        Clock.schedule_interval(game.update, 1.0 / 60.0)
        return game


if __name__ == "__main__":
    PongApp().run()

  ```
</details>

<details>
  <summary>kv</summary>
  
  ```kv
#:kivy 2.1.0

<PongBall>:
    size: 50, 50 
    canvas:
        Ellipse:
            source: "./img/ナエトル.png"
            pos: self.pos
            size: self.size          

<PongPaddleVer>:
    size: 25, 200
    canvas:
        Rectangle:
            pos: self.pos
            size: self.size

<PongPaddleHor>:
    size: 200, 25
    canvas:
        Rectangle:
            pos: self.pos
            size: self.size

<PongGame>:
    ball: pong_ball
    player1: player_left
    player2: player_right
    player3: player_top
    player4: player_bottom
    message: message
    play_control_btn: play_control_btn
    control_text: control_text

    canvas:
        Rectangle:
            pos: self.center_x - 5, 0
            size: 10, self.height
    
    Label:
        font_size: 70  
        center_x: root.width / 4
        top: root.height / 4
        text: "1:"  +str(root.player1.score)
        
    Label:
        font_size: 70  
        center_x: root.width * 3 / 4
        top: root.height / 5 * 4
        text: "2:" + str(root.player2.score)

    Label:
        font_size: 70  
        center_x: root.width / 4
        top: root.height / 5 * 4
        text: "3:"  +str(root.player3.score)
        
    Label:
        font_size: 70  
        center_x: root.width * 3 / 4
        top: root.height / 4
        text: "4:" + str(root.player4.score)

    Label:
        font_size: 70  
        center_x: root.width / 2
        color:"red"
        top: root.top / 2 + 50
        text: str(root.message.text)
    
    Message:
        id: message
        text: ""
    
    Button:
        id: play_control_btn
        size: 100, 100
        pos: (root.width - self.width) / 2, (root.height - self.height) / 4 * 3
        text: str(root.control_text.text)
    
    ControlText:
        id: control_text
        text: "Pause"
    
    PongBall:
        id: pong_ball
        center: self.parent.center
        
    PongPaddleVer:
        id: player_left
        x: root.x
        center_y: root.center_y
        
    PongPaddleVer:
        id: player_right
        x: root.width - self.width
        center_y: root.center_y
    
    PongPaddleHor:
        id: player_top
        y: root.height - self.height
        center_x: root.center_x
    
    PongPaddleHor:
        id: player_bottom
        y: root.y
        center_x: root.center_x
  ```
</details>

# Exeファイルにしよう！
これはチュートリアルにはなかったが，せっかくだしやる

https://kivy.org/doc/stable/guide/packaging-windows.html

ここでよく出てくる```touchtracer```はプロジェクトの名前なので，自分のやつに変更する必要がある．

1: ```python -m PyInstaller --name touchtracer examples-path\demo\touchtracer\main.py```
2: ```.spec```を編集する
- ```from kivy_deps import sdl2, glew```を最初の方に追記
- EXE内を下のように編集する
 
```
exe = EXE(pyz, Tree('examples-path\\demo\\touchtracer\\'),
     a.scripts,
     a.binaries,
     a.zipfiles,
     a.datas,
     *[Tree(p) for p in (sdl2.dep_bins + glew.dep_bins)],
     upx=True
     name='touchtracer')
``` 

- Analysis内に必要なデータ（画像のパス）を追加する

```
datas=[ ('learn_kivy/img/ナエトル.png', '.') ],
```

3: ```python -m PyInstaller touchtracer.spec```

# さいごに
kivy, クソめんどくさい．座標もrootとかselfとか親とか子とか色々あるし，pythonファイルとは別にkvファイルにも色々書かないといけないし...
なので，きっともう触らない気がする...
スマホアプリはFlutter/Kotlinが楽なのかな???