aboutsummaryrefslogtreecommitdiff
path: root/draughts
diff options
context:
space:
mode:
authorThomas Voss <thomasvoss@live.com> 2022-01-13 22:33:07 +0100
committerThomas Voss <thomasvoss@live.com> 2022-01-13 22:33:07 +0100
commit3fb39ddd8fcbe0d61c6f29d08649fd4b2e18f84a (patch)
treef2e6070831821b66192f4acf95ba5603d44afa54 /draughts
parentad4cc6e221072acc3fbbd47b3f69b9b424e18d6d (diff)
Add king piece support
Diffstat (limited to 'draughts')
-rw-r--r--draughts/app.js13
-rw-r--r--draughts/game.js89
-rw-r--r--draughts/piece.js56
-rw-r--r--draughts/public/images/bluepiece-king.svg16
-rw-r--r--draughts/public/images/redpiece-king.svg16
-rw-r--r--draughts/public/javascripts/game.js22
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)
}