「15パズル」ランキングの表示

学習の目的

  • ・ランキングの表示

最後に、ゲームクリア後にランキングの表示を行いましょう。

ランキングの表示

今回は、架空の得点をいくつか用意しておき、クリア後に自分のデータと比較してランキング表示を行います。

ランキングの対象になるのは、クリアタイムです。

ランキングがかかってくると、燃えてくるよね!!

絶対一位目指すぞ!!

それではいきましょう。

現状のプログラム7を書いておきますね。

プログラム1:盤面モデルの作成(修正前)

# プログラム1
def init():
    global tiles,moves,start_time
    tiles = [
        [1,2,3,4],
        [5,6,7,8],
        [9,10,11,12],
        [13,14,15,0]
    ]
    moves = 0
    start_time = None

プログラム7:ボタン設定・配置(修正前)

import ipywidgets as widgets
from IPython.display import display, clear_output
import random
# 表示を更新
def update_display():
    clear_output(wait=True)
    grid = []
    for i in range(4):
        row = []
        for j in range(4):
            num = tiles[i][j]
            label = str(num) if num != 0 else ""
            btn = widgets.Button(description=label, layout=widgets.Layout(width='50px', height='50px'))
            def on_click(b, i=i, j=j):
                global moves , start_time # 修正点①
                if can_move(i, j):
                    move_tile(i, j)
                    moves = moves + 1 #movesに1を加算する処理を追加
                    if start_time is None:          # 追加
                        start_time = time.time()    # 追加
                    update_display()
            btn.on_click(on_click)
            row.append(btn)
        grid.append(row)

    if is_goal():
        status = widgets.HTML(value="You Win!")
        if start_time:      # 追加
            elapsed = int(time.time() - start_time) # 追加
        else: # 追加
            elapsed = 0     # 追加
        time_html = widgets.HTML(value=f"時間: {format_mmss(elapsed)}")  # 追加
    else:
        status = widgets.HTML(value="")
        time_html = None  # 追加

    # リセットボタン
    reset_btn = widgets.Button(description="Reset", layout=widgets.Layout(width='100px'))
    def on_reset(b):
        global moves # movesを関数内で使用するための宣言を追加
        shuffle()
        moves = 0 # movesを0で初期化する処理を追加
        update_display()
    reset_btn.on_click(on_reset)

    moves_html = widgets.HTML(value=f"移動回数: {moves}")
    # VBox に time_html を条件付きで挿入
    children = [
        widgets.GridBox(sum(grid, []), layout=widgets.Layout(grid_template_columns="repeat(4, 50px)")),
        status,
        moves_html,
        reset_btn
    ]
    if time_html is not None:      # ← クリア時だけ追加
        children.insert(2, time_html)  # status のすぐ下あたりに差し込む

    display(widgets.VBox(children))

①架空の得点

新しいセルに、以下のプログラムを入れて実行してください。

# ランキング(time=秒, moves=手数)
leaderboard = [
    {"name": "JUN", "moves": 160, "time": 230},
    {"name": "RIN", "moves": 110, "time": 200},
    {"name": "AAA", "moves": 120, "time": 125},
    {"name": "MAO", "moves": 140, "time": 250},
    {"name": "KAZ", "moves": 150, "time": 170},
]
TOP_K = 10
PLAYER_NAME = "YOU"   # 任意の表示名

続いて、プログラム1に、以下の修正を入れて実行してください。

# プログラム1
def init():
    global tiles,moves,start_time,ranked #rankedの追加
    tiles = [
        [1,2,3,4],
        [5,6,7,8],
        [9,10,11,12],
        [13,14,15,0]
    ]
    moves = 0
    start_time = None
    ranked = False # クリア記録を「今回分」を登録済みかどうか

まずは、架空のメンバー5人分の得点を保存しました。

これは、どんなデータなんですか?

辞書データと、リストの組み合わせです。

辞書データとは、リストのようにインデックスを数値ではなく、キーの値、例えば文字列で表したものです。

キーの値?

今回だと、"name"や、"time"がそれにあたります。

例えば、以下のプログラムを打つと、データの内容が確認できます。

print(leaderboard[0]["name"]) # JUNと表示
print(leaderboard[2]["time"]) # 125と表示

なるほど、"name"に名前、"moves"に移動回数、"time"にクリア時間が記録されているんだね。


②整列する関数の作成

以前に学んだ整列法を使って、ランキング表を整列しましょう。

新しいセルに、以下のプログラムを完成させて実行してください。

このプログラムは、引数で受け取ったリスト「lb」を時間(lb[添字]["time"])の昇順で整列します。

時間が等しい場合は、移動回数(lb[添字]["moves"])の小さい方を先にします。

ここでは、今までに学んだ3つの整列法、それぞれで表現したものが、下の三つのプログラムです。


整列法①

def sort_leaderboard(lb):
    n = len(lb)
    for i in range(n - 1):
        min_idx = i
        for j in range(i + 1, n):
            a = lb[j]
            b = lb[min_idx]
            if (a["time"] < b["time"]) or (a["time"] == b["time"] and a["moves"] < b["moves"]):
                min_idx = j
        temp = lb[i]
        lb[i] = lb[min_idx]
        lb[min_idx] = temp

このプログラムは、どの整列法(交換法・選択法・挿入法のどれか)でしょうか?

ヒント

これは、繰り返しの中で最も小さい値を選んで、最後に交換していますね。

整列法②

def sort_leaderboard(lb):
    n = len(lb)
    for i in range(n - 1):
        swapped = False
        for j in range(n - 1 - i):
            if (lb[j + 1]["time"] < lb[j]["time"]) or (lb[j + 1]["time"] == lb[j]["time"] and lb[j + 1]["moves"] < lb[j]["moves"]):
                temp = lb[j]
                lb[j] = lb[j + 1]
                lb[j + 1] = temp
                swapped = True
        if swapped == False:
            break

このプログラムは、どの整列法(交換法・選択法・挿入法のどれか)でしょうか?

ヒント

隣同士の比較をしているようです。

整列法③

def sort_leaderboard(lb):
    n = len(lb)
    for i in range(1, n):
        key = lb[i]
        j = i - 1
        while j >= 0 and (
            (key["time"] < lb[j]["time"]) or (key["time"] == lb[j]["time"] and key["moves"] < lb[j]["moves"])
        ):
            lb[j + 1] = lb[j]
            j = j - 1
        lb[j + 1] = key

このプログラムは、どの整列法(交換法・選択法・挿入法のどれか)でしょうか?

ヒント

データを一つずつ後ろへずらしているようです。

3つの整列法のうち1つを選んで、新しいセルに入力し、実行してください。

どれを選んでも結果は変わらないので、好きなものを選んでください。


③ランキングを表示する関数

新しいセルに、以下のプログラムを入れて実行してください。

def build_leaderboard_html(lb):
    rows = []
    for idx, rec in enumerate(lb, start=1):
        rows.append(
            f"<tr>"
            f"<td>{idx}</td>"
            f"<td>{rec['name']}</td>"
            f"<td style='text-align:right;'>{format_mmss(rec['time'])}</td>"
            f"<td style='text-align:right;'>{rec['moves']}</td>"
            f"</tr>"
        )
    return (
        "<table style='border-collapse:collapse; font-family:sans-serif;'>"
        "<tr><th style='text-align:left;'>RANK</th>"
        "<th style='text-align:left;'>NAME</th>"
        "<th style='text-align:right;'>TIME</th>"
        "<th style='text-align:right;'>MOVES</th></tr>"
        + "".join(rows) + "</table>"
    )

ここでは、ランキングを表示するためのHTML文書を作成しています。

引数で先ほど作成したleaderboardを渡すと、ランキング表の表示を行います。

HTMLについての説明は、ここでは省きますね。


④クリア時にデータの登録

プログラム7に、以下の修正を加えます。

※ここで記載されている部分は、プログラムの一部になるので注意してください。

元のプログラムの行番号です。コピーしたプログラムを貼り付ける位置に注意してください。

    if is_goal():
        status = widgets.HTML(value="You Win!")
        elapsed = int(time.time() - start_time) if start_time else 0 
        time_html = widgets.HTML(value=f"時間: {format_mmss(elapsed)}") 
        # 追加ここから
        global ranked
        if not ranked:
            leaderboard.append({"name": PLAYER_NAME, "moves": moves, "time": elapsed})
            sort_leaderboard(leaderboard)
            del leaderboard[TOP_K:]   # 上位のみ保持
            ranked = True
        # 追加ここまで
    else:
        status = widgets.HTML(value="")
        time_html = None  # 追加

ここで、クリアした時に自分の記録を追加し、整列をしています。


⑤ランキングの表示

プログラム7に、以下の変更を加えます。

※ここで記載されている部分は、プログラムの一部になるので注意してください。

ここは、大きく変更を加えています。元のプログラムの54行目〜57行目を全て、以下のプログラムに置き換えてください。

    if time_html is not None:      # ← クリア時だけ追加
        children.insert(2, time_html) 

    # 追加ここから
    rank_title = widgets.HTML(value="ランキング")
    rank_box = widgets.HTML(value=build_leaderboard_html(leaderboard))
    children.extend([rank_title, rank_box])
    # 追加ここまで

    display(widgets.VBox(children))

これで、クリア時にランキングが表示されるはずです。

ただ、リセット時の処理も追加しましょう。

以下の図は実行例です。


⑥プログラムの実行

プログラム8を実行し、ゲームをプレイましょう。

これで、クリア時にランキングが表示されるはずです。

以下の図は実行例です。

うまくできましたか?

お疲れ様でした。

※この再現版では提出は行いません(送信先は未接続です)

Well done!

次のステップに進みましょう!

次のステップに進む

← 前のステップにもどる