aboutsummaryrefslogtreecommitdiff
path: root/draughts
diff options
context:
space:
mode:
authorThomas Voss <thomasvoss@live.com> 2022-01-13 00:36:47 +0100
committerThomas Voss <thomasvoss@live.com> 2022-01-13 00:36:47 +0100
commitcac180626f278bbe328783a6db2d1b99a547df13 (patch)
tree63a8d0a2399a38d86c1d2c697087d56e22a50c0e /draughts
parent0d9018ab38542291161b3d1d8078ed52ea58cba2 (diff)
Basically make the whole game work
Too many changes to write a commit message, just read the code.
Diffstat (limited to 'draughts')
-rw-r--r--draughts/app.js40
-rw-r--r--draughts/environment.js19
-rw-r--r--draughts/game.js285
-rw-r--r--draughts/piece.js35
-rw-r--r--draughts/public/game.html1
-rw-r--r--draughts/public/images/position.svg3
-rw-r--r--draughts/public/javascripts/game.js124
-rw-r--r--draughts/public/javascripts/messages.js8
-rw-r--r--draughts/public/stylesheets/game.css5
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;
+}