Python と Flask と JavaScript で作る一人用○×ゲーム(VS AI)

# はじめに

こんにちは！今回は Python の Flask と JavaScript を使って、一人用の「○×ゲーム」を簡単に作成する方法を紹介します。AIとの対戦が可能で、シンプルなデザインながら、楽しめるウェブアプリケーションを作ります。

---

## 🎮 完成イメージ

- **ゲームの流れ**:
  1. プレイヤー（X）がマスをクリックして記号を配置。
  2. AI（O）が自動で次のマスに記号を配置。
  3. 勝敗または引き分けが判定されると、ゲームがリセット。
- **シンプルな操作**:
  - クリックして遊ぶだけ！
  - デザインも基本的なCSSで作成。

![スクリーンショット 2024-12-26 15.29.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75802/6e0385ca-a43b-bfe7-2feb-9cdc4a17924d.png)


---

## 🛠 必要なツール

- Python 3.x
- Flask
- HTML/CSS/JavaScript

---

## 📁 プロジェクト構成

以下のような構成でファイルを作成します。

```
project/
├── static/
│   ├── style.css         # CSSファイル
│   └── script.js         # JavaScriptファイル
├── templates/
│   └── index.html        # HTMLテンプレート
└── app.py                # Flaskアプリケーション
```

---

## 🔧 コード解説

### 1. Flaskアプリケーション (`app.py`)

以下のコードを `app.py` に保存します。AI（O）の動きを含めたロジックを組み込んでいます。

```python
from flask import Flask, render_template, jsonify, request
import random

app = Flask(__name__)

# 初期化
board = [" "] * 9
current_player = "X"

def check_winner(board, player):
    win_combinations = [
        [0, 1, 2], [3, 4, 5], [6, 7, 8],  # 横
        [0, 3, 6], [1, 4, 7], [2, 5, 8],  # 縦
        [0, 4, 8], [2, 4, 6]             # 斜め
    ]
    for combo in win_combinations:
        if all(board[i] == player for i in combo):
            return True
    return False

def ai_move():
    empty_cells = [i for i, cell in enumerate(board) if cell == " "]
    return random.choice(empty_cells) if empty_cells else -1

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/play", methods=["POST"])
def play():
    global board, current_player

    # データを受け取る
    data = request.json
    cell = data.get("cell")

    if board[cell] != " ":
        return jsonify({"status": "error", "message": "Invalid move"})

    # プレイヤーの手を配置
    board[cell] = current_player

    # 勝敗判定
    if check_winner(board, current_player):
        winner = current_player
        board[:] = [" "] * 9  # ボードリセット
        current_player = "X"  # 次ゲームは常にXが開始
        return jsonify({"status": "win", "winner": winner, "board": board})

    # 引き分け判定
    if " " not in board:
        board[:] = [" "] * 9  # ボードリセット
        current_player = "X"  # 次ゲームは常にXが開始
        return jsonify({"status": "draw", "board": board})

    # プレイヤー交代（AIのターン）
    current_player = "O" if current_player == "X" else "X"
    if current_player == "O":
        ai_choice = ai_move()
        if ai_choice != -1:
            board[ai_choice] = current_player
            if check_winner(board, current_player):
                winner = current_player
                board[:] = [" "] * 9  # ボードリセット
                current_player = "X"  # 次ゲームは常にXが開始
                return jsonify({"status": "win", "winner": winner, "board": board})
            elif " " not in board:
                board[:] = [" "] * 9  # ボードリセット
                current_player = "X"  # 次ゲームは常にXが開始
                return jsonify({"status": "draw", "board": board})

        # プレイヤー交代
        current_player = "X"

    return jsonify({"status": "continue", "board": board, "current_player": current_player})

@app.route("/reset", methods=["POST"])
def reset():
    global board, current_player
    board = [" "] * 9
    current_player = "X"
    return jsonify({"status": "reset", "board": board})

if __name__ == "__main__":
    app.run(debug=True)
```

---

### **AIロジックの解説**

AIが動作する部分は `ai_move()` 関数で定義されています。この関数の仕組みは以下の通りです：

#### **AIの動作**
1. **空いているマスを取得**:
   ```python
   empty_cells = [i for i, cell in enumerate(board) if cell == " "]
   ```
   - `board` から空いているマスのインデックスを取得します。

2. **ランダムでマスを選択**:
   ```python
   return random.choice(empty_cells) if empty_cells else -1
   ```
   - 空いているマスからランダムで1つ選びます。
   - 空きがない場合は `-1` を返します（ゲーム終了時の処理）。

#### **改良案**
現在のロジックは「空きマスにランダムで配置」ですが、以下のように改良することでより強いAIを実現できます：
1. 勝利可能なマスがあれば優先して配置。
2. プレイヤーの勝利を防ぐためのブロックを配置。
3. 中央や角を優先的に選択する戦略。

---

### 2. HTMLテンプレート (`templates/index.html`)

以下を `templates/index.html` に保存します。

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>まるばつゲーム</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <h1>まるばつゲーム</h1>
    <p id="status">現在のプレイヤー: X</p>
    <div class="board" id="board"></div>
    <button id="reset">リセット</button>
    <script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>
```

---

### 3. CSS (`static/style.css`)

以下を `static/style.css` に保存します。

```css
body {
    font-family: Arial, sans-serif;
    text-align: center;
}

.board {
    display: grid;
    grid-template-columns: repeat(3, 100px);
    gap: 10px;
    justify-content: center;
    margin: 20px auto;
}

.cell {
    width: 100px;
    height: 100px;
    font-size: 2rem;
    display: flex;
    justify-content: center;
    align-items: center;
    border: 2px solid #000;
    cursor: pointer;
    background-color: #f9f9f9;
}

.cell:hover {
    background-color: #e0e0e0;
}

button {
    padding: 10px 20px;
    font-size: 1rem;
    cursor: pointer;
}
```

---

### 4. JavaScript (`static/script.js`)

以下を `static/script.js` に保存します。

```javascript
document.addEventListener("DOMContentLoaded", () => {
    const boardElement = document.getElementById("board");
    const statusElement = document.getElementById("status");
    const resetButton = document.getElementById("reset");

    function renderBoard(board) {
        boardElement.innerHTML = "";
        board.forEach((cell, index) => {
            const cellElement = document.createElement("div");
            cellElement.classList.add("cell");
            cellElement.textContent = cell;
            if (cell === " ") {
                cellElement.addEventListener("click", () => playMove(index));
            }
            boardElement.appendChild(cellElement);
        });
    }

    async function playMove(cell) {
        const response = await fetch("/play", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ cell }),
        });
        const data = await response.json();

        if (data.status === "win") {
            alert(`${data.winner} の勝利！`);
        } else if (data.status === "draw") {
            alert("引き分け！");
        }

        renderBoard(data.board);
        statusElement.textContent = `現在のプレイヤー: ${data.current_player}`;
    }

    resetButton.addEventListener("click", async () => {
        const response = await fetch("/reset", { method: "POST" });
        const data = await response.json();
        renderBoard(data.board);
        statusElement.textContent = "現在のプレイヤー: X";
    });

    fetch("/reset", { method: "POST" })
        .then(response => response.json())
        .then(data => renderBoard(data.board));
});
```

---

## 🚀 実行方法

1. ターミナルでサーバーを起動:
   ```bash
   python app.py
   ```
2. ブラウザで [http://127.0.0.1:5000/](http://127.0.0.1:5000/) にアクセスしてゲームをプレイ！

---

## 📝 まとめ

このプロジェクトでは、Flaskを使ったサーバーサイドロジックとJavaScriptによるフロントエンド連携を学べます。また、シンプルなAIロジックを追加して、一人用のまるばつゲームを実現しました。さらに強いAIロジックを実装することで、ゲーム性を向上させることも可能です！

ぜひ試してみて、自分だけの改良を加えてみてください😊