PythonでWEBアプリの三目並べを作る(試作品:FlaskとBrythonを利用)
はじめに
ブラウザ上で動く三目並べを作っていきます。とりあえず試作品が完成したのでソースコードを載せていきます。細かい解説は省略します。
動作環境
Windows10
Python 3.7.5
Flask 1.1.1
Brython 3.8.7
作業フォルダ/ ├ game1/ │ ├ templates/ │ │ └ game1/ │ │ └ oxgame.html │ └ server.py ├ static/ │ ├ brython.js │ ├ brython_stdlib.js │ ├ oxgame.css │ └ oxgame.py ├ templates/ │ └ index.html └ main.py
ソースコード
「作業フォルダ/game1/templates/game1/」
oxgame.html
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/static/oxgame.css">
<title>三目並べ</title>
</head>
<body onload="brython()">
<script type="text/python" src="/static/oxgame.py"></script>
<div id="wrapper" class="wrapper">
<div class="game-container">
<div class="motigoma1_container">
<div id="nokori1_s"></div>
<div id="nokori1_m"></div>
<div id="nokori1_l"></div>
</div>
<div class="motigoma1_box">
<div id="box1" class="motigoma1 red_s"></div>
<div id="box2" class="motigoma1 red_m"></div>
<div id="box3" class="motigoma1 red_l"></div>
</div>
<div class="motigoma1_box">
<div id="box4" class="motigoma1 red_s"></div>
<div id="box5" class="motigoma1 red_m"></div>
<div id="box6" class="motigoma1 red_l"></div>
</div>
<div class="message-container">
<ul class="message-list">
<li>
<span id="turn_text" class="maru">■</span>のばん
</li>
<li class="js-hidden">
<span class="batsu">■</span>のばん
</li>
<li class="js-hidden">
<span class="maru">■</span>の勝ち!
</li>
<li class="js-hidden">
<span class="batsu">■</span>の勝ち!
</li>
<li class="js-hidden">
引き分け
</li>
</ul>
</div>
<div class="squares-container">
<div class="squares-box">
<div id="0" class="square"></div>
<div id="1" class="square"></div>
<div id="2" class="square"></div>
<div id="3" class="square"></div>
<div id="4" class="square"></div>
<div id="5" class="square"></div>
<div id="6" class="square"></div>
<div id="7" class="square"></div>
<div id="8" class="square"></div>
<div id="win_msg">赤の勝ちです
</div>
</div>
</div>
<div class="btn-container">
<span id="btn_text" class="btn btn-reset">
もう一回遊ぶ
</span>
</div>
<div class="motigoma2_box">
<div id="box7" class="motigoma2 blue_s"></div>
<div id="box8" class="motigoma2 blue_m"></div>
<div id="box9" class="motigoma2 blue_l"></div>
</div>
<div class="motigoma2_box">
<div id="box10" class="motigoma2 blue_s"></div>
<div id="box11" class="motigoma2 blue_m"></div>
<div id="box12" class="motigoma2 blue_l"></div>
</div>
<div class="motigoma2_container">
<div id="nokori2_s"></div>
<div id="nokori2_m"></div>
<div id="nokori2_l"></div>
</div>
</div>
<div id="koma_move">
</div>
</div>
<script src="/static/brython.js"></script>
<script src="/static/brython_stdlib.js"></script>
</body>
</html>
「作業フォルダ/game1/」
server.py
# Blueprint(pyファイルを分割するための関数)をインポート from flask import Blueprint #「app」を「Blueprint()」を使って定義 #「game1」の部分は、url_forで使用(今回は未使用) app = Blueprint('game1', __name__, template_folder='templates') # 必要なモジュールをインポート from flask import Flask, render_template #「/」へアクセスがあった場合 @app.route('/') def index(): return render_template('game1/oxgame.html')
「作業フォルダ/static/」
oxgame.css
html {
box-sizing: border-box;
font-size: 16px;
}
body {
margin: 0;
line-height: normal;
}
.wrapper {
max-width: 500px;
margin: 0 auto;
padding: 0 10px;
text-align: center;
}
.motigoma1_container {
margin: 0 auto;
width: 246px;
height: 15px;
display: flex;
flex-wrap: wrap;
}
#nokori1_s {
width: 80px;
}
#nokori1_m {
width: 80px;
}
#nokori1_l {
width: 80px;
}
.motigoma1_box {
margin: 0 auto;
width: 246px;
height: 64px;
display: flex;
flex-wrap: wrap;
border: solid 1px #fff;
}
.motigoma1 {
position: relative;
width: 80px;
height: 64px;
border: solid 1px #fff;
}
.motigoma1::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
border-radius: 20%;
transform: translate(-50%, -50%);
}
.message-container {
font-size: 1.2rem;
font-weight: bold;
}
.message-list {
margin: 0;
padding: 0;
}
ul {
list-style: none;
}
#turn_text {
color: #00b0f0;
}
.squares-container {
margin: 0 auto;
width: 246px;
}
.squares-box {
width: 246px;
height: 246px;
display: flex;
flex-wrap: wrap;
border: solid 1px #333;
}
.square {
position: relative;
width: calc(240px / 3);
height: calc(240px / 3);
border: solid 1px #888;
}
.square::before {
content: '';
border-radius: 20%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.motigoma2_container {
margin: 0 auto;
width: 246px;
height: 5px;
display: flex;
}
#nokori2_s {
width: 80px;
}
#nokori2_m {
width: 80px;
}
#nokori2_l {
width: 80px;
}
.motigoma2_box {
margin: 0 auto;
width: 246px;
height: 64px;
display: flex;
flex-wrap: wrap;
border: solid 1px #fff;
}
.motigoma2 {
position: relative;
width: 80px;
height: 64px;
border: solid 1px #fff;
opacity: 0.5;
}
.motigoma2::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
border-radius: 20%;
transform: translate(-50%, -50%);
}
.btn-container {
padding: 10px 0;
}
.btn {
display: inline-block;
padding: 5px 20px;
width: 200px;
color: #fff;
background-color: #ffc000;
font-weight: bold;
border-radius: 5px;
cursor: pointer;
}
#btn-reset {
color: #fff;
background-color: #ffc000;
font-weight: bold;
}
.btn-reset:hover {
background-color: #ffd347;
transition-duration: 0.4s;
}
.red_s::before {
width: 30px;
height: 30px;
background-color: #ff0000;
cursor: pointer;
}
.blue_s::before {
width: 30px;
height: 30px;
background-color: #00b0f0;
cursor: pointer;
}
.red_m::before {
width: 45px;
height: 45px;
background-color: #ff0000;
cursor: pointer;
}
.blue_m::before {
width: 45px;
height: 45px;
background-color: #00b0f0;
cursor: pointer;
}
.red_l::before {
width: 60px;
height: 60px;
background-color: #ff0000;
cursor: pointer;
}
.blue_l::before {
width: 60px;
height: 60px;
background-color: #00b0f0;
cursor: pointer;
}
#koma_move {
position: absolute;
background-color: #00b0f0;
border-radius: 20%;
cursor: pointer;
}
#win_msg {
position: absolute;
padding: 93px 3px;
font-size: 40px;
background-color: #ccc;
opacity: 0.8;
display: none;
}
.js-hidden {
display: none;
}
oxgame.py
from browser import document, alert import random class main(): def __init__(self): # 盤面を初期化 self.board = [0] * 9 #alert(self.board) # ゲーム開始 self.state = "ゲーム開始" # 先攻後攻 self.my_turn = True # プレイヤーの入力 self.state = "青の番" document["btn_text"].textContent = "駒を選択してください" # 駒の選択 for motigoma in document.select(".blue_s"): motigoma.bind("click", self.koma_sentaku) for motigoma in document.select(".blue_m"): motigoma.bind("click", self.koma_sentaku) for motigoma in document.select(".blue_l"): motigoma.bind("click", self.koma_sentaku) # 駒の移動 #document.bind("mousemove", self.mousemove) # 駒を非表示 document["koma_move"].bind("mousedown", self.movedelete) # 駒を打つ for i in range(9): document.select(".square")[i].bind("mouseup", self.change_class) document["wrapper"].bind("mouseup", self.put_back) # ゲームの初期化 document["btn_text"].bind("click", self.initgame) # コンピュータの入力 #self.state = "赤の番" #document["test"].textContent = self.state #for motigoma in document.select(".red_s"): # motigoma.bind("mouseup", self.koma_sentaku) #for motigoma in document.select(".red_m"): # motigoma.bind("mouseup", self.koma_sentaku) #for motigoma in document.select(".red_l"): # motigoma.bind("mouseup", self.koma_sentaku) # 駒を選択 def koma_sentaku(self, event): #if self.state == "青の番" or self.koma[:4] == "blue": if self.state == "青の番": # 透明度を設定(青駒を全て) 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 document["koma_move"].style.backgroundColor = "#00b0f0" # 選択した駒だけ不透明 self.target = event.target self.target.style.opacity = 1 self.koma = "" if event.target.classList.contains("blue_s"): self.koma = "blue_s" self.center = 15 if event.target.classList.contains("blue_m"): 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.bind("mousemove", self.mousemove) #event.preventDefault() self.state = "駒の移動" document["btn_text"].textContent = "打つ場所を選んでください" #if self.state == "赤の番" or self.koma[:3] == "red": elif self.state == "赤の番": # 透明度を設定(赤駒を全て) 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 document["koma_move"].style.backgroundColor = "#00b0f0" # 選択した駒だけ不透明 self.target = event.target self.target.style.opacity = 1 self.koma = "" if event.target.classList.contains("red_s"): self.koma = "red_s" self.center = 15 if event.target.classList.contains("red_m"): self.koma = "red_m" self.center = 23 if event.target.classList.contains("red_l"): self.koma = "red_l" self.center = 30 document["koma_move"].style.backgroundColor = "#ff0000" # 赤駒をクリックした場合 if self.koma[:3] == "red": # 駒の移動 document.bind("mousemove", self.mousemove) #event.preventDefault() self.state = "駒の移動" document["btn_text"].textContent = "打つ場所を選んでください" # 駒の移動 def mousemove(self, event): 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 self.state = "非表示" #document["test"].textContent = self.state # 移動中の駒を非表示 def movedelete(self, event): if self.state != "非表示": return element = document["koma_move"] element.style.display = "none" # 駒の移動を解除 document.unbind("mousemove") # 駒を打つ #for i in range(9): # document.select(".square")[i].bind("mouseup", self.change_class) #document["wrapper"].bind("mouseup", self.put_back) #document["test"].textContent = self.state # 元に戻す(駒以外にクリックした場合) def put_back(self, event): if self.state != "非表示": return # 範囲外でクリックした場合 if self.koma[:4] == "blue": # 透明度を設定(青駒を全て) 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 self.state = "青の番" document["btn_text"].textContent = "駒を選択してください" elif self.koma[:3] == "red": # 透明度を設定(赤駒を全て) 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 self.state = "赤の番" document["btn_text"].textContent = "駒を選択してください" # 駒を打つ def change_class(self, event): if self.state != "非表示": return # 打てない場所か調べる #if event.target.classList.contains("blue_s") \ #or event.target.classList.contains("blue_m") \ #or event.target.classList.contains("blue_l"): # alert("そこには打てません") # 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("そこには打てません") 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("そこには打てません") return elif self.koma == "blue_l": if event.target.classList.contains("blue_l") \ or event.target.classList.contains("red_l"): alert("そこには打てません") 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); # 盤面を記録 #alert(self.target.id + "a") #alert(str(self.board[0])[0]) 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 #alert(self.board) # テスト表示 #alert(str(self.board[0:3])[0]) # テスト表示 # IDを文字で返す alert(event.target.id *2) # 透明度を解除(自駒を全て) 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 # 勝敗の判定 self.check_state() # 勝敗が付いてる場合 if document["btn_text"].textContent == "もう一回遊ぶ": return # ターン交代 self.my_turn = not(self.my_turn) self.state = "赤の番" document["btn_text"].textContent = "駒を選択してください" # 駒の選択用をリセット for motigoma in document.select(".red_s"): motigoma.bind("click", self.koma_sentaku) for motigoma in document.select(".red_m"): motigoma.bind("click", self.koma_sentaku) for motigoma in document.select(".red_l"): motigoma.bind("click", self.koma_sentaku) # 透明度を設定(赤駒を全て) for motigoma in document.select(".red_s"): self.style = motigoma.style if self.target.id != "": self.style.opacity = 0.5 for motigoma in document.select(".red_m"): self.style = motigoma.style if self.target.id != "": self.style.opacity = 0.5 for motigoma in document.select(".red_l"): self.style = motigoma.style if self.target.id != "": self.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("そこには打てません") 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("そこには打てません") return elif self.koma == "red_l": if event.target.classList.contains("blue_l") \ or event.target.classList.contains("red_l"): alert("そこには打てません") return # 駒を設置 #event.target.classList.add(self.koma) 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 #alert(self.board) # テスト表示 # 透明度を解除(自駒を全て) 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"): motigoma.bind("click", self.koma_sentaku) for motigoma in document.select(".blue_m"): motigoma.bind("click", self.koma_sentaku) for motigoma in document.select(".blue_l"): motigoma.bind("click", self.koma_sentaku) # 透明度を設定(赤駒を全て) 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 # テキストを変更 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, event): if document["btn_text"].textContent != "もう一回遊ぶ": alert(self.board) # テスト表示 return #alert(self.board) # テスト表示 # 盤面を初期化 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(12): 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") #num = random.randint(1, 3) n = random.randrange(6) if i < 6 and n <= 2: document["box"+str(i+1)].classList.add("red_s") if i < 6 and (n == 3 or n == 4): document["box"+str(i+1)].classList.add("red_m") if i < 6 and n == 5: document["box"+str(i+1)].classList.add("red_l") if i >= 6 and n <= 2: document["box"+str(i+1)].classList.add("blue_s") if i >= 6 and (n == 3 or n == 4): document["box"+str(i+1)].classList.add("blue_m") if i >= 6 and n == 5: document["box"+str(i+1)].classList.add("blue_l") ''' if i == 0 or i == 3: document["box"+str(i+1)].classList.add("red_s") if i == 1 or i == 4: document["box"+str(i+1)].classList.add("red_m") if i == 2 or i == 5: document["box"+str(i+1)].classList.add("red_l") if i == 6 or i == 9: document["box"+str(i+1)].classList.add("blue_s") if i == 7 or i == 10: document["box"+str(i+1)].classList.add("blue_m") if i == 8 or i == 11: document["box"+str(i+1)].classList.add("blue_l") ''' # ゲーム開始 self.state = "青の番" if __name__ == '__main__': main()
「作業フォルダ/templates/」
index.html
<!DOCTYPE html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ふたり暮らし メニュー画面</title>
</head>
<body>
<h2>メインメニュー</h2>
<p><a href= "/blog" >ブログ(登録とログイン)</a></p>
<p><a href= "/game1" >三目並べ(〇×ゲーム)</a></p>
</body>
</html>
「作業フォルダ/」
main.py
from flask import Flask, render_template app = Flask(__name__) # 三目並べ from game1.server import app as app3 app.register_blueprint(app3, url_prefix="/game1") @app.route('/') def index(): #return render_template('oxgame.html') return render_template('index.html') if __name__ == '__main__': app.run(debug=True)
動作確認

まとめ
まだまだ試作段階なのでソースで使っていない部分も消せてないです。バグもいくつか見つかっています。「もう一回遊ぶ」で持ち駒をランダムにしてみましたが赤と青の力の差がありすぎてしまいます。先攻後攻も決められるようにしたいです。
今回はこちらのサイトを参考にさせていただきました。