SpringBootでPCインタラクティブ型リアル脱出ゲームを作る

PCに表示される問題に対して、1~9のキーボードを用いたり、部屋のオブジェクトを用いたりして解答していく、脱出ゲームを作ります。
主に処理フローが3つあります。
### サーバPCでの受付表示
```
// 待機画面の切り替えロジック
let pollingInterval = null;
	function waitingPolling(){
		if (pollingInterval !== null)return; //二重起動防止
		pollingInterval = setInterval(async() =>{
			if(!stopFlg){
				const response = await fetch("/api/validPC");
				const useKahi = await response.json();
				if (useKahi){
					waiting.style.display = "none";
					QRwait.style.display= "block";
				}else{
					waiting.style.display = "block";
					QRwait.style.display= "none";
						
				}
			}
		},3000) //3秒ごとに確認
	}
```
この部分でQRコードを読み取るか、待機してもらうかの表示を管理しています。
3秒ごとに、PCの空き具合をポーリングし、ゲームPCの空き状況によって表示を変更します。
> stopFlgがtrueの場合は画面表示処理は行われません。

↓ポーリングで呼ばれるmainコントローラ
```
	@ResponseBody
	@GetMapping("/api/validPC")
	public boolean haveValidPC() {
		return mainService.haveValidPC();
	}
```
↓使えるPCがあるかを調べるmainサービス
```
	/**
	 * 
	 * @return validFlg 使えるPCがあったらtrue
	 */
	public boolean haveValidPC() {
		boolean validFlg = false;
		for (boolean using :PCUsing) {
			if(!using) {
				validFlg = true;
			}
		}
		return validFlg;
	}
```
PCUsingはPCUsing[i]が使用中の場合はtrueを返すようなbooleanのリストです。

### サーバPCでの受付ロジック
サーバPCでユーザがQRコードを読み取らせた時の処理についてです。
```
		document.addEventListener("DOMContentLoaded",() => {
			const input = document.getElementById("qrid");
			
			// QRリーダーがエンターを押したタイミングで入力欄の内容を使って、nextPlayerのセット処理を起動
			input.addEventListener("keypress" , async(event) =>{
				if(!stopFlg){
					if (event.key ==="Enter"){
						const qrid = input.value.trim();
						if (!qrid) return;
			
						try{
							const response = await fetch("/api/next/player",{
								method: "POST",
								headers:{"Content-Type": "application/json"},
								body:JSON.stringify({QRId:qrid})
							});
							startOP();
						} catch(err){
							console.log("エラー",err);
						} finally{
							input.value = "";
							input.focus();
						}
					}
				}
			} )
		})
```
Enterキーを押した際に(QRコードを読み取った際に)nextPlayer(次に案内されるPlayer)を登録する処理が呼び出されます。
> stopFlgがtrueの時はエンターが押されても、処理は実行されません。(本来はinputも無効にしたいので、それができているから再考の必要があります。
さらに動画再生中というか、受付でプレイヤーの案内をしている最中はinputがdisable=trueになっています。

↓上記jsから呼び出されるコントローラです。
```
	// QRコード読み取りで呼び出されて、サーバの次のplayerを登録する処理を起動
	@ResponseBody
	@PostMapping("/api/next/player")
	public void setNextPlayer(@RequestBody Map<String,String> payload) {
		String QRId = payload.get("QRId");
		mainService.entory(QRId);
		return;
	}
```
標準入力からストリーム処理によってQRId(各QRコードに割り振られたlong型の固有ID)を取得し、サービスクラスに渡します。
```
	/**
	 *  QRコードリーダーに起動されて、次のプレイヤーを登録するやつ
	 * @param QRId
	 */
	public void entory(String QRId) {
		// データベースとPlayerの色々やるやつ
		Player player = getPlayerData(QRId);
		this.nextPlayer = player;
	}
```
getPlayerDataを用いて、Player情報を取得しそれをnextPlayerにセットする。

```
	/**
	 * 
	 * @param QRId QRコードのID
	 * @return Player DBになかったら新しいのを生成、あったらそれをDBから持ってくる
	 */
	public Player getPlayerData(String QRId) {
		Optional<Player> playerOpt = repository.findByQRId(QRId);
		Player player;
		// データがあるかどうか
		if (playerOpt.isPresent()) {
			player = playerOpt.get();
		}else {
			player = new Player(QRId);
		}
		return  player;
	}
```

QRコードをキーにしてPlayerリポジトリを検索します。そのプレイヤーが2回目以降の挑戦だった場合はDBからプレイヤー情報を取得して、returnします。初回挑戦だった場合はPlayerのコンストラクタを用いて、新しいPlayerデータを作成してreturnします。

### ゲームPCからのポーリング
```
	// サーバーに起動合図の確認をしに行く
	let pollingInterval = null;
	
	function startPolling(pcNum){
		if (pollingInterval !== null)return; //二重起動防止
		
		pollingInterval = setInterval(async() =>{
			const response = await fetch(`http://localhost:8081/api/PC/${pcNum}`);
			player = await response.json();
			if (player.qrid){
				clearInterval(pollingInterval);
				document.getElementById("floor").textContent = player.floor + "階";
				
				setTimeout(() => {
					// 受付の案内が終わったらボタンを押すように指示
					gameStartPush.style.display = "block";
					hasPlayer = true;
				},55000)
			}else{
			}
		},3000) //3秒ごとに確認
	}
```
ゲームPCのこの部分で、サーバPCにアクセスし、nextPlayerがあり自身のUsingがfalseの時のみPLayerをreturnで受け取る。
qridがあったら(実質Playerがnullじゃないかを確認している)ポーリングをやめて、プレイヤー情報を画面にセットした後、サーバPCの受付案内の終了を待ち、ゲーム起動のトリガー指示を表示する。
> 現在は受付が終わったかを読み取っているのではなく、ポーリングが成功した瞬間からの経過秒で行っている

```
	// ゲームPCが自分のPCへの割り当て情報を確認してプレイヤー情報をもらう
	@ResponseBody
	@GetMapping("/api/PC/{pcNum}")
	public ResponseEntity<Player> assignPC(@PathVariable int pcNum) {
		Player player = mainService.getPlayer(pcNum);
		if (player == null) {
			return ResponseEntity.ok(new Player());
		}else {
			return ResponseEntity.ok(player);
		}
	}
```
このコントローラでポーリングを受ける。本来viewを返すところを、戻り値をそのままJSONとして返すためにResponsiBodyのアノテーションがついている。

```
	/**
	 * ゲーム用PCにプレイヤーデータを渡す
	 * @param pcNum リクエストを送ってきたPCのPC番号
	 * @return 受付をしているPlayerのデータ
	 */
	public synchronized  Player getPlayer(int pcNum) {
		if (!(this.nextPlayer == null)) {
			Player player = new Player(this.nextPlayer);
			if (!PCUsing[pcNum]) {
				PCUsing[pcNum] = true;
				nextPlayer = null;

				return player;
			}
		}
		return null;
	}
```
PCUsingがfalseだったらnextPlayerをPlayerに入れてreturnしたのち、nextPlayerをnullに戻す。
> おそらくだがこの時点でゲーム開始のポーリングの状態はリセットされている。

### ゲーム開始処理
先ほどのポーリングが成功したさいに、受付の動画再生が終わったタイミングで(時間による指定)画像が切り替わり、ゲーム側の動画開始ボタンが使えるようになる。ボタンを押すとゲームのOP動画が流れる。
```
	// ゲーム開始ボタン
	document.addEventListener("keydown",() => {
		if(hasPlayer  && !videoFlg){
			video.style.display = "block";
			gameStartPush.style.display = "none";
			video.play();
		}
	})
```
> videoFlgは2回目のゲームOP動画が再生されないように、一度動画が再生されたらtrueとなる。hasPlayerはゲームPCにプレイヤーがセットされているかを表している。
>hasPlayerはポーリングの後、実際にPlayerが存在することが確認され、ゲーム開始指示の画像の提示とともにtrueになるため、待ち時間に起動してしまう心配はない。

```
	// 冒頭の動画が終わったらゲーム開始
	video.addEventListener("ended",() =>{
		playableFlg = true;
		videoFlg = true;
		video.style.display = "none";
		gameStart();
	});
```
ビデオが終わると同時にゲームがスタートされる。(gameStart()が実行される)
>ここでvideoFlgがtrueとなり、videoは非表示にされる。(ゲーム開始ボタンを押した際にこれはまたblockにされる.

```
	function gameStart(){
		setTimer();
	}
```
このゲーム開始関数が呼び出され、setTimer()で制限時間が動き出す。
>動画が終わると同時にゲームが始まる仕様なので、動画の最後をカウントダウンor始めます的なにしたい。
>ここにはいろんな処理を追記する予定です。

```
	// ゲーム開始時に呼び出され、タイマーを管理する関数
	function setTimer(){
		let isValid = true;
		let totalSeconds = 20; // 制限時間を秒数で
		const timerElem = document.getElementById('timer');
	
		function formatTime(seconds) {
		  const m = Math.floor(seconds / 60);
		  const s = seconds % 60;
		  return m + ':' + (s < 10 ? '0' + s : s);
		}
	
		timerElem.textContent = formatTime(totalSeconds);
	
		const intervalId = setInterval(() => {
		  totalSeconds--;
		  timerElem.textContent = formatTime(totalSeconds);
	
		  // タイムオーバーの時の処理
		  if (totalSeconds <= 0 && isValid) {
			// 動画流す？
			// 成績表示する？
			// 退出案内を提示する？
			// サーバーに情報を送信する？
			// 成績表示ロジック確認のためのtempコード
			player.floor = 5;
			
			gameEnd();
			sendPlayer();
			isValid = false;
			pollingInterval = null;
			startPolling(pcNum);
			// PC使用可否のリセット
			fetch(`http://localhost:8081/api/PCUsing/${pcNum}`,{
				method: 'POST'
			});
			clearInterval(intervalId);
		  }
		}, 1000);
	}
```
上部は基本的にカウントダウンをしているだけです。ゲームオーバーの処理はここから呼び出されます。
gameEnd()でサーバにPlayerデータを送信→それを元に計算された成績クラス(Score)が返却→それを表示が行われている。
>現状この後に、videoFlgのリセットやポーリングの再開処理、があるが(つまりゲーム終了後のリセット処理のようなもの)その前にED動画や退出指示を入れる予定。