前回:Pythonの二次元リストでゲームの盤面を作る!行と列の順番と作り方の落とし穴|ゲームで学ぶアルゴリズム 第1回

「クリックした場所が、盤面のどのマスなのかわからない…」「(x, y) で考えていたのに、リストに入れると順番がこんがらがる」

ゲームを作り始めると、ここで手が止まりますよね。この記事は連載「ゲームで学ぶアルゴリズム」の第2回です。今回は、座標(x・y)とリストの番号を行き来する計算を3つ取り上げます。

対象は、前回の二次元リストを読んだ初心者〜初級者の方です。動作確認は Python 3.13 で行いました。一緒に見ていきましょう! 😊

まず覚えること:x は列、y は行

grid paper coordinates
grid paper coordinates / Photo by Sergey Meshkov via Pexels

ゲームでは、横の位置を x、縦の位置を y で表します。前回の board[行][列] に当てはめると、board[y][x] です。

つまり「(x, y) と書くときは x が先、リストを読むときは y が先」。この記事のコードは、すべてこの約束で書いています。

サンプル1:(x, y) と「1列の番号」を変換する

盤面を二次元リストではなく、1本のリストで持つこともあります。マスに通し番号を付けておくと、マスを1つの数で表せて便利なんです。そのための変換を作ります。

# 横5マス × 縦3マスの盤面
WIDTH = 5
HEIGHT = 3


def to_index(x, y):
    """(x, y) → 1列に並べたときの番号"""
    return y * WIDTH + x


def to_xy(i):
    """番号 → (x, y)"""
    y, x = divmod(i, WIDTH)  # 商が y(何行目)、余りが x(何列目)
    return x, y


# 盤面を「1列のリスト」で持つ(15マス)
cells = ["."] * (WIDTH * HEIGHT)
cells[to_index(3, 1)] = "$"  # x=3, y=1 にコインを置く

print("各マスの番号")
for y in range(HEIGHT):
    print(" ".join(f"{to_index(x, y):2}" for x in range(WIDTH)))

print("盤面")
for y in range(HEIGHT):
    print("".join(cells[to_index(x, y)] for x in range(WIDTH)))

for i in [0, 4, 5, 8, 14]:
    print(i, "→", to_xy(i))
サンプル1の実行結果:(x, y)と番号を変換する
サンプル1の実行結果:(x, y)と番号を変換する
各マスの番号
 0  1  2  3  4
 5  6  7  8  9
10 11 12 13 14
盤面
.....
...$.
.....
0 → (0, 0)
4 → (4, 0)
5 → (0, 1)
8 → (3, 1)
14 → (4, 2)

ここが重要です👇

  • (x, y) → 番号は y * WIDTH + x。「上の行のぶん(1行=WIDTH マス)を飛ばして、x 個進む」と考えます
  • 番号 → (x, y) は divmod(i, WIDTH)。商が y、余りが x です。8 なら 8 ÷ 5 = 1 あまり 3 なので (3, 1) でした
  • 番号の表を見ると、右端の 4 の次の 5 が、次の行の左端になっているのがわかります

⚠️ かけるのは幅(WIDTH)です。うっかり y * HEIGHT + x と書いて全15マスの番号を作ってみたら、番号は11種類しかなく、(3, 0) と (0, 1) がどちらも 3 になりました。エラーは出ないまま、別のマスが同じ番号になってしまいます。正方形の盤面では幅と高さが同じなので、ここでも気づけません。

サンプル2:クリックした位置(ピクセル)からマスを求める

画面に描くときは、1マスを 32 ピクセルの正方形にする、のように決めます。クリックされた位置はピクセル単位で届くので、マスに直す計算が必要です。

TILE = 32  # 1マスの大きさ(ピクセル)

board = [
    list("#####"),
    list("#.$.#"),
    list("#####"),
]
WIDTH = len(board[0])  # 5マス → 画面の幅は 160px
HEIGHT = len(board)    # 3マス → 画面の高さは 96px


def cell_to_pixel(x, y):
    """マス (x, y) → そのマスの左上のピクセル座標"""
    return x * TILE, y * TILE


def pixel_to_cell(px, py):
    """クリックした位置(ピクセル)→ マス (x, y)"""
    return px // TILE, py // TILE


def clicked_cell(px, py):
    """盤面の外をクリックしたら None を返す"""
    x, y = pixel_to_cell(px, py)
    if 0 <= x < WIDTH and 0 <= y < HEIGHT:
        return x, y
    return None


print("マス(2, 1)の左上:", cell_to_pixel(2, 1))

for px, py in [(70, 40), (95, 63), (96, 40), (-5, 40)]:
    x, y = pixel_to_cell(px, py)
    print(f"クリック({px}, {py}) → マス({x}, {y}) 中身 {board[y][x]}")

print("範囲チェックあり:", clicked_cell(-5, 40), clicked_cell(96, 40))
サンプル2の実行結果:クリック位置からマスを求める
サンプル2の実行結果:クリック位置からマスを求める
マス(2, 1)の左上: (64, 32)
クリック(70, 40) → マス(2, 1) 中身 $
クリック(95, 63) → マス(2, 1) 中身 $
クリック(96, 40) → マス(3, 1) 中身 .
クリック(-5, 40) → マス(-1, 1) 中身 #
範囲チェックあり: None (3, 1)

ポイントをまとめるとこんな感じです👇

  • マス → ピクセルは x * TILE。マス (2, 1) の左上は (64, 32) でした
  • ピクセル → マスは px // TILE(切り捨ての割り算)。(70, 40) も (95, 63) も (64, 32) から始まる同じマスの中なので、どちらもマス (2, 1) になりました。1ピクセル右の (96, 40) からは隣のマス (3, 1) です
  • 盤面の左外 (-5, 40) をクリックすると、マス (-1, 1) になりました。それなのに エラーが出ず、右端の壁 # を読んでいます

これは、リストの番号に -1 を使うと「最後の要素」を指すためです。盤面の外をクリックしたのに、反対側のマスが反応してしまうわけですね。そこで clicked_cell() のように、0 以上・幅(高さ)未満かを確かめてから使います。範囲外なら None が返りました。

もう1つのつまずきが、// ではなく / で割ってしまうミスです。試したところ、(70, 40) は 2.1875 1.25 という小数になり、board[y][x] で TypeError: list indices must be integers or slices, not float が出て止まりました。リストの番号は整数だけです。

サンプル3:上下左右のマスを調べる

迷路の探索や塗りつぶしでは「隣のマス」を何度も調べます。1列の番号のまま ±1・±WIDTH するだけでよさそうに見えますが、これ、実行せずに答えられますか? 右上の角(番号4)の隣はどこでしょう。

WIDTH = 5
HEIGHT = 3


def to_xy(i):
    y, x = divmod(i, WIDTH)
    return x, y


def neighbors_naive(i):
    """番号に ±1・±WIDTH するだけの上下左右(まちがい)"""
    result = []
    for j in [i - WIDTH, i + 1, i + WIDTH, i - 1]:
        if 0 <= j < WIDTH * HEIGHT:
            result.append(j)
    return result


def neighbors(i):
    """(x, y) にもどしてから上下左右を調べる"""
    x, y = to_xy(i)
    result = []
    for dx, dy in [(0, -1), (1, 0), (0, 1), (-1, 0)]:  # 上・右・下・左
        nx, ny = x + dx, y + dy
        if 0 <= nx < WIDTH and 0 <= ny < HEIGHT:  # 盤面の外は除く
            result.append(ny * WIDTH + nx)
    return result


i = 4  # 右上の角のマス
print("マス", i, "=", to_xy(i))
print("まちがい:", [(j, to_xy(j)) for j in neighbors_naive(i)])
print("正しい  :", [(j, to_xy(j)) for j in neighbors(i)])
マス 4 = (4, 0)
まちがい: [(5, (0, 1)), (9, (4, 1)), (3, (3, 0))]
正しい  : [(9, (4, 1)), (3, (3, 0))]

ここが重要です👇

  • 番号に +1 しただけの版では、右上の角 (4, 0) の「右」として 5 番=(0, 1) が入りました。右端から、次の行の左端へ回りこんでしまったのです
  • 番号の範囲(0〜14)だけを見ても、この回りこみは防げません
  • 正しい版は、いったん to_xy() で (x, y) にもどし、dx, dy を足してから x と y をそれぞれ範囲チェックしています。結果は下の (4, 1) と左の (3, 0) の2マスでした

まとめ

  • x は列・y は行。リストは board[y][x] で読む
  • (x, y) → 番号は y * WIDTH + x、番号 → (x, y) は divmod(i, WIDTH)。かけるのも割るのも幅
  • ピクセル → マスは px // TILE。/ だと小数になって TypeError
  • マイナスの番号はエラーにならず反対側を読むので、範囲チェックしてから使う
  • 隣のマスは (x, y) で計算して、x と y を別々にチェックする

次回の第3回は、スタックを使って「1手もどす」機能を作ります。サンプル2の TILE を 48 に変えて、同じクリック位置がどのマスになるか試してみてください。ぜひ試してみてください! 🚀

\ Pythonでゲームを作りたい方へ /

筆者は、入門を終えた人向けに、画面の中でキャラクターが動き、キーボードで操作するゲームを7本作る講座を公開しています。最短の手数を自動で探すパズルや、歩数の地図を見て追いかけてくるねこを、自分の手で書きながら「答えにたどり着く手順の考え方(アルゴリズム)」を身につけます。完成版の7本は、パソコンのブラウザでそのまま遊べます。

→ 作って学ぶPythonゲームプログラミング(完成した7本を遊んでみる)

基礎の文法に不安がある方は、先に無料の入門コース「作って学ぶPythonプログラミング入門」(全7章52レッスン)からどうぞ。
→ 無料の入門コースを見る

📚 関連商品・おすすめ書籍

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

もしも

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

初心者に定番のPython入門書

Amazonで見る
徹底攻略! 電子工作&プログラミング Arduinoで学ぶ電子工作完全ガイド

もしも

徹底攻略! 電子工作&プログラミング Arduinoで学ぶ電子工作完全ガイド

電子工作とプログラミングを同時に学べる

Amazonで見る
実践Claude Code入門―現場で活用するためのAIコーディングの思考法

もしも

実践Claude Code入門―現場で活用するためのAIコーディングの思考法

AIコーディングの現場活用法を学ぶ一冊

Amazonで見る

※本記事にはアフィリエイトリンクが含まれます。

ABOUT ME
やまちゃん
これまで学生と社会人を合わせて5000人以上にプログラミング学習を指導。 ゼロからイチをわかりやすく解説する専門家として活動しており、本業ではArduinoを用いたIoT開発とロボットプログラミングが専門。 Pythonを用いたアプリ開発、ウェブアプリケーションの開発で業務の効率化をサポートしています。