【Python】レッスン4-S3:マルバツゲームを作ろう

ながみえ
【Python学習記事のアイキャッチ画像】Lesson4-☆3 マルバツゲームを作ろう

一つ前のLessonではナインゲームを作成しました。

今回はマルバツゲームを作成しましょう。

Lesson1:基礎文法編
Lesson2:制御構造編
Lesson3:関数とスコープ編

Lesson4:データ構造編
・Lesson4-1:リストの定義と要素の追加を理解しよう
・Lesson4-2:リストの要素を削除しよう
・Lesson4-3:リストの情報を調べよう
・Lesson4-4:リストの集計・並べ替えを理解しよう
・Lesson4-5:リストのスライスを理解しよう
・Lesson4-6:リストのループ処理を理解しよう
・Lesson4-7:リストの内包表記を理解しよう
・Lesson4-8:リスト・タプル・辞書・集合の概要と違いを理解しよう
・Lesson4-9:タプルの基本を理解しよう
・Lesson4-10:タプルのアンパックを理解しよう
・Lesson4-11:辞書の基本を理解しよう
・Lesson4-12:辞書のループ処理を理解しよう
・Lesson4-13:辞書の内包表記を使ってリストから辞書を作ろう
・Lesson4-13:集合(セット)の基本を理解しよう
・練習問題4-1:宝探しゲームを作ろう
・練習問題4-2:ナインゲームを作ろう 
・練習問題4-3:マルバツゲームを作ろう ◁今回はココ
Lesson5:オブジェクト指向編
次のステップ:Python基礎習得者にお勧めの道5選(実務or副業)

Pythonのゲームコード一覧は こちらをクリック
Pythonでのゲームプリ開発は こちらをクリック

<<前のページ

【Python学習記事のアイキャッチ画像】Lesson4-☆2 ナインゲームを作ろう

Pythonの記事一覧

Python学習カテゴリの親ページ用アイキャッチ画像(テキスト&問題集)、記事一覧へのリンク案内

次のページ>>

Pythonの学習の区切りを表し、記事一覧へ戻ることを促す画像

マルバツゲームを作ろう|リスト・タプルを使う練習問題

コンピュータと対戦する簡単なマルバツゲームを作成しましょう。

プレイヤーとコンピュータが交互に○と×を選択し、最初に縦・横・斜めのラインを揃えた方が勝ちです。

このプログラムでは、ユーザーからの入力に応じて盤面を更新し、勝利または引き分けを判定します。

ゲームが終了した後は結果を表示します。

マルバツゲームのルールとプログラムの仕様

以下の要件に従ってコードを完成させてください。

  • 3×3 のグリッドを用意し、開始時は 1〜9 の番号で表示されていること。
  • プレイヤーは「○」, コンピュータは「×」を使用すること。
  • プレイヤーは入力で指定したマスに「○」を置き、コンピュータは空いているマスからランダムに選んで「×」を置くこと。
  • 縦・横・斜めのいずれかで同じマークが 3 つ並んだら勝利、すべて埋まって勝者がいなければ 引き分けとすること。
  • 各ターンごとに盤面を表示し、埋まっているマスは選べない(その場合は別の入力を促す)こと。

また、以下のようにデータ構造を使用すること。

データ構造使い方
リスト盤面は 長さ 9 のリストで管理し、開始時は "1""9" の文字列で初期化する。
着手は インデックス指定の代入で反映する(例:board[idx] = "○")。
タプル勝利パターンは、インデックス3つのタプルを要素に持つ タプル(タプルのタプル)として定義し、不変の定数として扱う(例:((0,1,2), (3,4,5), ...))。
必要に応じて マーク集合の固定表現にタプルを用いてよい(例:("○", "×") での包含判定)。

ただし、以下のような実行結果となるコードを書くこと。

マルバツゲームを開始します!
1 | 2 | 3
--+---+--
4 | 5 | 6
--+---+--
7 | 8 | 9
どのマスに置きますか?(1-9で入力してください)> 1
○ | 2 | 3
--+---+--
4 | 5 | 6
--+---+--
7 | 8 | 9
コンピュータのターンです...
コンピュータが選んだマス: 5
○ | 2 | 3
--+---+--
4 | × | 6
--+---+--
7 | 8 | 9

【ヒント】難しいと感じる人だけ見よう

1からコードを組み立てることが難しい場合は、以下のヒントを開いて参考にしましょう。

Q
ヒント1【コードの構成を見る】

正解のコードは上から順に以下のような構成となっています。

1:盤面を表すリストから、三目並べの3×3レイアウトを整形して表示する。
 ・9要素の並びを先頭から3つずつ区切って、上・中・下の3行を作る。
 ・各行の要素は縦線で区切り、行の間に区切り線を挟んで視認性を上げる。
 ・書式付き文字列を使って、要素の値をそのまま文字として埋め込む。

2:不変の勝利パターンを使って、指定マークがそろっているかを一括で判定する。
 ・勝利パターンは「3つのインデックス」を要素に持つタプルのタプルで定義する。
 ・1本のラインでは「3か所すべてが同じマークか」をまとめて確認する。
 ・全体では「どれか1本でも満たせば勝ち」という集約にする。

3:盤面の全マスが埋まっているかを確認し、引き分けかどうかを判定する。
 ・盤面の要素がすべてマークのどちらかに含まれるかを一括で確認する。
 ・2種類のマークは変更しない集合としてまとめておくと判定が簡潔になる。
 ・「引き分け=全マス埋まり」判定は、勝利判定より後に使う前提で設計する。

4:空いているマスだけを候補に集め、その中から等確率でコンピュータの手を選ぶ。
 ・盤面を番号付きで走査し、「未使用のマス」のインデックスだけを集める。
 ・マークが置かれていない条件を、2種類のマークに含まれないこととして判定する。
 ・候補リストから1つを等確率で返す乱数関数を使う。

5:ゲーム開始用に「1〜9」の盤面を用意し、開始メッセージを表示する。
 ・リスト内包表記で “1”〜”9″ の文字列を生成して盤面にする。
 ・盤面は左上から右下へ 9要素の並びとして保持する。
 ・開始時に 案内メッセージを1行表示する。

6:プレイヤーの入力を検証して盤面に反映し、勝ち/引き分けを判定するターン処理。
 ・外側はゲーム全体のループ、内側は入力を再試行するループで構成する。
 ・入力は数値化の失敗や範囲外、埋まっているマスをそれぞれ検出し、妥当になるまで再入力させる。
 ・置いた直後に勝利→引き分けの順で判定し、どちらか成立したら盤面を表示してループを抜ける。

7:コンピュータの手を決めて盤面に反映し、その直後に勝ち/引き分けを判定する。
 ・候補から手を選ぶ関数を呼び、戻り値のインデックスをそのまま使って盤面に反映する。
 ・盤面への反映は インデックス指定の代入で行い、選んだマス番号(1始まり)を案内表示する。
 ・判定は 勝利 → 引き分け の順に行い、成立したら盤面を表示して ループを抜ける。

8:このファイルを直接実行したときだけゲームを起動するエントリーポイントを定義する。
 ・「直接実行時のみ動く」ための慣用的な条件を使う。
 ・条件が真のときに、ゲームのメイン関数を1度だけ呼び出す。
 ・インポート時は起動しない設計にして、再利用とテストをしやすくする。

Q
ヒント2【穴埋め問題にする】

以下のコードをコピーし、コメントに従ってコードを完成させて下さい。

import random

# グリッドを表示する関数
def display_board(board):
    print(f"{board[0]} | {board[1]} | {board[2]}")
    print("--+---+--")
    print(f"{board[3]} | {board[4]} | {board[5]}")
    print("--+---+--")
    print(f"{board[6]} | {board[7]} | {board[8]}")

# 勝利を判定する関数(タプルのタプルを使用)
def check_win(board, mark):
    '''(穴埋め)勝利パターン(タプルのタプル)を定義する:
    ((0, 1, 2), (3, 4, 5), (6, 7, 8),
     (0, 3, 6), (1, 4, 7), (2, 5, 8),
     (0, 4, 8), (2, 4, 6)) を WIN_PATTERNS に代入する'''
    '''(穴埋め)WIN_PATTERNS を用いて、任意のパターンで board[i] が mark で3つ揃っているかを
    any と all を組み合わせた一行で判定して返す'''

# 引き分けを判定する関数
def check_draw(board):
    '''(穴埋め)盤面の全要素が "○" または "×" になっているかを
    内包表記 + all で判定して返す一行を書く'''

# コンピュータがランダムにマスを選ぶ関数
def computer_move(board):
    '''(穴埋め)enumerate を使って空きマスのインデックスだけを集めた
    available_moves のリスト内包表記を書く("○" と "×" 以外を抽出)'''
    return '''(穴埋め)'''.choice(available_moves)

# マルバツゲームのメイン関数
def play_game():
    '''(穴埋め)"1"〜"9" の文字列で初期化した 9 要素のリストを board に代入する
    (リスト内包表記と range を使用)'''
    print("マルバツゲームを開始します!")

    while True:
        # 現在のグリッドを表示
        display_board(board)

        # プレイヤーのターン
        while True:
            try:
                player_move = int(input("どのマスに置きますか?(1-9で入力してください)> ")) - 1
                if board[player_move] not in ("○", "×"):
                    '''(穴埋め)board の player_move 番目に "○" を代入して着手を反映する'''
                    break
                else:
                    print("そのマスはすでに埋まっています。別のマスを選んでください。")
            except (ValueError, IndexError):
                print("無効な入力です。1から9の数字を入力してください。")

        # 勝利判定
        if '''(穴埋め)'''(board, "○"):
            display_board(board)
            print("プレイヤーの勝ちです!")
            break

        # 引き分け判定
        if check_draw(board):
            display_board(board)
            print("引き分けです!")
            break

        # コンピュータのターン
        print("コンピュータのターンです...")
        comp_move = computer_move(board)
        board[comp_move] = "×"
        print(f"コンピュータが選んだマス: {'''(穴埋め)''' + 1}")

        # 勝利判定
        if check_win(board, "×"):
            display_board(board)
            print("コンピュータの勝ちです!")
            break

        # 引き分け判定
        if check_draw(board):
            display_board(board)
            print("引き分けです!")
            break

if __name__ == "__main__":
    '''(穴埋め)'''
    play_game()

このヒントを見てもまだ回答を導き出すのが難しいと感じる場合は、先に正解のコードと解説を見て内容を理解するようにしましょう。

Pythonでゲームアプリを作ろう!

当サイトではテキストベースのゲームコードを多数紹介していますが、それだけでなく 画面上で遊べるゲームアプリ の作り方を紹介たコースも用意してあります。

Pythonの実践力強化にもなりますので、ぜひ挑戦して下さい^^

あわせて読みたい
PythonとPygameで始めるゲーム制作|初心者の学習を楽しくステップアップ!
PythonとPygameで始めるゲーム制作|初心者の学習を楽しくステップアップ!

解答と解説|マルバツゲームのサンプルコード

この問題の一つの正解例とそのコードの解説を以下に示します。

解答例|マルバツゲームプログラム

例えば以下のようなプログラムが考えられます。

Q
正解コード
import random

# グリッドを表示する関数
def display_board(board):
    print(f"{board[0]} | {board[1]} | {board[2]}")
    print("--+---+--")
    print(f"{board[3]} | {board[4]} | {board[5]}")
    print("--+---+--")
    print(f"{board[6]} | {board[7]} | {board[8]}")

# 勝利を判定する関数(タプルのタプルを使用)
def check_win(board, mark):
    WIN_PATTERNS = (
        (0, 1, 2), (3, 4, 5), (6, 7, 8),   # 横
        (0, 3, 6), (1, 4, 7), (2, 5, 8),   # 縦
        (0, 4, 8), (2, 4, 6)               # 斜め
    )
    return any(all(board[i] == mark for i in pattern) for pattern in WIN_PATTERNS)

# 引き分けを判定する関数
def check_draw(board):
    return all(space in ("○", "×") for space in board)

# コンピュータがランダムにマスを選ぶ関数
def computer_move(board):
    available_moves = [i for i, space in enumerate(board) if space not in ("○", "×")]
    return random.choice(available_moves)

# マルバツゲームのメイン関数
def play_game():
    board = [str(i) for i in range(1, 10)]  # 1〜9の番号で初期化
    print("マルバツゲームを開始します!")

    while True:
        # 現在のグリッドを表示
        display_board(board)

        # プレイヤーのターン
        while True:
            try:
                player_move = int(input("どのマスに置きますか?(1-9で入力してください)> ")) - 1
                if board[player_move] not in ("○", "×"):
                    board[player_move] = "○"
                    break
                else:
                    print("そのマスはすでに埋まっています。別のマスを選んでください。")
            except (ValueError, IndexError):
                print("無効な入力です。1から9の数字を入力してください。")

        # 勝利判定
        if check_win(board, "○"):
            display_board(board)
            print("プレイヤーの勝ちです!")
            break

        # 引き分け判定
        if check_draw(board):
            display_board(board)
            print("引き分けです!")
            break

        # コンピュータのターン
        print("コンピュータのターンです...")
        comp_move = computer_move(board)
        board[comp_move] = "×"
        print(f"コンピュータが選んだマス: {comp_move + 1}")

        # 勝利判定
        if check_win(board, "×"):
            display_board(board)
            print("コンピュータの勝ちです!")
            break

        # 引き分け判定
        if check_draw(board):
            display_board(board)
            print("引き分けです!")
            break

if __name__ == "__main__":
    play_game()

解答例の解説|マルバツゲームの考え方

解答例の詳細解説は以下の通りです。

Q
詳細解説

グリッドを表示する関数

import random

# グリッドを表示する関数
def display_board(board):
    print(f"{board[0]} | {board[1]} | {board[2]}")
    print("--+---+--")
    print(f"{board[3]} | {board[4]} | {board[5]}")
    print("--+---+--")
    print(f"{board[6]} | {board[7]} | {board[8]}")

ここでは、ゲームの現在の状態を人が見て分かる形に出力します。
盤面は9個の要素を持つ並びとして管理され、左上から右下へと順に並んでいます。
表示では、1行につき3つずつ取り出して縦線で区切り、行と行の間には区切り線を挟むことで、三目並べらしい見た目を作ります。
文字の合成には書式付きの文字列を使い、数値や記号がそのまま読みやすく並ぶようにしています。

勝利を判定する関数(タプルのタプルを使用)

# 勝利を判定する関数(タプルのタプルを使用)
def check_win(board, mark):
    WIN_PATTERNS = (
        (0, 1, 2), (3, 4, 5), (6, 7, 8),   # 横
        (0, 3, 6), (1, 4, 7), (2, 5, 8),   # 縦
        (0, 4, 8), (2, 4, 6)               # 斜め
    )
    return any(all(board[i] == mark for i in pattern) for pattern in WIN_PATTERNS)

三目並べの「勝ち筋」はあらかじめ決まっています。
横3本、縦3本、斜め2本の合計8通りを、変更されない一覧として用意し、そのどれか1つでもすべて同じマークで埋まっていれば勝ちです。
ここでは、その勝ち筋一覧を“入れ子のタプル”として保持します。タプルは後から書き換えられないため、定数としての意図がはっきりします。
判定自体は、1本のライン内で「全部同じか」を確かめる処理と、8本の中で「どれか1本でも成立しているか」を確かめる処理を組み合わせて行います。
これにより、盤面とマークを渡すだけで真偽値を返す、再利用しやすい関数になります。

引き分けを判定する関数

# 引き分けを判定する関数
def check_draw(board):
    return all(space in ("○", "×") for space in board)

この関数は、勝敗がつかなかった状態でゲームを終了させるための判定を担当します。
三目並べでは、9マスがすべて埋まっていて、しかもどの勝ち筋も成立していないときに引き分けになります。
ここでは「盤面の各要素がいずれかのマークで占有されているか」を一括で確かめ、すべて埋まっているなら真、それ以外なら偽を返すだけのシンプルな役割です。
勝利の確認は別の関数が先に行う前提なので、ここでは「埋まりきったか」のチェックだけに集中しています。

コンピュータがランダムにマスを選ぶ関数

# コンピュータがランダムにマスを選ぶ関数
def computer_move(board):
    available_moves = [i for i, space in enumerate(board) if space not in ("○", "×")]
    return random.choice(available_moves)

この関数は、コンピュータがどこに印を置くかを決めます。
まず盤面の中から、まだマークが置かれていない位置だけを調べて候補の一覧を作ります。
次に、その候補の中からどれを選ぶかを乱数に任せることで、毎回の対戦で手が偏らないようにします。
つまり、埋まっていない場所を見つける処理と、そこから1つを無作為に選ぶ処理の二段構えになっています。

マルバツゲームのメイン関数

# マルバツゲームのメイン関数
def play_game():
    board = [str(i) for i in range(1, 10)]  # 1〜9の番号で初期化
    print("マルバツゲームを開始します!")

ここでは、プレイヤーが入力しやすく状況も把握しやすいように、1から9までの番号を並べた盤面を最初に作ります。
番号はそのまま表示に使うため文字の形で保持し、以降はこの並びを更新していくことでゲームの進行を表現します。
あわせて、ゲーム開始の案内文を表示して、これから対戦が始まることをユーザーに伝えます。

プレイヤーのターン

    while True:
        # 現在のグリッドを表示
        display_board(board)

        # プレイヤーのターン
        while True:
            try:
                player_move = int(input("どのマスに置きますか?(1-9で入力してください)> ")) - 1
                if board[player_move] not in ("○", "×"):
                    board[player_move] = "○"
                    break
                else:
                    print("そのマスはすでに埋まっています。別のマスを選んでください。")
            except (ValueError, IndexError):
                print("無効な入力です。1から9の数字を入力してください。")

        # 勝利判定
        if check_win(board, "○"):
            display_board(board)
            print("プレイヤーの勝ちです!")
            break

        # 引き分け判定
        if check_draw(board):
            display_board(board)
            print("引き分けです!")
            break

ここではゲームの本体となる繰り返し処理が動きます。
まず毎ターンの冒頭で現在の盤面を表示し、続いてプレイヤーの手を受け付けます。
入力は数値化できないケースや範囲外の指定があるため、例外を使って安全にやり直せるようにしています。
また、すでに埋まっている場所が選ばれた場合は、別の場所を選ぶように促し、妥当な場所が選ばれたら初めて自分の印を置いて確定します。
配置が終わったら、勝ち筋がそろっていないかをまず確認し、そろっていればその場で終了します。
そろっていない場合は、全マスが埋まって引き分けになっていないかを続けて確認し、該当すれば同様に終了します。
これらのチェックは「置いた直後」に行うため、最小限の回数でゲームが終わるようになっています。

コンピュータのターン

        # コンピュータのターン
        print("コンピュータのターンです...")
        comp_move = computer_move(board)
        board[comp_move] = "×"
        print(f"コンピュータが選んだマス: {comp_move + 1}")

        # 勝利判定
        if check_win(board, "×"):
            display_board(board)
            print("コンピュータの勝ちです!")
            break

        # 引き分け判定
        if check_draw(board):
            display_board(board)
            print("引き分けです!")
            break

ここではコンピュータの番を処理します。
まず、空いているマスから1つを選ぶ関数を呼び出して置き場所を決めます。
次に、その位置にコンピュータの印を反映し、どこを選んだかを案内として表示します。
続いて、勝利条件が整っていないかを確認し、達成していれば盤面を示して終了します。
勝っていなければ、全マスが埋まっているかを調べ、引き分けなら同様に盤面を表示して終了します。
いずれも成立しなければゲームは続行され、次のターンの冒頭で最新の盤面が表示されます。

ゲームを起動するエントリーポイント

if __name__ == "__main__":
    play_game()

ここでは、プログラムの「起動スイッチ」を用意しています。Python では、ファイルが直接実行された場合と、他のファイルから読み込まれた場合で挙動を分けられます。
直接実行のときだけゲームを開始し、学習用に関数をインポートして使うときは自動実行しないようにすることで、再利用やテストがしやすくなります。
役割は「条件を満たしたらゲームのメイン処理を1回呼び出す」という一点に絞っておくのがポイントです。

Pythonのゲームコード一覧は こちらをクリック
Pythonでのゲームアプリ開発は こちらをクリック

Q
サイト改善アンケート|ご意見をお聞かせください(1分で終わります)

本サイトでは、みなさまの学習をよりサポートできるサービスを目指しております。
そのため、みなさまの「プログラミングを学習する理由」などをアンケート形式でお伺いしています。

1分だけ、ご協力いただけますと幸いです。

【Python】サイト改善アンケート
1.このサイトをどのように活用していますか?また、今後どのように活用したいですか?
5.気になっているサービス・商品があれば教えてください。
(複数選択可)
※ 特定の記事に関する内容の場合は、記事番号(Python Lesson〇-〇)をご記入願います。 また、ご質問はここではなく問い合わせフォームからお願いします。

<<前のページ

【Python学習記事のアイキャッチ画像】Lesson4-☆2 ナインゲームを作ろう

Pythonの記事一覧

Python学習カテゴリの親ページ用アイキャッチ画像(テキスト&問題集)、記事一覧へのリンク案内

次のページ>>

Pythonの学習の区切りを表し、記事一覧へ戻ることを促す画像
記事URLをコピーしました