こんにちは。RockinWoolです。前回の続きということで、今回はFrontendのjavascriptとbackendのpythonを同期させて行こうと思います。それでは早速はじめていきましょう。

Flaskのインストールとサーバのセットアップ

それではまず、backend環境を作るためにFlaskをインストールしていきます。自分はanaconda環境でやっているのでcondaで入れていきます。

conda update -n base -c defaults conda
conda install flask flask-cors 

新しいフォルダ構成でアプリを再構築する

前回のcreate-react-appでのReactアプリの構築では、最新のnodejsをビルドできないことが判明したのでビルドツールをviteに変更します。新しくmaze_gameという親フォルダを作ってその中に移動したら次のコマンドでViteプロジェクトを作ります。

npm create vite@latest
? Project name: frontend
? Select a framework: React
? Select a variant: JavaScript
cd frontend
npm install
npm run dev

ここまででViteサンプルアプリが動くようになります。

迷路側(frontend)の作成

index.html

まずはindex.htmlから順番に作っていきます。Viteになったことで最初に起動させるエントリーポイントJavascriptのファイルがmain.jsxになっています。今回も慣例にしたがってmain.jsxを起動させるようにします。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Maze Game</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

main.jsx

エントリーポイントです。ここではReactの初期設定を行い、アプリケーション全体をブラウザにレンダリングします。

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

2行目のReactDOMとはなんぞや?と思って調べてみたら、DOM(Document Object Model)という概念についてChatGPTが教えてくれました。Reactはhtmlの要素(Node)を動的に変更することで画面を変化させているということを知らなかったので、DOMによってNodeの塊として表現されているhtmlを変化させるためにReactDOMが必要なんだと理解することができました。
3行目はルートコンポーネントであるApp.jsxをインポートすることを宣言しているそうですが、拡張子抜きで宣言するのに違和感があります。
6行目ではレンダリング対象を宣言していますが、index.htmlで<div id=”root”></div>と宣言したroot要素を対象にすると言っています。<React.StrictMode>は開発モード機能であると宣言しているので、本番ビルドでは対象ではないと言っています。App /はAppをレンダリングするということらしいですので、次はApp.jsxについて見ていきます。

App.jsx

実態はcomponent/Maze.jsxに記述しているので、これは単純に仲介役を果たすファイルです。

// src/App.jsx
import React from 'react';
import Maze from './components/Maze';

function App() {
  return (
    <div className="App">
      <h1>Maze Game</h1>
      <Maze />
    </div>
  );
}

export default App;

最後のexport default App;でfunction App()を他のjsxから呼び出せるようにしています。

component/Maze.jsx

今回の開発で最もキモとなった部分の1つです。前回はここに迷宮情報を格納して迷路を作っていましたが、backend側に迷宮情報を持たさたほうが良いと判断して、frontend起動時にサーバから迷路情報を取得するように変更しました。そして、Enterを押したら自動で決められた動きをするように変更しています。

import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import './Maze.css';

const Maze = () => {
  const [maze, setMaze] = useState([]);
  const [playerPos, setPlayerPos] = useState([1, 1]);
  const [autoMoving, setAutoMoving] = useState(false);

  // フォーカス管理用の参照
  const mazeRef = useRef(null);

  // 初期化時に迷路情報をサーバーから取得
  useEffect(() => {
    const fetchMaze = async () => {
      try {
        const response = await axios.get('/maze');
        setMaze(response.data.maze);
      } catch (error) {
        console.error("迷路情報の取得に失敗しました:", error);
      }
    };
    fetchMaze();

    // 初期フォーカスの設定
    mazeRef.current.focus();
  }, []);

  // 自動移動のAPI呼び出し
  const autoMove = async () => {
    try {
      const response = await axios.post('/move', { direction: 'auto' });
      setPlayerPos(response.data.position);

      if (response.data.status === 'finished') {
        console.log('自動移動が完了しました');
        setAutoMoving(false);  // 自動移動の停止
      }
    } catch (error) {
      console.error('自動移動エラー:', error);
      setAutoMoving(false);
    }
  };

  // 自動移動を繰り返し呼び出す処理
  useEffect(() => {
    if (autoMoving) {
      const interval = setInterval(() => {
        autoMove();  // 自動移動の呼び出し
      }, 1000);  // 移動間隔
      return () => clearInterval(interval);  // クリーンアップ処理
    }
  }, [autoMoving]);

  // 手動移動と自動移動の処理
  const handleKeyDown = (e) => {
    const [x, y] = playerPos;
    let newPos = [...playerPos];

    if (e.key === 'Enter' && !autoMoving) {
      console.log("自動移動を開始します");
      setAutoMoving(true);  // 自動移動の開始
      return;
    }

    // 手動移動の処理
    switch (e.key) {
      case 'ArrowUp':
        if (maze[x - 1]?.[y] === 0) newPos = [x - 1, y];
        break;
      case 'ArrowDown':
        if (maze[x + 1]?.[y] === 0) newPos = [x + 1, y];
        break;
      case 'ArrowLeft':
        if (maze[x]?.[y - 1] === 0) newPos = [x, y - 1];
        break;
      case 'ArrowRight':
        if (maze[x]?.[y + 1] === 0) newPos = [x, y + 1];
        break;
      default:
        return;  // 無効なキー入力を無視
    }

    console.log(`手動移動: 新しい位置 ${newPos}`);
    setPlayerPos(newPos);  // 手動移動の更新
  };

  // 描画処理
  return (
    <div
      className="maze"
      tabIndex="0"
      ref={mazeRef}
      onKeyDown={handleKeyDown}
    >
      {maze.length === 0 ? (
        <h2>ロード中...</h2>
      ) : (
        maze.map((row, rowIndex) => (
          <div className="row" key={rowIndex}>
            {row.map((cell, colIndex) => (
              <div
                className={`cell ${
                  playerPos[0] === rowIndex && playerPos[1] === colIndex
                    ? 'player'
                    : cell === 1
                    ? 'wall'
                    : 'path'
                }`}
                key={colIndex}
              ></div>
            ))}
          </div>
        ))
      )}
    </div>
  );
};

export default Maze;

backend/server.py

最後にbackend側の実装を行っていきます。backend側は迷宮情報を渡すmaze関数と自動移動を行うmavoe関数を実装します。

from flask import Flask, request, jsonify
from flask_cors import CORS
import time
import threading

app = Flask(__name__)
CORS(app)

# 迷宮の定義
initial_maze = [
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1],
  [1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1],
  [1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1],
  [1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1],
  [1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1],
  [1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
  [1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1],
  [1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1],
  [1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1],
  [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
];

player_position = [1, 1]
# ロックオブジェクトの作成
move_lock = threading.Lock()

# 自動移動のためのジェネレーター関数
def create_move_generator():
    moves = ["right", "right", "down", "down", "left", "left"]
    for move in moves:
        yield move
        time.sleep(1)  # 移動間隔を追加

# 初期ジェネレーターの作成
move_generator = create_move_generator()

# 迷路情報を提供するエンドポイント
@app.route('/maze', methods=['GET'])
def get_maze():
    return jsonify({"maze": initial_maze})

# 移動の処理を行うエンドポイント
@app.route('/move', methods=['POST'])
def move():
    global player_position, move_generator

    data = request.json
    direction = data.get('direction')

    with move_lock:  # ジェネレーターの同期制御
        if direction == 'auto':
            try:
                direction = next(move_generator)
            except StopIteration:
                # 自動移動が完了したらリセット
                move_generator = create_move_generator()  # ジェネレーターのリセット
                return jsonify({
                    'status': 'finished',
                    'position': player_position
                })

        # プレイヤーの新しい位置を計算
        new_pos = list(player_position)
        if direction == 'up':
            new_pos[0] -= 1
        elif direction == 'down':
            new_pos[0] += 1
        elif direction == 'left':
            new_pos[1] -= 1
        elif direction == 'right':
            new_pos[1] += 1

        # 壁の判定
        if initial_maze[new_pos[0]][new_pos[1]] == 0:
            player_position = new_pos

        return jsonify({
            'status': 'moving',
            'position': player_position
        })

if __name__ == '__main__':
    app.run(port=5000)

vite.config.js

Viteで起動したときに通信をどうするかを定義します。

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/maze': 'http://localhost:5000',
      '/move': 'http://localhost:5000',
    },
  },
});

ここまでで一通り動くものが作れました。
いやーコードばっかりになってしまい申し訳無いです。
次回はコードの詳しい解説と自動移動関数の強化学習を目標にしようと思います。
それではまた次回!

Leave a Reply

Your email address will not be published. Required fields are marked *