乙Py先生のプログラミング教室
初学者のためのプログラミング学習サイト

Python

今回はインスタ風のQRコードを作成する
方法についてです。

解説動画はこちら



通常のQRコード

sample

普通のQRコードはこんな感じです。
四角い黒で表現され、お堅い感じです。

インスタ風は、丸みがあり少しポップな色味です。

これを再現するには通常のPythonライブラリでは
難しいようでした。



インスタ風QRコードを再現する

ということで、自作することにしました。

Google Colabで実行できるコードを作成しました。


ライブラリのインストール

実行にはqrcodeライブラリが必要なのでインストールしましょう。
pip install qrcode

QRコード作成コード
import qrcode
from PIL import Image, ImageDraw

def create_round_qr(data, file_path):
    # QRコードを生成
    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_H,
        box_size=10,
        border=4,
    )
    qr.add_data(data)
    qr.make(fit=True)

    # QRコードをイメージに変換
    img = qr.make_image(fill='black', back_color='white').convert('RGB')
    pix = img.load()

    # 新しいイメージを作成
    width, height = img.size
    round_img = Image.new('RGB', (width, height), 'white')
    draw = ImageDraw.Draw(round_img)

    # 各ピクセルをチェックして形状を描画
    cell_size = 10  # box_sizeと同じにする
    pink = (255, 105, 180)
    white = (255, 255, 255)

    # 描画開始位置を計算(余白を除く)
    start_pos = cell_size * 4

    # 形状を変換
    for y in range(start_pos, height, cell_size):
        for x in range(start_pos, width, cell_size):
            if pix[x, y] == (0, 0, 0):
                draw.ellipse((x, y, x + cell_size, y + cell_size), fill=pink)

    # 隅の四角
    corner_size = cell_size * 7
    for dx, dy in [(0, 0), (width - corner_size - start_pos * 2, 0), (0, height - corner_size - start_pos * 2)]:
        x0, y0 = start_pos + dx, start_pos + dy
        x1, y1 = x0 + corner_size, y0 + corner_size
        draw.rounded_rectangle((x0, y0, x1, y1), radius=0, fill=white)
        draw.rounded_rectangle((x0, y0, x1, y1), radius=15, fill=pink)
        draw.rounded_rectangle((x0 + 1 * cell_size, y0 + 1 * cell_size, x1 - 1 * cell_size, y1 - 1 * cell_size), radius=7, fill=white)
        draw.rounded_rectangle((x0 + 2 * cell_size, y0 + 2 * cell_size, x1 - 2 * cell_size, y1 - 2 * cell_size), radius=5, fill=pink)

    # 保存
    round_img.save(file_path)

一旦QRコードを作成して画像化し
それを加工する内容になっています。

色は pink で指定していますが
変えたい場合はコードのRGB値を変えて貰えば変わります。



実際に使ってみましょう
from PIL import Image
from IPython.display import display

# QRコードを作成(URL , ファイルパス)
create_round_qr("http://www.otupy.net/", "insta.png")

# 画像を読み込む
img = Image.open("insta.png")

# 画像を表示
display(img)
download
すごくpopな形のQRコードが作成できました。

これを配布したら、オシャレですね

今回はインスタ風の
QRコードを作成するコードについてでした

色々試して遊んでみてください。

それでは。

今回は大谷選手の
ホームラン記録を可視化してみました。


解説動画はこちら



先日2024年09月20日
6打数3安打3ホームランなど
前人未到の記録を打ち立てた
ドジャース大谷選手

2004年シーズンの
今日までの成績を可視化してみました。


データの取得


可視化の元となるデータを取得します。

2024/09/21日現在
今季通算 603打数179安打 打率.297 52本塁打
122打点 125得点 52盗塁
という驚異的な成績です

この結果になるように加工していきます。

import requests
from bs4 import BeautifulSoup
import pandas as pd
import warnings
warnings.filterwarnings('ignore')

url = "https://times.abema.tv/articles/-/10018233"

res = requests.get(url)
soup = BeautifulSoup(res.content,"lxml")
table = soup.table

columns = ['日付', '対戦相手', '打順', '第1', '第2', '第3', '第4', '第5', '第6', '第7']
data = []
trs = table.find_all("tr")
for tr in trs[1:]:
    tds = tr.find_all(["td","th"])
    if "なし" in str(tds) or "中止" in str(tds):
        continue
    tmp = [td.text.replace("\xa0","").replace("\n","").replace("\r","").replace("\t","") for td in tds]
    tmp = tmp[0:3]+[t.replace("ニ","二").replace("2","二").replace("3","三") for t in tmp[3:]]
    if len(tmp)>=11:
        tmp = tmp[:10]
    data.append(tmp)

data2 = []
for d in data:
    t1,t2 = d[0:3],d[3:]
    for t in t2:
        if ""!=t:
            data2.append(t1+[t])

# データの確認
data2[0:2]
[['3月20日', 'パドレス', '2指', '遊ゴ'], ['3月20日', 'パドレス', '2指', '右安']]


これを集計しやすいようにデータフレームに加工していきます。
df = pd.DataFrame(data2,columns=["date","match","order","result"])

# 日本語の日付を変換する関数
def convert_date(date_str):
    month,day = date_str.replace("日","").split("月")
    return f"2024-{int(month):02}-{int(day):02}"

df['date'] = df['date'].apply(convert_date)
df['date'] = pd.to_datetime(df['date'])

# 本塁打
df['home_run'] = df['result'].apply(lambda x: 1 if '本' in x else 0)

# 安打
df['hit1'] = df['result'].apply(lambda x: 1 if '安' in x else 0)

# 2塁打
df['hit2'] = df['result'].apply(lambda x: 1 if x.endswith('二') else 0)

# 3塁打
df['hit3'] = df['result'].apply(lambda x: 1 if x.endswith('三') else 0)

# 指定された用語のリスト
terms = ['四球', '敬遠', '死球', '打妨', '中犠', '右犠', '左犠']

# 打数計算
df['flag'] = df['result'].apply(lambda x: 0 if x in terms else 1)

# 打席数合計
df['strokes'] = df['flag'].cumsum()

# ヒット数合計
df['hit_total'] = df[['hit1','hit2','hit3']].sum(axis=1).cumsum()

# ホームラン数合計
df['home_run_total'] = df['home_run'].cumsum()

# 打率
df['average'] = df[['hit_total','home_run_total']].sum(axis=1) / df['strokes']

# 603打数179安打 打率.297 52本塁打
df.tail()
index,date,match,order,result,home_run,hit1,hit2,hit3,flag,strokes,hit_total,home_run_total,average
688,2024-09-20 00:00:00,マーリンズ,1指,右本,1,0,0,0,1,599,125,51,0.2938230383973289
689,2024-09-21 00:00:00,ロッキーズ,1指,空振,0,0,0,0,1,600,125,51,0.29333333333333333
690,2024-09-21 00:00:00,ロッキーズ,1指,中安,0,1,0,0,1,601,126,51,0.2945091514143095
691,2024-09-21 00:00:00,ロッキーズ,1指,中本,1,0,0,0,1,602,126,52,0.2956810631229236
692,2024-09-21 00:00:00,ロッキーズ,1指,一安,0,1,0,0,1,603,127,52,0.296849087893864

これで同じ結果になりました。



Altairでプロットする

ここからこのデータを用いて
可視化を行います。

import pandas as pd
import altair as alt

# Altairで折れ線グラフを作成
chart = alt.Chart(df).mark_line().encode(
    x='date',
    y='average'
).properties(
    title='Data vs Average',
    width=800
)
chart.display()
visualization (1)



打率とホームラン数の推移を
2軸でプロットするとこうなります。
# averageの折れ線グラフ
average_line = alt.Chart(df).mark_line(color='blue').encode(
    x='date',
    y=alt.Y('average', axis=alt.Axis(title='Average'))
)

# home_run_totalの折れ線グラフ
home_run_line = alt.Chart(df).mark_line(color='red').encode(
    x='date',
    y=alt.Y('home_run_total', axis=alt.Axis(title='Home Run Total'))
)

# 二重軸のグラフを作成
chart = alt.layer(
    average_line,
    home_run_line
).resolve_scale(
    y='independent'
).properties(
    title='Data vs Average and Home Run Total',
    width=800
)
chart.display()
visualization


途中から急にホームラン数のピッチが上がっているようです。

ここからどれだけ伸びるのか
残り試合のホームラン数の予測もしてみます。


ホームラン数を時系列予測

残り試合は8試合です。
ARIMAという時系列予測モデルを用いて
予測をしてきます。

先ほどのデータフレームを加工して
予測データを作っていきます。

import pandas as pd
from statsmodels.tsa.arima.model import ARIMA
import matplotlib.pyplot as plt

# 日付をインデックスに設定
df2 = df.copy()
df2 = df.groupby('date').max()

# 欠損日を埋めて連続した日付にする
df2 = df2.asfreq('D')

# ARIMAモデルのフィッティング
model = ARIMA(df2['home_run_total'], order=(1, 1, 1))
model_fit = model.fit()

# 8日先までの予測
forecast = model_fit.forecast(steps=8)

# 結果のプロット
plt.figure(figsize=(12, 4))
plt.plot(df2.index, df2['home_run_total'], label='Actual')
plt.plot(pd.date_range(df2.index[-1] + pd.Timedelta(days=1), periods=8, freq='D'), forecast, label='Forecast', color='red')
plt.legend()
plt.show()
download

実測と予測を組み合わせて可視化しました。

残り8試合だとすると
およそ55本くらいまでは伸びそうです。

予測が外れて60本塁打くらいまで
行って欲しいですね!!!!

今回は大谷選手の打席結果を用いた
ホームラン成績の可視化でした

それでは。

今回はレイトン教授シリーズに出てきた
パズルゲームを解くプログラムについてです。


解説動画はこちら


 



レイトン教授シリーズとは

レベルファイブから発売された
ニンテンドーDSのアドベンチャーゲームシリーズ

謎解きと称した
たくさんの小ゲームで構成されており
これを解いてクリアしていく

パズルゲームなどもかなりたくさん有る

今回はその中でも代表的な
スライドパズル系のパズルを解くものです。


1.ナイトツアー

チェスのナイトの駒で旅をしよう

ナイトは将棋の桂馬のように、2つ先のマス目の左右どちらかに移動できる
ただし、上下左右、どちらの方向にも移動可能である
ただし、1度訪れたマス目には2度と入ることができない

さあ、すべてのマス目を旅してみよう(8aがスタート)
knight-movement


こういったパズルを特には
次のような考え方が重要になってきます。

・探索の深さと幅:

バックトラッキング: 深さ優先探索(DFS)の一種で
 ある経路を可能な限り深く探索する

幅優先探索(BFS): 各レベルのノードを順に探索し
 より広く浅く探索する

・データ構造:

バックトラッキング: 再帰呼び出しやスタックを使って探索を進める

幅優先探索(BFS): キューを使って探索を進める

これらを用いて解くコードを
考えてみました。

Google colabで実行できるようになっています。
日本語表示ように先にこれをインストールしておいてください。
pip install japanize_matplotlib


ナイトツアーを解くコード

バックトラッキングを用いて再帰で探索を行うコードです

colab上ではwidgetsでスライダーを用いて
画像を変更できるようにしています。

import matplotlib.pyplot as plt
import numpy as np
import ipywidgets as widgets
from IPython.display import display

# ナイトの移動可能な8つの方向
dx = [2, 1, -1, -2, -2, -1, 1, 2]
dy = [1, 2, 2, 1, -1, -2, -2, -1]

def is_valid_move(x, y, board):
    return 0 <= x < N and 0 <= y < N and board[x][y] == -1

def solveKnightTour(x, y, move_count, board):
    if move_count == N * N:
        return True
    for i in range(8):
        next_x = x + dx[i]
        next_y = y + dy[i]
        if is_valid_move(next_x, next_y, board):
            board[next_x][next_y] = move_count
            if solveKnightTour(next_x, next_y, move_count + 1, board):
                return True
            board[next_x][next_y] = -1
    return False

def knightTour(board):
    if solveKnightTour(0, 0, 1, board):
        return board
    else:
        print("解が見つかりませんでした")
        return None

# ステップごとのボード状態を画像として保存
def make_img(N):
    img_data = []
    for step in range(N * N):
        fig, ax = plt.subplots(figsize=(8, 8))
        ax.set_xticks(np.arange(N+1)-0.5, minor=True)
        ax.set_yticks(np.arange(N+1)-0.5, minor=True)
        ax.grid(which="minor", color="black", linestyle='-', linewidth=2)
        ax.set_xticks(np.arange(N), minor=False)
        ax.set_yticks(np.arange(N), minor=False)
        ax.grid(which="major", color="black", linestyle='-', linewidth=0)
        
        for i in range(N):
            for j in range(N):
                if board[i][j] <= step:
                    color = 'pink' if board[i][j] == step else ('bisque' if (i + j) % 2 == 0 else 'saddlebrown')
                    ax.text(j, i, str(board[i][j]), ha='center', va='center', fontsize=12)
                    ax.add_patch(plt.Rectangle((j-0.5, i-0.5), 1, 1, fill=True, color=color))
        
        ax.set_xlim(-0.5, N-0.5)
        ax.set_ylim(N-0.5, -0.5)
        ax.set_xticklabels([])
        ax.set_yticklabels([])
        
        fig.canvas.draw()
        img_data.append(np.frombuffer(fig.canvas.buffer_rgba(), dtype=np.uint8).reshape(fig.canvas.get_width_height()[::-1] + (4,)))
        plt.close(fig)
    return img_data

N = 8
board = [[-1 for _ in range(N)] for _ in range(N)]
board[0][0] = 0 # スタート位置

# 解読スタート
board = knightTour(board)
img_data = make_img(N)

# スライダーウィジェットの作成
step_selector = widgets.IntSlider(min=0, max=N*N-1, step=1, description='Step:')

def update_board(step):
    plt.figure(figsize=(8, 8))
    plt.imshow(img_data[step])
    plt.axis('off')
    plt.show()

# インタラクティブなプロットの作成と表示
interactive_plot = widgets.interactive(update_board, step=step_selector)
display(interactive_plot)
スクリーンショット 2024-09-07 17.03.31





2.箱入り娘

じゃまするブロックをスライドさせて
赤い大きなブロックを右側の出口から出してほしい

これは少し実問題と内容が違います。
下記のような構造のパズルを解くものです。

pazzru1

同じ色のブロックはスライドできます。
互いに重ね合わせは出来ず、出口までスライドさせます。
これをデータに定義して解くコードになります。

ここでは
幅優先探索(BFS): キューを使って探索を進める
を用いて探索を行います。

キュー : データを先入れ先出しのリスト構造で保持するもの

from collections import deque
import copy
import japanize_matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import io
from IPython.display import display
import ipywidgets as widgets
from PIL import Image

# ボードのサイズを定義
BOARD_WIDTH = 4
BOARD_HEIGHT = 5

# 駒の初期配置を定義
INITIAL_STATE = [
    ['父', '娘', '娘', '母'],
    ['父', '娘', '娘', '母'],
    ['下', '番', '番', '小'],
    ['下', '中', '', ''],
    ['', '', '', '']
]

# 駒のサイズを定義
PIECE_SIZES = {
    '娘': (2, 2),
    '父': (1, 2),
    '母': (1, 2),
    '番': (2, 1),
    '小': (1, 1),
    '中': (1, 1),
    '下': (2, 1)
}

# 駒の色を定義
PIECE_COLORS = {
    '娘': 'red',
    '父': 'blue',
    '母': 'green',
    '番': 'purple',
    '小': 'orange',
    '中': 'gray',
    '下': 'brown'
}

DIRECTIONS = {
    'up': (-1, 0),
    'down': (1, 0),
    'left': (0, -1),
    'right': (0, 1)
}

### 画像を描画

def draw_board(state):
    fig, ax = plt.subplots()
    ax.set_xlim(0, BOARD_WIDTH)
    ax.set_ylim(0, BOARD_HEIGHT)
    ax.set_xticks([])
    ax.set_yticks([])
    ax.set_aspect('equal')

    for x in range(BOARD_HEIGHT):
        for y in range(BOARD_WIDTH):
            piece = state[x][y]
            if piece:
                width, height = 1, 1
                color = PIECE_COLORS.get(piece, 'gray')
                rect = patches.Rectangle((y, BOARD_HEIGHT - x - 1), width, height, linewidth=1, edgecolor='black', facecolor=color)
                ax.add_patch(rect)
                cx, cy = y + width / 2, BOARD_HEIGHT - x - 1 + height / 2
                ax.text(cx, cy, piece, color='white', ha='center', va='center', fontsize=12, fontweight='bold')

    # 画像をバイトデータとして保存
    buf = io.BytesIO()
    plt.savefig(buf, format='png')
    buf.seek(0)
    plt.close(fig)
    return buf

def generate_images(solution):
    images = []
    for state in solution:
        buf = draw_board(state)
        image = Image.open(buf)
        images.append(image)
    return images

### パズルを解く

def is_goal(state):
    """ゴール状態のチェック"""
    # 例として、特定の位置に特定の駒があるかをチェック
    return state[4][1] == '娘' and state[4][2] == '娘'

def can_move(state, piece, direction):
    """駒が指定の方向に動けるかをチェック"""
    dx, dy = DIRECTIONS[direction]
    for i in range(BOARD_HEIGHT):
        for j in range(BOARD_WIDTH):
            if state[i][j] == piece:
                ni, nj = i + dx, j + dy
                if ni < 0 or ni >= BOARD_HEIGHT or nj < 0 or nj >= BOARD_WIDTH or (state[ni][nj] != '' and state[ni][nj] != piece):
                    return False
    return True

def move_piece(state, piece, direction):
    """駒を指定の方向に動かす"""
    dx, dy = DIRECTIONS[direction]
    new_state = copy.deepcopy(state)
    positions = [(i, j) for i in range(BOARD_HEIGHT) for j in range(BOARD_WIDTH) if state[i][j] == piece]
    for i, j in positions:
        new_state[i][j] = ''
    for i, j in positions:
        new_state[i + dx][j + dy] = piece
    return new_state

def solve(initial_state):
    """幅優先探索でパズルを解く"""
    queue = deque([(initial_state, [])])
    visited = set()
    visited.add(tuple(tuple(row) for row in initial_state))

    while queue:
        current_state, path = queue.popleft()

        if is_goal(current_state):
            return path + [current_state]

        for piece in PIECE_SIZES.keys():
            for direction in DIRECTIONS.keys():
                if can_move(current_state, piece, direction):
                    new_state = move_piece(current_state, piece, direction)
                    state_tuple = tuple(tuple(row) for row in new_state)
                    if state_tuple not in visited:
                        visited.add(state_tuple)
                        queue.append((new_state, path + [current_state]))

    return None

# パズルを解く
solution_path = solve(INITIAL_STATE)

# solution_pathから各ステップの状態を取得
if solution_path:
    solution = solution_path
else:
    solution = []

# 画像データを生成
images = generate_images(solution)

# 画像を表示する関数
def show_image(index):
    display(images[index])

# スライダーウィジェットの作成
slider = widgets.IntSlider(min=0, max=len(images)-1, step=1, description='Step:')
output = widgets.Output()
def on_slider_change(change):
    with output:
        output.clear_output(wait=True)
        show_image(change['new'])

slider.observe(on_slider_change, names='value')

# ウィジェットの表示
with output:
    show_image(0)
display(slider, output)
スクリーンショット 2024-09-07 17.03.11





3.玉を出せ

赤い玉を出口(一番下のマス目)まで持っていけるかな?
じゃまなブロックをうまく動かして道を作ろう。
(灰色のブロックは動かせない)

pazzru2


箱入り娘と違って動かせないブロックが
存在する部分の違いがあります。
これも先ほどと同様、幅優先探索(BFS)で解きます。
from collections import deque
import copy
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import io
from IPython.display import display
import ipywidgets as widgets
from PIL import Image

# ボードのサイズ
BOARD_WIDTH = 6
BOARD_HEIGHT = 6

# 駒の初期配置
INITIAL_STATE = [
    ['0', '0', '10', '0', '0', '0'],
    ['', '1', '1', '7', '5', ''],
    ['2', '2', '9', '9', '5', '0'],
    ['0', '6', '9', '9', '3', '3'],
    ['', '6', '8', '4', '4', ''],
    ['0', '0', '0', '', '0', '0']
]

# 駒のサイズ
PIECE_SIZES = {
    '1': (2, 1),
    '2': (2, 1),
    '3': (2, 1),
    '4': (2, 1),
    '5': (1, 2),
    '6': (1, 2),
    '7': (1, 1),
    '8': (1, 1),
    '9': (2, 2),
    '10': (1, 1),
}

DIRECTIONS = {
    'up': (-1, 0),
    'down': (1, 0),
    'left': (0, -1),
    'right': (0, 1)
}

# 駒の色を定義
PIECE_COLORS = {
    "0": 'gray',
    "1": 'green',
    "2": 'blue',
    "3": 'green',
    "4": 'blue',
    "5": 'magenta',
    "6": 'magenta',
    "7": 'purple',
    "8": 'purple',
    "9": 'yellow',
    "10": 'red'
}

### 画像を描画

def draw_board(state):
    fig, ax = plt.subplots()
    ax.set_xlim(0, BOARD_WIDTH)
    ax.set_ylim(0, BOARD_HEIGHT)
    ax.set_xticks([])
    ax.set_yticks([])
    ax.set_aspect('equal')

    for x in range(BOARD_HEIGHT):
        for y in range(BOARD_WIDTH):
            piece = state[x][y]
            if piece:
                width, height = 1, 1
                color = PIECE_COLORS.get(piece, 'gray')
                rect = patches.Rectangle((y, BOARD_HEIGHT - x - 1), width, height, linewidth=1, edgecolor='black', facecolor=color)
                ax.add_patch(rect)
                cx, cy = y + width / 2, BOARD_HEIGHT - x - 1 + height / 2
                ax.text(cx, cy, piece, color='white', ha='center', va='center', fontsize=12, fontweight='bold')

    # 画像をバイトデータとして保存
    buf = io.BytesIO()
    plt.savefig(buf, format='png')
    buf.seek(0)
    plt.close(fig)
    return buf

def generate_images(solution):
    images = []
    for state in solution:
        buf = draw_board(state)
        image = Image.open(buf)
        images.append(image)
    return images

def is_goal(state):
    """ゴール状態のチェック"""
    return state[5][3] == '10'

def can_move(state, piece, direction):
    """駒が指定の方向に動けるかをチェック"""
    dx, dy = DIRECTIONS[direction]
    for i in range(BOARD_HEIGHT):
        for j in range(BOARD_WIDTH):
            if state[i][j] == piece:
                ni, nj = i + dx, j + dy
                if ni < 0 or ni >= BOARD_HEIGHT or nj < 0 or nj >= BOARD_WIDTH or (state[ni][nj] != '' and state[ni][nj] != piece):
                    return False
    return True

def move_piece(state, piece, direction):
    """駒を指定の方向に動かす"""
    dx, dy = DIRECTIONS[direction]
    new_state = copy.deepcopy(state)
    positions = [(i, j) for i in range(BOARD_HEIGHT) for j in range(BOARD_WIDTH) if state[i][j] == piece]
    for i, j in positions:
        new_state[i][j] = ''
    for i, j in positions:
        new_state[i + dx][j + dy] = piece
    return new_state

def solve(initial_state):
    """幅優先探索でパズルを解く"""
    queue = deque([(initial_state, [])])
    visited = set()
    visited.add(tuple(tuple(row) for row in initial_state))

    while queue:
        current_state, path = queue.popleft()

        if is_goal(current_state):
            return path + [current_state]

        for piece in PIECE_SIZES.keys():
            if piece == '0':
                continue
            for direction in DIRECTIONS.keys():
                if can_move(current_state, piece, direction):
                    new_state = move_piece(current_state, piece, direction)
                    state_tuple = tuple(tuple(row) for row in new_state)
                    if state_tuple not in visited:
                        visited.add(state_tuple)
                        queue.append((new_state, path + [current_state]))

    return None

# パズルを解く
solution_path = solve(INITIAL_STATE)

# solution_pathから各ステップの状態を取得
if solution_path:
    solution = solution_path
else:
    solution = []

#画像データを生成する関数を再利用して描画(この関数はユーザーが提供するものと仮定)
images = generate_images(solution)

#画像を表示する関数(この関数はユーザーが提供するものと仮定)
def show_image(index):
   display(images[index])

#スライダーウィジェットの作成
slider = widgets.IntSlider(min=0, max=len(images)-1, step=1, description='Step:')
output = widgets.Output()

def on_slider_change(change):
   with output:
       output.clear_output(wait=True)
       show_image(change['new'])

slider.observe(on_slider_change, names='value')

#初期表示
with output:
   show_image(0)

#ウィジェットの表示
display(slider, output)
スクリーンショット 2024-09-07 17.02.56


動画内では1枚ずつ解く過程が見られるので
答えを見たい方は最後のおまけを見てみてください。


おわりに

レイトン教授の最後の方のパズルはかなり難しく
人力ではなかなか解けずに、諦めた方も多いのではないでしょうか

そんなパズルもPythonプログラムであれば
解く事ができるコードも作成できるので
パズルゲームに困ったら
プログラミングの力を借りると良いかもしれません。

それでは。



今回はハノイの塔を解くプログラムについてです。


解説動画はこちら



ハノイの塔(Tower of Hanoi)とは

古来からあるパズルゲーム



以下のルールに従って
すべての円盤を右端の杭に移動させられれば完成です。

・3本の杭と、大きさの異なる複数の円盤から構成
・初期配置は左端の杭に昇順で積み重ねられている
・1回に1枚ずつ移動させるられるが
 小さいものの上に大きな円盤を乗せることはできない


ハノイの塔は
「再帰的な方法」で解くことができる古典的な問題です。

ディスクの数 n、出発点の棒 source
目的地の棒 target、および補助の棒 auxiliaryとします。

基底条件として、ディスクが1枚だけの場合は
sourceからtargetに直接移動
それ以外の場合は、再帰的に以下のステップを実行:

n-1枚のディスクをsourceからauxiliaryに移動
残りの1枚のディスクをsourceからtargetに移動
n-1枚のディスクをauxiliaryからtargetに移動

配置するディスクの数 n を変更することで
他のケースも同様に解くことができます。



ハノイの塔を解くコード

# ハノイの塔を攻略
def hanoi(n, source, target, auxiliary):
    if n == 1:
        print(f"{n}を{source}から{target}に移動")
        return
    hanoi(n-1, source, auxiliary, target)
    print(f"{n}を{source}から{target}に移動")
    hanoi(n-1, auxiliary, target, source)

n = 3
hanoi(n, 'A', 'C', 'B')
1をAからCに移動
2をAからBに移動
1をCからBに移動
3をAからCに移動
1をBからAに移動
2をBからCに移動
1をAからCに移動



ハノイの塔を描画するコード

widgetsを使ってstepごとの画像を描画しています。
# ハノイの塔を描画する
import ipywidgets as widgets
import matplotlib.pyplot as plt
import numpy as np

data,img_data = [],[]

# ディスクの色リスト
colors = ['red', 'green', 'blue', 'purple', 'yellow', 'silver', 'orange', 'pink']

# 棒の位置
positions = {'A': 0, 'B': 1, 'C': 2}

# ハノイの塔を攻略
def hanoi(n, source, target, auxiliary):
    if n == 1:
        data.append([n,source,target])
        return
    hanoi(n-1, source, auxiliary, target)
    data.append([n,source,target])
    hanoi(n-1, auxiliary, target, source)

# ディスクを描画するための関数
def draw_towers(state, step):
    fig, ax = plt.subplots()
    ax.set_xlim(-1, 3)
    ax.set_ylim(0, 5)
    ax.set_xticks([0, 1, 2])
    ax.set_xticklabels(['A', 'B', 'C'])
    ax.set_yticks([])
    for tower in 'ABC':
        for j, disk in enumerate(state[tower]):
            color = colors[disk - 1]
            width = 0.2 * disk
            height = 0.5
            ax.bar(positions[tower], height, bottom=j * height, width=width, color=color, edgecolor='black')
    ax.set_title(f'Step {step}')
    fig.canvas.draw()
    img_data.append(np.array(fig.canvas.renderer.buffer_rgba()))
    plt.close(fig)

# 初期状態
n = 4  # ディスクの数
hanoi(n, 'A', 'C', 'B')
towers = {'A': list(range(n, 0, -1)), 'B': [], 'C': []}
draw_towers(towers, 0)

# 各ステップごとに状態を描画
for step, (disk, source, target) in enumerate(data):
    towers[target].append(towers[source].pop())
    draw_towers(towers, step+1)

# img_data に保存された画像を表示するためのウィジェット
def show_image(step):
    plt.imshow(img_data[step])
    plt.axis('off')
    plt.show()

slider = widgets.IntSlider(value=0, min=0, max=len(img_data)-1, step=1, description='Step:')
widgets.interactive(show_image, step=slider)
スクリーンショット 2024-08-31 12.59.50


終わりに

インドの巨大な寺院の話が元ネタのようです。

天地創造の時に神が64枚の純金の円盤を重ねて置いた
司祭たちはそこで、昼夜を通して円盤を別の柱に移し替えている
全ての円盤の移し替えが終わったときに
世界は崩壊し終焉を迎える・・・



ハノイの塔のstep数は 2のディスクの枚数乗 -1 なので
2 ** 64 -1 stepかかります。

すっごい回数
おじさん達が移し替えてると思うと
大変だなーと思う今日この頃です。

それでは。


今回はブロックチェーンのプログラムを
みていきたいと思います。

解説動画はこちら


 
今回は「ブロックチェーン」についてです。


ブロックチェーンとは

分散型台帳技術の一種であり、データを安全かつ
改ざん不可能な形で記録するための仕組みのことです。

連続するブロックで鎖のように構成されています
(参考:NTTデータ)

fig_02_01


最初のブロックは、ジェネシスブロックと呼ばれています。

各ブロックには次のような情報が格納されています。
・トランザクションデータ:取引の詳細情報
・タイムスタンプ:取引が行われた日時
・前のブロックのハッシュ:直前のブロックの暗号学的ハッシュ値
・現在のブロックのハッシュ:現在のブロックのデータから生成されたハッシュ値


ハッシュとは

取引情報やタイムスタンプなどから計算される数値のことです。
ハッシュ計算にはsha256(Secure Hash Algorithm 256-bit)などが使われています。

このハッシュ値を前のブロックの物と比較して
整合性をチェックしているので、改竄が難しくなっています。

ブロック1:データ + ハッシュ0(ジェネシスブロックのハッシュ)
ブロック2:データ + ハッシュ1
ブロック3:データ + ハッシュ2

チェーンの途中のブロックのデータを改竄すると
それ以降全てのハッシュ値を改竄しないといけないため
ものすごい難易度になります。


ブロックチェーンの簡単なプログラム

ブロックチェーンを作ってブロックを繋げるだけの
簡単なサンプルです。
import hashlib
import time

# ブロックを定義するクラス
class Block:
    def __init__(self, index, previous_hash, timestamp, data):
        self.index = index
        self.previous_hash = previous_hash
        self.timestamp = timestamp
        self.data = data
        self.hash = self.calculate_hash()

    def calculate_hash(self):
        sha = hashlib.sha256()
        sha.update((str(self.index) + str(self.previous_hash) + str(self.timestamp) + str(self.data)).encode('utf-8'))
        return sha.hexdigest()

# ブロックチェーンを定義するクラス
class Blockchain:
    def __init__(self):
        self.chain = [self.create_genesis_block()]

    def create_genesis_block(self):
        return Block(0, "0", int(time.time()), "Genesis Block")

    def get_latest_block(self):
        return self.chain[-1]

    def add_block(self, new_block):
        new_block.previous_hash = self.get_latest_block().hash
        new_block.hash = new_block.calculate_hash()
        self.chain.append(new_block)

# 最初のブロックチェーンの初期化
my_blockchain = Blockchain()

# 最初のブロックチェーンに新しいブロックを追加
my_blockchain.add_block(Block(1, "", int(time.time()), "Block 1 Data"))
my_blockchain.add_block(Block(2, "", int(time.time()), "Block 2 Data"))

# ブロックチェーンの内容を表示
for block in my_blockchain.chain:
    print(f"Index: {block.index}")
    print(f"Previous Hash: {block.previous_hash}")
    print(f"Timestamp: {block.timestamp}")
    print(f"Data: {block.data}")
    print(f"Hash: {block.hash}")
    print("-" * 30)
Index: 0
Previous Hash: 0
Timestamp: 1724488836
Data: Genesis Block
Hash: 042bf4db73e03d3047fe19aee7a9d5e4d95a446f8420f56704d2084f231f1350
------------------------------
Index: 1
Previous Hash: 042bf4db73e03d3047fe19aee7a9d5e4d95a446f8420f56704d2084f231f1350
Timestamp: 1724488836
Data: Block 1 Data
Hash: 3cff362cc92bcf78e7d78f0f01435ab18e17d0f543b57634953ee163b6d344f3
------------------------------
Index: 2
Previous Hash: 3cff362cc92bcf78e7d78f0f01435ab18e17d0f543b57634953ee163b6d344f3
Timestamp: 1724488836
Data: Block 2 Data
Hash: bbd7cdf536222d1b757061d459dd7c240dfef1eb311de3b1f5e1ca9f6dfae37e


ブロックの作成時にはHash値が計算され
ブロックには前のブロックのHash値も格納されています。


次は実際の取引っぽいデータでブロックを作り
ブロックチェーンの有効性も検証してみます。
import hashlib
import time
from typing import List

class Block:
    def __init__(self, index: int, previous_hash: str, timestamp: int, transactions: List[dict]):
        self.index = index
        self.previous_hash = previous_hash
        self.timestamp = timestamp
        self.transactions = transactions
        self.hash = self.calculate_hash()

    def calculate_hash(self):
        block_string = f"{self.index}{self.previous_hash}{self.timestamp}{self.transactions}"
        return hashlib.sha256(block_string.encode()).hexdigest()

class Blockchain:
    def __init__(self):
        self.chain = [self.create_genesis_block()]

    def create_genesis_block(self):
        return Block(0, "0", int(time.time()), [{"sender": "genesis", "receiver": "network", "amount": 0}])

    def get_latest_block(self):
        return self.chain[-1]

    def add_block(self, transactions: List[dict]):
        latest_block = self.get_latest_block()
        new_block = Block(latest_block.index + 1, latest_block.hash, int(time.time()), transactions)
        self.chain.append(new_block)

    def is_chain_valid(self):
        for i in range(1, len(self.chain)):
            current_block = self.chain[i]
            previous_block = self.chain[i-1]
            print(f"current_block.hash : {current_block.hash} , current_block.calculate_hash : {current_block.calculate_hash()}")
            print(f"current_block.previous_hash : {current_block.previous_hash} , previous_block.hash : {previous_block.hash}")
            if current_block.hash != current_block.calculate_hash():
                return False
            if current_block.previous_hash != previous_block.hash:
                return False
        return True

# ブロックチェーンの初期化
my_blockchain = Blockchain()

# 取引の例
transactions1 = [
    {"sender": "Alice", "receiver": "Bob", "amount": 50},
    {"sender": "Bob", "receiver": "Charlie", "amount": 30}
]
my_blockchain.add_block(transactions1)

transactions2 = [
    {"sender": "Alice", "receiver": "Charlie", "amount": 20},
    {"sender": "Charlie", "receiver": "Bob", "amount": 10}
]
my_blockchain.add_block(transactions2)

# ブロックチェーンの内容を表示
for block in my_blockchain.chain:
    print(f"Index: {block.index}")
    print(f"Previous Hash: {block.previous_hash}")
    print(f"Timestamp: {block.timestamp}")
    print(f"Transactions: {block.transactions}")
    print(f"Hash: {block.hash}")
    print("-" * 30)

# ブロックチェーンの有効性を検証
print("Is blockchain valid?", my_blockchain.is_chain_valid())
Index: 0 Previous Hash: 0 Timestamp: 1724483719 Transactions: [{'sender': 'genesis', 'receiver': 'network', 'amount': 0}] Hash: ec68f33868e558eca7f3fd6260101430eb12e7e8eb9b328a18882ef6f3ed229c ------------------------------ Index: 1 Previous Hash: ec68f33868e558eca7f3fd6260101430eb12e7e8eb9b328a18882ef6f3ed229c Timestamp: 1724483719 Transactions: [{'sender': 'Alice', 'receiver': 'Bob', 'amount': 50}, {'sender': 'Bob', 'receiver': 'Charlie', 'amount': 30}] Hash: 3a39a53e1423ece90ce7418689a3593959f3041e5b5c5ae7ddf3f95ec9264d25 ------------------------------ Index: 2 Previous Hash: 3a39a53e1423ece90ce7418689a3593959f3041e5b5c5ae7ddf3f95ec9264d25 Timestamp: 1724483719 Transactions: [{'sender': 'Alice', 'receiver': 'Charlie', 'amount': 20}, {'sender': 'Charlie', 'receiver': 'Bob', 'amount': 10}] Hash: b4e7a335791d6b846fba35dece29095f8c2792cde02b6a012403ab6322cd1132 ------------------------------ current_block.hash : 3a39a53e1423ece90ce7418689a3593959f3041e5b5c5ae7ddf3f95ec9264d25 , current_block.calculate_hash : 3a39a53e1423ece90ce7418689a3593959f3041e5b5c5ae7ddf3f95ec9264d25 current_block.previous_hash : ec68f33868e558eca7f3fd6260101430eb12e7e8eb9b328a18882ef6f3ed229c , previous_block.hash : ec68f33868e558eca7f3fd6260101430eb12e7e8eb9b328a18882ef6f3ed229c current_block.hash : b4e7a335791d6b846fba35dece29095f8c2792cde02b6a012403ab6322cd1132 , current_block.calculate_hash : b4e7a335791d6b846fba35dece29095f8c2792cde02b6a012403ab6322cd1132 current_block.previous_hash : 3a39a53e1423ece90ce7418689a3593959f3041e5b5c5ae7ddf3f95ec9264d25 , previous_block.hash : 3a39a53e1423ece90ce7418689a3593959f3041e5b5c5ae7ddf3f95ec9264d25 Is blockchain valid? True

現在のブロックのハッシュと計算されたハッシュ値
現在のブロックにある前のハッシュ値と前のブロックのハッシュ値
で整合性を見ています。





暗号通貨のマイニングの仕組み

マイニング(採掘)とは、分散型ネットワークにて新しい取引を検証し
それをブロックチェーンに追加するプロセスのことです。

特にビットコインなどのProof of Work(PoW)ベースの
暗号通貨において重要な役割を果たしています。

コンセンサスアルゴリズムと呼ばれる
新しいブロックの追加に合意するための仕組みが備わっており
ビットコインなどではProof of Work (PoW)と呼ばれるものが使われています。

Proof of Work (PoW):
計算能力を競い合い、最初に問題を解いたノードが
ブロックを追加する権利を得るというものです。
計算には莫大なリソースが必要になります。


マイニングの基本的な仕組み

ここではすごい荒く説明です。

1.取引の収集と検証:
各取引が有効であることを確認するために
デジタル署名や残高の確認などの検証が行われる

2.ブロックの形成:
検証された取引は、一定数集まると1つのブロックにまとめられる
ブロックには、取引情報の他に、前のブロックのハッシュ値、タイムスタンプ
ナンス(nonce)という特別な値が含まれる。

3.ハッシュ計算とナンスの探索:
マイナーは、ブロックヘッダー(ブロックの要約情報)に
対してハッシュ関数を適用し、ハッシュ値を計算する

目的は、そのハッシュ値が特定の条件(例えば、ハッシュ値が一定の数の先頭ゼロを持つこと)
を満たすようにすること

ナンスはこの条件を満たすために調整される値で
マイナーはナンスを変化させながら何度もハッシュ計算を繰り返します。

4.ブロックの追加:
条件を満たすハッシュ値が見つかると、そのブロックが承認され
ブロックチェーンに追加される

このプロセスを「プルーフ・オブ・ワーク」と呼び
計算量の多さがブロックの正当性を保証する

5.報酬の獲得:
ブロックを成功裏に追加したマイナーには
暗号通貨の新規発行分(ブロック報酬)や取引手数料などの報酬が与えられる

ビットコインの場合、最初のブロック(ジェネシスブロック)以降
約4年ごとにブロック報酬は半減する仕組み(半減期)が導入されている



マイニングのコード

ハッシュ計算とナンスの探索部分を難易度を下げて
実験してみます。


ハッシュを計算する際に
先頭に0が難易度の数値分並ぶかどうかを目的にしています。
0が並ぶまで計算を続け、ヒットしたらハッシュを追加します。

難易度の数値を上げると時間が掛かるようになります。


import hashlib
import time

class Block:
    def __init__(self, index, previous_hash, timestamp, data, nonce=0):
        self.index = index
        self.previous_hash = previous_hash
        self.timestamp = timestamp
        self.data = data
        self.nonce = nonce
        self.hash = self.calculate_hash()

    def calculate_hash(self):
        value = (str(self.index) + str(self.previous_hash) + str(self.timestamp) +
                 str(self.data) + str(self.nonce)).encode()
        return hashlib.sha256(value).hexdigest()

    def mine_block(self, difficulty):
        target = '0' * difficulty
        while self.hash[:difficulty] != target:
            self.nonce += 1
            self.hash = self.calculate_hash()
        print(f"Block mined: {self.hash}")

class Blockchain:
    def __init__(self):
        self.chain = [self.create_genesis_block()]
        self.difficulty = 5  # 難易度

    def create_genesis_block(self):
        return Block(0, "0", time.time(), "Genesis Block")

    def get_latest_block(self):
        return self.chain[-1]

    def add_block(self, new_block):
        new_block.previous_hash = self.get_latest_block().hash
        new_block.mine_block(self.difficulty)
        self.chain.append(new_block)

# ブロックチェーンの初期化
blockchain = Blockchain()

# 新しいブロックを追加
print("Mining block 1...")
new_block = Block(1, "", time.time(), "Block 1 Data")
blockchain.add_block(new_block)

print("Mining block 2...")
new_block = Block(2, "", time.time(), "Block 2 Data")
blockchain.add_block(new_block)

print("Mining block 3...")
new_block = Block(3, "", time.time(), "Block 3 Data")
blockchain.add_block(new_block)
Mining block 1...
Block mined: 00000b1dcfcdb93abbe9d2656e7c36851aae271bfcd45aecb3ec1a0f43474228
Mining block 2...
Block mined: 0000083671360216d051d4ca6a2fef3fc783e311ec3540a484a24a86c85471cb
Mining block 3...
Block mined: 000001d4692c5f50c4e90b74ce7a0bf87839de5893667b424846b8df1c7e5db4


なんとなくではありますが
マイニングの大変さが分かるかと思います。



まとめ

ブロックチェーン技術は暗号通貨の取引や
透明性、セキュリティ、分散化の特性により
今後さらに多くの分野での活用が期待されています。

正当性の証明や、改竄防止関連の仕事が
増えてくると予想しているので
覚えておくと使う機会が来るかもしれません。

それでは





このページのトップヘ