PyScript④(時刻を動的に表示)

時刻を動的に表示

PyScriptを使って、時刻を動的に表示してみます。

サンプルコード

動的な表示をするためにはまずimport asyncioと宣言しasyncioライブラリをインポートしておきます。

次に動的に動かしたい関数(今回はfoo関数)にasyncという修飾子をつけます。

foo関数内では、繰り返したい処理をwhile True配下に記載します。

今回は、時刻を取得しそれをoutputDiv2outputDiv3に反映しています。

またawait asyncio.sleep(1)を使って、1秒待ってから実行するようにしています。

[ソースコード]

clock.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<html>
<head>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>

<body>

<div class="font-mono">start time: <label id="outputDiv"></label></div>
<div id="outputDiv2" class="font-mono"></div>
<div id="outputDiv3" class="font-mono"></div>

<py-script>
import asyncio
from datetime import datetime as dt

def format_date(dt_, fmt="%m/%d/%Y, %H:%M:%S"):
return f"{dt_:{fmt}}"

def now(fmt="%m/%d/%Y, %H:%M:%S"):
return format_date(dt.now(), fmt)

async def foo():
while True:
await asyncio.sleep(1)
output = now()
Element("outputDiv2").write(output)

out3 = Element("outputDiv3")
if output[-1] in ["0", "4", "8"]:
out3.write("It's espresso time!")
else:
out3.clear()

pyscript.run_until_complete(foo())
</py-script>
</body>
</html>

[ブラウザ表示]

ブラウザ上に時刻が1秒ごとに更新される(動的に表示される)ことを確認できました。

PyScript③(イベント)

イベント

PyScriptではイベント処理を行うことができます。

今回は、赤・白・黄を選択するラジオボタンを表示し、どのボタンが選択されているかを確認してみます。

サンプルコード

イベント処理を行うためには、まずfrom pyodide.ffi import create_proxyを宣言します。


次にイベントが発生した際に呼び出される関数(select_color関数)を定義します。

この関数内では、Html上のコンポーネントから情報を読み取り、その情報を表示しています。


Html上のコンポーネントはjs.document.getElementsByName関数を使って取得し、取得した各コンポーネントに対してaddEventListener関数を使ってイベントリスナーを設定します。

このaddEventListener関数の第1引数にはイベントの種類を設定し、第2引数にはcreate_proxy関数で作成したプロキシを設定します。

なおcreate_proxy関数の第1引数には最初に定義したselect_color関数を指定しています。

[ソースコード]

Event.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<html>
<head>
<title>Color Picker</title>
<meta charset="utf-8">
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>

<py-script>
from pyodide.ffi import create_proxy

def select_color(event):
print('--- イベント発生 ---')
for ele in color_elements:
print(ele.id, ele.value, ele.checked)

color_elements = js.document.getElementsByName("color")
ele_proxy = create_proxy(select_color)
for ele in color_elements:
if ele.value == "red":
ele.checked = True
ele.addEventListener("change", ele_proxy)
</py-script>

<div id="input" style="margin: 20px;">
色を選んでください。: <br/>
<input type="radio" id="id1" name="color" value="red">
<label for="all"></label>
<input type="radio" id="id2" name="color" value="white">
<label for="chocolate"></label>
<input type="radio" id="id3" name="color" value="yellow">
<label for="cherrie"></label>
</div>

</body>
</html>

[ブラウザ表示]

選択肢の色をクリックするたびにイベントが発生し、選択された色についてはele.checkedTrueとなっていることが確認できました。

PyScript②(ライブラリ)

ライブラリ

PyScriptでライブラリを使用するときには、py-config タグを使います。

例えば matplotlibpandas を使用するためには、下記のように記述します。

1
2
3
<py-config>
packages = ["matplotlib", "pandas"]
</py-config>

サンプルコード

使用するライブラリをpy-configタグで宣言することにより、py-scriptタグ内でそのライブラリをインポートすることができるようになります。

下記のサンプルコードでは、CSVファイルをダウンロードし、pandasライブラリを使ってそのCSVファイルを読み込み、matplotlibライブラリを使って棒グラフを描画しています。

[ソースコード]

matplotlib.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<html>
<head>
<title>Ice Cream Picker</title>
<meta charset="utf-8">
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>

<py-config>
packages = ["matplotlib", "pandas"]
</py-config>

<py-script>
import pandas as pd
import matplotlib.pyplot as plt

from pyodide.http import open_url

url = (
"https://raw.githubusercontent.com/Cheukting/pyscript-ice-cream/main/bj-products.csv"
)
ice_data = pd.read_csv(open_url(url))

def plot(data):
plt.rcParams["figure.figsize"] = (24,12)
fig, ax = plt.subplots()
bars = ax.barh(data["name"], data["rating"], height=0.7)
ax.bar_label(bars)
plt.title("Rating of ice cream flavours of your choice")
display(fig, target="graph-area", append=False)

plot(ice_data)
</py-script>

<div id="graph-area"></div>
</body>
</html>

[ブラウザ表示]

上記のようにブラウザ上に、棒グラフを描画することができました。

PyScript①(Hello, World!を表示)

PyScript

PyScriptを使うと、Htmlの中でPythonのコードを実行することができます。

インストール等は必要ありませんが、HTML読み込み時にランライムのロードやコンポーネントの初期化処理があるため数秒待つ必要があります。

サンプルコード

「Hello, World!」を表示するサンプルを作成します。

PyScriptを実行するためには、HTMLのヘッダー部に次の2行を追加します。

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

Pythonコードは、py-scriptタグの間に記載します。(7~9行目)

[ソースコード]

helloworld.html
1
2
3
4
5
6
7
8
9
10
11
<html>
<head>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>
<py-script>
print('Hello, World!')
</py-script>
</body>
</html>

[ブラウザ表示]

ブラウザ上に、Pythonコードのprintで表示した文字列‘Hello, World!’を表示することができました。

重複組み合わせ

問題(重複組み合わせ)

$ n $ 種類の品物があり、 $ i $ 番目の品物は $ a_i $ 個あります。

異なる種類の品物同士は区別できますが、同じ種類の品物同士は区別できません。

これらの品物の中から $ m $ 個選ぶ組み合わせの総数を求めて下さい。

[制約]
🔹$ 1 \leqq n \leqq 1000 $
🔹$ 1 \leqq m \leqq 1000 $
🔹$ 1 \leqq a_i \leqq 1000 $

解き方・ソースコード

重複なく数え上げるために、同じ種類の品物を一度に処理します。

漸化式を次のように定義します。

$$ dp[i+1][j] := i 番目までの品物からj個選ぶ組み合わせの総数 $$

$ i $ 番目までの品物から $ j $ 個選ぶためには、$ i - 1 $ 番目までの品物から $ j - k $ 個選んで、$ i $ 番目の品物を $ k $ 加えればよいので

$$ dp[i+1][j] = \sum_{k=0}^{min(j,a[i])}dp[i][j-k] $$

という漸化式が成り立ちます。

この漸化式は計算量が $ O(nm^2) $ となりますが、

$$ \sum_{k=0}^{min(j,a[i])}dp[i][j-k] = \sum_{k=0}^{min(j-1,a[i])}dp[i][j-1-k]+dp[i][j]-dp[i][j-1-a_i] $$

であるので、

$$ dp[i+1][j]=dp[i+1][j-1] + dp[i][j]-dp[i][j-1-a_i] $$

と変形することができ、計算量は$ O(nm) $ となります。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#--------- 入力例 ----------
n = 3 # 品物の数
a = [1, 2, 3] # 各品物の個数
m = 3 # 分割数
#---------------------------
memo = {}

# 1つも選ばない方法は常に一通り
for i in range(n + 1):
memo[i, 0] = 1

for i in range(n): # 品物の数に対応するループ
for j in range(1, m + 1): # 分割数に対応するループ
if j - i -a[i] >= 0:
memo[i + 1, j] = memo.get((i + 1, j - 1), 0) + memo.get((i, j), 0) - memo.get((i, j - 1 - a[i]), 0)
else:
memo[i + 1, j] = memo.get((i + 1, j - 1), 0) + memo.get((i, j), 0)

print('解:', memo[m, n])

[実行結果]

解: 6

[1 2 0]、[1 1 1]、[1 0 2]、[0 2 1]、[0 1 2]、[0 0 3]の6パターンあるので、解は6となります。

分割数

問題(分割数)

$ n $ 個の品物を、$ m $ 個以下に分割する方法の総数を求めて下さい。

解き方・ソースコード

n の m 分割 $ a_i ( \sum_{i=1}^{m}a_i=n ) $ を考えてみます。

全ての $ i $ で $ a_i > 0 $ ならば、$ {a_i - 1} $ は $ n - m $ の $ m $ 分割となります。

また、$ a_i = 0 $ となる $ i $ が存在したら、これは $ n $ の $ m - 1 $ 分割となります。

したがって、次のような漸化式を立てることができます。

$$ dp[i][j] = dp[i][j - i] + dp[i - 1][j] $$

この漸化式ならば重複なく分割数を数え上げることができ、計算量は $ 0(mn) $ となります。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
#--------- 入力例 -----------
n = 4 # 品物の数
m = 3 # 分割数
#----------------------------
memo = {(0,0): 1}

for i in range(1, m + 1): # 分割数に対応するループ
for j in range(n + 1): # 品物の数に対応するループ
if j - i >= 0:
memo[i, j] = memo.get((i - 1, j), 0) + memo.get((i, j - i), 0)
else:
memo[i, j] = memo[i - 1, j]
print('解:', memo[m, n])

[実行結果]

解: 4

4個の品物を3分割以下にする方法は、[1 1 2]、[1 3]、[2 2]、[4]の4パターンあるので、解は4となります。

逆ポーランド記法

逆ポーランド記法

数学的な演算を簡単にするのに逆ポーランド記法があります。

逆ポーランド記法では、「1 + 2」という書き方を「1 2 +」というように演算子を数字の後ろに記述します。

逆ポーランド記法で表現されたものはスタックで処理しやすいという特徴があります。

先頭から順に読み込んで、であればスタックに積み、演算子であればスタックから値を2つ取り出して計算した結果をまたスタックに積む、という操作を繰り返すだけで計算ができます。

ソースコード

「4 * (8 + 2) - (3 -1) * 4」という数式を逆ポーランド記法で表現すると「4 8 2 + * 3 1 - 4 * -」となります。

これを計算するソースコードは次のようになります。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def calc(s):
stack = []
for i in s.split(' '):
# スタックの内容を表示
print(stack)
if i == '+':
# +の時はスタックから数字を2つ取り出して加算し、結果をスタックに格納する
b, a = stack.pop(), stack.pop()
stack.append(a + b)
elif i == '-':
# -の時はスタックから数字を2つ取り出して減算し、結果をスタックに格納する
b, a = stack.pop(), stack.pop()
stack.append(a - b)
elif i == '*':
# *の時はスタックから数字を2つ取り出して乗算し、結果をスタックに格納する
b, a = stack.pop(), stack.pop()
stack.append(a * b)
elif i == '/':
# /の時はスタックから数字を2つ取り出して減算し、結果をスタックに格納する
b, a = stack.pop(), stack.pop()
stack.append(a // b)
else:
# 演算子以外(数字)のときはその値を格納する
stack.append(int(i))
return stack[0]

print('解:', calc('4 8 2 + * 3 1 - 4 * -'))

[実行結果]

[]

[4]

[4, 8]

[4, 8, 2]

[4, 10]

[40]

[40, 3]

[40, 3, 1]

[40, 2]

[40, 2, 4]

[40, 8]

解: 32

解は 32 となり「4 * (8 + 2) - (3 -1) * 4」の計算結果として正しいことが確認できます。

A*(エースター)

A*(エースター)

A*(エースター)アルゴリズムは、ダイクストラ法を発展させたアルゴリズムで、ゴールから遠ざかるような無駄な経路は探索しないように工夫することで処理を高速化します。

解き方・ソースコード

ゴールから遠ざかっていることを判断するため、現在値からゴールへのコストの推定値を考えます。

今回はコストの推定値として、マンハッタン距離が与えられているものとします。(21行目)

マンハッタン距離は、各座標の差の絶対値を使うためどの経路でも同じ距離が得られます。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import heapq

def astar(edges, nodes, goal):
dist = [float('inf')] * len(nodes)
dist[0] = 0
q = []
heapq. heappush( q, [0, [0]])

while len(q) > 0:
_, u = heapq.heappop(q)
last = u[-1]
if last == goal:
return u
for i in edges[last]:
if dist[i[0]] > dist[last] + i[1]:
dist[i[0]] = dist[last] + i[1]
# 推定値を踏まえてコストを算出
heapq.heappush(q, [dist[last] + i[1] + nodes[i[0]], u + [i[0]]])
return []

# コストの推定値
nodes = [ 10, 14, 10, 10, 9, 9, 5, 0, 9, 8, 6, 4, 7, 3 ]

# 辺のリスト(終点とコストのリスト)
edges = [ [[4, 1], [5, 1]],
[[2, 12], [3, 4], [4, 15]],
[[1, 12], [9, 2], [11, 6]],
[[1, 4], [5, 3], [8, 3]],
[[1, 15], [0, 1], [6, 6]],
[[0, 1], [3, 3], [6, 4]],
[[4, 6], [5, 4], [10, 1]],
[[11, 4], [13, 5]],
[[3, 3], [9, 1], [10, 5]],
[[2, 2], [8, 1], [12, 1]],
[[6, 1], [8, 5], [13, 3]],
[[2, 6], [7, 4], [12, 5]],
[[9, 1], [11, 5], [13, 6]],
[[7, 5], [10, 3], [12, 6]] ]

print('解:', astar( edges, nodes, 7))

[実行結果]

解: [0, 5, 6, 10, 13, 7]

A*(エースター)アルゴリズムを使って最短経路を求めることができました。

3目並べ(ミニマックス法)

3目並べ(ミニマックス法)

前回は3目並べをランダムでプレイするコードを実装しました。

今回はミニマックス法を使って3目並べをプレイしてみます。

ミニマックス法は相手が自分にとって最も不利になる手を指すと仮定して、最善の手を探す方法です。

解き方・ソースコード

相手が手番の場合は選択できる手の中から最も評価値の高い手を選び、自分が手番の場合は選択できる手の中から最も評価値の低い手を選びます。

ゲームの評価値としては、勝てば $ 1 $ ポイント、負ければ $ -1 $ ポイント、引き分けであれば $ 0 $ ポイントとします。(minmax関数)

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
goal = [0b111000000, 0b000111000, 0b000000111,  # 横に3つ並んだケース
0b100100100, 0b010010010, 0b001001001, # 縦に3つ並んだケース
0b100010001, 0b001010100] # 斜めに3つ並んでケース

# 3つが並んだか判定
def check(player):
for mask in goal:
if player & mask == mask:
return True
return False

# ミニマックス法
def minmax(p1, p2, turn):
if check(p2):
if turn: # 自分の手番のときは勝ち
return 1
else:
return -1 # 相手の手番のときは負け

board = p1 | p2
if board == 0b111111111: # すべて置いたら引き分けで終了
return 0

w = [i for i in range(9) if (board & (1 << i)) == 0]

if turn: # 自分の手番の時は最小値を選ぶ
return min([minmax(p2, p1 | (1 << i), not turn) for i in w])
else: # 相手の手番の時は最大値を選ぶ
return max([minmax(p2, p1 | (1 << i), not turn) for i in w])

# 交互に置く
def play(p1, p2, turn):
if check(p2): # 3つ並んでいたら出力して終了
print('勝負あり')
print([bin(p1), bin(p2)])
return

board = p1 | p2
if board == 0b111111111: # すべて置いたら引き分けで終了
print('引き分け')
print([bin(p1), bin(p2)])
return

# 置ける場所を探す
w = [i for i in range(9) if (board & (1 << i)) == 0]
# 各場所に置いたときの評価値を調べる
r = [minmax(p2, p1 | (1 << i), True) for i in w]
print('選択可能なマス目', w)
print('マス目の評価値', r)

# 評価値が一番高いマス目を取得する
j = w[r.index(max(r))]
print('選択するマス目', j)
print('----------------------------')
play(p2, p1 | (1 << j), not turn) # 手番を入れ替えて次を探す

play(0, 0, True) # プレイ開始

[実行結果]

選択可能なマス目 [0, 1, 2, 3, 4, 5, 6, 7, 8]
マス目の評価値 [0, 0, 0, 0, 0, 0, 0, 0, 0]
選択するマス目 0
----------------------------
選択可能なマス目 [1, 2, 3, 4, 5, 6, 7, 8]
マス目の評価値 [-1, -1, -1, 0, -1, -1, -1, -1]
選択するマス目 4
----------------------------
選択可能なマス目 [1, 2, 3, 5, 6, 7, 8]
マス目の評価値 [0, 0, 0, 0, 0, 0, 0]
選択するマス目 1
----------------------------
選択可能なマス目 [2, 3, 5, 6, 7, 8]
マス目の評価値 [0, -1, -1, -1, -1, -1]
選択するマス目 2
----------------------------
選択可能なマス目 [3, 5, 6, 7, 8]
マス目の評価値 [-1, -1, 0, -1, -1]
選択するマス目 6
----------------------------
選択可能なマス目 [3, 5, 7, 8]
マス目の評価値 [0, -1, -1, -1]
選択するマス目 3
----------------------------
選択可能なマス目 [5, 7, 8]
マス目の評価値 [0, -1, -1]
選択するマス目 5
----------------------------
選択可能なマス目 [7, 8]
マス目の評価値 [0, 0]
選択するマス目 7
----------------------------
選択可能なマス目 [8]
マス目の評価値 [0]
選択するマス目 8
----------------------------
引き分け
['0b10011100', '0b101100011']

結果は引き分けとなりました。

3目並べでは両者が最善の手を選べば、確実に引き分けになります。

3目並べ(ランダム)

3目並べ

3目並べは、3×3のマス目に〇と×を交互に書いて、縦・横・斜めのいずれかに3つを並べたほうが勝ちとなるゲームです。

解き方・ソースコード

ビット演算を使って3目並べを実装してみます。

先手と後手を別々の変数で保持します。

また、2進数の各桁を9か所のマスに割り当てます。

縦3マス、横3マスを2進数の1次元配列に対応づけると、空いているマスは先手と後手の変数に対してOR演算を行うと簡単にチェックできます。
(すべてのマスが埋まっていると、OR演算の結果として全てのビットが1になります。)


勝負の判定は、同じ記号が3つ並んだ場合で行うため、3つ並んだパターンを事前に用意しておきます。(goal変数)

このパターンとAND演算を行った結果が、パターンと同じであれば3つ並んだと判定できます。(check関数)


プレイ方法として、コンピュータ同士の対戦で、マス目の空いているところにランダムに置いていくことにします。
(実行するたびに結果が変わります。)

空いている場所(マス目)は、現在の盤面(双方のOR演算した結果)に対して1桁ずつAND演算を行い、0となったものを探します。(28行目)

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import random

goal = [0b111000000, 0b000111000, 0b000000111, # 横に3つ並んだケース
0b100100100, 0b010010010, 0b001001001, # 縦に3つ並んだケース
0b100010001, 0b001010100] # 斜めに3つ並んでケース

# 3つが並んだか判定
def check(player):
for mask in goal:
if player & mask == mask:
return True
return False

# 交互に置く
def play(p1, p2):
if check(p2): # 3つ並んでいたら出力して終了
print('勝負あり')
print([bin(p1), bin(p2)])
return

board = p1 | p2
if board == 0b111111111: # すべて置いたら引き分けで終了
print('引き分け')
print([bin(p1), bin(p2)])
return

# 置ける場所(マス目)を探す
w = [i for i in range(9) if (board & (1 << i)) == 0]
# ランダムに置く
r = random.choice(w)
play(p2, p1 | (1 << r)) # 手番を入れ替えて次を探す

play(0, 0) # プレイ開始

[実行結果(勝負がつく場合)]

勝負あり
['0b10101000', '0b1000111']

下段が3つ並んでいるので勝負がついています。

[実行結果(引き分けの場合)]

引き分け
['0b10100101', '0b101011010']

全てのマスが埋まっていて、3つ並んでいる箇所がないため引き分けとなります。