├── .gitignore ├── README.md ├── firebase.json ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.js ├── App.test.js ├── chess │ ├── assets │ │ ├── chessBoard.png │ │ └── moveSoundEffect.mp3 │ ├── model │ │ ├── chess.js │ │ ├── chesspiece.js │ │ └── square.js │ └── ui │ │ ├── chessgame.js │ │ ├── piece.js │ │ └── piecemap.js ├── connection │ ├── socket.js │ └── videochat.js ├── context │ └── colorcontext.js ├── index.js ├── logo.svg ├── onboard │ ├── joingame.js │ ├── joinroom.js │ └── onboard.js ├── serviceWorker.js └── setupTests.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Multiplayer Chess Game built with React and Node.js + Express. 2 | 3 | - Users can play their friends anonymously via link. 4 | - Users are also able to chat with each other during the game via camera + microphone. 5 | - Tech stack: React, webRTC, Node.js, Express, Socket.io 6 | 7 | Link to the backend: [Backend](https://github.com/ProjectsByJackHe/multiplayer-chess-game-backend) -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chess-game", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "chess.js": "^0.10.3", 10 | "konva": "6.0.0", 11 | "react": "^16.13.1", 12 | "react-dom": "^16.13.1", 13 | "react-konva": "^16.13.0-3", 14 | "react-router-dom": "^5.2.0", 15 | "react-scripts": "3.4.1", 16 | "simple-peer": "^9.7.2", 17 | "socket.io-client": "2.3.0", 18 | "styled-components": "^5.1.1", 19 | "use-image": "^1.0.5", 20 | "use-sound": "^1.0.2", 21 | "uuid": "^7.0.3" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": "react-app" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackHeTech/multiplayer-chess-game/859918336b7fa7e04b1855ec5d0cd4bfa21e52db/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackHeTech/multiplayer-chess-game/859918336b7fa7e04b1855ec5d0cd4bfa21e52db/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackHeTech/multiplayer-chess-game/859918336b7fa7e04b1855ec5d0cd4bfa21e52db/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Route, Redirect, Switch } from 'react-router-dom'; 3 | import JoinRoom from './onboard/joinroom' 4 | import { ColorContext } from './context/colorcontext' 5 | import Onboard from './onboard/onboard' 6 | import JoinGame from './onboard/joingame' 7 | import ChessGame from './chess/ui/chessgame' 8 | /* 9 | * Frontend flow: 10 | * 11 | * 1. user first opens this app in the browser. 12 | * 2. a screen appears asking the user to send their friend their game URL to start the game. 13 | * 3. the user sends their friend their game URL 14 | * 4. the user clicks the 'start' button and waits for the other player to join. 15 | * 5. As soon as the other player joins, the game starts. 16 | * 17 | * 18 | * Other player flow: 19 | * 1. user gets the link sent by their friend 20 | * 2. user clicks on the link and it redirects to their game. If the 'host' has not yet 21 | * clicked the 'start' button yet, the user will wait for when the host clicks the start button. 22 | * If the host decides to leave before they click on the "start" button, the user will be notified 23 | * that the host has ended the session. 24 | * 3. Once the host clicks the start button or the start button was already clicked on 25 | * before, that's when the game starts. 26 | * Onboarding screen =====> Game start. 27 | * 28 | * Every time a user opens our site from the '/' path, a new game instance is automatically created 29 | * on the back-end. We should generate the uuid on the frontend, send the request with the uuid 30 | * as a part of the body of the request. If any player leaves, then the other player wins automatically. 31 | * 32 | */ 33 | 34 | 35 | function App() { 36 | 37 | const [didRedirect, setDidRedirect] = React.useState(false) 38 | 39 | const playerDidRedirect = React.useCallback(() => { 40 | setDidRedirect(true) 41 | }, []) 42 | 43 | const playerDidNotRedirect = React.useCallback(() => { 44 | setDidRedirect(false) 45 | }, []) 46 | 47 | const [userName, setUserName] = React.useState('') 48 | 49 | return ( 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | {didRedirect ? 58 | 59 | 60 | 61 | 62 | : 63 | } 64 | 65 | 66 | 67 | 68 | ); 69 | } 70 | 71 | export default App; 72 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/chess/assets/chessBoard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackHeTech/multiplayer-chess-game/859918336b7fa7e04b1855ec5d0cd4bfa21e52db/src/chess/assets/chessBoard.png -------------------------------------------------------------------------------- /src/chess/assets/moveSoundEffect.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackHeTech/multiplayer-chess-game/859918336b7fa7e04b1855ec5d0cd4bfa21e52db/src/chess/assets/moveSoundEffect.mp3 -------------------------------------------------------------------------------- /src/chess/model/chess.js: -------------------------------------------------------------------------------- 1 | import Chess from 'chess.js' 2 | import ChessPiece from './chesspiece' 3 | import Square from './square' 4 | // when indexing, remember: [y][x]. 5 | /** 6 | * If the player color is black, make sure to invert the board. 7 | */ 8 | 9 | 10 | 11 | 12 | class Game { 13 | constructor(thisPlayersColorIsWhite) { 14 | this.thisPlayersColorIsWhite = thisPlayersColorIsWhite // once initialized, this value should never change. 15 | // console.log("this player's color is white: " + this.thisPlayersColorIsWhite) 16 | this.chessBoard = this.makeStartingBoard() // the actual chessBoard 17 | this.chess = new Chess() 18 | 19 | this.toCoord = thisPlayersColorIsWhite ? { 20 | 0:8, 1:7, 2: 6, 3: 5, 4: 4, 5: 3, 6: 2, 7: 1 21 | } : { 22 | 0:1, 1:2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8 23 | } 24 | 25 | this.toAlphabet = thisPlayersColorIsWhite ? { 26 | 0:"a", 1:"b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g", 7: "h" 27 | } : { 28 | 0:"h", 1:"g", 2: "f", 3: "e", 4: "d", 5: "c", 6: "b", 7: "a" 29 | } 30 | 31 | this.toCoord2 = thisPlayersColorIsWhite ? { 32 | 8:0, 7:1, 6: 2, 5: 3, 4: 4, 3: 5, 2: 6, 1: 7 33 | } : { 34 | 1:0, 2:1, 3: 2, 4: 3, 5: 4, 6: 5, 7: 6, 8: 7 35 | } 36 | 37 | this.toAlphabet2 = thisPlayersColorIsWhite ? { 38 | "a":0, "b":1, "c":2, "d":3, "e":4, "f":5, "g":6, "h":7 39 | } : { 40 | "h":0, "g":1, "f":2, "e":3, "d":4, "c":5, "b":6, "a":7 41 | } 42 | 43 | this.nQueens = 1 44 | } 45 | 46 | getBoard() { 47 | return this.chessBoard 48 | } 49 | 50 | // nextPlayersTurn() { 51 | // this.isWhitesTurn = !this.isWhitesTurn 52 | // } 53 | 54 | setBoard(newBoard) { 55 | this.chessBoard = newBoard 56 | } 57 | 58 | movePiece(pieceId, to, isMyMove) { 59 | 60 | const to2D = isMyMove ? { 61 | 105:0, 195:1, 285: 2, 375: 3, 465: 4, 555: 5, 645: 6, 735: 7 62 | } : { 63 | 105:7, 195:6, 285: 5, 375: 4, 465: 3, 555: 2, 645: 1, 735: 0 64 | } 65 | 66 | 67 | var currentBoard = this.getBoard() 68 | const pieceCoordinates = this.findPiece(currentBoard, pieceId) 69 | 70 | 71 | // can't find piece coordinates (piece doesn't exist on the board) 72 | if (!pieceCoordinates) { 73 | return 74 | } 75 | 76 | const y = pieceCoordinates[1] 77 | const x = pieceCoordinates[0] 78 | 79 | // new coordinates 80 | const to_y = to2D[to[1]] 81 | const to_x = to2D[to[0]] 82 | 83 | const originalPiece = currentBoard[y][x].getPiece() 84 | 85 | if (y === to_y && x === to_x) { 86 | return "moved in the same position." 87 | } 88 | 89 | /** 90 | * In order for this method to do anything meaningful, 91 | * the 'reassign const' line of code must run. Therefore, 92 | * for it to run, we must check first that the given move is valid. 93 | */ 94 | 95 | const isPromotion = this.isPawnPromotion(to, pieceId[1]) 96 | const moveAttempt = !isPromotion ? this.chess.move({ 97 | from: this.toChessMove([x, y], to2D), 98 | to: this.toChessMove(to, to2D), 99 | piece: pieceId[1]}) 100 | : 101 | this.chess.move({ 102 | from: this.toChessMove([x, y], to2D), 103 | to: this.toChessMove(to, to2D), 104 | piece: pieceId[1], 105 | promotion: 'q' 106 | }) 107 | 108 | 109 | console.log(moveAttempt) 110 | // console.log(isPromotion) 111 | 112 | if (moveAttempt === null) { 113 | return "invalid move" 114 | } 115 | 116 | 117 | if (moveAttempt.flags === 'e') { 118 | const move = moveAttempt.to 119 | const x = this.toAlphabet2[move[0]] 120 | let y 121 | if (moveAttempt.color === 'w') { 122 | y = parseInt(move[1], 10) - 1 123 | } else { 124 | y = parseInt(move[1], 10) + 1 125 | } 126 | currentBoard[this.toCoord2[y]][x].setPiece(null) 127 | } 128 | 129 | 130 | 131 | // Check castling 132 | const castle = this.isCastle(moveAttempt) 133 | if (castle.didCastle) { 134 | /** 135 | * The main thing we are doing here is moving the right rook 136 | * to the right position. 137 | * 138 | * - Get original piece by calling getPiece() on the original [x, y] 139 | * - Set the new [to_x, to_y] to the original piece 140 | * - Set the original [x, y] to null 141 | */ 142 | 143 | const originalRook = currentBoard[castle.y][castle.x].getPiece() 144 | currentBoard[castle.to_y][castle.to_x].setPiece(originalRook) 145 | currentBoard[castle.y][castle.x].setPiece(null) 146 | } 147 | 148 | 149 | // ___actually changing the board model___ 150 | 151 | const reassign = isPromotion ? currentBoard[to_y][to_x].setPiece( 152 | new ChessPiece( 153 | 'queen', 154 | false, 155 | pieceId[0] === 'w' ? 'white' : 'black', 156 | pieceId[0] === 'w' ? 'wq' + this.nQueens : 'bq' + this.nQueens)) 157 | : currentBoard[to_y][to_x].setPiece(originalPiece) 158 | 159 | if (reassign !== "user tried to capture their own piece") { 160 | currentBoard[y][x].setPiece(null) 161 | } else { 162 | return reassign 163 | } 164 | 165 | // ___actually changing the board model___ 166 | 167 | 168 | const checkMate = this.chess.in_checkmate() ? " has been checkmated" : " has not been checkmated" 169 | console.log(this.chess.turn() + checkMate) 170 | if (checkMate === " has been checkmated") { 171 | return this.chess.turn() + checkMate 172 | } 173 | // changes the fill color of the opponent's king that is in check 174 | const check = this.chess.in_check() ? " is in check" : " is not in check" 175 | console.log(this.chess.turn() + check) 176 | if (check === " is in check") { 177 | return this.chess.turn() + check 178 | } 179 | 180 | console.log(currentBoard) 181 | // update board 182 | this.setBoard(currentBoard) 183 | } 184 | 185 | 186 | 187 | isCastle(moveAttempt) { 188 | /** 189 | * Assume moveAttempt is legal. 190 | * 191 | * {moveAttempt} -> {boolean x, y to_x, to_y} 192 | * 193 | * returns if a player has castled, the final position of 194 | * the rook (to_x, to_y), and the original position of the rook (x, y) 195 | * 196 | */ 197 | 198 | 199 | const piece = moveAttempt.piece 200 | const move = {from: moveAttempt.from, to: moveAttempt.to} 201 | 202 | const isBlackCastle = ((move.from === 'e1' && move.to === 'g1') || (move.from === 'e1' && move.to === 'c1')) 203 | const isWhiteCastle = (move.from === 'e8' && move.to === 'g8') || (move.from === 'e8' && move.to === 'c8') 204 | 205 | 206 | if (!(isWhiteCastle || isBlackCastle) || piece !== 'k') { 207 | return { 208 | didCastle: false 209 | } 210 | } 211 | 212 | let originalPositionOfRook 213 | let newPositionOfRook 214 | 215 | if ((move.from === 'e1' && move.to === 'g1')) { 216 | originalPositionOfRook = 'h1' 217 | newPositionOfRook = 'f1' 218 | } else if ((move.from === 'e1' && move.to === 'c1')) { 219 | originalPositionOfRook = 'a1' 220 | newPositionOfRook = 'd1' 221 | } else if ((move.from === 'e8' && move.to === 'g8')) { 222 | originalPositionOfRook = 'h8' 223 | newPositionOfRook = 'f8' 224 | } else { // e8 to c8 225 | originalPositionOfRook = 'a8' 226 | newPositionOfRook = 'd8' 227 | } 228 | 229 | 230 | return { 231 | didCastle: true, 232 | x: this.toAlphabet2[originalPositionOfRook[0]], 233 | y: this.toCoord2[originalPositionOfRook[1]], 234 | to_x: this.toAlphabet2[newPositionOfRook[0]], 235 | to_y: this.toCoord2[newPositionOfRook[1]] 236 | } 237 | } 238 | 239 | 240 | isPawnPromotion(to, piece) { 241 | const res = piece === 'p' && (to[1] === 105 || to[1] === 735) 242 | if (res) { 243 | this.nQueens += 1 244 | } 245 | return res 246 | } 247 | 248 | 249 | toChessMove(finalPosition, to2D) { 250 | 251 | let move 252 | 253 | if (finalPosition[0] > 100) { 254 | move = this.toAlphabet[to2D[finalPosition[0]]] + this.toCoord[to2D[finalPosition[1]]] 255 | } else { 256 | move = this.toAlphabet[finalPosition[0]] + this.toCoord[finalPosition[1]] 257 | } 258 | 259 | // console.log("proposed move: " + move) 260 | return move 261 | } 262 | 263 | findPiece(board, pieceId) { 264 | // ChessBoard, String -> [Int, Int] 265 | // console.log("piecetofind: " + pieceId) 266 | for (var i = 0; i < 8; i++) { 267 | for (var j = 0; j < 8; j++) { 268 | if (board[i][j].getPieceIdOnThisSquare() === pieceId) { 269 | return [j, i] 270 | } 271 | } 272 | } 273 | } 274 | 275 | makeStartingBoard() { 276 | const backRank = ["rook", "knight", "bishop", "queen", "king", "bishop", "knight", "rook"] 277 | var startingChessBoard = [] 278 | for (var i = 0; i < 8; i++) { 279 | startingChessBoard.push([]) 280 | for (var j = 0; j < 8; j++) { 281 | // j is horizontal 282 | // i is vertical 283 | const coordinatesOnCanvas = [((j + 1) * 90 + 15), ((i + 1) * 90 + 15)] 284 | const emptySquare = new Square(j, i, null, coordinatesOnCanvas) 285 | 286 | startingChessBoard[i].push(emptySquare) 287 | } 288 | } 289 | const whiteBackRankId = ["wr1", "wn1", "wb1", "wq1", "wk1", "wb2", "wn2", "wr2"] 290 | const blackBackRankId = ["br1", "bn1", "bb1", "bq1", "bk1", "bb2", "bn2", "br2"] 291 | for (var j = 0; j < 8; j += 7) { 292 | for (var i = 0; i < 8; i++) { 293 | if (j == 0) { 294 | // top 295 | // console.log(backRank[i]) 296 | startingChessBoard[j][this.thisPlayersColorIsWhite ? i : 7 - i].setPiece(new ChessPiece(backRank[i], false, this.thisPlayersColorIsWhite ? "black" : "white", this.thisPlayersColorIsWhite ? blackBackRankId[i] : whiteBackRankId[i])) 297 | startingChessBoard[j + 1][this.thisPlayersColorIsWhite ? i : 7 - i].setPiece(new ChessPiece("pawn", false, this.thisPlayersColorIsWhite ? "black" : "white", this.thisPlayersColorIsWhite ? "bp" + i : "wp" + i)) 298 | } else { 299 | // bottom 300 | startingChessBoard[j - 1][this.thisPlayersColorIsWhite ? i : 7 - i].setPiece(new ChessPiece("pawn", false, this.thisPlayersColorIsWhite ? "white" : "black", this.thisPlayersColorIsWhite ? "wp" + i : "bp" + i)) 301 | startingChessBoard[j][this.thisPlayersColorIsWhite ? i : 7 - i].setPiece(new ChessPiece(backRank[i], false, this.thisPlayersColorIsWhite ? "white" : "black", this.thisPlayersColorIsWhite ? whiteBackRankId[i] : blackBackRankId[i])) 302 | } 303 | } 304 | } 305 | return startingChessBoard 306 | } 307 | } 308 | 309 | export default Game -------------------------------------------------------------------------------- /src/chess/model/chesspiece.js: -------------------------------------------------------------------------------- 1 | 2 | class ChessPiece { 3 | constructor(name, isAttacked, color, id) { 4 | this.name = name // string 5 | this.isAttacked = isAttacked // boolean 6 | this.color = color // string 7 | this.id = id // string 8 | } 9 | 10 | setSquare(newSquare) { 11 | // set the square this piece is sitting top of. 12 | // on any given piece (on the board), there will always be a piece on top of it. 13 | // console.log(newSquare) 14 | if (newSquare === undefined) { 15 | this.squareThisPieceIsOn = newSquare 16 | return 17 | } 18 | 19 | if (this.squareThisPieceIsOn === undefined) { 20 | this.squareThisPieceIsOn = newSquare 21 | newSquare.setPiece(this) 22 | } 23 | 24 | const isNewSquareDifferent = this.squareThisPieceIsOn.x != newSquare.x || this.squareThisPieceIsOn.y != newSquare.y 25 | 26 | if (isNewSquareDifferent) { 27 | // console.log("set") 28 | this.squareThisPieceIsOn = newSquare 29 | newSquare.setPiece(this) 30 | } 31 | } 32 | 33 | getSquare() { 34 | return this.squareThisPieceIsOn 35 | } 36 | } 37 | 38 | 39 | export default ChessPiece -------------------------------------------------------------------------------- /src/chess/model/square.js: -------------------------------------------------------------------------------- 1 | class Square { 2 | constructor(x, y, pieceOnThisSquare, canvasCoord) { 3 | this.x = x // Int 0 < x < 7 4 | this.y = y // Int 0 < y < 7 5 | this.canvasCoord = canvasCoord 6 | this.pieceOnThisSquare = pieceOnThisSquare // ChessPiece || null 7 | } 8 | 9 | setPiece(newPiece) { 10 | if (newPiece === null && this.pieceOnThisSquare === null) { 11 | return 12 | } else if (newPiece === null) { 13 | // case where the function caller wants to remove the piece that is on this square. 14 | this.pieceOnThisSquare.setSquare(undefined) 15 | this.pieceOnThisSquare = null 16 | } else if (this.pieceOnThisSquare === null) { 17 | // case where the function caller wants assign a new piece on this square 18 | this.pieceOnThisSquare = newPiece 19 | newPiece.setSquare(this) 20 | } else if (this.getPieceIdOnThisSquare() != newPiece.id && this.pieceOnThisSquare.color != newPiece.color) { 21 | // case where the function caller wants to change the piece on this square. (only different color allowed) 22 | console.log("capture!") 23 | this.pieceOnThisSquare = newPiece 24 | newPiece.setSquare(this) 25 | } else { 26 | return "user tried to capture their own piece" 27 | } 28 | } 29 | 30 | removePiece() { 31 | this.pieceOnThisSquare = null 32 | } 33 | 34 | getPiece() { 35 | return this.pieceOnThisSquare 36 | } 37 | 38 | getPieceIdOnThisSquare() { 39 | if (this.pieceOnThisSquare === null) { 40 | return "empty" 41 | } 42 | return this.pieceOnThisSquare.id 43 | } 44 | 45 | isOccupied() { 46 | return this.pieceOnThisSquare != null 47 | } 48 | 49 | getCoord() { 50 | return [this.x, this.y] 51 | } 52 | 53 | getCanvasCoord() { 54 | return this.canvasCoord 55 | } 56 | } 57 | 58 | export default Square -------------------------------------------------------------------------------- /src/chess/ui/chessgame.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Game from '../model/chess' 3 | import Square from '../model/square' 4 | import { Stage, Layer } from 'react-konva'; 5 | import Board from '../assets/chessBoard.png' 6 | import useSound from 'use-sound' 7 | import chessMove from '../assets/moveSoundEffect.mp3' 8 | import Piece from './piece' 9 | import piecemap from './piecemap' 10 | import { useParams } from 'react-router-dom' 11 | import { ColorContext } from '../../context/colorcontext' 12 | import VideoChatApp from '../../connection/videochat' 13 | const socket = require('../../connection/socket').socket 14 | 15 | 16 | class ChessGame extends React.Component { 17 | 18 | state = { 19 | gameState: new Game(this.props.color), 20 | draggedPieceTargetId: "", // empty string means no piece is being dragged 21 | playerTurnToMoveIsWhite: true, 22 | whiteKingInCheck: false, 23 | blackKingInCheck: false 24 | } 25 | 26 | 27 | componentDidMount() { 28 | console.log(this.props.myUserName) 29 | console.log(this.props.opponentUserName) 30 | // register event listeners 31 | socket.on('opponent move', move => { 32 | // move == [pieceId, finalPosition] 33 | // console.log("opponenet's move: " + move.selectedId + ", " + move.finalPosition) 34 | if (move.playerColorThatJustMovedIsWhite !== this.props.color) { 35 | this.movePiece(move.selectedId, move.finalPosition, this.state.gameState, false) 36 | this.setState({ 37 | playerTurnToMoveIsWhite: !move.playerColorThatJustMovedIsWhite 38 | }) 39 | } 40 | }) 41 | } 42 | 43 | startDragging = (e) => { 44 | this.setState({ 45 | draggedPieceTargetId: e.target.attrs.id 46 | }) 47 | } 48 | 49 | 50 | movePiece = (selectedId, finalPosition, currentGame, isMyMove) => { 51 | /** 52 | * "update" is the connection between the model and the UI. 53 | * This could also be an HTTP request and the "update" could be the server response. 54 | * (model is hosted on the server instead of the browser) 55 | */ 56 | var whiteKingInCheck = false 57 | var blackKingInCheck = false 58 | var blackCheckmated = false 59 | var whiteCheckmated = false 60 | const update = currentGame.movePiece(selectedId, finalPosition, isMyMove) 61 | 62 | if (update === "moved in the same position.") { 63 | this.revertToPreviousState(selectedId) // pass in selected ID to identify the piece that messed up 64 | return 65 | } else if (update === "user tried to capture their own piece") { 66 | this.revertToPreviousState(selectedId) 67 | return 68 | } else if (update === "b is in check" || update === "w is in check") { 69 | // change the fill of the enemy king or your king based on which side is in check. 70 | // play a sound or something 71 | if (update[0] === "b") { 72 | blackKingInCheck = true 73 | } else { 74 | whiteKingInCheck = true 75 | } 76 | } else if (update === "b has been checkmated" || update === "w has been checkmated") { 77 | if (update[0] === "b") { 78 | blackCheckmated = true 79 | } else { 80 | whiteCheckmated = true 81 | } 82 | } else if (update === "invalid move") { 83 | this.revertToPreviousState(selectedId) 84 | return 85 | } 86 | 87 | // let the server and the other client know your move 88 | if (isMyMove) { 89 | socket.emit('new move', { 90 | nextPlayerColorToMove: !this.state.gameState.thisPlayersColorIsWhite, 91 | playerColorThatJustMovedIsWhite: this.state.gameState.thisPlayersColorIsWhite, 92 | selectedId: selectedId, 93 | finalPosition: finalPosition, 94 | gameId: this.props.gameId 95 | }) 96 | } 97 | 98 | 99 | this.props.playAudio() 100 | 101 | // sets the new game state. 102 | this.setState({ 103 | draggedPieceTargetId: "", 104 | gameState: currentGame, 105 | playerTurnToMoveIsWhite: !this.props.color, 106 | whiteKingInCheck: whiteKingInCheck, 107 | blackKingInCheck: blackKingInCheck 108 | }) 109 | 110 | if (blackCheckmated) { 111 | alert("WHITE WON BY CHECKMATE!") 112 | } else if (whiteCheckmated) { 113 | alert("BLACK WON BY CHECKMATE!") 114 | } 115 | } 116 | 117 | 118 | endDragging = (e) => { 119 | const currentGame = this.state.gameState 120 | const currentBoard = currentGame.getBoard() 121 | const finalPosition = this.inferCoord(e.target.x() + 90, e.target.y() + 90, currentBoard) 122 | const selectedId = this.state.draggedPieceTargetId 123 | this.movePiece(selectedId, finalPosition, currentGame, true) 124 | } 125 | 126 | revertToPreviousState = (selectedId) => { 127 | /** 128 | * Should update the UI to what the board looked like before. 129 | */ 130 | const oldGS = this.state.gameState 131 | const oldBoard = oldGS.getBoard() 132 | const tmpGS = new Game(true) 133 | const tmpBoard = [] 134 | 135 | for (var i = 0; i < 8; i++) { 136 | tmpBoard.push([]) 137 | for (var j = 0; j < 8; j++) { 138 | if (oldBoard[i][j].getPieceIdOnThisSquare() === selectedId) { 139 | tmpBoard[i].push(new Square(j, i, null, oldBoard[i][j].canvasCoord)) 140 | } else { 141 | tmpBoard[i].push(oldBoard[i][j]) 142 | } 143 | } 144 | } 145 | 146 | // temporarily remove the piece that was just moved 147 | tmpGS.setBoard(tmpBoard) 148 | 149 | this.setState({ 150 | gameState: tmpGS, 151 | draggedPieceTargetId: "", 152 | }) 153 | 154 | this.setState({ 155 | gameState: oldGS, 156 | }) 157 | } 158 | 159 | 160 | inferCoord = (x, y, chessBoard) => { 161 | // console.log("actual mouse coordinates: " + x + ", " + y) 162 | /* 163 | Should give the closest estimate for new position. 164 | */ 165 | var hashmap = {} 166 | var shortestDistance = Infinity 167 | for (var i = 0; i < 8; i++) { 168 | for (var j = 0; j < 8; j++) { 169 | const canvasCoord = chessBoard[i][j].getCanvasCoord() 170 | // calculate distance 171 | const delta_x = canvasCoord[0] - x 172 | const delta_y = canvasCoord[1] - y 173 | const newDistance = Math.sqrt(delta_x**2 + delta_y**2) 174 | hashmap[newDistance] = canvasCoord 175 | if (newDistance < shortestDistance) { 176 | shortestDistance = newDistance 177 | } 178 | } 179 | } 180 | 181 | return hashmap[shortestDistance] 182 | } 183 | 184 | render() { 185 | // console.log(this.state.gameState.getBoard()) 186 | // console.log("it's white's move this time: " + this.state.playerTurnToMoveIsWhite) 187 | /* 188 | Look at the current game state in the model and populate the UI accordingly 189 | */ 190 | // console.log(this.state.gameState.getBoard()) 191 | 192 | return ( 193 | 194 |
199 | 200 | 201 | {this.state.gameState.getBoard().map((row) => { 202 | return ( 203 | {row.map((square) => { 204 | if (square.isOccupied()) { 205 | return ( 206 | ) 220 | } 221 | return 222 | })} 223 | ) 224 | })} 225 | 226 | 227 |
228 |
) 229 | } 230 | } 231 | 232 | 233 | 234 | const ChessGameWrapper = (props) => { 235 | /** 236 | * player 1 237 | * - socketId 1 238 | * - socketId 2 ??? 239 | * player 2 240 | * - socketId 2 241 | * - socketId 1 242 | */ 243 | 244 | 245 | 246 | // get the gameId from the URL here and pass it to the chessGame component as a prop. 247 | const domainName = 'http://localhost:3000' 248 | const color = React.useContext(ColorContext) 249 | const { gameid } = useParams() 250 | const [play] = useSound(chessMove); 251 | const [opponentSocketId, setOpponentSocketId] = React.useState('') 252 | const [opponentDidJoinTheGame, didJoinGame] = React.useState(false) 253 | const [opponentUserName, setUserName] = React.useState('') 254 | const [gameSessionDoesNotExist, doesntExist] = React.useState(false) 255 | 256 | React.useEffect(() => { 257 | socket.on("playerJoinedRoom", statusUpdate => { 258 | console.log("A new player has joined the room! Username: " + statusUpdate.userName + ", Game id: " + statusUpdate.gameId + " Socket id: " + statusUpdate.mySocketId) 259 | if (socket.id !== statusUpdate.mySocketId) { 260 | setOpponentSocketId(statusUpdate.mySocketId) 261 | } 262 | }) 263 | 264 | socket.on("status", statusUpdate => { 265 | console.log(statusUpdate) 266 | alert(statusUpdate) 267 | if (statusUpdate === 'This game session does not exist.' || statusUpdate === 'There are already 2 people playing in this room.') { 268 | doesntExist(true) 269 | } 270 | }) 271 | 272 | 273 | socket.on('start game', (opponentUserName) => { 274 | console.log("START!") 275 | if (opponentUserName !== props.myUserName) { 276 | setUserName(opponentUserName) 277 | didJoinGame(true) 278 | } else { 279 | // in chessGame, pass opponentUserName as a prop and label it as the enemy. 280 | // in chessGame, use reactContext to get your own userName 281 | // socket.emit('myUserName') 282 | socket.emit('request username', gameid) 283 | } 284 | }) 285 | 286 | 287 | socket.on('give userName', (socketId) => { 288 | if (socket.id !== socketId) { 289 | console.log("give userName stage: " + props.myUserName) 290 | socket.emit('recieved userName', {userName: props.myUserName, gameId: gameid}) 291 | } 292 | }) 293 | 294 | socket.on('get Opponent UserName', (data) => { 295 | if (socket.id !== data.socketId) { 296 | setUserName(data.userName) 297 | console.log('data.socketId: data.socketId') 298 | setOpponentSocketId(data.socketId) 299 | didJoinGame(true) 300 | } 301 | }) 302 | }, []) 303 | 304 | 305 | return ( 306 | 307 | {opponentDidJoinTheGame ? ( 308 |
309 |

Opponent: {opponentUserName}

310 |
311 | 316 | 322 |
323 |

You: {props.myUserName}

324 |
325 | ) : gameSessionDoesNotExist ? ( 326 |
327 |

:(

328 |
329 | ) : ( 330 |
331 |

337 | Hey {props.myUserName}, copy and paste the URL 338 | below to send to your friend: 339 |

340 | 349 |

350 | 351 |

352 | {" "} 353 | Waiting for other opponent to join the game...{" "} 354 |

355 |
356 | )} 357 |
358 | ); 359 | }; 360 | 361 | export default ChessGameWrapper 362 | -------------------------------------------------------------------------------- /src/chess/ui/piece.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Image } from 'react-konva'; 3 | import useImage from 'use-image' 4 | 5 | const Piece = (props) => { 6 | const choiceOfColor = props.isWhite ? 0 : 1 7 | const [image] = useImage(props.imgurls[choiceOfColor]); 8 | const isDragged = props.id === props.draggedPieceTargetId 9 | 10 | const canThisPieceEvenBeMovedByThisPlayer = props.isWhite === props.thisPlayersColorIsWhite 11 | const isItThatPlayersTurn = props.playerTurnToMoveIsWhite === props.thisPlayersColorIsWhite 12 | 13 | const thisWhiteKingInCheck = props.id === "wk1" && props.whiteKingInCheck 14 | const thisBlackKingInCheck = props.id === "bk1" && props.blackKingInCheck 15 | 16 | 17 | // console.log("this piece ID:" + props.thisPieceTargetId) 18 | // console.log("dragged piece ID:" + props.draggedPieceTargetId) 19 | return ; 30 | }; 31 | 32 | export default Piece -------------------------------------------------------------------------------- /src/chess/ui/piecemap.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'pawn': ['https://upload.wikimedia.org/wikipedia/commons/0/04/Chess_plt60.png', 'https://upload.wikimedia.org/wikipedia/commons/c/cd/Chess_pdt60.png'], 3 | 'knight':['https://upload.wikimedia.org/wikipedia/commons/2/28/Chess_nlt60.png','https://upload.wikimedia.org/wikipedia/commons/f/f1/Chess_ndt60.png'], 4 | 'bishop':['https://upload.wikimedia.org/wikipedia/commons/9/9b/Chess_blt60.png','https://upload.wikimedia.org/wikipedia/commons/8/81/Chess_bdt60.png'], 5 | 'king':['https://upload.wikimedia.org/wikipedia/commons/3/3b/Chess_klt60.png','https://upload.wikimedia.org/wikipedia/commons/e/e3/Chess_kdt60.png'], 6 | 'queen':['https://upload.wikimedia.org/wikipedia/commons/4/49/Chess_qlt60.png','https://upload.wikimedia.org/wikipedia/commons/a/af/Chess_qdt60.png'], 7 | 'rook':['https://upload.wikimedia.org/wikipedia/commons/5/5c/Chess_rlt60.png','https://upload.wikimedia.org/wikipedia/commons/a/a0/Chess_rdt60.png'] 8 | } -------------------------------------------------------------------------------- /src/connection/socket.js: -------------------------------------------------------------------------------- 1 | import io from 'socket.io-client' 2 | 3 | const URL = 'http://localhost:8000' 4 | 5 | const socket = io(URL) 6 | 7 | var mySocketId 8 | // register preliminary event listeners here: 9 | 10 | 11 | socket.on("createNewGame", statusUpdate => { 12 | console.log("A new game has been created! Username: " + statusUpdate.userName + ", Game id: " + statusUpdate.gameId + " Socket id: " + statusUpdate.mySocketId) 13 | mySocketId = statusUpdate.mySocketId 14 | }) 15 | 16 | export { 17 | socket, 18 | mySocketId 19 | } 20 | -------------------------------------------------------------------------------- /src/connection/videochat.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef } from 'react'; 2 | import Peer from "simple-peer"; 3 | import styled from "styled-components"; 4 | const socket = require('../connection/socket').socket 5 | 6 | 7 | const Container = styled.div` 8 | height: 100vh; 9 | width: 100%; 10 | flex-direction: column; 11 | `; 12 | 13 | const Row = styled.div` 14 | width: 100%; 15 | `; 16 | 17 | const Video = styled.video` 18 | border: 1px solid blue; 19 | `; 20 | 21 | function VideoChatApp(props) { 22 | /** 23 | * initial state: both player is neutral and have the option to call each other 24 | * 25 | * player 1 calls player 2: Player 1 should display: 'Calling {player 2 username},' and the 26 | * 'CallPeer' button should disappear for Player 1. 27 | * Player 2 should display '{player 1 username} is calling you' and 28 | * the 'CallPeer' button for Player 2 should also disappear. 29 | * 30 | * Case 1: player 2 accepts call - the video chat begins and there is no button to end it. 31 | * 32 | * Case 2: player 2 ignores player 1 call - nothing happens. Wait until the connection times out. 33 | * 34 | */ 35 | 36 | const [stream, setStream] = useState(); 37 | const [receivingCall, setReceivingCall] = useState(false); 38 | const [caller, setCaller] = useState(""); 39 | const [callerSignal, setCallerSignal] = useState(); 40 | const [callAccepted, setCallAccepted] = useState(false); 41 | const [isCalling, setIsCalling] = useState(false) 42 | const userVideo = useRef(); 43 | const partnerVideo = useRef(); 44 | 45 | useEffect(() => { 46 | navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(stream => { 47 | setStream(stream); 48 | if (userVideo.current) { 49 | userVideo.current.srcObject = stream; 50 | } 51 | }) 52 | 53 | socket.on("hey", (data) => { 54 | setReceivingCall(true); 55 | setCaller(data.from); 56 | setCallerSignal(data.signal); 57 | }) 58 | }, []); 59 | 60 | function callPeer(id) { 61 | setIsCalling(true) 62 | const peer = new Peer({ 63 | initiator: true, 64 | trickle: false, 65 | stream: stream, 66 | }); 67 | 68 | peer.on("signal", data => { 69 | socket.emit("callUser", { userToCall: id, signalData: data, from: props.mySocketId}) 70 | }) 71 | 72 | peer.on("stream", stream => { 73 | if (partnerVideo.current) { 74 | partnerVideo.current.srcObject = stream; 75 | } 76 | }); 77 | 78 | socket.on("callAccepted", signal => { 79 | setCallAccepted(true); 80 | peer.signal(signal); 81 | }) 82 | 83 | } 84 | 85 | function acceptCall() { 86 | setCallAccepted(true); 87 | setIsCalling(false) 88 | const peer = new Peer({ 89 | initiator: false, 90 | trickle: false, 91 | stream: stream, 92 | }); 93 | peer.on("signal", data => { 94 | socket.emit("acceptCall", { signal: data, to: caller }) 95 | }) 96 | 97 | peer.on("stream", stream => { 98 | partnerVideo.current.srcObject = stream; 99 | }); 100 | 101 | peer.signal(callerSignal); 102 | } 103 | 104 | let UserVideo; 105 | if (stream) { 106 | UserVideo = ( 107 |