Start_python’s diary

ふたり暮らし

アラフィフ夫婦のフリーランスプラン

PythonでWEBアプリの三目並べを作る(完成品:FlaskとBrythonを利用)

はじめに

今回はソースコードを一から見直して作り直します。新たに追加したいのはこちらです。

  • クリックイベントの整理
  • ドロップアンドドラッグの実装
  • スマホのタップにも対応

こちらから遊べます。

http://startpython.pythonanywhere.com/game2/

 

クリックイベントの整理

JavaScriptだと当たり前かもしれないですが、イベントの発生条件がどのようにすればわかりませんでした。前回までは各要素をクリックした時に関数を呼び出すようにしていましたが、「マウスダウン」「マウスムーブ」「マウスアップ」にまとめて関数を作ります。

# マウスイベント
document["wrapper"].bind('mousedown', self.click)
document["wrapper"].bind('mousemove', self.move)
document["wrapper"].bind('mouseup', self.release)

 

ドロップアンドドラッグの実装

前回までは「クリックして駒を選択しもう一度クリックした場所に駒を打つ」でしたが、「クリックしたまま移動(ドラッグ)してクリックを離した場所に駒を置く」ように変更します。

 

問題点1

移動用の駒を別に用意してクリックした時に表示させるようにしていましたが、マウスアップ(クリックを離した時)の要素が移動用の駒になってしまいます。

解決策1

クリックなどのイベントを無効化するCSSを使います。

#koma_move {
  pointer-events: none;
}

 

問題点2

駒を移動中(ドラッグ中)にテキストの文字が選択されて反転してしまいます。そのままでも特に問題ないのですが気になるので調べました。

解決策2

テキストを選択させないCSSを使います。

.message-container {
  user-select: none;
}

 

スマホのタップにも対応

PCでは「マウスダウン」「マウスムーブ」「マウスアップ」のイベントが、スマホの場合は「タッチスタート」「タッチムーブ」「タッチエンド」のイベントがあります。
(「マウスダウン」「マウスアップ」はスマホでも使えるのですが「マウスムーブ」はスマホでは認識しません。※最終的に理想のドラッグはスマホではできませんでした)

# タッチイベント
document["wrapper"].bind('touchstart', self.click)
document["wrapper"].bind('touchmove', self.move)
document["wrapper"].bind('touchend', self.release)

 

タッチイベントとマウスイベントの順番

  1. touchstart
  2. touchmove
  3. touchend
  4. mousemove
  5. mousedown
  6. mouseup
  7. click

問題点3

スマホでドラッグすると画面がスクロールしてしまいます。全体的にひとまわり小さくして全体表示できるようにしましたが、それでも上下に少し動いてしまいました。

解決策3

全体「body」の配置方法を「position: fixed;」で固定絶対値にします。
left: calc(50% - 268px/2);」で中央表示にします。(要素の幅が268pxの場合)

 

問題点4

要素をドラッグした時にタップの位置とずれてしまいます。おそらく原因は画面全体が少しスクロールしているためだと思います。

問題点5

タップを離した時の位置にある要素が検出できません。(PCでマウスを離した時の位置にある要素は検出できます)「タッチスタート」の位置にある要素がそのまま「タッチエンド」で検出する要素になってしまいます。

問題点6

「タッチエンド」イベントの後に「マウスダウン」イベントと「マウスアップ」イベントが発生してしまいます。(スマホの時も「マウスダウン」「マウスアップ」は発生するためです)

 

解決策4~6

「問題点3」の画面スクロールしないようにするのを諦めました。それで「問題点4」のドラッグした時の要素とのズレがなくなりました。
「問題点5」「問題点6」は「タッチエンド」を使わずに「マウスアップ」を利用することにしました。これで「タッチスタート」「タッチムーブ」「マウスアップ」の順番で処理できます。
「問題点5」のタップを離した時の位置に駒を置くのを諦めました。もう一度同じ位置でタップすると駒を置けます。

 

ソースコード

from browser import document, alert
import random


class main():

    def __init__(self):
        # 移動確認用
        self.drawing = False
        # 盤面を初期化
        self.board = [0] * 9
        # 先攻後攻
        self.my_turn = True

        # プレイヤーの入力
        self.state = "青の番"
        document["btn_text"].textContent = "コマをえらんでください"

        # マウスイベント
        document["wrapper"].bind('mousedown', self.click)
        document["wrapper"].bind('mousemove', self.move)
        document["wrapper"].bind('mouseup', self.release)
        # タッチイベント
        document["wrapper"].bind('touchstart', self.click)
        document["wrapper"].bind('touchmove', self.move)
        #document["wrapper"].bind('touchend', self.release)


    def click(self, event):
        # 駒を選択
        if self.state == "青の番":

            # 選択した駒を記録・駒を不透明
            self.target = event.target
            self.target.style.opacity = 1

            # 選択した駒の設定・駒の大きさ
            self.koma = ""
            if event.target.classList.contains("blue_s"):
                if not (event.target.classList.contains("red_m") \
                or event.target.classList.contains("red_l")):
                    self.koma = "blue_s"
                    self.center = 15
            if event.target.classList.contains("blue_m"):
                if not event.target.classList.contains("red_l"):
                    self.koma = "blue_m"
                    self.center = 23
            if event.target.classList.contains("blue_l"):
                self.koma = "blue_l"
                self.center = 30

            # 青駒をクリックした場合
            if self.koma[:4] == "blue":
                # 移動用の色を設定
                document["koma_move"].style.backgroundColor = "#00b0f0"

                # 駒の移動
                self.state = "駒の移動"
                document["btn_text"].textContent = "どこにおきますか?"

                # 移動用
                self.drawing = True

        elif self.state == "赤の番":

            # 選択した駒を記録・駒を不透明
            self.target = event.target
            self.target.style.opacity = 1

            # 選択した駒の設定・駒の大きさ
            self.koma = ""
            if event.target.classList.contains("red_s"):
                if not (event.target.classList.contains("blue_m") \
                or event.target.classList.contains("blue_l")):
                    self.koma = "red_s"
                    self.center = 15
            if event.target.classList.contains("red_m"):
                if not event.target.classList.contains("blue_l"):
                    self.koma = "red_m"
                    self.center = 23
            if event.target.classList.contains("red_l"):
                self.koma = "red_l"
                self.center = 30

            # 赤駒をクリックした場合
            if self.koma[:3] == "red":
                # 移動用の色を設定
                document["koma_move"].style.backgroundColor = "#ff0000"

                # 駒の移動
                self.state = "駒の移動"
                document["btn_text"].textContent = "どこにおきますか?"

                # 移動用
                self.drawing = True


    def move(self, event):
        if not self.drawing:
            return

        # 駒の移動
        element = document["koma_move"]
        element.style.display = "inline"
        element.left = event.x - self.center
        element.top = event.y - self.center
        element.width = self.center * 2
        element.height = self.center * 2


    def release(self, event):
        if event.target.id == "btn_text":
            # ゲームの初期化
            if document["btn_text"].textContent == "もう一回あそぶ":
                self.initgame()
            return

        if self.target == event.target:
            document["koma_move"].style.display = "none"
            return

        if self.state != "駒の移動":
            return

        # 移動中の駒を非表示
        document["koma_move"].style.display = "none"

        # 移動用
        self.drawing = False

        # 盤面内か調べる
        if not event.target.classList.contains("square"):
            # 範囲外でクリックした場合
            if self.koma[:4] == "blue":
                # 透明度を設定(青駒を全て)
                for motigoma in document.select(".blue_s"):
                    if not (motigoma.classList.contains("red_m") \
                    or motigoma.classList.contains("red_l")):
                        motigoma.style.opacity = 0.5
                for motigoma in document.select(".blue_m"):
                    if not motigoma.classList.contains("red_l"):
                        motigoma.style.opacity = 0.5
                for motigoma in document.select(".blue_l"):
                    motigoma.style.opacity = 0.5

                self.state = "青の番"
                document["btn_text"].textContent = "コマをえらんでください"

            elif self.koma[:3] == "red":
                # 透明度を設定(赤駒を全て)
                for motigoma in document.select(".red_s"):
                    if not (motigoma.classList.contains("blue_m") \
                    or motigoma.classList.contains("blue_l")):
                        motigoma.style.opacity = 0.5
                for motigoma in document.select(".red_m"):
                    if not motigoma.classList.contains("blue_l"):
                        motigoma.style.opacity = 0.5
                for motigoma in document.select(".red_l"):
                    motigoma.style.opacity = 0.5

                self.state = "赤の番"
                document["btn_text"].textContent = "コマをえらんでください"

            return

        if self.koma[:4] == "blue":
            # 打てない場所か調べる
            if self.koma == "blue_s":
                if event.target.classList.contains("blue_s") \
                or event.target.classList.contains("blue_m") \
                or event.target.classList.contains("blue_l") \
                or event.target.classList.contains("red_s") \
                or event.target.classList.contains("red_m") \
                or event.target.classList.contains("red_l"):
                    alert("そこには打てません")
                    self.target.style.opacity = 0.5
                    self.state = "青の番"
                    document["btn_text"].textContent = "コマをえらんでください"
                    return
            elif self.koma == "blue_m":
                if event.target.classList.contains("blue_m") \
                or event.target.classList.contains("blue_l") \
                or event.target.classList.contains("red_m") \
                or event.target.classList.contains("red_l"):
                    alert("そこには打てません")
                    self.target.style.opacity = 0.5
                    self.state = "青の番"
                    document["btn_text"].textContent = "コマをえらんでください"
                    return
            elif self.koma == "blue_l":
                if event.target.classList.contains("blue_l") \
                or event.target.classList.contains("red_l"):
                    alert("そこには打てません")
                    self.target.style.opacity = 0.5
                    self.state = "青の番"
                    document["btn_text"].textContent = "コマをえらんでください"
                    return

            # 駒を設置
            if self.koma == "blue_s":
                event.target.classList.add("blue_s")
            elif self.koma == "blue_m":
                event.target.classList.add("blue_m")
            elif self.koma == "blue_l":
                event.target.classList.add("blue_l")

            # 選択した駒を削除
            self.target.classList.remove(self.koma);

            # 盤面を記録
            if self.koma == "blue_s":
                self.board[int(event.target.id)] += 1
                if self.target.id[0:3] != "box":
                    self.board[int(self.target.id)] -= 1
            elif self.koma == "blue_m":
                self.board[int(event.target.id)] += 10
                if self.target.id[0:3] != "box":
                    self.board[int(self.target.id)] -= 10
            elif self.koma == "blue_l":
                self.board[int(event.target.id)] += 100
                if self.target.id[0:3] != "box":
                    self.board[int(self.target.id)] -= 100

            # 透明度を解除(青駒を全て)
            for motigoma in document.select(".blue_s"):
                motigoma.style.opacity = 1
            for motigoma in document.select(".blue_m"):
                motigoma.style.opacity = 1
            for motigoma in document.select(".blue_l"):
                motigoma.style.opacity = 1

            # 勝敗の判定
            self.check_state()

            # 勝敗が付いてる場合
            if document["btn_text"].textContent == "もう一回あそぶ":
                return

            # ターン交代
            self.state = "赤の番"
            document["btn_text"].textContent = "コマをえらんでください"

            # 透明度を設定(赤駒を全て)
            for motigoma in document.select(".red_s"):
                if not (motigoma.classList.contains("blue_m") \
                or motigoma.classList.contains("blue_l")):
                    motigoma.style.opacity = 0.5
            for motigoma in document.select(".red_m"):
                if not motigoma.classList.contains("blue_l"):
                    motigoma.style.opacity = 0.5
            for motigoma in document.select(".red_l"):
                motigoma.style.opacity = 0.5

            # テキストを変更
            document["turn_text"].style.color = "#ff0000"

        if self.koma[:3] == "red":
            # 打てない場所か調べる
            if self.koma == "red_s":
                if event.target.classList.contains("blue_s") \
                or event.target.classList.contains("blue_m") \
                or event.target.classList.contains("blue_l") \
                or event.target.classList.contains("red_s") \
                or event.target.classList.contains("red_m") \
                or event.target.classList.contains("red_l"):
                    alert("そこには打てません")
                    self.target.style.opacity = 0.5
                    self.state = "赤の番"
                    document["btn_text"].textContent = "コマをえらんでください"
                    return
            elif self.koma == "red_m":
                if event.target.classList.contains("blue_m") \
                or event.target.classList.contains("blue_l") \
                or event.target.classList.contains("red_m") \
                or event.target.classList.contains("red_l"):
                    alert("そこには打てません")
                    self.target.style.opacity = 0.5
                    self.state = "赤の番"
                    document["btn_text"].textContent = "コマをえらんでください"
                    return
            elif self.koma == "red_l":
                if event.target.classList.contains("blue_l") \
                or event.target.classList.contains("red_l"):
                    alert("そこには打てません")
                    self.target.style.opacity = 0.5
                    self.state = "赤の番"
                    document["btn_text"].textContent = "コマをえらんでください"
                    return

            # 駒を設置
            if self.koma == "red_s":
                event.target.classList.add("red_s")
            elif self.koma == "red_m":
                event.target.classList.add("red_m")
            elif self.koma == "red_l":
                event.target.classList.add("red_l")

            # 選択した駒を削除
            self.target.classList.remove(self.koma);

            # 盤面を記録
            if self.koma == "red_s":
                self.board[int(event.target.id)] += 2
                if self.target.id[0:3] != "box":
                    self.board[int(self.target.id)] -= 2
            elif self.koma == "red_m":
                self.board[int(event.target.id)] += 20
                if self.target.id[0:3] != "box":
                    self.board[int(self.target.id)] -= 20
            elif self.koma == "red_l":
                self.board[int(event.target.id)] += 200
                if self.target.id[0:3] != "box":
                    self.board[int(self.target.id)] -= 200

            # 透明度を解除(赤駒を全て)
            for motigoma in document.select(".red_s"):
                motigoma.style.opacity = 1
            for motigoma in document.select(".red_m"):
                motigoma.style.opacity = 1
            for motigoma in document.select(".red_l"):
                motigoma.style.opacity = 1

            # 勝敗の判定
            self.check_state()

            # 勝敗が付いてる場合
            if document["btn_text"].textContent == "もう一回あそぶ":
                return

            # ターン交代
            self.state = "青の番"
            document["btn_text"].textContent = "コマをえらんでください"

            # 透明度を設定(青駒を全て)
            for motigoma in document.select(".blue_s"):
                if not (motigoma.classList.contains("red_m") \
                or motigoma.classList.contains("red_l")):
                    motigoma.style.opacity = 0.5
            for motigoma in document.select(".blue_m"):
                if not motigoma.classList.contains("red_l"):
                    motigoma.style.opacity = 0.5
            for motigoma in document.select(".blue_l"):
                motigoma.style.opacity = 0.5

            # テキストを変更
            document["turn_text"].style.color = "#00b0f0"


    # 勝敗の判定
    def check_state(self):
        if self.koma[:4] == "blue":
            # 赤を先に判定
            a = "2"
            b = "1"
            c = "赤"
            d = "青"
        elif self.koma[:3] == "red":
            # 青を先に判定
            a = "1"
            b = "2"
            c = "青"
            d = "赤"

        # 8列を調べる
        if (str(self.board[0])[0] == a and str(self.board[1])[0] == a and str(self.board[2])[0] == a) \
        or (str(self.board[3])[0] == a and str(self.board[4])[0] == a and str(self.board[5])[0] == a) \
        or (str(self.board[6])[0] == a and str(self.board[7])[0] == a and str(self.board[8])[0] == a) \
        or (str(self.board[0])[0] == a and str(self.board[3])[0] == a and str(self.board[6])[0] == a) \
        or (str(self.board[1])[0] == a and str(self.board[4])[0] == a and str(self.board[7])[0] == a) \
        or (str(self.board[2])[0] == a and str(self.board[5])[0] == a and str(self.board[8])[0] == a) \
        or (str(self.board[0])[0] == a and str(self.board[4])[0] == a and str(self.board[8])[0] == a) \
        or (str(self.board[2])[0] == a and str(self.board[4])[0] == a and str(self.board[6])[0] == a):
            self.state = c + "の勝ちです"
            element = document["win_msg"]
            element.textContent = self.state
            element.style.display = "inline"
            document["btn_text"].textContent = "もう一回あそぶ"
            return

        if (str(self.board[0])[0] == b and str(self.board[1])[0] == b and str(self.board[2])[0] == b) \
        or (str(self.board[3])[0] == b and str(self.board[4])[0] == b and str(self.board[5])[0] == b) \
        or (str(self.board[6])[0] == b and str(self.board[7])[0] == b and str(self.board[8])[0] == b) \
        or (str(self.board[0])[0] == b and str(self.board[3])[0] == b and str(self.board[6])[0] == b) \
        or (str(self.board[1])[0] == b and str(self.board[4])[0] == b and str(self.board[7])[0] == b) \
        or (str(self.board[2])[0] == b and str(self.board[5])[0] == b and str(self.board[8])[0] == b) \
        or (str(self.board[0])[0] == b and str(self.board[4])[0] == b and str(self.board[8])[0] == b) \
        or (str(self.board[2])[0] == b and str(self.board[4])[0] == b and str(self.board[6])[0] == b):
            self.state = d + "の勝ちです"
            element = document["win_msg"]
            element.textContent = self.state
            element.style.display = "inline"
            document["btn_text"].textContent = "もう一回あそぶ"
            return


    # ゲームの初期化
    def initgame(self):
        # 盤面を初期化
        self.board = [0] * 9
        for square in document.select(".square"):
            square.classList.remove("blue_s")
            square.classList.remove("blue_m")
            square.classList.remove("blue_l")
            square.classList.remove("red_s")
            square.classList.remove("red_m")
            square.classList.remove("red_l")
        document["win_msg"].style.display = "none"
        # 持ち駒の初期化
        for i in range(6):
            document["box"+str(i+1)].classList.remove("red_s")
            document["box"+str(i+1)].classList.remove("red_m")
            document["box"+str(i+1)].classList.remove("red_l")
            document["box"+str(i+1)].classList.remove("blue_s")
            document["box"+str(i+1)].classList.remove("blue_m")
            document["box"+str(i+1)].classList.remove("blue_l")

            document["box"+str(i+7)].classList.remove("red_s")
            document["box"+str(i+7)].classList.remove("red_m")
            document["box"+str(i+7)].classList.remove("red_l")
            document["box"+str(i+7)].classList.remove("blue_s")
            document["box"+str(i+7)].classList.remove("blue_m")
            document["box"+str(i+7)].classList.remove("blue_l")

            n = random.randrange(6)
            if n <= 2:
                document["box"+str(i+1)].classList.add("red_s")
                document["box"+str(i+7)].classList.add("blue_s")
            if n == 3 or n == 4:
                document["box"+str(i+1)].classList.add("red_m")
                document["box"+str(i+7)].classList.add("blue_m")
            if n == 5:
                document["box"+str(i+1)].classList.add("red_l")
                document["box"+str(i+7)].classList.add("blue_l")

        # ゲーム開始
        if document["win_msg"].textContent == "赤の勝ちです":
            self.state = "青の番"
            # テキストを変更
            document["turn_text"].style.color = "#00b0f0"
            # 透明度を解除(赤駒を全て)
            for motigoma in document.select(".red_s"):
                motigoma.style.opacity = 1
            for motigoma in document.select(".red_m"):
                motigoma.style.opacity = 1
            for motigoma in document.select(".red_l"):
                motigoma.style.opacity = 1
            # 透明度を設定(青駒を全て)
            for motigoma in document.select(".blue_s"):
                self.style = motigoma.style
                self.style.opacity = 0.5
            for motigoma in document.select(".blue_m"):
                self.style = motigoma.style
                self.style.opacity = 0.5
            for motigoma in document.select(".blue_l"):
                self.style = motigoma.style
                self.style.opacity = 0.5
        else:
            self.state = "赤の番"
            # テキストを変更
            document["turn_text"].style.color = "#ff0000"
            # 透明度を解除(青駒を全て)
            for motigoma in document.select(".blue_s"):
                style = motigoma.style
                style.opacity = 1
            for motigoma in document.select(".blue_m"):
                style = motigoma.style
                style.opacity = 1
            for motigoma in document.select(".blue_l"):
                style = motigoma.style
                style.opacity = 1
            # 透明度を設定(赤駒を全て)
            for motigoma in document.select(".red_s"):
                self.style = motigoma.style
                self.style.opacity = 0.5
            for motigoma in document.select(".red_m"):
                self.style = motigoma.style
                self.style.opacity = 0.5
            for motigoma in document.select(".red_l"):
                self.style = motigoma.style
                self.style.opacity = 0.5



if __name__ == '__main__':
    main()

 

 

次回からは、AIを使ってコンピュータと対戦できるようにしたいと思います。通常の三目並べと違い、駒の移動ができるため複雑で難しくなりそうです。