In the last couple posts, we looked at:
- Rethinking Gameboards: looking at the CSS driving a chess board, and
- Chess Pieces, Inheritance vs Composition: Building pieces by composing them of functionality, being able to plug-and-play parts as we need.
In this one, we will revisit the gameboard itself, but we'll look at the javascript this time. The replit contains a working chess board, but it leaves something to be desired. this post will address the what, why and how of that refactoring and rewrite.
The Why
The gameboard doesn't require much for functionality. In the current version, we can add()
pieces to the board, and then the board itself handles the moves. Works, and it works pretty well. But it isn't really a functional solution. It isn't a great interface.
When we talk about interfaces, we mean "How we communicate to and from this thing." We want a mechanism to tell an object or component something, and we would like a mechanism to let that thing tell us something in return. The javascript engine itself, right in your browser, includes some objects with well-defined interfaces. Some examples:
// the Math object contains a number of useful interface methods!
// Math.random() is an interface method that tells us something...
const someValue = Math.random();
const someOtherValue = Math.random();
// Math.min() is another one: we tell it any number of values,
// and it tells us something about them.
const minValue = Math.min( someValue, someOtherValue );
// the Date object is another. We can pass data in and
// get data back, or we can simply ask for data without
// passing anything in.
const rightNow = Date.now();
Interface methods are all about communication. We want to hide all the implementation details for our object or component inside the thing so we don't have to get dirty with it, but then we want to be able to communicate with that implementation, still without getting dirty. We do so by providing a means of getting in there.
So in the case of the Queen
, for example, here's the entire code:
const Queen = (...classNames) => (starting) => {
let current = starting;
let hasMoved=false;
const piece = Piece(starting);
const domNode = piece.domEl();
domNode.classList.add("queen",...classNames)
const isValidMove = (target) =>{
const [start, end] = [Piece.toXY(current), Piece.toXY(target)];
return moves.diagonal(current)(target)
|| moves.lateral(current)(target);
}
const to = (target)=>{
if(isValidMove(target)){
hasMoved=true;
current = target;
piece.to(target)
} else {
console.log("Nope nope nope!")
}
}
const domEl = () => piece.domEl()
// The returned object defines the interface!
return {
to,
isValidMove,
domEl
}
}
Everything within the queen is hidden away. We don't have to explicitly tell her how to move, we simply say .to("C3")
and she knows to check if its valid, and to make the move. The implementation is internalized, the interface is externalized. In this case, it's three exposed methods: to
, isValidMove
and domEl
. We don't know how the Queen does them, we just tell her something (in the first two cases), and if needed, we get a reply (in the last two cases).
But in the case of the Gameboard
, the exposed interface is this:
const Chessboard = function(domNode) {
const cols = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
let selected;
let pieces = [];
const getSquareSize = (board) => {
// internal functionality, commented out for brevity
}
domNode.addEventListener("click", (event) => {
// commented out for brevity
})
const add = (piece) => {
pieces = [...pieces, piece]
domNode.appendChild(piece.domEl() );
}
return {
add
}
}
So the exposed interface is one method, .add()
. And to me, that is kind of... limited. I would like to make this a little more useful, really. It would be nice to make this something we can play from the console or commandline, for example, or to be able to read in a text file containing an entire game and play it out move-by-move. But to do that, we would need to tell the board "Hey! This piece here? Move it there." Or "Hey, when we move this piece there, and it captures that piece? Remove that piece."
We need to improve the interface.
The What
What might be a useful interface for a Gameboard? I can think of a couple methods might be handy, so let's start there:
const Gameboard = (domNode) => {
// let's leave implementation out for a minute, and focus
// on what our interface might be:
return {
at: {
// at is a nested interface, providing us with some
// drill-down functionality:
place: /* lets us put something on the board */,
remove: /* removes something from the board */,
get: /* Just gimme the piece if there is one */
},
from: {
// this might also have a nested interface, so we can
// communicate in a declarative way:
to: /* lets us move from one location to another */
},
board: {
// again, another nested interface?
toString: /* return a JSON object maybe? */,
toDOM: /* Could return a reference to the board's DOM? */
}
}
}
So I'm not worrying about the implementation yet. I don't know how we'll be doing this stuff internally, I'm simply building my "wish-list" interface. With that, it would be easy to communicate with the board:
// Putting something on the board...
myChessboard.at("C1").place(Bishop("black","queens") );
// and moving that?
myChessboard.from("C1").to("F4");
// We can remove pieces explicitly, if we like:
if(myChessboard.at("F4").get()!==null){
myChessboard.at("F4").remove();
}
So an interface like that is a bit more expressive, but how might we implement that?
The How
Creating the interface is not much more difficult than planning it out, though it still requires some planning and forethought. For example, in the new mechanism of .at(...).place(...)
we are using the cell as the "key" of our piece, as only one thing can be at that cell. In the older version, we did this:
chessboard.add(Rook("black", "kings")("H1"))
With that, the board is unaware of what is where. The piece knows where it sits, but the board doesn't know what it contains.
With the new version, we do this:
chessboard.at("C1").place(Bishop("black", "queens") );
Now the chessboard is aware that it contains cells, and it handles creating and placing that piece on that cell. So where before our Chessboard
internally had an array of pieces that was simply the piece, we need to change that some. Now, it needs to keep track of both the piece, and its location. So that changes the state of that array of pieces to something more like:
let pieces = [
{
piece: Rook("black", "queens"),
location: "A1"
},
{
piece: Knight("black", "queens"),
location: "B1"
}
// and so on
]
It isn't a huge change, and so far as anything outside the chessboard knows, it isn't significant. The pieces themselves still work in the same way, and while the way we communicate with the board has changed, it isn't bad. Lets take a look at the implementation of the .at(cell)
functionality:
const at = (cell) => {
// placing a piece takes two steps:
// add the piece to the array as an object, and
// tell the piece itself which grid-area to use.
const place = (piece) => {
const addMe = {
location:cell,
piece:piece(cell)
}
pieces = [...pieces, addMe];
domNode.appendChild(addMe.piece.domEl())
}
// removing a piece is simply removing the one with
// a `cell` property that matches.
const remove = () => {
const item= pieces.find( piece = piece.location===cell);
// pull it out of the DOM...
item.piece.remove();
// and out of the array.
pieces = pieces.filter(piece => piece !== item);
}
// Now, the sub-interface!
return {
place,
remove,
get value(){
return pieces.find(piece=>piece.location===cell).piece;
}
}
}
So, when we .at(cell).place(piece)
, we are doing two things: first, we add an object to the array using the format we've standardized, and second, we add the piece to the DOM within the gameboard.
When we remove the piece from the board, we reverse that. We remove that particular node from the DOM, and then we tell the array to filter for only those pieces that are not the one we wish removed.
Finally, we want a getter for the value
of the given cell. The value is not the entire object but solely the piece within that object.Note that here, I did change the interface a little: get
is a keyword in javascript, and I didn't want to cause confusion.
We can do much the same for the .from(starting).to(ending)
functionality, creating another interface for it. Something like this:
const from = (starting) => {
const to = (ending) => {
// Lets simply map over the pieces array, and update
// the appropriate one.
pieces = pieces.map( (item) => {
if (item.location===starting){
item.piece.to(ending);
// we'll return a new object for the moved piece
return {
piece: item.piece,
location: ending
}
} else {
// we simply pass along the object for the rest.
return item;
}
})
}
// and remember to return the interface itself!
return {
to
}
}
So those two will let us do things like this:
chessboard.at("A1").place(Rook("black", "queens"))
chessboard.at("B1").place(Knight("black", "queens"))
chessboard.at("C1").place(Bishop("black", "queens"))
chessboard.at("D1").place(Queen("black"))
chessboard.at("E1").place(King("black"))
chessboard.at("F1").place(Bishop("black", "kings"))
chessboard.at("G1").place(Knight("black", "kings"))
chessboard.at("H1").place(Rook("black", "kings"))
chessboard.at("A8").place(Rook("white", "queens"))
chessboard.at("B8").place(Knight("white", "queens"))
chessboard.at("C8").place(Bishop("white", "queens"))
chessboard.at("D8").place(Queen("white"))
chessboard.at("E8").place(King("white"))
chessboard.at("F8").place(Bishop("white", "kings"))
chessboard.at("G8").place(Knight("white", "kings"))
chessboard.at("H8").place(Rook("white", "kings"))
// Let's try moving one by code now!
chessboard.from("H8").to("H4");
With this, the state of pieces and their locations is being maintained in the board, and the board notifies the piece to update its CSS as needed. With that, we have a more useful and extensible Gameboard
!
The Recap
Interfaces are powerful, and fundamental to good OOP. The three principles that are foundational to OOP are encapsulation (we hide stuff), communication (we provide a way to talk to stuff) and late instantiation (we can make new stuff as we need, at runtime). And in our Piece
and Gameboard
, we have built a good solid base on those three principles.
To see this one in action, here's another replit. I have made one other change to that replit that wasn't in the other: the chessboard
is defined on the global window
object. This is not a thing done in production, but what that does here is... you can test moving a piece by typing our interface methods directly in the console as well as using the GUI!
Top comments (0)