Pythonで座標とリストの番号を変換する!divmodとクリック位置の計算|ゲームで学ぶアルゴリズム 第2回
前回:Pythonの二次元リストでゲームの盤面を作る!行と列の順番と作り方の落とし穴|ゲームで学ぶアルゴリズム 第1回
「クリックした場所が、盤面のどのマスなのかわからない…」「(x, y) で考えていたのに、リストに入れると順番がこんがらがる」
ゲームを作り始めると、ここで手が止まりますよね。この記事は連載「ゲームで学ぶアルゴリズム」の第2回です。今回は、座標(x・y)とリストの番号を行き来する計算を3つ取り上げます。
対象は、前回の二次元リストを読んだ初心者〜初級者の方です。動作確認は Python 3.13 で行いました。一緒に見ていきましょう! 😊
まず覚えること:x は列、y は行

ゲームでは、横の位置を 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))

各マスの番号
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, 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レッスン)からどうぞ。
→ 無料の入門コースを見る
こちらも読まれています
📚 関連商品・おすすめ書籍
※本記事にはアフィリエイトリンクが含まれます。




