diff options
-rw-r--r-- | draughts/app.js | 13 | ||||
-rw-r--r-- | draughts/game.js | 89 | ||||
-rw-r--r-- | draughts/piece.js | 56 | ||||
-rw-r--r-- | draughts/public/images/bluepiece-king.svg | 16 | ||||
-rw-r--r-- | draughts/public/images/redpiece-king.svg | 16 | ||||
-rw-r--r-- | draughts/public/javascripts/game.js | 22 |
6 files changed, 161 insertions, 51 deletions
diff --git a/draughts/app.js b/draughts/app.js index 11a01f0..fc0b273 100644 --- a/draughts/app.js +++ b/draughts/app.js @@ -17,12 +17,10 @@ if (!port) { /* Initialize the routes */ const app = Express() - -app.set("view engine", "ejs"); -app.use(Express.static(__dirname + "/public")); - -app.get("/play", indexRouter); -app.get("/", indexRouter); +app.set("view engine", "ejs") +app.use(Express.static(__dirname + "/public")) +app.get("/", indexRouter) +app.get("/play", indexRouter) /* Initialize the server and websocket server */ @@ -75,7 +73,8 @@ wss.on("connection", ws => { body: { id: msg.body.id, position: msg.body.new, - captures: msg.body.captures + captures: msg.body.captures, + king: msg.body.king } }, ws) game.move(msg.body) diff --git a/draughts/game.js b/draughts/game.js index d034746..067cb9a 100644 --- a/draughts/game.js +++ b/draughts/game.js @@ -53,14 +53,44 @@ const inBounds = (x, y) => x >= 0 && x <= 9 && y >= 0 && y <= 9 /* * Signature: - * (Piece, Piece[][]) => { x: Number, y: Number, captures: [] }[] + * ({ x: Number, y: Number }, { x: Number, y: Number }, Piece[][], Optional Boolean) => Boolean + * + * Description: + * Check if all the board squares between the starting position `s' and the ending position `e' + * are empty on the board `board'. This is used to validate moves, as you cannot simply jump over + * another piece. The function takes an optional parameter `ignoreLast' which if set to `true' + * will ignore the last piece in the chain. This let's the function be used for calculating legal + * captures where the last square will obviously be a piece. + */ +const allEmpty = (s, e, board, ignoreLast) => { + let dx = e.x > s.x ? 1 : -1 + let dy = e.y > s.y ? 1 : -1 + + let pieces = [] + let [x, y] = [s.x, s.y] + + while (x != e.x && y != e.y) { + x += dx + y += dy + pieces.push(board[y][x]) + } + if (ignoreLast) + pieces.pop() + + return pieces.every(p => p == null) +} + +/* + * Signature: + * (Piece, Piece[][]) => { x: Number, y: Number, captures: [], king: Boolean }[] * * 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. + * this function so an empty array is assigned to it. The `king' field specifies if the piece + * became a king as a result of the move. */ const nonCapturingMoves = /* Get all the possible moves we can make by calling the `Piece.nonCapturingMoves()' method. @@ -75,20 +105,22 @@ const nonCapturingMoves = * 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: [] } }) + .filter(([x, y]) => inBounds(x, y) && allEmpty(p.position, { x: x, y: y }, board)) + .map(([x, y]) => { return { x: x, y: y, captures: [], + king: p.isKing + || (p.color == Color.BLUE ? y == 9 : y == 0) } }) -/** +/* * Signature: - * (Piece, { x: Number, y: Number, captures: Piece[] }[], Piece[][]) => { x: Number, y: Number, - * captures: Piece[] }[] + * (Piece, Piece[], Piece[][]) => { x: Number, y: Number, captures: Piece[], king: Boolean }[] * * 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. + * that the user chooses to make the move. The `king' field specifies if the piece `p' became a + * king piece as a result of the capture sequence. */ const capturingMoves = (p, captures, board) => { /* Get all the possible moves we can make. This is done by calling `p.capturingMoves()' to get @@ -100,12 +132,15 @@ const capturingMoves = (p, captures, board) => { * - 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 + * - All the squares between the current position and the captured piece are empty */ 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]) + && !board[landed.y][landed.x] + && allEmpty({ x: p.position.x, y: p.position.y }, + skipped, board, true)) /* 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 @@ -118,7 +153,7 @@ const capturingMoves = (p, captures, board) => { */ if (moves.length == 0) return captures.length > 0 - ? [{ x: p.position.x, y: p.position.y, captures: captures }] + ? [{ x: p.position.x, y: p.position.y, captures: captures, king: p.isKing }] : [] /* Now we get to the fun part. For each of the moves in `moves' we first concatenate the piece @@ -128,18 +163,25 @@ const capturingMoves = (p, captures, board) => { * 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. + * + * We also need to perform a check to see if we are on the promotion line (the back row). In this + * case we must remember to promote the piece to a king. */ return moves.reduce((acc, { landed, skipped }) => { const c = captures.concat(board[skipped.y][skipped.x]) board[skipped.y][skipped.x] = null p.position = landed + + if (p.color == Color.BLUE ? p.position.y == 9 : p.position.y == 0) + p.isKing = true + return acc.concat(capturingMoves(p, c, board)) }, []) } /* * Signature: - * (Piece, Piece[][]) => { x: Number, y: Number, captures: Piece[] }[] + * (Piece, Piece[][]) => { x: Number, y: Number, captures: Piece[], king: Boolean }[] * * Description: * A wrapper function around `nonCapturingMoves()' and `capturingMoves()' which simply calls the @@ -147,8 +189,11 @@ const capturingMoves = (p, captures, board) => { * 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))) +const calculateMoves = (p, board) => { + let npiece = new Piece(p.position.x, p.position.y, p.id, p.color) + npiece.isKing = p.isKing + return nonCapturingMoves(p, board).concat(capturingMoves(npiece, [], deepCopy(board))) +} /* * Signature: @@ -211,7 +256,7 @@ Game.prototype.messageClient = function(msg, ws) { /* * Signature: - * () => { x: Number, y: Number, captures: Piece[] }[] + * () => { x: Number, y: Number, captures: Piece[], king: Boolean }[] * * Description: * Return all of the moves that can legally be made on the current turn of the game. The format @@ -276,21 +321,21 @@ Game.prototype.nextTurn = function() { /* * Signature: - * ({ old: { x: Number, y: Number }, - * new: { x: Number, y: Number }, - * captures: Piece[] }) => Nothing + * ({ old: { x: Number, y: Number }, new: { x: Number, y: Number }, captures: Piece[], + * king: Boolean }) => 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. + * The message only has 4 fields that matter to us, the `old', `new', `captures', and `king' + * 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. The `king' field tells us if the piece got promoted to a king. */ 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 + this.board[msg.new.y][msg.new.x].isKing = msg.king + this.board[msg.old.y][msg.old.x] = null msg.captures.forEach(c => this.board[c.position.y][c.position.x] = null) } diff --git a/draughts/piece.js b/draughts/piece.js index f8aaf09..62a221f 100644 --- a/draughts/piece.js +++ b/draughts/piece.js @@ -1,4 +1,40 @@ -const Color = require("./color") +const Color = require("./public/javascripts/color") + +/* Return all of the potential non-capturing moves that the piece can make */ +const nonCapturingMovesStandard = + p => p.color == Color.BLUE + ? [[p.position.x + 1, p.position.y + 1], + [p.position.x - 1, p.position.y + 1]] + : [[p.position.x + 1, p.position.y - 1], + [p.position.x - 1, p.position.y - 1]] + +const nonCapturingMovesKing = + p => [1, 2, 3, 4, 5, 6, 7, 8, 9].map(n => [[n, n], [n, -n], [-n, n], [-n, -n]]) + .flat() + .map(([x, y]) => [p.position.x + x, p.position.y + y]) + +const capturingMovesStandard = p => [ + { skipped: { x: p.position.x + 1, y: p.position.y + 1 }, + landed: { x: p.position.x + 2, y: p.position.y + 2 } }, + { skipped: { x: p.position.x - 1, y: p.position.y + 1 }, + landed: { x: p.position.x - 2, y: p.position.y + 2 } }, + { skipped: { x: p.position.x + 1, y: p.position.y - 1 }, + landed: { x: p.position.x + 2, y: p.position.y - 2 } }, + { skipped: { x: p.position.x - 1, y: p.position.y - 1 }, + landed: { x: p.position.x - 2, y: p.position.y - 2 } } +] + +const capturingMovesKing = + p => [1, 2, 3, 4, 5, 6, 7, 8].map( + n => [{ skipped: { x: p.position.x + n, y: p.position.y + n }, + landed: { x: p.position.x + n + 1, y: p.position.y + n + 1 } }, + { skipped: { x: p.position.x + n, y: p.position.y - n }, + landed: { x: p.position.x + n + 1, y: p.position.y - n - 1 } }, + { skipped: { x: p.position.x - n, y: p.position.y + n }, + landed: { x: p.position.x - n - 1, y: p.position.y + n + 1 } }, + { skipped: { x: p.position.x - n, y: p.position.y - n }, + landed: { x: p.position.x - n - 1, y: p.position.y - n - 1 } } + ]).flat() /* 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. @@ -13,26 +49,12 @@ const Piece = function(x, y, id, color) { this.isKing = false } -/* 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]] + return this.isKing ? nonCapturingMovesKing(this) : nonCapturingMovesStandard(this) } 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 } } - ] + return this.isKing ? capturingMovesKing(this) : capturingMovesStandard(this) } Piece.prototype.sameTeam = function(other) { diff --git a/draughts/public/images/bluepiece-king.svg b/draughts/public/images/bluepiece-king.svg new file mode 100644 index 0000000..779d3d5 --- /dev/null +++ b/draughts/public/images/bluepiece-king.svg @@ -0,0 +1,16 @@ +<svg version="1.1" width="10" height="10" xmlns="http://www.w3.org/2000/svg"> + <circle cx="5" cy="5" r="5" fill="#204C6D" /> + <circle cx="5" cy="5" r="3.5" fill="#007BBD" /> + <path d="M 4.439246594 3.011134512 C 1.4486027246 4.061861054 1 6.613596256 1.5233698453 + 8.264726262 C 1.5233698453 6.538542410 2.719630124 5.712977407 4.514013715 + 4.287008940 C 4.663547957 4.812372211 3.588777422 5.788031253 3.542054799 + 6.163286831 C 3.482238371 6.643620525 3.915890403 6.988851835 4.214945232 + 6.988851835 C 4.738315077 6.988851835 5.186917802 6.388448371 5.485972631 + 6.013192792 C 5.635506873 6.838757795 5.037383560 8.339780109 3.841123282 + 8.790089533 C 6.084109597 8.865143380 7.953260309 6.163286831 8.102794551 + 4.662264518 C 7.728972601 5.037520096 7.205602755 5.337721828 6.682232910 + 5.337721828 C 6.084109597 5.337721828 5.934575356 5.112573943 5.934575356 + 4.587210672 C 5.934575356 3.461443937 9 2.410731048 8.925232879 1.1348566201 C + 7.878493189 2.260626086 6.158876718 2.406976308 4.439246594 3.011134512 Z" + fill="gold" /> +</svg> diff --git a/draughts/public/images/redpiece-king.svg b/draughts/public/images/redpiece-king.svg new file mode 100644 index 0000000..b4a97cf --- /dev/null +++ b/draughts/public/images/redpiece-king.svg @@ -0,0 +1,16 @@ +<svg version="1.1" width="10" height="10" xmlns="http://www.w3.org/2000/svg"> + <circle cx="5" cy="5" r="5" fill="#803B2F" /> + <circle cx="5" cy="5" r="3.5" fill="#9E3C2F" /> + <path d="M 4.439246594 3.011134512 C 1.4486027246 4.061861054 1 6.613596256 1.5233698453 + 8.264726262 C 1.5233698453 6.538542410 2.719630124 5.712977407 4.514013715 + 4.287008940 C 4.663547957 4.812372211 3.588777422 5.788031253 3.542054799 + 6.163286831 C 3.482238371 6.643620525 3.915890403 6.988851835 4.214945232 + 6.988851835 C 4.738315077 6.988851835 5.186917802 6.388448371 5.485972631 + 6.013192792 C 5.635506873 6.838757795 5.037383560 8.339780109 3.841123282 + 8.790089533 C 6.084109597 8.865143380 7.953260309 6.163286831 8.102794551 + 4.662264518 C 7.728972601 5.037520096 7.205602755 5.337721828 6.682232910 + 5.337721828 C 6.084109597 5.337721828 5.934575356 5.112573943 5.934575356 + 4.587210672 C 5.934575356 3.461443937 9 2.410731048 8.925232879 1.1348566201 C + 7.878493189 2.260626086 6.158876718 2.406976308 4.439246594 3.011134512 Z" + fill="gold" /> +</svg> diff --git a/draughts/public/javascripts/game.js b/draughts/public/javascripts/game.js index 20c9935..a60401e 100644 --- a/draughts/public/javascripts/game.js +++ b/draughts/public/javascripts/game.js @@ -1,3 +1,8 @@ +const Color = { + BLUE: "b", + RED: "r" +} + /* Initialize a new web socket to connect to the server */ const ws = new WebSocket("ws://localhost:3000") @@ -15,7 +20,7 @@ const removeMarkers = () => Array .forEach(pos => pos.remove()) // Remove them all /* Add a position marker to the position specified by the given coordinates */ -const addMarker = ({ x, y, captures }) => { +const addMarker = ({ x, y, captures, king }) => { /* 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 @@ -38,6 +43,8 @@ const addMarker = ({ x, y, captures }) => { removeCaptures(captures, opponentPrefix()) /* Move the selected piece, unselect it, remove the markers, and end our turn */ + if (king) + selectedPiece.src = selectedPiece.src.replace("piece.svg", "piece-king.svg") selectedPiece.style.transform = img.style.transform selectedPiece = null ourTurn = false @@ -52,7 +59,8 @@ const addMarker = ({ x, y, captures }) => { id: id, old: { x: ox, y: oy }, new: { x: x, y: y }, - captures: captures + captures: captures, + king: king } })) }) @@ -64,9 +72,13 @@ const removeCaptures = (captures, color) => { node.innerHTML = `+${Number(node.innerHTML) + captures.length}` } -const movePiece = ({ id, position, captures }) => { - document.getElementById(opponentPrefix() + id).style.transform = - `translate(${position.x * 100}%, ${position.y * 100}%)` +const movePiece = ({ id, position, captures, king }) => { + let node = document.getElementById(opponentPrefix() + id); + node.style.transform = `translate(${position.x * 100}%, ${position.y * 100}%)` + + if (king) + node.src = node.src.replace("piece.svg", "piece-king.svg") + removeCaptures(captures, colorPrefix) } |