Reactで作る1人用人狼ゲーム

# はじめに
こんばんは！
MYJLab Advent Calendar 2024の13日目担当します、宮地研究室3年大川です。
今回どのような記事を書こうかなと考えている際にたまたま見ていたYouTuBeで人狼ゲームをやっていたので1人用全露王ゲームをReactで作ってみようと思います。

# ルール
6人遊ぶ人狼ゲームです。実際にプレイするのは1人のみで他の5人はCPUとして自動で人狼ゲームに参加しています。
ローカルルールが多い人狼ゲームなので一番スタンダードなルールを適用したいと思います。
役職は市民、騎士、霊媒師、占い師、人狼、狂人の6種類とします。

勝敗の決め方
市民サイドの勝利：人狼を処刑する
人狼サイドの勝利：人狼サイドの人数が市民サイドと同じになる

# コードの説明
### 1.ゲームのメインのコード
ゲームのメイン機能を果たすコード。ゲームの初期化、プレイヤーの役職の割り当て、夜と昼のターンの進行を管理しています。実際のゲームの言うところのゲームマスターのような部分です。

    import React, { useState, useEffect } from 'react';
    
    const roles = ['市民', '人狼', '騎士', '占い師', '霊媒師', '狂人'];
    const roleDescriptions = {
      '市民': '役職なし',
      '人狼': '毎晩市民を1人殺す',
      '騎士': '毎晩1人指定し、その人が人狼に襲われた際は守ることができます',
      '占い師': '毎晩1人指定し、その人が人狼かそうでないかを知ることができる',
      '霊媒師': '死んだ人が人狼かどうかを知ることができる',
      '狂人': '役職はない。しかし、人狼サイドの人間',
    };
    
    const Game = () => {
      const [players, setPlayers] = useState([]);
      const [alivePlayers, setAlivePlayers] = useState([]);
      const [rolesAssignment, setRolesAssignment] = useState({});
      const [currentPhase, setCurrentPhase] = useState('昼');
      const [nightActions, setNightActions] = useState({});
      const [gameOver, setGameOver] = useState(false);
      const [winner, setWinner] = useState('');
      const [playerName, setPlayerName] = useState('');
      const [startGameDisabled, setStartGameDisabled] = useState(false);
      const [startGameClickedCount, setStartGameClickedCount] = useState(0);
      const [selectedTargets, setSelectedTargets] = useState({});
      const [log, setLog] = useState([]);
    
      const initializeGame = () => {
        if (playerName.trim() === '') {
          alert('名前を入力してください');
          return;
        }
    
        setStartGameClickedCount(startGameClickedCount + 1);
        if (startGameClickedCount >= 1) {
          setStartGameDisabled(true);
        }
    
        const list = [playerName, ...Array(5).fill().map((_, i) => `CPU${i + 1}`)];
        const shuffledRoles = shuffleArray([...roles]).slice(0, list.length);
        const assignedRoles = assignRoles(shuffledRoles);
    
        setPlayers(list);
        setRolesAssignment(assignedRoles);
        setAlivePlayers(list);
    
        // Log roles to console
        console.log('Assigned Roles:');
        list.forEach(player => {
          console.log(`${player}: ${assignedRoles[player]}`);
        });
      };
    
      const shuffleArray = (array) => {
        for (let i = array.length - 1; i > 0; i--) {
          const j = Math.floor(Math.random() * (i + 1));
          [array[i], array[j]] = [array[j], array[i]];
        }
        return array;
      };
    
      const assignRoles = (rolesArray) => {
        const rolesObj = {};
        rolesArray.forEach((role, index) => {
          rolesObj[players[index]] = role || '市民';  // 役職が未設定なら「市民」を割り当てる
        });
        return rolesObj;
      };
    
      const handleNightActions = (role, target) => {
        setNightActions((prevActions) => ({
          ...prevActions,
          [role]: target,
        }));
      };
    
      const handleNightTurn = async () => {
        const actions = {};
        for (let i = 0; i < alivePlayers.length; i++) {
          const player = alivePlayers[i];
          const role = rolesAssignment[player];
    
          if (role !== '市民') { // プレイヤーの役職だけ表示
            if (role === '人狼') {
              // 人狼: プレイヤーの役職の仕事のみ行う
              const target = selectedTargets[player] || await simulateOpenAIAction(player, '襲撃');
              actions['人狼'] = target;
            } else if (role === '騎士') {
              // 騎士: プレイヤーの役職の仕事のみ行う
              const target = selectedTargets[player] || await simulateOpenAIAction(player, '守る');
              actions['騎士'] = target;
            } else if (role === '占い師') {
              // 占い師: プレイヤーの役職の仕事のみ行う
              const target = selectedTargets[player] || await simulateOpenAIAction(player, '占う');
              actions['占い師'] = target;
            } else if (role === '霊媒師') {
              // 霊媒師: プレイヤーの役職の仕事のみ行う
              const target = selectedTargets[player] || await simulateOpenAIAction(player, '霊媒師');
              actions['霊媒師'] = target;
            }
          }
        }
    
        handleNightActions(actions);
    
        // Night actions complete, resolve them
        resolveNightActions(nightActions);
      };
    
      const simulateOpenAIAction = async (role, actionType) => {
        // Simulate OpenAI decision for CPU roles
        console.log(`${role} performs ${actionType}`);
        // For simplicity, we'll just return a random alive player as the target
        const target = alivePlayers[Math.floor(Math.random() * alivePlayers.length)];
        setLog(prevLog => [...prevLog, `${role} action: ${actionType} on ${target}`]);
        return target;
      };
    
      const resolveNightActions = (actions) => {
        let newAlivePlayers = alivePlayers;
    
        // 人狼が誰かを襲う
        if (actions['人狼']) {
          newAlivePlayers = newAlivePlayers.filter(player => player !== actions['人狼']);
        }
    
        // 騎士が守る対象が襲われる場合、その襲撃を防ぐ
        if (actions['騎士'] && actions['騎士'] === actions['人狼']) {
          newAlivePlayers = newAlivePlayers;
        }
    
        // 霊媒師の仕事
        if (actions['霊媒師']) {
          // 霊媒師は死者が人狼だったかどうかを知る
          // 偽の情報を与えたりせず、襲撃された死者が人狼かどうかを後で知るシステムが考慮される
        }
    
        // 市民・人狼の数が同じになったらゲーム終了
        const cityCount = newAlivePlayers.filter(player => rolesAssignment[player] === '市民').length;
        const wolfCount = newAlivePlayers.filter(player => rolesAssignment[player] === '人狼').length;
    
        if (wolfCount === 0) { // 人狼が全滅した場合、市民サイドの勝利
          setWinner('市民');
          setGameOver(true);
        } else if (newAlivePlayers.length === 1) {
          setWinner('市民');
          setGameOver(true);
        } else {
          setAlivePlayers(newAlivePlayers);
          setCurrentPhase('昼');
        }
      };
    
      const handleDayActions = (target) => {
        const newAlivePlayers = alivePlayers.filter(player => player !== target);
        setAlivePlayers(newAlivePlayers);
    
        // 市民のターン終了
        setCurrentPhase('夜');
      };
    
      const handleRoleSelection = (role, player) => {
        setSelectedTargets((prevTargets) => ({
          ...prevTargets,
          [player]: role === '人狼' ? player : null,  // 人狼の選択時はそのまま選択、その他は選択解除
        }));
      };
    
      return (
        <div>
          <h1>人狼ゲーム</h1>
          {gameOver ? (
            <h2>勝者は {winner} サイドです！</h2>
          ) : (
            <>
              <h2>現在のフェーズ: {currentPhase}</h2>
              <input
                type="text"
                value={playerName}
                onChange={(e) => setPlayerName(e.target.value)}
                placeholder="あなたの名前を入力"
              />
              <button onClick={initializeGame} disabled={startGameDisabled}>
                ゲームスタート
              </button>
              <p>※2回スタートボタンを押してください</p>
    
              <ul>
                {players.map(player => (
                  <li key={player}>
                    {player}
                  </li>
                ))}
              </ul>
    
              {currentPhase === '夜' && (
                <div>
                  <h3>夜のターンです</h3>
                  {Object.keys(rolesAssignment).map(player => (
                    <div key={player}>
                      <h4>{player} ({rolesAssignment[player]})</h4>
                      <button onClick={() => handleRoleSelection('人狼', player)}>襲撃する</button>
                      <button onClick={() => handleRoleSelection('騎士', player)}>守る</button>
                      <button onClick={() => handleRoleSelection('占い師', player)}>占う</button>
                      <button onClick={() => handleRoleSelection('霊媒師', player)}>霊媒師</button>
                    </div>
                  ))}
                  <button onClick={handleNightTurn}>夜のターン終了</button>
                </div>
              )}
    
              {currentPhase === '昼' && (
                <div>
                  <h3>昼のターンです</h3>
                  <ul>
                    {alivePlayers.map(player => (
                      <li key={player}>
                        {player} ({rolesAssignment[player]})
                        <button onClick={() => handleDayActions(player)}>処刑する</button>
                      </li>
                    ))}
                  </ul>
                  <button onClick={() => setCurrentPhase('夜')}>昼のターン終了</button>
                </div>
              )}
    
              {log.length > 0 && (
                <div>
                  <h3>ログ</h3>
                  <ul>
                    {log.map((entry, index) => (
                      <li key={index}>{entry}</li>
                    ))}
                  </ul>
                </div>
              )}
            </>
          )}
        </div>
      );
    };
    
    export default Game;

### 2.夜に行うイベントを管理するコード
人狼、占い師、霊媒師、騎士が夜に行動するためのコードです。書く役職ごとに役職の仕事を行う対象を選びます。
ただし、プレイヤーの役職以外は全て自動でやってくれるのでプレイヤーの役職のみ出力されます。

    import React from 'react';
    
    const RoleSelector = ({ roles, selectedTargets, handleRoleSelection }) => {
      return (
        <div>
          {roles.map((role, index) => (
            <div key={index}>
              <h4>{role}のターン</h4>
              {role !== '市民' && (
                <>
                  <button onClick={() => handleRoleSelection('人狼', role)}>襲撃する</button>
                  <button onClick={() => handleRoleSelection('騎士', role)}>守る</button>
                  <button onClick={() => handleRoleSelection('占い師', role)}>占う</button>
                  <button onClick={() => handleRoleSelection('霊媒師', role)}>霊媒師</button>
                </>
              )}
            </div>
          ))}
        </div>
      );
    };
    
    export default RoleSelector;

# 実際の画面
### 昼
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935470/fcca4920-6dec-8638-562e-3a0915667050.png)

###　夜
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935470/c5892dc6-6762-f794-fae1-d16abef0b2d6.png)

# 終わりに
今回、1人用の人狼ゲームを作成してみました。
だいぶ前にReactを使ってから触れていなかったので忘れていることも多かったのですが一旦はゲームが形になったのでよかったです。
ゲームの内容に関してですが、今回は時間の都合上とても簡易的なものになってしまい、人狼ゲームというよりは人狼当てゲームになってしまったので悔しいです。今後はOpenAIや音声認識を用いて実際に昼のフェーズ時に相談などができるようにしたらより本物の人狼ゲームに近づけるのかなと思います。
パーティーゲームを1人でも行えるようなゲームを今後も作っていきたいなと感じたのでReactを用いて他にも開発していきたいです。また、GitHubを用いて公開もしていけたらいいなと思いました。

私でちょうどMYJLab Advent Calendar 2024も折り返しです。残り半分も記事作成頑張ってMYJLab Advent Calendarを完成させましょう！