「ゲームの盤面って、プログラムの中ではどうやって持っているの?」「board[1][4] みたいに [ ] が2つ並ぶと、どっちが縦でどっちが横かわからなくなる…」

そう感じたこと、ありますよね。この記事は、ゲームづくりを題材にアルゴリズムの考え方を1つずつ身につける連載「ゲームで学ぶアルゴリズム」の第1回です。今回は、迷路・パズル・ボードゲームの土台になる二次元リストで盤面を表す方法を取り上げます。

対象は、リストと for 文をひととおり使ったことがある初心者〜初級者の方です。動作確認は Python 3.13 で行いました。一緒に見ていきましょう! 😊

二次元リストとは?「リストを並べたリスト」

board game grid
board game grid / Photo by Ann Hannes via Pexels

二次元リストとは、要素がリストになっているリストのことです。

イメージとしては、方眼紙です。横1列ぶんのマスを1つのリストにして、その行を上から順に並べたものが盤面になります。

  • board[行] で、1行ぶんのリストが取り出せる
  • board[行][列] で、1マスの中身が取り出せる
  • 番号はどちらも 0から数える

サンプル1:小さなダンジョンの盤面を作って動かす

4行×6列のマップを作り、マスを読む・書きかえる・表示する、の3つをやってみます。

# 盤面(4行×6列)を二次元リストで表す
# "#"=壁  "."=床  "P"=プレイヤー  "$"=コイン  "G"=ゴール
board = [
    ["#", "#", "#", "#", "#", "#"],
    ["#", "P", ".", ".", "$", "#"],
    ["#", ".", "#", ".", "G", "#"],
    ["#", "#", "#", "#", "#", "#"],
]

print("行の数:", len(board))
print("列の数:", len(board[0]))
print("board[1]:", board[1])
print("board[1][4]:", board[1][4])

# プレイヤーを右に1マス動かす(元の場所は床にもどす)
board[1][1] = "."
board[1][2] = "P"

# 1行ずつ文字をつなげて盤面を表示する
for row in board:
    print("".join(row))
サンプル1の実行結果:マスを読んで書きかえ、盤面を表示する
サンプル1の実行結果:マスを読んで書きかえ、盤面を表示する

実行結果はこうなりました。

行の数: 4
列の数: 6
board[1]: ['#', 'P', '.', '.', '$', '#']
board[1][4]: $
######
#.P.$#
#.#.G#
######

ここが重要です👇

  • len(board) は行の数(4)、len(board[0]) は1行の長さ=列の数(6)です
  • board[1][4] は「1行目の、4列目」なので $(コイン)でした。先に行(縦の位置)、あとに列(横の位置)の順です
  • マスに代入するだけで盤面が変わります。プレイヤーの移動は「元のマスを床にもどす → 新しいマスに P を置く」の2行です
  • "".join(row) で1行の文字をつなげると、盤面らしく表示できます

⚠️ よくあるのが、行と列を逆に書いてしまうミスです。コインを「横に4、縦に1」と考えて board[4][1] と書くと、行は0〜3までしかないので、実行したところ IndexError: list index out of range で止まりました。盤面が正方形だとエラーにならずに別のマスを読んでしまうので、さらに気づきにくくなります。迷ったら「board[縦][横]」と覚えておきましょう。

サンプル2:空の盤面を作るときの落とし穴

マップを手で書くのは小さいうちだけで、ふつうは「全部床の盤面」をコードで作ります。ここで多くの人がつまずきます。これ、実行せずに答えられますか? bad の0行目だけを変えたとき、盤面はどう表示されるでしょうか。

# まちがい:* で行をふやす
bad = [["."] * 4] * 3
bad[0][1] = "X"          # 0行目の1列目だけ変えたつもり
print("--- * で作った盤面 ---")
for row in bad:
    print("".join(row))
print("0行目と1行目は同じもの?", bad[0] is bad[1])

# 正しい:行ごとに新しいリストを作る
good = [["."] * 4 for _ in range(3)]
good[0][1] = "X"
print("--- for で作った盤面 ---")
for row in good:
    print("".join(row))
print("0行目と1行目は同じもの?", good[0] is good[1])
サンプル2の実行結果:*で作った盤面は全行が変わる
サンプル2の実行結果:*で作った盤面は全行が変わる

実行結果はこちらです。

--- * で作った盤面 ---
.X..
.X..
.X..
0行目と1行目は同じもの? True
--- for で作った盤面 ---
.X..
....
....
0行目と1行目は同じもの? False

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

  • [["."] * 4] * 3 で作った盤面は、0行目だけ変えたつもりが3行すべてに X が入りました
  • bad[0] is bad[1] が True。つまり、3つの行は同じ1本のリストを3回並べただけだったのです
  • [["."] * 4 for _ in range(3)] と書くと、行ごとに新しいリストが作られるので、変えたマスだけが変わりました

1行の中の ["."] * 4 は問題ありません。行をふやすときだけは for を使う、と覚えておけば大丈夫です。

サンプル3:三目並べの「そろったか」を判定する

盤面を作れたら、次は盤面を調べる処理です。3×3の三目並べで、縦・横・斜めのどれかに同じ記号が3つ並んだかを判定してみます。

def find_winner(board):
    """三目並べの盤面(3×3)から勝者を返す。いなければ None"""
    lines = []
    for r in range(3):
        lines.append(board[r])                          # 横の3本
    for c in range(3):
        lines.append([board[r][c] for r in range(3)])   # 縦の3本
    lines.append([board[i][i] for i in range(3)])       # 斜め(左上→右下)
    lines.append([board[i][2 - i] for i in range(3)])   # 斜め(右上→左下)

    for line in lines:
        if line[0] != "." and line.count(line[0]) == 3:
            return line[0]
    return None


game1 = [
    ["O", "X", "."],
    ["X", "O", "."],
    ["X", ".", "O"],
]
game2 = [
    ["O", "X", "O"],
    ["O", "X", "."],
    [".", "X", "."],
]
game3 = [
    ["O", "X", "O"],
    [".", "X", "."],
    [".", "O", "."],
]

for name, g in [("game1", game1), ("game2", game2), ("game3", game3)]:
    print(name, "→", find_winner(g))
game1 → O
game2 → X
game3 → None

ここが重要です👇

  • 横の1本は board[r] をそのまま使えます。縦の1本は、列番号 c を固定して行番号 r だけ動かして集めます
  • 斜めは board[i][i](0,0 → 1,1 → 2,2)と board[i][2 - i](0,2 → 1,1 → 2,0)です。番号の並びを紙に書くとすぐ納得できます
  • 8本の「線」を1つのリストに集めてから同じ方法で調べるので、判定の部分は短く書けます。"."(空きマス)が3つそろった場合を勝ちにしないよう、line[0] != "." を入れています
  • 実行すると、斜めにそろった game1 は O、真ん中の縦にそろった game2 は X、どこもそろっていない game3 は None になりました

まとめ

  • 二次元リストはリストを並べたリスト。方眼紙の1行が1つのリスト
  • マスは board[行][列]=board[縦][横] の順。逆にすると IndexError か、別のマスを読んでしまう
  • 行数は len(board)、列数は len(board[0])
  • 空の盤面は [["."] * 列数 for _ in range(行数)]。* 行数 で作ると全行がいっしょに変わる
  • 縦・横・斜めの「線」を集めれば、そろったかの判定も同じ書き方でできる

次回の第2回は、画面上の x・y 座標とリストの番号を行き来する方法を取り上げます。「むずかしそう」を「できそう」に変えていきましょう。サンプル1のマップに壁やコインを足して、プレイヤーを好きなマスへ動かしてみてください。ぜひ試してみてください! 🚀

\ 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を用いたアプリ開発、ウェブアプリケーションの開発で業務の効率化をサポートしています。