JavaScript用の教材、ぷよぷよプログラミングをPythonでやってみた！

![image.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922095/2ce26fe3-cd79-4968-bb63-6fbc14346261.jpeg)

左：JavaScript版
右：Python移植版

## はじめに
　今回はJavaScript用の学習教材「ぷよぷよプログラミング」を参考に、Pythonでぷよぷよを作ってみたので、その感想を書いていこうと思います。

## ぷよぷよプログラミングとは
　ぷよぷよプログラミングは、セガが出しているJavaScript向けの学習教材で、写経を通じてJavaScriptを学べます。コースが基礎、初級、中級、上級と分かれているので、自分のレベルにあったものを選べます。リンクを貼っておくので、気になる方はご覧ください。（本家の方にPythonは出てきません）

 https://puyo.sega.jp/program_2020

:::note
筆者のように他の言語に移植したい人は、基礎コースだけやって、画像をもらいつつ、動きを観察すると良いと思います。
:::

## この試みについて
　もともとは、Python以外の言語にも触れて比較したいと思い、JavaScriptの勉強をするつもりでした。しかし、せっかくなら上級をやりたいものの、上級だと無からすべて写経することになるので、流石に時間かかるな～という思いがありました。そこで、どうせ無から作るなら、Pythonに移植してみればよくね？と思い立ち、今に至ります。

### お勧めしたい人
・ぷよぷよを作りたい人
・他の言語がメインで、JavaScriptがある程度読める人
・メインの言語でゲームを作ったことがある人

以上の3つに当てはまる人には、おすすめできると思います。特に３つ目ですが、ブロック崩しとかでもいいので、何かしら経験がないと厳しいかなと思います。あとは、描画関連のために、JavaScriptからCSSのプロパティを弄ったりもしているので、CSSの知識も少しは欲しいですね。

### 良かった点
#### 本格的な画像をもらえる
　画像の用意をしなくていいので、コードを書くことだけに集中できた点が、とてもありがたいです。

#### 他の人が書いたコードを読める
　GitHubに行けば、いくらでも読めると言われればそうなのですが、ぷよぷよという題材であることに意味があると感じます。先に、ある程度動きのイメージがついた状態で、これを実装したいけど、この人はどう実装したのかな？という視点でコードを観察できました。学習教材というだけあって、親切すぎるくらいにコメントがついている点もありがたいです。

#### ただ写経するより理解度が高まった
　言語が違うので、写経のように、上から書き換えていっても動きません。処理の流れを考えて、その順番に移植をしていく必要があります。関数なら、引数を受け取って、処理をして返すという流れが一貫しているので楽なのですが、描画とイベントの扱いが難しかったですね。この点は悪かった点でも触れます。

### 悪かった点
#### 一部参考にならない
　良かった点でも触れましたが、JavaScriptとPython(pygame)では、画面の描画部分が違い過ぎて、その辺りは純粋に書き換えるのが難しかったです。参考になるものがあるから～となめてかかると痛い目を見ます。ゲームを作ったことがない人にお勧めできない理由がこれです。

#### JavaScriptを書けるようにはならない
　当たり前ですが、最終的ににらめっこしているのは、移植先のコードです。なので、PythonとJavaScriptそれぞれの特徴は分かりますが、JavaScriptを書けるようにはなりません。読めるようになるという観点なら、大いに効果があったと思います。

## 移植中に気になったもの
　そもそも文法が全然違うじゃん！とかは置いといて、JavaScriptからpygameへの移植の過程で、面白いなと思ったものや、大幅に変更が必要になったものを挙げていきます。

### キー入力の受け取り方
　JavaScriptは`addEventListener`を使い、pygameはメインループ内で、イベントを普段通り受け取る感じですね。pygameのほうのplayer1とかいうのは気にしないでください。一応インスタンスを増やすだけで、2P、3Pと増やせるように一般化してあるので、暫定的にそのような名前になっているだけです。

```js:JavaScript
// ブラウザのキーボードの入力を取得するイベントリスナを登録する
document.addEventListener('keydown', (e) => {
    // キーボードが押された場合
    switch(e.keyCode) {
        case 37: // 左向きキー
            this.keyStatus.left = true;
            e.preventDefault(); return false;
        case 38: // 上向きキー
            this.keyStatus.up = true;
            e.preventDefault(); return false;
        case 39: // 右向きキー
            this.keyStatus.right = true;
            e.preventDefault(); return false;
        case 40: // 下向きキー
            this.keyStatus.down = true;
            e.preventDefault(); return false;
    }
});
document.addEventListener('keyup', (e) => {
    // キーボードが離された場合
    switch(e.keyCode) {
        case 37: // 左向きキー
            this.keyStatus.left = false;
            e.preventDefault(); return false;
            
    //以下略
```

```python:Python(pygame)
#pygameのメインループから一部抜粋
if event.type == pygame.KEYDOWN:
    # キーが押された時の処理
    if event.key == pygame.K_LEFT:
        player1.player.left = True
    elif event.key == pygame.K_RIGHT:
        player1.player.right = True
    elif event.key == pygame.K_UP:
        player1.player.up = True
    elif event.key == pygame.K_DOWN:
        player1.player.down = True
if event.type == pygame.KEYUP:
    # キーが離された時の処理
    if event.key == pygame.K_LEFT:
        player1.player.left = False
    # 以下略
```

### 画像の用意
　JavaScriptではCSSのプロパティを以下のように設定して、画像を作っていました。`style.display`を`none`にしたり`block`にしたりすることで、表示と非表示を切り替えるみたいです。`position`は`absolute`にすることで、絶対位置で座標指定しています(pygameと同じように指定できる)。
 
```js:JavaScript
const zenkeshiImage = document.getElementById("zenkeshi");
zenkeshiImage.width = Config.puyoImgWidth * 6;
zenkeshiImage.style.position = 'absolute';
zenkeshiImage.style.display = 'none';        
this.zenkeshiImage = zenkeshiImage;
```

以下のように、必要な画像はimgタグをid付きで記述してあるので、`document.getElementById`が`pygame.image.load`に当たる部分です。
 
```html:index.html
 <img src="img/zenkeshi.png" id="zenkeshi">
```

pygameでやるとこんな感じです。CSSと違って`height`を`auto`でいい感じにしてくれないので、アスペクト比を計算しています。表示と非表示の切り替えにはフラグを使っていて、実際に描画が行われる際に、`blit`するかを判定しています。
```python:Python(pygame)
self.width, self.height = PUYO_IMG_WIDTH, PUYO_IMG_HEIGHT
zenkeshi = pygame.image.load('img/zenkeshi.png').convert_alpha()
# 新しい幅を指定し、高さを比例計算
new_width = self.width * 6
aspect_ratio = batankyu.get_height() / zenkeshi.get_width()
new_height = int(new_width * aspect_ratio)
self.zenkeshi_image = pygame.transform.smoothscale(zenkeshi, (new_width, new_height))
```

### ぷよの管理
　JavaScriptでは辞書のリストを使って管理していましたが、pygameで実装するにあたり、描画関連のもろもろの変更とかがあって、辞書よりもクラス作ってインスタンスで管理したほうが楽だなと思ったので、そのように管理しています。
```js:JavaScript
// 落ちるリストに入れる
this.fallingPuyoList.push({
    element: cell.element,
    position: y * Config.puyoImgHeight,
    destination: dst * Config.puyoImgHeight,
    falling: true
});
```

```python:Python
#落ちるリストに入れる
tmp = Puyo(
    master = self.master,
    x = x, 
    y = y, 
    x_pos = x * PUYO_IMG_WIDTH,
    y_pos = y * PUYO_IMG_HEIGHT,
    puyo = puyo,
    falling = True,
    destination = dst * PUYO_IMG_HEIGHT
    )
self.falling_puyo_list.append(tmp)
```

### 命名規則
　意外と大事なポイントだと思うので、JavaScriptの変数名や定数名をそのまま使わず、Python風に書き直しました。JavaScriptだとキャメルケースを使っているものを、Pythonではスネークケースで記述するという感じですね。
```js:JavaScript
//定数名
playerFallingSpeed
//変数名
isFalling
//関数名
checkFall
```
```python:Python
#定数名
PLAYER_FALLING_SPEED
#変数名
is_falling
#関数名
check_fall
```

### リストの初期化
　Pythonのほうは空のリストを再代入することで、中身を空にしていますが、JavaScriptの方は、リストの長さを0にすることで、中身を空にしているようです。正直、最初見たときは何をしているのか分かりませんでした。
```js:JavaScript
this.fallingPuyoList.length = 0;
```

```python:Python
self.falling_puyo_list = []
```

### 三項演算子
　三項演算子の書き方が、結構違ったので、紹介しておきます。個人的にはPythonの方が分かりやすいと思うのですが、結局のところ慣れの問題でしょうか。
```js:JavaScript
const cx = (this.keyStatus.right) ? 1 : -1;
```

```python:Python
cx = 1 if self.right else -1
```

### 逆に変更がほぼ要らなかったもの
　条件分岐はそのまま書き換えればよかったので、接地判定やめり込み判定などは、ほぼ写経くらいの感じで進めることができました。
```js:JavaScript
if(sequencePuyoInfoList.length == 0 || sequencePuyoInfoList.length < Config.erasePuyoCount) {
```

```python:Python
if len(sequence_puyo_info_list) == 0 or len(sequence_puyo_info_list) < ERASE_PUYO_COUNT:
```

## 完成品
　筆者もまだまだ勉強中なので、拙い部分もありますが、一応コードを載せておきます。正直移植は厳しいけど、Pythonでぷよぷよを動かしたい！という人は、こちらを写経してください。画像はimgというフォルダに入れて、同じディレクトリに置いてください

<details><summary>コードを見る</summary>

```python
import pygame, sys, random, math

#Config
STAGE_WIDTH = 400
STAGE_HEIGHT = 800
STAGE_ROW = 12
STAGE_COLUMN = 6
STAGE_BG = (51, 51, 85)

SCORE_BG = (36, 208, 204)
FONT_HEIGHT = 30
FONT_LENGTH= 15

FREE_FALLING_SPEED = 16    #自由落下のスピード
ERASE_PUYO_COUNT = 4    #何個で消すか
PUYO_COLORS = 4    #何色使うか（１～５）
ERASE_ANIMATION_DURATION = 45    #点滅の長さ

PLAYER_FALLING_SPEED = 0.9    #操作中のぷよの自由落下速度
PLAYER_DOWN_SPEED = 15    #下を推しているときの落下速度
PLAYER_GROUND_FRAME = 20    #接地時間
PLAYER_MOVE_FRAME = 10    #移動時間
PLAYER_ROTATE_FRAME = 10     #回転時間

ZENKESHI_DURATION = 150    #全消し時のアニメーションミリセカンド
GAME_OVER_FRAME = 3000    #ゲームオーバー演出のサイクルフレーム


PUYO_IMG_WIDTH = STAGE_WIDTH // STAGE_COLUMN
PUYO_IMG_HEIGHT =  STAGE_HEIGHT // STAGE_ROW


pygame.init()    #おまじない
screen = pygame.display.set_mode((int(STAGE_WIDTH*1.5), STAGE_HEIGHT + FONT_HEIGHT))    #ウィンドウの作成
pygame.display.set_caption('ぷよぷよプログラミング')    #ウィンドウのタイトルを設定
clock = pygame.time.Clock()    #ゲームループのfpsを設定するためのやつ

class PuyoImage:
    def __init__(self, width, height):
        self.width, self.height = width, height
        self.puyo_images = []
        for i in range(5):
            tmp = pygame.image.load(f'../img/puyo_{i + 1}.png').convert_alpha()
            image = pygame.transform.smoothscale(tmp, (width, height))
            self.puyo_images.append(image)
        batankyu = pygame.image.load('../img/batankyu.png').convert_alpha()
        zenkeshi = pygame.image.load('../img/zenkeshi.png').convert_alpha()
        # 新しい幅を指定し、高さを比例計算
        new_width = self.width * 6
        aspect_ratio = batankyu.get_height() / batankyu.get_width()
        new_height = int(new_width * aspect_ratio)
        
        self.batankyu_image = pygame.transform.smoothscale(batankyu, (new_width, new_height))
        self.zenkeshi_image = pygame.transform.smoothscale(zenkeshi, (new_width, new_height))
    
    def get_puyo(self, index):
        return self.puyo_images[index-1]
    def batankyu(self, frame, master):
        ratio = (frame - GAME_OVER_FRAME) / GAME_OVER_FRAME
        x = math.cos(math.pi / 2 + ratio * math.pi * 2 * 10) * PUYO_IMG_WIDTH
        y = math.cos(math.pi + ratio * math.pi * 2) * PUYO_IMG_HEIGHT * STAGE_ROW / 4 + PUYO_IMG_HEIGHT * STAGE_ROW / 2
        master.blit(self.batankyu_image, (x, y))
        
class Puyo:
    def __init__(self, master, x, y, x_pos, y_pos, puyo, falling = False, destination = None, erasing = False):
        self.master = master
        self.x, self.y = x, y
        self.x_pos, self.y_pos = x_pos, y_pos
        self.puyo = puyo
        self.puyo_image = puyo_image.get_puyo(puyo)
        self.falling = falling
        self.destination = destination
        self.erasing = erasing
        self.vanish = False
        
        #以前追加されて、位置が重複しているぷよを削除
        tmp = master.puyo_list
        for puyo in tmp:
            if puyo.x_pos == self.x_pos and puyo.y_pos == self.y_pos:
                puyo.puyo_image = self.puyo_image
                master.puyo_list.remove(puyo)
                del puyo
        
        master.puyo_list.append(self)
    def blit(self, master):
        if not self.vanish:
            master.blit(self.puyo_image, (self.x_pos, self.y_pos))
        

class Stage(pygame.Surface):
    def __init__(self, master, width, height, row, column, bg):
        self.master = master
        self.bg = bg
        self.row, self.column = row, column
        super().__init__((width, height))
        self.fill(self.bg)
        
        self.board = [
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0],
        ]
        
        self.falling_puyo_list = []
        self.erasing_puyo_info_list = []
        
        self.erase_start_frame = None
        self.zenkeshi_start_frame = None
        
        puyo_count = 0
        for y in range(self.row):
            line = self.board[y]
            for x in range(self.column):
                puyo = line[x] if x < len(line) else 0
                if 1 <= puyo <= 5:
                    self.set_puyo(x, y, puyo)
                    puyo_count += 1
                else:
                    line[x] = 0
        self.puyo_count = puyo_count
        
    def set_puyo(self, x, y, puyo):
        self.board[y][x] = puyo
    #自由落下をチェックする  
    def check_fall(self):
        self.falling_puyo_list = []
        is_falling = False
        #下の行から上の行を見ていく（1番下は除く）
        for y in range(self.row - 1)[::-1]:
            line = self.board[y]
            for x in range(self.column):
                if not line[x]:
                    #このマスにぷよがなければ次
                    continue
                if not self.board[y + 1][x]:
                    #このぷよは落ちるので取り除く
                    puyo = self.board[y][x]
                    self.board[y][x] = 0
                    dst = y
                    while dst + 1 < self.row and self.board[dst + 1][x] == 0:
                        dst += 1
                    #最終目的地に置く
                    self.board[dst][x] = puyo
                    #落ちるリストに入れる
                    tmp = Puyo(
                        master = self.master,
                        x = x, 
                        y = y, 
                        x_pos = x * PUYO_IMG_WIDTH,
                        y_pos = y * PUYO_IMG_HEIGHT,
                        puyo = puyo,
                        falling = True,
                        destination = dst * PUYO_IMG_HEIGHT
                        )
                    self.falling_puyo_list.append(tmp)
                    
                    #落ちるものがあったことを記録しておく
                    is_falling = True
        return is_falling
    
    def fall(self):
        is_falling = False
        for falling_puyo in self.falling_puyo_list:
            if not falling_puyo.falling:
                # 既に自由落下が終わっているので次
                continue
            position = falling_puyo.y_pos
            position += FREE_FALLING_SPEED
            if position >= falling_puyo.destination:
                # 自由落下終了
                position = falling_puyo.destination
                falling_puyo.falling = False
            else:
                # まだ落下しているぷよがあることを記録する
                is_falling = True
            # 新しい位置を保存する
            falling_puyo.y_pos = position
            
        return is_falling
    
    #消せるかどうか判定する
    def check_erase(self, start_frame):
        self.erase_start_frame = start_frame
        self.erasing_puyo_info_list = []
        # 何色のぷよを消したかを記録する
        erased_puyo_color = {}
        # 隣接ぷよを確認する関数内関数を作成
        sequence_puyo_info_list = []
        existing_puyo_info_list = []
        def check_sequential_puyo(x, y):
            # ぷよがあるか確認する
            orig = self.board[y][x]
            if not orig:
                #無いなら何もしない
                return
            # あるなら一旦退避して、メモリ上から消す
            puyo = self.board[y][x]
            tmp = Puyo(
                master = self.master,
                x = x,
                y = y,
                x_pos = x * PUYO_IMG_WIDTH,
                y_pos = y * PUYO_IMG_HEIGHT,
                puyo = puyo
            )
            sequence_puyo_info_list.append(tmp)
            self.board[y][x] = 0
            
            # 四方向の周囲ぷよを確認する
            direction = [[0, 1], [1, 0], [0, -1], [-1, 0]]
            for i in range(4):
                dx = x + direction[i][0]
                dy = y + direction[i][1]
                if dx < 0 or dy < 0 or dx >= self.column or dy >= self.row:
                    # ステージの外にはみ出た
                    continue
                cell = self.board[dy][dx]
                if not cell or cell != puyo:
                    # ぷよの色が違う
                    continue
                # そのぷよのまわりのぷよも消せるか確認する
                check_sequential_puyo(dx, dy)
        
        #実際に削除できるかの確認を行う
        for y in range(self.row):
            for x in range(self.column):
                sequence_puyo_info_list = []
                puyo_color = self.board[y][x]
                check_sequential_puyo(x, y)
                
                if len(sequence_puyo_info_list) == 0 or len(sequence_puyo_info_list) < ERASE_PUYO_COUNT:
                    # 連続して並んでいる数が足りなかったので消さない
                    if len(sequence_puyo_info_list):
                        # 退避していたぷよを消さないリストに追加する
                        existing_puyo_info_list.extend(sequence_puyo_info_list)
                        # 重複を無くす
                        tmp = []
                        for x in existing_puyo_info_list:
                            if x not in tmp:
                                tmp.append(x)
                        existing_puyo_info_list = tmp
                else:
                    #これらは消してよいので消すリストに追加する
                    self.erasing_puyo_info_list.extend(sequence_puyo_info_list)
                    # 重複を無くす
                    tmp = []
                    for x in self.erasing_puyo_info_list:
                        if x not in tmp:
                            tmp.append(x)
                    self.erasing_puyo_info_list = tmp
                    erased_puyo_color[puyo_color] = True
        self.puyo_count -=  len(self.erasing_puyo_info_list)
        #消さないリストに入っていたぷよをリストに復帰させる
        for info in existing_puyo_info_list:
            self.board[info.y][info.x] = info.puyo
        if self.erasing_puyo_info_list:
            # もし消せるならば、消えるぷよの個数と色の情報をまとめて返す
            return {
                "piece": len(self.erasing_puyo_info_list),
                "color": len(erased_puyo_color)
            }
        return None
    
    #消すアニメーションをする
    def erasing(self, frame):
        elapsed_frame = frame - self.erase_start_frame  
        ratio = elapsed_frame / ERASE_ANIMATION_DURATION
        if ratio > 1:
            # アニメーションを終了する
            tmp = list(self.erasing_puyo_info_list)
            for info in self.erasing_puyo_info_list:
                tmp.remove(info)
                self.master.puyo_list.remove(info)
                
            self.erasing_puyo_info_list = tmp
            return False
        elif ratio > 0.75:
            # 消えるぷよを表示する
            for info in self.erasing_puyo_info_list:
                info.vanish = False
            return True
        elif ratio > 0.5:
            # 消えるぷよを消す
            for info in self.erasing_puyo_info_list:
                info.vanish = True
            return True
        elif ratio > 0.25:
            # 消えるぷよを表示する
            for info in self.erasing_puyo_info_list:
                info.vanish = False
            return True
        else:
            # 消えるぷよを消す
            for info in self.erasing_puyo_info_list:
                info.vanish = True
            return True
    
    def update(self):
        self.fill(self.bg)
        if self.zenkeshi_start_frame is not None:
            self.draw_zenkeshi(self.master.frame)
        for puyo in self.master.puyo_list:
            puyo.blit(self)
    
    def hide_zenkeshi(self):
        self.zenkeshi_start_frame = None
    def show_zenkeshi(self, frame):
        self.zenkeshi_start_frame = frame
    def draw_zenkeshi(self, frame):
        duration = frame - self.zenkeshi_start_frame
        ratio = min(duration / (ZENKESHI_DURATION//2), 1)
        y = (1 - ratio) * STAGE_HEIGHT
        self.blit(puyo_image.zenkeshi_image, (0, y))
        if duration == ZENKESHI_DURATION:
            self.hide_zenkeshi()
        
class Player:
    def __init__(self, master):
        self.master = master
        self.up = False
        self.down = False
        self.left = False
        self.right = False
        self.current = [random.randint(1, PUYO_COLORS), random.randint(1, PUYO_COLORS)]
        self.first = [random.randint(1, PUYO_COLORS), random.randint(1, PUYO_COLORS)]
        self.second = [random.randint(1, PUYO_COLORS), random.randint(1, PUYO_COLORS)]
    #ぷよ設置確認
    def create_new_puyo(self):
        # ぷよぷよが置けるかどうか、1番上の段の左から3つ目を確認する
        if self.master.stage.board[0][2]:
            # 空白でない場合は新しいぷよを置けない
            return False
        # 新しいぷよの色を決める
        self.current = self.first
        self.first = self.second
        self.second = [random.randint(1, PUYO_COLORS), random.randint(1, PUYO_COLORS)]
        self.center_puyo = self.current[0]
        self.movable_puyo = self.current[1]
        
        #ぷよの初期位置を定める
        self.x, self.y  = 2, -1
        self.dx, self.dy = 0, -1    #動くぷよの相対位置
        self.rotation = 90    #動くぷよの角度は90度（上向き）
        
        # 新しいぷよを作成する
        self.center_puyo_element = Puyo(
            master = self.master,
            x = self.x,
            y = self.y,
            x_pos = self.x * PUYO_IMG_WIDTH,
            y_pos = self.y * PUYO_IMG_HEIGHT,
            puyo = self.center_puyo
        )
        self.movable_puyo_element = Puyo(
            master = self.master,
            x = self.x + self.dx,
            y = self.y + self.dy,
            x_pos = (self.x + self.dx) * PUYO_IMG_WIDTH,
            y_pos = (self.y + self.dy) * PUYO_IMG_HEIGHT,
            puyo = self.movable_puyo
        )
        
        #接地時間はゼロ
        self.ground_frame = 0
        #ぷよを描画
        self.set_puyo_position()
        return True 
    
    def set_puyo_position(self):
        self.center_puyo_element.x = self.x
        self.center_puyo_element.y = self.y
        x = self.x + math.cos(self.rotation * math.pi / 180) * PUYO_IMG_WIDTH
        y = self.y - math.sin(self.rotation * math.pi / 180) * PUYO_IMG_HEIGHT
        self.movable_puyo_element.x = x
        self.movable_puyo_element.y = y
    
    def falling(self, is_down_pressed):
        # 現状の場所の下にブロックがあるかどうか確認する
        is_blocked = False
        x = self.x
        y = self.y

        dx = self.dx
        dy = self.dy
        if y + 1 >= STAGE_ROW or self.master.stage.board[y + 1][x] or (y + dy + 1 >= 0 and (y + dy + 1 >= STAGE_ROW or self.master.stage.board[y + dy + 1][x + dx])):
            is_blocked = True
        if not is_blocked:
            # 下にブロックがないなら自由落下してよい。プレイヤー操作中の自由落下処理をする
            self.center_puyo_element.y_pos += PLAYER_FALLING_SPEED
            self.movable_puyo_element.y_pos += PLAYER_FALLING_SPEED
            if is_down_pressed:
                # 下キーが押されているならもっと加速する
                self.center_puyo_element.y_pos += PLAYER_DOWN_SPEED
                self.movable_puyo_element.y_pos += PLAYER_DOWN_SPEED
            if self.center_puyo_element.y_pos // PUYO_IMG_HEIGHT != self.center_puyo_element.y:
                #ブロックの境を超えたので再チェックする
                #下キーが押されていたら、得点を加算する
                if is_down_pressed:
                    self.master.score.add_score(1)
                y += 1
                self.y = y
                self.center_puyo_element.y = y
                self.center_puyo_element.y_pos = y * PUYO_IMG_HEIGHT
                self.movable_puyo_element.y_pos = (y + dy) * PUYO_IMG_HEIGHT
                if y + 1 >= STAGE_ROW or self.master.stage.board[y + 1][x] or (y + dy + 1 >= 0 and (y + dy + 1 >= STAGE_ROW or self.master.stage.board[y + dy + 1][x + dx])):
                    is_blocked = True
                if not is_blocked:
                    # 境を超えたが特に問題はなかった。次回も自由落下を続ける
                    self.ground_frame = 0
                    return
                else:
                    # 境を超えたらブロックにぶつかった。位置を調節して、接地を開始する
                    self.center_puyo_element.y_pos = y * PUYO_IMG_HEIGHT
                    self.ground_frame = 1
                    return
            else:
                #自由落下で特に問題がなかった。次回も自由落下を続ける
                self.ground_frame = 0
                return
        if self.ground_frame == 0:
            #初接地である。接地を開始する
            self.ground_frame = 1
            return
        else:
            self.ground_frame += 1
            if self.ground_frame >= PLAYER_GROUND_FRAME:
                return True
            
    def playing(self, frame):
        # まず自由落下を確認する
        # 下キーが押されていた場合、それ込みで自由落下させる
        if self.falling(self.down):
            # 落下が終わっていたら、ぷよを固定する
            self.set_puyo_position()
            return 'fix'
        self.set_puyo_position()
        if self.right or self.left:
            #左右の確認をする
            cx = 1 if self.right else -1
            x = self.x
            y = self.y
            mx = x + self.dx
            my = y + self.dy
            # その方向にブロックがないことを確認する
            # まずは自分の左右を確認
            can_move = True
            if y < 0 or x + cx < 0 or x + cx >= STAGE_COLUMN or self.master.stage.board[y][x + cx]:
                if y >= 0:
                    can_move = False
            if my < 0 or mx + cx < 0 or mx + cx >= STAGE_COLUMN or self.master.stage.board[my][mx + cx]:
                if my >= 0:
                    can_move = False
            # 接地していない場合は、さらに1個下のブロックの左右も確認する
            if self.ground_frame == 0:
                if y + 1 < 0 or x + cx < 0 or x + cx >= STAGE_COLUMN or self.master.stage.board[y + 1][x + cx]:
                    if y + 1 >= 0:
                        can_move = False
                if my + 1 < 0 or mx + cx < 0 or mx + cx >= STAGE_COLUMN or self.master.stage.board[my + 1][mx + cx]:
                    if my + 1 >= 0:
                        can_move = False
            if can_move:       
                # 動かすことが出来るので、移動先情報をセットして移動状態にする   
                self.action_start_frame = frame
                self.move_source = x * PUYO_IMG_WIDTH
                self.move_destination = (x + cx) * PUYO_IMG_WIDTH
                self.x += cx
                return 'moving'
        elif self.up:
            #回転を確認する
            # 回せるかどうかは後で確認。まわすぞ
            x = self.x
            y = self.y
            mx = x + self.dx
            my = y + self.dy
            rotation = self.rotation
            can_rotate = True
            
            cx = 0
            cy = 0
            if rotation == 0:
                # 右から上には100% 確実に回せる。何もしない
                pass
            elif rotation == 90:
                # 上から左に回すときに、左にブロックがあれば右に移動する必要があるのでまず確認する
                if y + 1 < 0 or x - 1 < 0 or x - 1 >= STAGE_COLUMN or self.master.stage.board[y + 1][x - 1]:
                    if y + 1 >= 0:
                        # ブロックがある。右に1個ずれる
                        cx = 1
                #右にずれる必要があるとき、右にもブロックがあれば回転出来ないので確認する
                if cx == 1:
                    if y + 1 < 0 or x + 1 < 0 or y + 1 >= STAGE_ROW or x + 1 >= STAGE_COLUMN or self.master.stage.board[y + 1][x + 1]:
                        if y + 1 >= 0:
                            # ブロックがある。回転出来なかった
                            can_rotate = False
            elif rotation == 180:
                # 左から下に回す時には、自分の下か左下にブロックがあれば1個上に引き上げる。まず下を確認する
                    if y + 2 < 0 or y + 2 >= STAGE_ROW or self.master.stage.board[y + 2][x]:
                        if y + 2 >= 0:
                            # ブロックがある。上に引き上げる
                            cy = -1
                    # 左下も確認する
                    if y + 2 < 0 or y + 2 >= STAGE_ROW or x - 1 < 0 or self.master.stage.board[y + 2][x - 1]:
                        if y + 2 >= 0:
                            # ブロックがある。上に引き上げる
                            cy = -1
            elif rotation == 270:
                # 下から右に回すときは、右にブロックがあれば左に移動する必要があるのでまず確認する
                if y + 1 < 0 or x + 1 < 0 or x + 1 >= STAGE_COLUMN or self.master.stage.board[y + 1][x + 1]:
                    if y + 1 >= 0:
                        # ブロックがある。左に1個ずれる
                        cx = -1
                #左にずれる必要があるとき、左にもブロックがあれば回転できないので確認する
                if cx == -1:
                    if y + 1 < 0 or x - 1 < 0 or x - 1 >= STAGE_COLUMN or self.master.stage.board[y + 1][x - 1]:
                        if y + 1 >= 0:
                            # ブロックがある。回転出来なかった
                            can_rotate = False
            if can_rotate:
                #上に移動する必要があるときは、一気に上げてしまう
                if cy == -1:
                    self.y -= 1
                        
                    self.ground_frame = 0
                    self.center_puyo_element.y = self.y * PUYO_IMG_HEIGHT
                    
                #回すことが出来るので、回転後の情報をセットして回転状態にする
                self.action_start_frame = frame
                self.rotate_before_left = x * PUYO_IMG_HEIGHT
                self.rotate_after_left = (x + cx) * PUYO_IMG_HEIGHT
                self.rotate_from_rotation = self.rotation
                #次の状態を先に設定しておく
                self.x += cx
                dist_rotation = (self.rotation + 90) % 360
                d_combi = [[1, 0], [0, -1], [-1, 0], [0, 1]][dist_rotation // 90]
                self.dx = d_combi[0]
                self.dy = d_combi[1]
                return 'rotating'
        return 'playing'
    
    def moving(self, frame):
        # 移動中も自然落下はさせる
        self.falling(self.down)
        ratio = min(1, (frame - self.action_start_frame) / PLAYER_MOVE_FRAME)
        tmp = self.center_puyo_element.x_pos
        self.center_puyo_element.x_pos = ratio * (self.move_destination - self.move_source) + self.move_source
        diff = self.center_puyo_element.x_pos - tmp
        self.movable_puyo_element.x_pos += diff
        self.set_puyo_position()
        if ratio == 1:
            return False
        return True
    
    def rotating(self, frame):
        # 回転中も自然落下はさせる
        self.falling(self.down)
        ratio = min(1, (frame - self.action_start_frame) / PLAYER_ROTATE_FRAME)
        self.center_puyo_element.x_pos = (self.rotate_after_left - self.rotate_before_left) * ratio + self.rotate_before_left
        self.rotation = self.rotate_from_rotation + ratio * 90
        self.movable_puyo_element.x_pos = self.center_puyo_element.x_pos + math.cos(self.rotation * math.pi/180) * PUYO_IMG_WIDTH
        self.movable_puyo_element.y_pos = self.center_puyo_element.y_pos + math.sin((self.rotation + 180) * math.pi/180) * PUYO_IMG_HEIGHT
        self.set_puyo_position()
        if ratio == 1:
            self.rotation = (self.rotate_from_rotation + 90) % 360
            return False
        return True
    
    def fix(self):
        # 現在のぷよをステージ上に配置する
        x = self.x
        y = self.y
        dx = self.dx
        dy = self.dy
    
        if y >= 0:
            # 画面外のぷよは消してしまう
            self.master.stage.set_puyo(x, y, self.center_puyo)
            self.master.stage.puyo_count += 1

        if y + dy >= 0:
            # 画面外のぷよは消してしまう
            self.master.stage.set_puyo(x + dx, y + dy, self.movable_puyo)
            self.master.stage.puyo_count += 1
        
        #操作用に作成したぷよ画像を消す
        self.master.puyo_list.remove(self.center_puyo_element)
        self.master.puyo_list.remove(self.movable_puyo_element)
    
    def batankyu(self):
        if self.up:
            self.master.start()
            
            
class Score:
    @staticmethod
    def set_num_images():
        num_images = []
        for i in range(10):
            tmp = pygame.image.load(f'../img/{i}.png').convert_alpha()
            aspect = tmp.get_width() / tmp.get_height()
            tmp = pygame.transform.smoothscale(tmp, (int(FONT_HEIGHT * aspect), FONT_HEIGHT))
            num_images.append(tmp)
        return num_images
    
    num_images = set_num_images()
    rensa_bonus = [0, 8, 16, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 480, 512, 544, 576, 608, 640, 672]
    piece_bonus = [0, 0, 0, 0, 2, 3, 4, 5, 6, 7, 10, 10]
    color_bonus = [0, 0, 3, 6, 12, 24]
    
    def __init__(self, master):
        self.window = pygame.Surface((STAGE_WIDTH, FONT_HEIGHT))
        self.window.fill(SCORE_BG)
        self.score = 0   
        self.show_score()
        
    def show_score(self):
        score = self.score
        nums = []
        self.window = pygame.Surface((STAGE_WIDTH, FONT_HEIGHT))
        self.window.fill(SCORE_BG)

        # スコアを下の桁から埋めていく
        for i in range(FONT_LENGTH):
            # 10で割ったあまりを求めて、一番下の桁を取り出す
            number = score % 10
            # 一番うしろに追加するのではなく、一番前に追加することで、スコアの並びを数字と同じようにする
            nums.insert(0, Score.num_images[number])
            # 10 で割って次の桁の準備をしておく
            score = score // 10
            
        for i, n in enumerate(nums):
            self.window.blit(n, (i * (STAGE_WIDTH//FONT_LENGTH), 0))
            

    def calculate_score(self, rensa, piece, color):
        rensa = min(rensa, len(Score.rensa_bonus) - 1)
        piece = min(piece, len(Score.piece_bonus) - 1)
        color = min(color, len(Score.color_bonus) - 1)
        scale = Score.rensa_bonus[rensa] + Score.piece_bonus[piece] + Score.color_bonus[color]
        if scale == 0:
            scale = 1
        self.add_score(scale * piece * 10)

    def add_score(self, score):
        self.score += score
        self.show_score()         
            
            
class Main:
    def __init__(self, x, y):
        self.x, self.y = x, y
        self.start()
        
    def start(self):
        self.mode = 'start'
        self.frame = 0
        self.combination_count = 0
        self.puyo_list = []
        
        self.stage = Stage(self, STAGE_WIDTH, STAGE_HEIGHT, STAGE_ROW, STAGE_COLUMN, STAGE_BG)
        self.player = Player(self)
        self.score = Score(self)
        
        self.first_window = pygame.Surface((PUYO_IMG_WIDTH, PUYO_IMG_HEIGHT*2))
        self.second_window = pygame.Surface((PUYO_IMG_WIDTH, PUYO_IMG_HEIGHT*2))
        
    def loop(self):
        self.stage.update()
        if self.mode == 'start':
            # 最初は、もしかしたら空中にあるかもしれないぷよを自由落下させるところからスタート
            self.mode = 'checkFall'

        elif self.mode == 'checkFall':
            # 落ちるかどうか判定する
            if self.stage.check_fall():
                self.mode = 'fall'
            else:
                # 落ちないならば、ぷよを消せるかどうか判定する
                self.mode = 'checkErase'

        elif self.mode == 'fall':
            if not self.stage.fall():
                # すべて落ちきったら、ぷよを消せるかどうか判定する
                self.mode = 'checkErase'

        elif self.mode == 'checkErase':
            # 消せるかどうか判定する
            eraseInfo = self.stage.check_erase(self.frame)
            if eraseInfo:
                self.mode = 'erasing'
                self.combination_count += 1
                # 得点を計算する
                self.score.calculate_score(self.combination_count, eraseInfo['piece'], eraseInfo['color'])
                self.stage.hide_zenkeshi()
            else:
                if self.stage.puyo_count == 0 and self.combination_count > 0:
                    # 全消しの処理をする
                    self.stage.show_zenkeshi(self.frame)
                    self.score.add_score(3600)
                    pass
                self.combination_count = 0
                # 消せなかったら、新しいぷよを登場させる
                self.mode = 'newPuyo'

        elif self.mode == 'erasing':
            if not self.stage.erasing(self.frame):
                # 消し終わったら、再度落ちるかどうか判定する
                self.mode = 'checkFall'

        elif self.mode == 'newPuyo':
            if not self.player.create_new_puyo():
                # 新しい操作用ぷよを作成出来なかったら、ゲームオーバー
                self.mode = 'gameOver'
            else:
                # プレイヤーが操作可能
                self.mode = 'playing'

        elif self.mode == 'playing':
            # プレイヤーが操作する
            action = self.player.playing(self.frame)
            self.mode = action # 'playing', 'moving', 'rotating', 'fix' のどれかが帰ってくる

        elif self.mode == 'moving':
            if not self.player.moving(self.frame):
                # 移動が終わったので操作可能にする
               self.mode = 'playing'

        elif self.mode == 'rotating':
            if not self.player.rotating(self.frame):
                # 回転が終わったので操作可能にする
                self.mode = 'playing'

        elif self.mode == 'fix':
            # 現在の位置でぷよを固定する
            self.player.fix()
            # 固定したら、まず自由落下を確認する
            self.mode = 'checkFall'

        elif self.mode == 'gameOver':
            # ばたんきゅーの準備をする
            #puyo_image.prepare_batankyu(self.frame)
            self.mode = 'batankyu'

        elif self.mode == 'batankyu':
            puyo_image.batankyu(self.frame, self.stage)
            self.player.batankyu()

        self.frame += 1
    def blit(self, master):
        master.blit(self.stage, (self.x, self.y))
        master.blit(self.score.window, (self.x, self.y + STAGE_HEIGHT))
        fst_center = puyo_image.get_puyo(self.player.first[0])
        fst_movable = puyo_image.get_puyo(self.player.first[1])
        snd_center = puyo_image.get_puyo(self.player.second[0])
        snd_movable = puyo_image.get_puyo(self.player.second[1])
        
        self.first_window.fill((0, 0, 0))
        self.first_window.blit(fst_movable, (0, 0))
        self.first_window.blit(fst_center, (0, PUYO_IMG_HEIGHT))
        
        self.second_window.fill((0, 0, 0))
        self.second_window.blit(snd_movable, (0, 0))
        self.second_window.blit(snd_center, (0, PUYO_IMG_HEIGHT))
        
        master.blit(self.first_window, (self.x + STAGE_WIDTH, self.y))
        master.blit(self.second_window, (self.x + STAGE_WIDTH + PUYO_IMG_HEIGHT, self.y + PUYO_IMG_HEIGHT*2))
        
def main():
    screen.fill((0, 0, 0))
    while True:
        screen.fill(SCORE_BG)
        # 画面に描画
        player1.loop()
        player1.blit(screen)
        for event in pygame.event.get():
            if event.type == pygame.QUIT:    #右上のバッテンが押された時のイベント
                pygame.quit()    #pygameを終了
                sys.exit()    #プログラムを終了
            if event.type == pygame.KEYDOWN:
                # キーが押された時の処理
                if event.key == pygame.K_LEFT:
                    player1.player.left = True
                elif event.key == pygame.K_RIGHT:
                    player1.player.right = True
                elif event.key == pygame.K_UP:
                    player1.player.up = True
                elif event.key == pygame.K_DOWN:
                    player1.player.down = True
                
                if event.key == pygame.K_a:
                    player1.player.left = True
                elif event.key == pygame.K_d:
                    player1.player.right = True
                elif event.key == pygame.K_w:
                    player1.player.up = True
                elif event.key == pygame.K_s:
                    player1.player.down = True
                    
            elif event.type == pygame.KEYUP:
                # キーが離された時の処理
                if event.key == pygame.K_LEFT:
                    player1.player.left = False
                elif event.key == pygame.K_RIGHT:
                    player1.player.right = False
                elif event.key == pygame.K_UP:
                    player1.player.up = False
                elif event.key == pygame.K_DOWN:
                    player1.player.down = False
                
                if event.key == pygame.K_a:
                    player1.player.left = False
                elif event.key == pygame.K_d:
                    player1.player.right = False
                elif event.key == pygame.K_w:
                    player1.player.up = False
                elif event.key == pygame.K_s:
                    player1.player.down = False
                    
        pygame.display.update()    #画面を更新(ほかにもdisplay.flipとかもあるのでお好きな方をどうぞ)
        clock.tick(60)    #fpsを60に設定
        
if __name__ == '__main__':
    puyo_image = PuyoImage(PUYO_IMG_WIDTH, PUYO_IMG_HEIGHT)
    player1 = Main(30, 0)
    main()
```

</details>



## 最後に
　ここまで読んで下さりありがとうございました。JavaScirptからPythonに移植する過程で、言語によって全然違うな～ということもあれば、言語違っても一緒じゃんってこともありました。関数とかループみたいな、どの言語にもあるようなものは特色が出づらく、クラスや三項演算子のような、高級なものほど特色が出るという感じでしょうか。