オンライン対戦できるオセロをつくる(2)の続きです。今回はクライアントサイドの処理を実装します。
Contents
cshtml部分
cshtml部分を示します。操作用のボタン類を配置しています。
Pages\reversi\Index.cshtml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
@page @{ Layout = null; } <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>オンライン対戦できるオセロ - 鳩でもわかるASP.NET Core</title> <link rel="stylesheet" href="./style.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script> </head> <body> <div id="container"> <h1>鳩でもわかるオンライン対戦オセロ</h1> <div id="field"> <div id="canvas-outer"></div> <p id="game-infomation"></p> <details open id="entry-buttons-details"> <summary class="color1">エントリーボタンの表示/非表示</summary> <p id="entry-infomation"></p> <p>プレイヤー名を入力してエントリーボタンを押下してください。相手が見つかったら対戦がはじまります。</p> <p>PlayerName:<br><input type="text" id="player-name" maxlength="32"></p> <p><button id="entry">エントリー</button></p> </details> <p id="deny-reason"></p> <div id="past-match-controller"> <button id="prev">前へ</button> <button id="next">次へ</button> <button id="end">終了</button> </div> <details id="games-list-details"> <summary class="color2">現在おこなわれている対戦(クリックで開閉)</summary> <div id="games-list">現在おこなわれている対戦はありません</div> </details> <details id="past-matches-details"> <summary class="color3">過去におこなわれた対戦(クリックで開閉)</summary> <div id="past-matches">データはありません</div> </details> <div id="volume-ctrl"></div> </div> </div> <script> const connection = new signalR.HubConnectionBuilder().withUrl("/reversi-hub").build(); </script> <script src="./app.js"></script> </body> </html> |
wwwroot\reversi\style.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
body { background-color: #000; color: #fff; } #container { width: 360px; margin: 0 auto 0 auto; } #field { position: relative; margin-top: 40px; } #volume-ctrl { margin-top: 20px; margin-bottom: 20px; } #entry, #show-past-matches { width: 160px; height: 40px; margin-right: 10px; } #player-name { width: 200px; height: 20px; } h1 { font-size: 20px; color: magenta; text-align: center; } .player-name { color: aqua; font-weight: bold; } .reason { color: yellow; font-weight: bold; } .result { color: magenta; font-weight: bold; } .my-turn { color: magenta; font-weight: bold; } .rival-turn { color: yellow; font-weight: bold; } #games-list-details, #past-matches-details { margin-bottom: 10px; } #past-matches, #games-list { height: 200px; margin-top: 10px; overflow-y: scroll; } .td-players { width: 240px; border: #fff solid 1px; padding: 5px 10px 5px 10px; text-align: center; } .td-button { width: 100px; border: #fff solid 1px; text-align: center; } .watch { width: 80px; } #past-match-controller { margin-bottom: 10px; display: none; } #prev, #next, #end { width: 60px; height: 40px; margin-right: 10px; } .color1 { color:yellow; } .color2 { color: magenta; } .color3 { color: aqua; } |
グローバル変数と定数
グローバル変数と定数を示します。
wwwroot\reversi\app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
const RowMax = 8; // 盤面のマスは 8 × 8 const ColumMax = 8; const CellSize = 350 / RowMax; // マスの大きさ const cells = []; // マスを描画するためのCellオブジェクトの配列 let myturn = 'N'; // 自分は黒('B')か白('W')か let showHelper = false; // 残り時間がわずかのときは着手可能点を描画する let pastMatch = null; // 現在観戦中の過去の対戦 let pastMatchIndex = 0; // 現在観戦中の過去の対戦の何手目か? let ignoreClick = false; // クリックを無視する // DOM要素 const $canvasOuter = document.getElementById('canvas-outer'); const $canvas = document.createElement('canvas'); const ctx = $canvas.getContext('2d'); $canvasOuter?.appendChild($canvas); const $entryButtonsDetails = document.getElementById('entry-buttons-details'); const $entry = document.getElementById('entry'); const $playerName = document.getElementById('player-name'); const $entryInfomation = document.getElementById('entry-infomation'); const $gameInfomation = document.getElementById('game-infomation'); const $denyReason = document.getElementById('deny-reason'); const $gamesListDetails = document.getElementById('games-list-details'); const $gamesList = document.getElementById('games-list'); const $pastMatchesDetails = document.getElementById('past-matches-details'); const $pastMatches = document.getElementById('past-matches'); const $pastMatchController = document.getElementById('past-match-controller'); const $prev = document.getElementById('prev'); const $next = document.getElementById('next'); const $end = document.getElementById('end'); // 効果音 const matchedSound = new Audio('./sounds/matched.mp3'); const select1Sound = new Audio('./sounds/select1.mp3'); const select2Sound = new Audio('./sounds/select2.mp3'); const cancelSound = new Audio('./sounds/cancel.mp3'); const denySound = new Audio('./sounds/deny.mp3'); const winSound = new Audio('./sounds/win.mp3'); const loseSound = new Audio('./sounds/lose.mp3'); const timerSound = new Audio('./sounds/timer.mp3'); const sounds = [select1Sound, select2Sound, cancelSound, matchedSound, denySound, winSound, loseSound, timerSound]; |
Cellクラスの定義
マスと石を描画するためのCellクラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
class Cell { constructor(row, col) { this.Row = row; this.Col = col; this.X = CellSize * this.Col; // Row, Col から座標を計算する this.Y = CellSize * this.Row; this.Color = 'N'; // マスの上に置かれている石('N'ならなにも置かれていない) } Draw() { ctx.fillStyle = "green"; ctx.fillRect(this.X, this.Y, CellSize, CellSize); if(this.Color != 'B' && this.Color != 'W') return; // this.Color == 'B' または 'W' なら石を描画する if(this.Color == 'B') ctx.fillStyle = '#000'; if(this.Color == 'W') ctx.fillStyle = '#fff'; ctx.beginPath(); let centerX = this.X + CellSize / 2; let centerY = this.Y + CellSize / 2; ctx.arc(centerX, centerY, CellSize / 2 - 2, 0, 2 * Math.PI); ctx.fill(); } // 着手可能点の描画 DrawHelper() { if(this.Color == 'b') ctx.fillStyle = '#fff'; if(this.Color == 'w') ctx.fillStyle = '#fff'; ctx.beginPath(); let centerX = this.X + CellSize / 2; let centerY = this.Y + CellSize / 2; ctx.arc(centerX, centerY, 4, 0, 2 * Math.PI); ctx.fill(); } } |
ページが読み込まれたときの処理
ページが読み込まれたときにおこなわれる処理を示します。
Cellオブジェクトを生成してここからマスを描画します。テキストボックスに以前入力した文字列があるならローカルストレージに保存されているのでこれを読み出してセットします。そのあとイベントリスナの追加、ボリュームコントローラーの生成をおこない、ASP.NET SignalRでHubに接続します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
window.onload = () => { $canvas.width = CellSize * ColumMax; $canvas.height = CellSize * RowMax + 20; $canvas.style.border = "0px solid #111"; createCells(); // Cellオブジェクトの生成(後述) drawCells(); // マスの描画(後述) const savedName = localStorage.getItem('hatodemowakaru-player-name'); if(savedName) $playerName.value = savedName; $playerName?.addEventListener('change', () => localStorage.setItem('hatodemowakaru-player-name', $playerName.value )); addEventListeners(); // 後述 initVolume('volume-ctrl', sounds); // 後述 connection.start(); } |
マスの描画
Cellオブジェクトを生成してここからマスを描画する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
function createCells() { cells.length = RowMax; for (let r = 0; r < RowMax; r++){ cells[r] = []; cells[r].length = ColumMax; for (let c = 0; c < ColumMax; c++) cells[r][c] = new Cell(r, c); } } function drawCells() { for (let r = 0; r < RowMax; r++){ for (let c = 0; c < ColumMax; c++){ cells[r][c].Draw(); if(showHelper) cells[r][c].DrawHelper(); } } // 縦横の線を描画する ctx.strokeStyle = "black"; for (let i = 0; i <= RowMax; i++) { ctx.beginPath(); ctx.moveTo(0, CellSize * i); ctx.lineTo(CellSize * ColumMax, CellSize * i); ctx.stroke(); } for (let i = 0; i < ColumMax; i++) { ctx.beginPath(); ctx.moveTo(CellSize * i, 0); ctx.lineTo(CellSize * i, CellSize * ColumMax); ctx.stroke(); } } |
イベントリスナの追加
イベントリスナを追加する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
function addEventListeners(){ $entry.addEventListener('click', (e) => { let playerName = $playerName.value; if(playerName == '') playerName = '名無しさん'; connection.invoke('entry', playerName); }); // canvasがクリックされたらユーザーが着手しようとしている位置をサーバーに送信する $canvas.addEventListener('click', (e) => { const offsetX = e.offsetX; // => 要素左上からのx座標 const offsetY = e.offsetY; // => 要素左上からのy座標 connection.invoke('PutStone', Math.floor(offsetX / CellSize), Math.floor(offsetY / CellSize)); }); // [エントリーボタンの表示/非表示] の開閉操作 $entryButtonsDetails?.addEventListener('toggle', () => { if($entryButtonsDetails.open) playSound(select1Sound); else playSound(cancelSound); }); // [現在おこなわれている対戦(クリックで開閉)] が開いたら現在おこなわれている対戦の情報をサーバーにリクエストする $gamesListDetails?.addEventListener('toggle', () => { if($gamesListDetails.open) playSound(select1Sound); else playSound(cancelSound); }); // [過去におこなわれた対戦(クリックで開閉)] が開いたら過去の対戦の情報をサーバーにリクエストする $pastMatchesDetails?.addEventListener('toggle', () => { if($pastMatchesDetails.open){ connection.invoke('ShowPastMatches'); playSound(select1Sound); } else { playSound(cancelSound); } }); } function playSound(sound){ sound.currentTime = 0; sound.play(); } |
ボリュームコントローラーの表示
いまや完全に定番となったボリュームコントローラーを表示する処理です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
function initVolume(elementId, sounds){ let volume = 0.3; const savedVolume = localStorage.getItem('hatodemowakaru-volume'); if(savedVolume) volume = Number(savedVolume); const html = ` <div> 音量:<input type="range" id = "volume-range"> <span id = "volume-text">0</span> </div> <button id = "volume-test">音量テスト</button> `; document.getElementById(elementId).innerHTML = html; const $volumeRange = document.getElementById('volume-range'); const $volumeText = document.getElementById('volume-text'); $volumeRange.addEventListener('change', () => localStorage.setItem('hatodemowakaru-volume', volume.toString())); $volumeRange.addEventListener('input', () => { const value = $volumeRange.value; $volumeText.innerText = Math.round(value); volume = value / 100; setVolume(); }); setVolume(); $volumeText.innerText = Math.round(volume * 100); $volumeText.style.marginLeft = '16px'; $volumeRange.value = volume * 100; $volumeRange.style.width = '250px'; $volumeRange.style.verticalAlign = 'middle'; function setVolume(){ for(let i = 0; i < sounds.length; i++) sounds[i].volume = volume; } const $volumeTest = document.getElementById('volume-test'); $volumeTest.style.width = '120px'; $volumeTest.style.height = '45px'; $volumeTest.style.marginTop = '12px'; $volumeTest.style.marginLeft = '32px'; $volumeTest.addEventListener('click', () => sounds[0].play()); } |
接続完了時の処理
Hubに接続したときの処理を示します。このときはサーバーサイドから’SendToClientSuccessfulConnection’が送信されるのでいっしょに送られてくる文字列 entry を表示します。これでエントリーしているユーザーがいるとそのユーザー名も表示されます。いない場合はなにも表示されません。
1 2 3 |
connection.on('SendToClientSuccessfulConnection', (id, entry) => { $entryInfomation.innerHTML = entry; }); |
エントリー完了時の処理
ユーザーがエントリーしたときはサーバーサイドから’SendToClientSuccessfulEntry’が送信されます。
エントリーしたのが自分自身のときはエントリーボタンを消します。
1 2 3 4 5 6 7 |
connection.on('SendToClientSuccessfulEntry', (text, isSelf) => { if(isSelf) $entryButtonsDetails.style.display = 'none'; $entryInfomation.innerHTML = text; playSound(select1Sound); }); |
マッチング成功時の処理
マッチングに成功したときはサーバーサイドから’SendToClientMatched’が送信されます。
マッチング成功によって相手を待っているユーザーはいなくなるので待機ユーザーの名前は消します。またマッチングした両プレイヤーにはグローバル変数 myturn に自分が黒なのか白なのかを示す文字をセットします。両プレイヤーは対局者になるのでもし他の対戦の観戦をしているのであれば中止の処理をおこないます。観戦に関する要素も対局が終了するまですべて非表示とします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
connection.on('SendToClientMatched', (text, turn) => { $entryInfomation.innerHTML = ''; if(turn == 'B' || turn == 'W'){ // 2人の対戦者 myturn = turn; $entryButtonsDetails.style.display = 'none'; $gameInfomation.innerHTML = text; playSound(matchedSound); pastMatch = null; // 観戦しているのであれば観戦は中止 $gamesListDetails.style.display = 'none'; // 観戦に関する要素はすべて非表示に $pastMatchesDetails.style.display = 'none'; $pastMatchController.style.display = 'none'; } }); |
盤面の状態が変化したときの処理
対戦または観戦しているときに盤面の状態が変化したときは’SendToClientStatusChanged’が送信されます。このときの処理は対局者と観戦者とでは表示すべき内容が微妙に異なるので別の関数にわけています。
1 2 3 4 5 6 |
connection.on('SendToClientStatusChanged', (json) => { if(myturn == 'B' || myturn == 'W') showStatusToPlayers(json); // 対局者 else showStatusToWatchers(json); // 観戦者 }); |
対局者の場合、盤面の状態だけでなく、手番はどちらにあるのか、自分の手番の場合、持ち時間はどれだけあるのかも表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
function showStatusToPlayers(json){ const obj = JSON.parse(json); const players = obj['Players']; const times = obj['Times']; const turn = obj['Turn']; const isAccepted = obj['IsAccepted']; const isPassForced = obj['IsPassForced']; const isFinished = obj['IsFinished']; const scores = obj['Scores']; const winner = obj['Winner']; const rivalIndex = myturn == 'B' ? 1 : 0; const time = turn == 'B' ? times[0] : times[1]; // 手番は自分で残り5秒未満なら着手可能点も表示させる showHelper = (myturn == turn && time < 5) ? true : false; if(myturn == turn && time == 5) playSound(timerSound); drawCellsByStones(obj['Stones']); // 適切な着手がされた直後であれば着手の効果音も鳴らす if(isAccepted){ timerSound.pause(); if(myturn == turn) playSound(select1Sound); else playSound(select2Sound); } // 誰と対戦しているのか、手番はどちらにあるのか、残り時間はどれだけあるのかを表示する if(!isFinished){ $gameInfomation.innerHTML = `<p><span class ='player-name'>${players[rivalIndex]['Name']}</span> と対戦中</p>`; let curColor = ''; if(turn == 'B') curColor = ' (黒) '; if(turn == 'W') curColor = ' (白) '; if(myturn == turn) $gameInfomation.innerHTML += `<p><span class = "my-turn">あなた${curColor}の手番</span> です (残り <span class = "my-turn">${time}</span> 秒)</p>`; else if (turn != 'N') { if(!isPassForced) $gameInfomation.innerHTML += `<p><span class = "rival-turn">相手の手番</span> です</p>`; else $gameInfomation.innerHTML += '<p>着手できないので <span class = "rival-turn">パス</span> しました</p>'; } } // 終局直後であれば終局したことと勝敗を表示する if(isFinished) { timerSound.pause(); $gameInfomation.innerHTML = `<p><span class ='player-name'>${players[rivalIndex]['Name']}</span> との対戦が終了しました</p>`; const reason = (scores[0] != 0 || scores[1] != 0) ? `黒:${scores[0]} 白:${scores[1]}` : '時間切れ'; let result = ''; if(myturn == 'B' || myturn == 'W'){ if(winner == myturn){ result = `あなたの <span class ='result'>勝ち</span>`; playSound(winSound); } if(winner != 2 && winner != myturn){ result = `あなたの <span class ='result'>負け</span>`; playSound(loseSound); } if(winner == 2){ result = `<span class ='result'>引き分け</span>`; playSound(loseSound); } } $gameInfomation.innerHTML += `<p><span class ='reason'>${reason}</span> で ${result} です</p>`; // 終局したので他の対戦を観戦できるように非表示にしていた要素を再表示する $entryButtonsDetails.style.display = 'block'; $gamesListDetails.style.display = 'block'; $pastMatchesDetails.style.display = 'block'; myturn = 'N'; // 終局したので先手も後手もなくなる } } |
観戦者の場合、盤面の状態と手番のみを表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
function showStatusToWatchers(json){ const obj = JSON.parse(json); const players = obj['Players']; const turn = obj['Turn']; const isAccepted = obj['IsAccepted']; const isFinished = obj['IsFinished']; const scores = obj['Scores']; const winner = obj['Winner']; drawCellsByStones(obj['Stones']); if(isAccepted){ if(turn == 0) playSound(select1Sound); if(turn == 1) playSound(select2Sound); } // 対局中 if(!isFinished){ $gameInfomation.innerHTML = `<p>【観戦モード】<br><span class ='player-name'>${players[0]['Name']}</span> と <span class ='player-name'>${players[1]['Name']}</span> が対戦中</p>`; if(turn == 'B') $gameInfomation.innerHTML += '<p>黒番です</p>'; if(turn == 'W') $gameInfomation.innerHTML += '<p>白番です</p>'; } // 終局 if(isFinished){ $gameInfomation.innerHTML = `<p>【観戦モード】<br><span class ='player-name'>${players[0]['Name']}</span> と <span class ='player-name'>${players[1]['Name']}</span> の対戦が終了しました</p>`; const reason = (scores[0] != 0 || scores[1] != 0) ? `黒:${scores[0]} 白:${scores[1]}` : '時間切れ'; let result = ''; if(winner == 'B') result = `黒の <span class ='result'>勝ち</span>`; else if(winner == 'W') result = `白の <span class ='result'>勝ち</span>`; else result = `<span class ='result'>引き分け</span>`; $gameInfomation.innerHTML += `<p><span class ='reason'>${reason}</span> で ${result} です</p>`; } } |
不適切な着手がされたときの処理
不適切な着手がされたときは’SendToClientDeny’が送信されます。効果音を鳴らすとともに、textに着手不可の理由を示す文字列が格納されているのでこれを1秒間表示します。
1 2 3 4 5 |
connection.on('SendToClientDeny', (text) => { playSound(denySound); $denyReason.innerHTML = text; setTimeout(() => $denyReason.innerHTML = '', 1000); }); |
現在おこなわれている対戦情報の表示
現在おこなわれている対戦情報を表示する処理を示します。
1秒おきに’SendToClientUpdateGames’とともに現在おこなわれている対戦情報に関する文字列が送られてくるのでそれを表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
connection.on('SendToClientUpdateGames', (text) => { if(text == ''){ $gamesList.innerHTML = '現在おこなわれている対戦はありません'; return; } const arr = text.split('\n'); let tableHTML = ''; tableHTML += '<table>'; const gameNumbers = []; arr.forEach(_ => { const arr2 = _.split('\t'); gameNumbers.push(arr2[0]); tableHTML += '<tr>'; tableHTML += `<td class = "td-players">${arr2[1]}<br>${arr2[2]}</td>`; tableHTML += `<td class = "td-button"><button class = "watch" id="watch-${arr2[0]}">観戦する</button></td>`; tableHTML += '</tr>'; }); tableHTML += '</table>'; $gamesList.innerHTML = tableHTML; // [観戦する]ボタンをクリックしたら観戦できるようにイベントリスナを追加する gameNumbers.forEach(num => { const id = 'watch-' + num; const $btn = document.getElementById(id); $btn.onclick = () => { connection.invoke('AddWatcher', Number(num)); playSound(select1Sound); }; }); }); |
過去の対戦を観戦する処理
まず過去の対戦データをリクエストすると’SendToClientGotPastMatches’とともにjsonデータが送られてくるのでこれを表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
connection.on('SendToClientGotPastMatches', (json) => { const obj = JSON.parse(json); if(obj.length == 0){ $pastMatches.innerHTML = 'データはありません'; return; } const gameNumbers = []; let tableHTML = ''; tableHTML += '<table>'; for(let i = 0; i < obj.length; i++){ tableHTML += '<tr>'; const game = obj[i]['Name']; const endTime = `${game.substring(0, 4)}-${game.substring(4, 6)}-${game.substring(6, 8)} ${game.substring(8, 10)}:${game.substring(10, 12)}:${game.substring(12, 14)} 終了`; tableHTML += `<td class = "td-players">${obj[i]['Names'][0]}<br>${obj[i]['Names'][1]}<br>${endTime}</td>`; tableHTML += `<td class = "td-button"><button class = "watch" id="past-matches-${obj[i]['Name']}">観戦する</button></td>`; tableHTML += '</tr>'; gameNumbers.push(obj[i]['Name']); } tableHTML += '</table>'; $pastMatches.innerHTML = tableHTML; // [観戦する]ボタンをクリックしたら観戦できるようにイベントリスナを追加する gameNumbers.forEach(num => watchPastMatch(num, obj)); }); |
[観戦する]ボタンをクリック時のイベントリスナを追加する処理を示します。
観戦を希望している対戦のnumと一致する対戦データを探してpastMatchにセットします。もしリアルタイムで観戦している対局があればキャンセルしています。そのあと操作用のボタンを表示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
function watchPastMatch(num, obj){ const id = 'past-matches-' + num; const $btn = document.getElementById(id); $btn.onclick = () => { const find = obj.find(_ => _['Name'] == num); if(find != null){ connection.invoke('AddWatcher', -1); // リアルタイムで観戦中の対局があればキャンセル pastMatch = find; pastMatchIndex = 0; // 初手から再生 $pastMatchController.style.display = 'block'; // 操作用ボタンを表示 const stones = pastMatch['Histories'][pastMatchIndex]['Stones']; // 初期配置の石を取得 drawCellsByStones(stones); // 取得した石を表示 playSound(select1Sound); $gameInfomation.innerHTML = `<p>【観戦モード】<br><span class ='player-name'>${pastMatch['Names'][0]}</span> (黒) と <span class ='player-name'>${pastMatch['Names'][1]}</span> (白)</p>`; } }; } // 取得した石を表示する function drawCellsByStones(stones){ for (let r = 0; r < RowMax; r++) { for (let c = 0; c < ColumMax; c++) cells[r][c].Color = stones[r][c]; } drawCells(); } |
次の手を表示させる処理を示します。どんな手を着手したのかを表示したあと石をひっくり返す演出をしたいのでちょっとややこしい処理になっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
$next?.addEventListener('click', async() => { if(ignoreClick || pastMatch == null) return; const length = pastMatch['Histories'].length; const oldhistory = pastMatch['Histories'][pastMatchIndex]; const oldstones = oldhistory['Stones']; if(pastMatchIndex + 1 >= length) return; pastMatchIndex++; const history = pastMatch['Histories'][pastMatchIndex]; const row = history['Row']; // 着手 const col = history['Col']; const turn = history['Turn']; const copystones = copy2x2(oldstones); copystones[row][col] = turn; drawCellsByStones(copystones); if(turn == 'B') playSound(select1Sound); else playSound(select2Sound); ignoreClick = true; await new Promise(_ => setTimeout(_, 250)); // 0.25秒待機してひっくり返したあとの石を描画する ignoreClick = false; const stones = history['Stones']; // ひっくり返したあとの石を描画する drawCellsByStones(stones); // 次の手に進めた結果、終局した場合 if(pastMatchIndex == length - 1){ const reason = (pastMatch['Scores'][0] != 0 || pastMatch['Scores'][1] != 0) ? `黒:${pastMatch['Scores'][0]} 白:${pastMatch['Scores'][1]}` : '時間切れ'; let result; const winner = pastMatch['Winner']; if(winner == 'B') result = `黒の <span class ='result'>勝ち</span>`; else if(winner == 'W') result = `白の <span class ='result'>勝ち</span>`; else result = `<span class ='result'>引き分け</span>`; $gameInfomation.innerHTML = `【観戦モード】<br><span class ='player-name'>${pastMatch['Names'][0]}</span> (黒) と <span class ='player-name'>${pastMatch['Names'][1]}</span> (白) ---- <span class ='result'>終了</span><br>`; $gameInfomation.innerHTML += `<span class ='reason'>${reason}</span> で ${result} です`; } }); // ジャグ配列(配列の配列)のコピーを返す function copy2x2(arr2x2){ const copy = []; for (let r = 0; r < RowMax; r++) { const arr = []; for (let c = 0; c < ColumMax; c++) arr[c] = arr2x2[r][c]; copy[r] = arr; } return copy; } |
前の手を表示させる処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$prev?.addEventListener('click', () => { if(ignoreClick || pastMatch == null || pastMatchIndex <= 0) return; if(pastMatch != null){ pastMatchIndex--; const history = pastMatch['Histories'][pastMatchIndex]; const stones = history['Stones']; drawCellsByStones(stones); playSound(cancelSound); $gameInfomation.innerHTML = `【観戦モード】<br><span class ='player-name'>${pastMatch['Names'][0]}</span> (黒) と <span class ='player-name'>${pastMatch['Names'][1]}</span> (白)`; } }); |
[終了]をクリックしたら過去の対戦の観戦を終了させます。その処理を示します。
1 2 3 4 5 |
$end?.addEventListener('click', () => { pastMatch = null; $pastMatchController.style.display = 'none'; // 過去の対戦閲覧用のボタンを隠す playSound(select1Sound); }); |