diff options
-rw-r--r-- | draughts/app.js | 40 | ||||
-rw-r--r-- | draughts/environment.js | 19 | ||||
-rw-r--r-- | draughts/game.js | 285 | ||||
-rw-r--r-- | draughts/piece.js | 35 | ||||
-rw-r--r-- | draughts/public/game.html | 1 | ||||
-rw-r--r-- | draughts/public/images/position.svg | 3 | ||||
-rw-r--r-- | draughts/public/javascripts/game.js | 124 | ||||
-rw-r--r-- | draughts/public/javascripts/messages.js | 8 | ||||
-rw-r--r-- | draughts/public/stylesheets/game.css | 5 |
9 files changed, 480 insertions, 40 deletions
diff --git a/draughts/app.js b/draughts/app.js index fea0287..88b6b61 100644 --- a/draughts/app.js +++ b/draughts/app.js @@ -3,8 +3,9 @@ const WebSocket = require("ws") const Express = require("express") const Game = require("./game") +const Messages = require("./public/javascripts/messages") +const Environment = require("./environment") const indexRouter = require("./routes/index") -const env = require("./environment") /* Get the server port, if the user doesn't provide one then print an error to STDERR and exit */ const port = process.argv[2] @@ -23,19 +24,25 @@ app.use(Express.static(__dirname + "/public")) const server = Http.createServer(app) const wss = new WebSocket.Server({ server }) +/* Initialize the environment */ +let env = new Environment() + wss.on("connection", ws => { /* When a new client connects, get the first game whos status is pending and add the client to * that game. If there is no such game, then create a new one which will wait for a second client * to join. */ - let game = env.games.filter(g => g.ongoing)[0] + let game = env.games.filter(g => !g.ongoing)[0] if (!game) { game = new Game(env.games.length) /* The number of games servers as a unique game ID */ env.games.push(game) game.bluePlayer = ws + game.messageClient({ head: Messages.WELCOME, body: "b" }, ws) } else { game.redPlayer = ws - game.messageAll("START") + game.ongoing = true + game.messageClient({ head: Messages.WELCOME, body: "r" }, ws) + game.nextTurn() } /* When a client disconnects, check if the game they were part of was ongoing (the game could @@ -45,8 +52,31 @@ wss.on("connection", ws => { */ ws.on("close", () => { if (game.ongoing) - (ws == game.bluePlayer ? game.redPlayer : game.bluePlayer).send("DISCONNECT") - env.games = env.games.filter(g => g != game) + game.messageOpponent({ head: Messages.DISCONNECT }, ws) + env.removeGame(game) + }) + + ws.on("message", msg => { + msg = JSON.parse(msg) + switch (msg.head) { + case Messages.RESIGN: + if (game.ongoing) + game.messageOpponent(msg, ws) + env.removeGame(game) + break + case Messages.MOVED: + game.messageOpponent({ + head: Messages.MOVED, + body: { + id: msg.body.id, + position: msg.body.new, + captures: msg.body.captures + } + }, ws) + game.move(msg.body) + game.nextTurn() + break + } }) }) diff --git a/draughts/environment.js b/draughts/environment.js index a3ca053..7f1e997 100644 --- a/draughts/environment.js +++ b/draughts/environment.js @@ -1,11 +1,16 @@ -/* An object representing the complete environment. It holds all the games as well as some statistics +/* A class representing the complete environment. It holds all the games as well as some statistics * to be displayed on the splash screen. */ -const environment = { - minimumMoves: Infinity, - averageMoves: Infinity, - totalGames: 0, - games: [] +const Environment = function() { + this.minimumMoves = Infinity, + this.averageMoves = Infinity, + this.totalGames = 0, + this.games = [] } -module.exports = environment +/* Function to remove a game from the environment */ +Environment.prototype.removeGame = function(game) { + this.games = this.games.filter(g => g != game) +} + +module.exports = Environment diff --git a/draughts/game.js b/draughts/game.js index a2ae886..aa0bb6c 100644 --- a/draughts/game.js +++ b/draughts/game.js @@ -2,28 +2,167 @@ const WebSocket = require("ws") const Color = require("./color") const Piece = require("./piece") +const Messages = require("./public/javascripts/messages") -/* Initialize and return new 10x10 draughts board */ +/* + * Signature: + * () => int[10][10] + * + * Description: + * Initialize and return new 10x10 draughts board + */ const boardInit = () => [ /* Initialize the blue pieces */ - Array.from({ length: 10 }, (_, i) => (i & 1) ? null : new Piece(i, 0, i >> 1, Color.BLUE)), - Array.from({ length: 10 }, (_, i) => (i & 1) ? new Piece(i, 1, (i >> 1) + 5, Color.BLUE) : null), - Array.from({ length: 10 }, (_, i) => (i & 1) ? null : new Piece(i, 2, (i >> 1) + 10, Color.BLUE)), - Array.from({ length: 10 }, (_, i) => (i & 1) ? new Piece(i, 3, (i >> 1) + 15, Color.BLUE) : null), + Array.from({ length: 10 }, (_, i) => (i & 1) ? new Piece(i, 0, i >> 1, Color.BLUE) : null), + Array.from({ length: 10 }, (_, i) => (i & 1) ? null : new Piece(i, 1, (i >> 1) + 5, Color.BLUE)), + Array.from({ length: 10 }, (_, i) => (i & 1) ? new Piece(i, 2, (i >> 1) + 10, Color.BLUE) : null), + Array.from({ length: 10 }, (_, i) => (i & 1) ? null : new Piece(i, 3, (i >> 1) + 15, Color.BLUE)), /* Initialize the empty middle rows */ Array.from({ length: 10 }, () => null), Array.from({ length: 10 }, () => null), /* Initialize the red pieces */ - Array.from({ length: 10 }, (_, i) => (i & 1) ? null : new Piece(1, 6, i >> 1, Color.RED)), - Array.from({ length: 10 }, (_, i) => (i & 1) ? new Piece(1, 7, (i >> 1) + 5, Color.RED) : null), - Array.from({ length: 10 }, (_, i) => (i & 1) ? null : new Piece(1, 8, (i >> 1) + 10, Color.RED)), - Array.from({ length: 10 }, (_, i) => (i & 1) ? new Piece(1, 9, (i >> 1) + 15, Color.RED) : null), + Array.from({ length: 10 }, (_, i) => (i & 1) ? new Piece(i, 6, i >> 1, Color.RED) : null), + Array.from({ length: 10 }, (_, i) => (i & 1) ? null : new Piece(i, 7, (i >> 1) + 5, Color.RED)), + Array.from({ length: 10 }, (_, i) => (i & 1) ? new Piece(i, 8, (i >> 1) + 10, Color.RED) : null), + Array.from({ length: 10 }, (_, i) => (i & 1) ? null : new Piece(i, 9, (i >> 1) + 15, Color.RED)), ] -/* A class representing a game. It contains the current game state as well as the complete move - * history and the websockets of both players. +/* + * Signature: + * (Any[][]) => Any[][] + * + * Description: + * Create and return a deep copy of the given 2D array. This is used to create deep copies of the + * draughts board so that other functions such as `capturingMoves()' don't modify the original by + * accident. + */ +const deepCopy = m => m.map(r => r.slice()) + +/* + * Signature: + * (Number, Number) => Boolean + * + * Description: + * Check if the given pair of coordinates are within the bounds of a draughts board. In other + * words this function returns `true' if the given coordinates fit in a 10x10 grid and `false' + * otherwise. + */ +const inBounds = (x, y) => x >= 0 && x <= 9 && y >= 0 && y <= 9 + +/* + * Signature: + * (Piece, Piece[][]) => { x: Number, y: Number, captures: [] }[] + * + * Description: + * Find and return all of the valid moves that the piece `p' can make on the board `board' which + * do not capture any pieces. The moves are returned as an array of objects where each object has + * an `x', `y', and `captures' field. The `x' and `y' fields specify the new location of the + * piece `p' on the board if the user chooses to make the move. The `captures' field is unused in + * this function so an empty array is assigned to it. + */ +const nonCapturingMoves = + /* Get all the possible moves we can make by calling the `Piece.nonCapturingMoves()' method. + * This will return an array of coordinates that the piece `p' can move to. Then filter the + * returned moves so that only valid ones remain. + * + * A move is considered valid if it satisfies the following conditions: + * - The end position of the piece `p' is in bounds + * - The square where our piece ends up in is not occupied + * + * After performing the filter, a map is performed to turn the arrays into objects of the + * correct format so that they can be processed by the calling function. + */ + (p, board) => p.nonCapturingMoves() + .filter(([x, y]) => inBounds(x, y) && !board[y][x]) + .map(([x, y]) => { return { x: x, y: y, captures: [] } }) + +/** + * Signature: + * (Piece, { x: Number, y: Number, captures: Piece[] }[], Piece[][]) => { x: Number, y: Number, + * captures: Piece[] }[] + * + * Description: + * Recursively find and return all of the valid moves that the piece `p' can make on the board + * `board' which captures atleast one piece. The moves are returned in the same format as they + * are in the `nonCapturingMoves()' function except this time the `captures' field is actually + * used. The `captures' field is an array of Piece objects which will be captured in the case + * that the user chooses to make the move. + */ +const capturingMoves = (p, captures, board) => { + /* Get all the possible moves we can make. This is done by calling `p.capturingMoves()' to get + * all the moves that the piece `p' could make if it were to capture another piece, and then + * filter the result to keep only moves that are legal. + * + * The filter checks the following properties: + * - The end position of the piece `p' is in bounds + * - The square we skip over actually had a piece to capture + * - The piece we captured is of the opposing player + * - The square where our piece ends up in is not occupied + */ + const moves = p.capturingMoves() + .filter(({ landed, skipped }) => inBounds(landed.x, landed.y) + && board[skipped.y][skipped.x] + && !board[skipped.y][skipped.x].sameTeam(p) + && !board[landed.y][landed.x]) + + /* Check to see if any valid moves were found. If no moves were found then we have reached the + * end of our chain of captures, or in other words, we have reached the algorithms base case. In + * this case we can simply return the move with the current positions of the piece `p' and the + * captures in `captures'. There is one special case we need to handle though, and that is when + * the captures array is empty. This can only occur if no valid moves have been found and we have + * not yet made any recursive calls. In this scenario we return an empty array (so nothing). If + * we don't handle this case properly then the user will be able to move a piece to the square it + * is already on. + */ + if (moves.length == 0) + return captures.length > 0 + ? [{ x: p.position.x, y: p.position.y, captures: captures }] + : [] + + /* Now we get to the fun part. For each of the moves in `moves' we first concatenate the piece + * that the move would have captured with the `captures' array and store the result in a new + * array called `c'. We then set the newly captured piece in the board to `null' so that future + * recursive calls don't try to capture the piece after it has already been captured. We then + * update the position of the piece `p' to it's new position after performing the capture. + * Finally we recursively call the function with `p', `c', and `board' which will return to us an + * array of possible moves. These arrays are then all accumulated in an accumulator and returned. + */ + return moves.reduce((acc, { landed, skipped }) => { + const c = captures.concat(board[skipped.y][skipped.x]) + board[skipped.y][skipped.x] = null + p.position = landed + return acc.concat(capturingMoves(p, c, board)) + }, []) +} + +/* + * Signature: + * (Piece, Piece[][]) => { x: Number, y: Number, captures: Piece[] }[] + * + * Description: + * A wrapper function around `nonCapturingMoves()' and `capturingMoves()' which simply calls the + * two and concatinates the results together into a single array. The `capturingMoves()' function + * is given a brand new instance of `Piece' instead of just taking `p' as an argument as we want + * to avoid making any changes to `p'. + */ +const calculateMoves = (p, board) => nonCapturingMoves(p, board).concat( + capturingMoves(new Piece(p.position.x, p.position.y, p.id, p.color), [], deepCopy(board))) + +/* + * Signature: + * this.id :: Number + * this.board :: Piece[][] + * this.ongoing :: Boolean + * this.bluePlayer :: WebSocket + * this.redPlayer :: WebSocket + * this.currentTurn :: Color + * this.history :: { blue: String, red: String }[] + * + * Description: + * A class representing a game. It contains the current game state as well as the complete move + * history and the websockets of both players. */ const Game = function(gameID) { this.id = gameID @@ -31,10 +170,128 @@ const Game = function(gameID) { this.ongoing = false this.bluePlayer = null this.redPlayer = null - this.history = [] /* [ { blue: "42x31", red: "19-24" }, ... ] */ + this.currentTurn = null + this.history = [] } -/* Send the message `msg' to both players */ -Game.prototype.messageAll = msg => [this.bluePlayer, this.redPlayer].forEach(p => p.send(msg)) +/* + * Signature: + * ({ head: Message, body: Any }) => Nothing + * + * Description: + * Send the message `msg' to both of the players. `msg' is an object where the first attribute + * (the head) is a `Message' and the second attribute (the body) can be anything you want to + * send. + */ +Game.prototype.messageAll = function(msg) { + [this.bluePlayer, this.redPlayer].forEach(p => p.send(JSON.stringify(msg))) +} + +/* + * Signature: + * ({ head: Message, body: Any }, WebSocket) => Nothing + * + * Description: + * Send the message `msg' to the opponent of the player specified by the websocket `ws'. + */ +Game.prototype.messageOpponent = function(msg, ws) { + (ws == this.bluePlayer ? this.redPlayer : this.bluePlayer).send(JSON.stringify(msg)) +} + +/* + * Signature: + * ({ head: Message, body: Any }, WebSocket) => Nothing + * + * Description: + * Send the message `msg' to the player specified by the websocket `ws'. + */ +Game.prototype.messageClient = function(msg, ws) { + ws.send(JSON.stringify(msg)) +} + +/* + * Signature: + * (Nothing) => { x: Number, y: Number, captures: Piece[] }[] + * + * Description: + * Return all of the moves that can legally be made on the current turn of the game. The format + * for the output object is the same as previously described for the `nonCapturingMoves()' and + * `capturingMoves()' functions. + */ +Game.prototype.legalMoves = function() { + /* Initialize an array of length 20 containing a bunch of empty arrays as elements. Each slot of + * the array corresponds to one of a players 20 pieces. When indexed as `moves[N]', the returned + * array will contain all of the moves that can legally be made by the piece with the ID `N'. + */ + let moves = Array.from({ length: 20 }, () => []) + + /* Calculate all of the possible moves for each piece and store them in the `moves' array. This + * is done by first flattening the game board from a 2D array of `Piece' objects to a 1D array. + * We then filter the array so that we only get the pieces that correspond to the player who's + * turn it is. Once we have filtered out everything we don't need, we calculate the moves for + * each of our pieces and store the result in the `moves' array. + */ + this.board + .flat() + .filter(p => p && p.color == this.currentTurn) + .forEach(p => moves[p.id] = calculateMoves(p, this.board)) + + /* Calculate the maximum number of captures that can be made in a single move. Like we did above, + * we first flatten the board so that it is 1D. Then we use a reduction to iterate over all of + * the moves and store the maximum number of captures in the accumulator `acc'. + */ + const maxCaptures = moves.flat().reduce((acc, o) => Math.max(acc, o.captures.length), 0) + + /* Accumulate an array of coordinates of valid moves. This is done by performing a reduction on + * the `moves' array where we iterate over each pieces array of valid moves. Then for each array + * of valid moves we filter out all the moves that do not make the maximum number of captures and + * concatenate the result to the accumuator array `acc'. + */ + return moves.reduce( + (acc, arr, i) => acc.concat([arr.filter(m => m.captures.length == maxCaptures)]), []) +} + +/* + * Signature: + * (Nothing) => Nothing + * + * Description: + * Initiate the next turn of the game. This is done by swapping the player that the `currentTurn' + * attribute is assigned to and sending the `Messages.COMMENCE' message to the player who's turn + * has just begun. In the body of the message we include all the legal moves that the player can + * perform. + */ +Game.prototype.nextTurn = function() { + /* When the game starts for the first time, `this.currentTurn' will be null, so we let blue make + * the first move. + */ + if (this.currentTurn == null) + this.currentTurn = Color.BLUE + else + this.currentTurn = this.currentTurn == Color.BLUE ? Color.RED : Color.BLUE + + this.messageClient({ head: Messages.COMMENCE, body: this.legalMoves() }, + this.currentTurn == Color.BLUE ? this.bluePlayer : this.redPlayer) +} + +/* + * Signature: + * ({ old: { x: Number, y: Number }, + * new: { x: Number, y: Number }, + * captures: Piece[] }) => Nothing + * + * Description: + * Update the games board with the move that was just sent from the client in the message `msg'. + * The message only has 3 fields that matter to us, the `old', `new', and `captures' fields. The + * `old' and `new' fields contain the old and new positions of the piece that was moved. The + * `captures' field holds an array of `Piece' objects that were captures and need to be removed + * from the game. + */ +Game.prototype.move = function(msg) { + this.board[msg.new.y][msg.new.x] = this.board[msg.old.y][msg.old.x] + this.board[msg.old.y][msg.old.x] = null + this.board[msg.new.y][msg.new.x].position = msg.new + msg.captures.forEach(c => this.board[c.position.y][c.position.x] = null) +} module.exports = Game diff --git a/draughts/piece.js b/draughts/piece.js index 4635aee..f8aaf09 100644 --- a/draughts/piece.js +++ b/draughts/piece.js @@ -1,23 +1,42 @@ +const Color = require("./color") + /* A class representing a piece on a draughts board. It has a position within the board, a color, an * ID which corresponds to its ID in the HTML DOM, and whether or not its a kinged piece. */ -const Piece = function(x, y, color, id) { +const Piece = function(x, y, id, color) { this.position = { x: x, y: y } - this.color = color this.id = id + this.color = color this.isKing = false } -/* Move the given piece to the coordinates specified by `direction' */ -Piece.prototype.movePiece = direction => { - let x = direction[0] - let y = direction[1] +/* Return all of the potential non-capturing moves that the piece can make */ +Piece.prototype.nonCapturingMoves = function() { + return this.color == Color.BLUE + ? [[this.position.x + 1, this.position.y + 1], + [this.position.x - 1, this.position.y + 1]] + : [[this.position.x + 1, this.position.y - 1], + [this.position.x - 1, this.position.y - 1]] +} + +Piece.prototype.capturingMoves = function() { + return [ + { skipped: { x: this.position.x + 1, y: this.position.y + 1 }, + landed: { x: this.position.x + 2, y: this.position.y + 2 } }, + { skipped: { x: this.position.x - 1, y: this.position.y + 1 }, + landed: { x: this.position.x - 2, y: this.position.y + 2 } }, + { skipped: { x: this.position.x + 1, y: this.position.y - 1 }, + landed: { x: this.position.x + 2, y: this.position.y - 2 } }, + { skipped: { x: this.position.x - 1, y: this.position.y - 1 }, + landed: { x: this.position.x - 2, y: this.position.y - 2 } } + ] +} - if (x <= 9 && x >= 0 && y <= 9 && y >= 0) - this.position = direction +Piece.prototype.sameTeam = function(other) { + return this.color == other.color } module.exports = Piece diff --git a/draughts/public/game.html b/draughts/public/game.html index 726007a..f49df7d 100644 --- a/draughts/public/game.html +++ b/draughts/public/game.html @@ -6,6 +6,7 @@ <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" type="text/css" href="stylesheets/game.css" /> + <script src="javascripts/messages.js" defer></script> <script src="javascripts/game.js" defer></script> <title>Draughts</title> </head> diff --git a/draughts/public/images/position.svg b/draughts/public/images/position.svg new file mode 100644 index 0000000..f1dcd8c --- /dev/null +++ b/draughts/public/images/position.svg @@ -0,0 +1,3 @@ +<svg version="1.1" width="10" height="10" xmlns="http://www.w3.org/2000/svg"> + <circle cx="5" cy="5" r="1.5" fill="grey" /> +</svg> diff --git a/draughts/public/javascripts/game.js b/draughts/public/javascripts/game.js index ede6459..ffaf246 100644 --- a/draughts/public/javascripts/game.js +++ b/draughts/public/javascripts/game.js @@ -1,4 +1,122 @@ -//const ws = new WebSocket("ws://localhost:3000") -//ws.addEventListener("open", () => console.log("We are connected!")) +/* Initialize a new web socket to connect to the server */ +const ws = new WebSocket("ws://localhost:3000") -document.getElementById("resign").addEventListener("click", () => ws.send("RESIGN")) +/* Define some variables */ +let legalMoves, // Array of moves :: Moves that each piece can legally make + ourTurn, // Boolean :: Is it currently our turn? + selectedPiece, // Piece :: The current HTML piece we have selected + colorPrefix, // Character :: Either 'b' or 'r' depending on the clients side + opponentPrefix // Character :: The inverse of `colorPrefix' + +/* Remove all the position markers from the board */ +const removeMarkers = () => Array + .from(document.getElementsByClassName("position")) // Obtain the markers + .forEach(pos => pos.remove()) // Remove them all + +/* Add a position marker to the position specified by the given coordinates */ +const addMarker = ({ x, y, captures }) => { + /* Create and add the marker */ + let img = document.createElement("img") // Create a new image element + img.className = "piece position" // Add it to the correct classes + img.style.transform = `translate(${x * 100}%, ${y * 100}%)` // Move it to the correct position + img.setAttribute("src", "images/position.svg") // Assign the actual image to it + document.querySelector("#board-container").append(img) // Add it to the board container + + /* Add an event listener to move the selected piece when the marker is clicked */ + img.addEventListener("click", () => { + /* Grab the old positions of the piece */ + let [ox, oy] = selectedPiece + .style // Get the style attributes + .transform // Get the transformation style attribute + .match(/[0-9]+/g) // Get the percentage offsets in the translation + .map(n => n / 100) // Divide them by 100 to get board indicies + + /* Get the ID of the selected piece so we can include it in the message to the server */ + let id = selectedPiece.id.slice(1) + + captures.forEach(p => document.getElementById(opponentPrefix + p.id).remove()) + + /* Move the selected piece, unselect it, remove the markers, and end our turn */ + selectedPiece.style.transform = img.style.transform + selectedPiece = null + ourTurn = false + removeMarkers(); + + /* Inform the server that we have moved a piece, and tell it where the piece used to be and + * where it has moved to + */ + ws.send(JSON.stringify({ + head: Messages.MOVED, + body: { + id: id, + old: { x: ox, y: oy }, + new: { x: x, y: y }, + captures: captures + } + })) + }) +} + +const movePiece = ({ id, position, captures }) => { + document.getElementById(opponentPrefix + id).style.transform = + `translate(${position.x * 100}%, ${position.y * 100}%)` + captures.forEach(p => document.getElementById(colorPrefix + p.id).remove()) +} + +const setupPieceEventListeners = () => { + Array + .from(document.getElementsByClassName("piece")) // Get an array of pieces on the HTML board + .filter(p => p.id[0] == colorPrefix) // Get only the pieces of our own color + .forEach(p => p.addEventListener("click", () => { // Add an event listener to each piece + /* If it's not our turn, do nothing */ + if (!ourTurn) + return + + /* Remove any existing position markers from the board */ + removeMarkers() + + /* If we clicked on a piece that was not previously selected, we add the position markers to + * indicate where that piece can legally move to. If it was the piece that was already + * selected however, then do nothing, simply unselect the piece. + */ + if (selectedPiece != p) { + legalMoves[p.id.slice(1)].forEach(addMarker) + selectedPiece = p + } else + selectedPiece = null + })) +} + +/* Listen for a message from the client, and perform a different action based on the message. + * The `public/javascripts/game.js' file explains what each of the message headers mean. + */ +ws.addEventListener("message", ({ data }) => { + data = JSON.parse(data) + switch (data.head) { + case Messages.WELCOME: + colorPrefix = data.body + opponentPrefix = colorPrefix == "b" ? "r" : "b" + setupPieceEventListeners() + break + case Messages.RESIGN: + console.log("The opponent has resigned, you win!") + break + case Messages.DISCONNECT: + console.log("The opponent has disconnected, you win!") + break + case Messages.COMMENCE: + ourTurn = true + legalMoves = data.body + break + case Messages.MOVED: + movePiece(data.body) + break + } +}) + +/* Add an event listener to the resign button which will inform the server about when we have + * resigned the game + */ +document + .getElementById("resign") + .addEventListener("click", () => ws.send(JSON.stringify({ head: Messages.RESIGN }))) diff --git a/draughts/public/javascripts/messages.js b/draughts/public/javascripts/messages.js index 7f70576..94837d8 100644 --- a/draughts/public/javascripts/messages.js +++ b/draughts/public/javascripts/messages.js @@ -1,5 +1,7 @@ (function (exports) { - exports.START = 0 // Server -> Client :: Inform the client that the game has started - exports.RESIGN = 1 // Server <-> Client :: Inform the server/opponent that the client resigned - exports.DISCONNECT = 2 // Server -> Client :: Inform the client that the opponent disconnected + exports.COMMENCE = 0 // S --> C :: Inform the client that they may commence their turn + exports.RESIGN = 1 // S <-> C :: Inform the server/opponent that the client resigned + exports.DISCONNECT = 2 // S --> C :: Inform the client that the opponent disconnected + exports.MOVED = 3 // S <-> C :: Inform the server/opponent that the client has moved a piece + exports.WELCOME = 4 // S --> C :: Inform the client of their color when they join the game })(typeof(exports) == "undefined" ? (this.Messages = {}) : exports) diff --git a/draughts/public/stylesheets/game.css b/draughts/public/stylesheets/game.css index fe2fbaa..e636b92 100644 --- a/draughts/public/stylesheets/game.css +++ b/draughts/public/stylesheets/game.css @@ -199,3 +199,8 @@ body > div { opacity: 0.7; height: 100%; } + +.position:hover { + cursor: click; + background-color: grey; +} |