const namespaces = require('./namespaces');
let Chess = require('chess.js');
// why? see https://github.com/jhlywa/chess.js/issues/196
if (Chess.Chess) {
Chess = Chess.Chess;
}
/**
* This is class represents a chess game using semantic annotations.
*/
class SemanticChess {
/**
* @param {Object} options: options to initialize the chess game
* @param {null|string} options.chess: Chess game from chess.js
* @param {null|string} options.startPosition: start position of the game, using FEN
* @param {string} options.url: url that represents the game
* @param {string} options.userWebId: WebId of the user
* @param {string} options.opponentWebId: WebId of the opponent
* @param {string|function(): string} options.moveBaseUrl: base url used to create urls for new moves
* @param {null|string} options.name: name of the game
* @param {null|Object} options.lastMove: latest move made in the game
* @param {null|Object} options.lastUserMove: last move made by the use in the game
* @param {null|string} options.colorOfUser: color of the user ('w' or 'b', default is 'w')
* @param {null|function()} options.uniqid: function that will return a unique id for the moves
* @param {null|string} options.givenUpBy: WebId of the player that gave up
*/
constructor(options) {
if (options.chess) {
this.chess = options.chess;
this.startPosition = options.startPosition; // FEN
} else {
if (options.startPosition) {
// if there is start position we se the chess game to that
this.chess = new Chess(options.startPosition);
} else {
// else use default start position
this.chess = new Chess();
}
this.startPosition = this.chess.fen();
}
this.url = options.url;
this.userWebId = options.userWebId;
this.opponentWebId = options.opponentWebId;
this.name = options.name;
this.lastMove = options.lastMove;
this.lastUserMove = options.lastUserMove;
this.moveBaseUrl = options.moveBaseUrl;
this.realTime = !!options.realTime;
// if move base url is a string create function that returns this string
// else a function so we leave it
if (typeof this.moveBaseUrl === 'string') {
const t = this.moveBaseUrl;
this.moveBaseUrl = function() {
return t;
}
}
// the default color of the user is white
if (!options.colorOfUser) {
this.colorOfUser = 'w';
} else {
this.colorOfUser = options.colorOfUser;
}
// set the color of the opponent opposite of the user
if (this.colorOfUser === 'w') {
this.colorOfOpponent = 'b';
} else {
this.colorOfOpponent = 'w';
}
// an empty string as name does not make much sense
if (this.name === '') {
this.name = null;
}
// if we don't have a last move, the game just started so we can check who which color started the game
if (!this.lastMove) {
this.colorOfFirstTurn = this.chess.turn();
}
// set the default uniqid function to the function of the package 'uniqid'
if (!options.uniqid) {
this.uniqid = require('uniqid');
} else {
this.uniqid = options.uniqid;
}
this.givenUpWebId = options.givenUpBy ? options.givenUpBy : null;
}
/**
* The method returns true if the next half move has to be made by the opponent.
* @returns {boolean}: true if the next half move has to be made by the opponent
*/
isOpponentsTurn() {
return this.chess.turn() === this.colorOfOpponent;
}
/**
* This method returns true if the game is over because someone gave up.
* @returns {boolean}: true if someone give up on the game
*/
isGivenUp() {
return this.givenUpWebId !== null;
}
/**
* This method returns the WebId of the player that gave up.
* @returns {string|null}: WebId of the player that gave up or null if nobody gave up
*/
givenUpBy() {
return this.givenUpWebId;
}
/**
* This method is called to give up to game by a player.
* @param webId: the WebId of the player that gives up on the game
* @returns {{sparqlUpdate: string, notification: string}}: corresponding SPARQL update and (inbox) notification
* that is generated by giving up
*/
giveUpBy(webId) {
if ((webId === this.userWebId || webId === this.opponentWebId) && this.givenUpWebId === null) {
this.givenUpWebId = webId;
const giveUpActionUrl = this.moveBaseUrl() + `#` + this.uniqid();
//todo use existing class
const sparqlUpdate = `<${giveUpActionUrl}> a <${namespaces.schema}GiveUpAction>;
<${namespaces.schema}agent> <${webId}>;
<${namespaces.schema}object> <${this.url}>.
<${this.url}> <${namespaces.chess}givenUpBy> <${webId}>.`;
const notification = `<${giveUpActionUrl}> a <${namespaces.schema}GiveUpAction>.`;
return {sparqlUpdate, notification};
} else {
return null;
}
}
/**
* This methods loads the giving up of a player.
* @param webId: the WebId of the player that gives up on the game
*/
loadGiveUpBy(webId) {
if ((webId === this.userWebId || webId === this.opponentWebId) && this.givenUpWebId === null) {
this.givenUpWebId = webId;
}
}
/**
* This method does the next move, which specified via the san and the options.
* It returns the corresponding SPARQL update and inbox notification if the move is valid.
* It returns null if the move was invalid. For example, when the san is invalid or
* when it's the opponents turn.
* Note that opponents turns can only be added via the method loadMove.
* @param {string|Object} move: new move either via SAN (string) or object (inherited from chess.js)
* @param {Object} options: the options of the move, inherited from Chess from chess.js
* @returns {null|{sparqlUpdate: string, notification: string}}: corresponding SPARQL update and inbox notification
* that is generated by doing the move
*/
doMove(move, options) {
// check if it's the user's turn
if (!this.isOpponentsTurn()) {
// save the current turn for later
const currentTurn = this.chess.turn();
const createdMove = this.chess.move(move, options);
// check if the move is valid
if (createdMove) {
// if the color of the first turn has not been set that means that this move is the first move
if (!this.colorOfFirstTurn) {
this.colorOfFirstTurn = currentTurn;
}
return this._addUserMove(createdMove.san);
} else {
// invalid mode
return null;
}
} else {
// not the user's turn
return null;
}
}
/**
* This method load a user's or opponent's move.
* This method returns {url, san}, where `url` is the url of the newly loaded move and
* `san` is the SAN of the newly loaded move.
* This method returns null if the SAN of the move is invalid.
* When multiple moves need to be loaded, they have to be loaded in the order that they are played.
* @param {string} san: SAN of the new move
* @param {Object} options: is inherited from Chess.move() from chess.js extended with the key url that represents the move
* @returns {null|{url: string, san:string}}: null if the move is invalid or the url and san of the move if valid
*/
loadMove(san, options) {
const currentTurn = this.chess.turn();
const createdMove = this.chess.move(san, options);
// check if the move is valid
if (createdMove) {
if (!this.colorOfFirstTurn) {
this.colorOfFirstTurn = currentTurn;
}
// check if the current turn is made by the user
if (currentTurn === this.colorOfUser) {
this.lastUserMove = {url: options.url, san};
}
this.lastMove = {url: options.url, san};
return this.lastMove;
} else {
// invalid move
return null;
}
}
/**
* This method returns the RDF (Turtle) representation of the game, without any moves.
* @returns {string}: RDF representation of the game
*/
getMinimumRDF() {
if (!this.minimumRDF) {
const userAgentRole = this.moveBaseUrl() + `#` + this.uniqid();
const opponentAgentRole = this.moveBaseUrl() + `#` + this.uniqid();
let whiteWebId;
let blackWebId;
// determine the WebIds per color
if (this.colorOfUser === 'w') {
whiteWebId = this.userWebId;
blackWebId = this.opponentWebId;
} else {
whiteWebId = this.opponentWebId;
blackWebId = this.userWebId;
}
this.minimumRDF = `<${this.url}> <${namespaces.rdf}type> <${namespaces.chess}ChessGame>;\n` +
`<${namespaces.chess}providesAgentRole> <${userAgentRole}>, <${opponentAgentRole}>.\n\n` +
`<${userAgentRole}> <${namespaces.rdf}type> <${namespaces.chess}WhitePlayerRole>;\n` +
`<${namespaces.chess}performedBy> <${whiteWebId}>.\n\n` +
`<${opponentAgentRole}> <${namespaces.rdf}type> <${namespaces.chess}BlackPlayerRole>;\n` +
`<${namespaces.chess}performedBy> <${blackWebId}>.\n\n` +
`<${this.url}> <${namespaces.chess}isRealTime> ${this.realTime}.\n\n`;
if (this.name) {
this.minimumRDF += `<${this.url}> <http://schema.org/name> "${this.name}".\n`;
}
if (this.startPosition) {
this.minimumRDF += `<${this.url}> <${namespaces.chess}startPosition> "${this.startPosition}".\n`;
}
if (this.colorOfFirstTurn === 'w') {
this.minimumRDF += `<${this.url}> <${namespaces.chess}starts> <${userAgentRole}>.\n`;
} else if (this.colorOfFirstTurn === 'b') {
this.minimumRDF += `<${this.url}> <${namespaces.chess}starts> <${opponentAgentRole}>.\n`;
}
}
return this.minimumRDF;
}
/**
* This method returns {san, url} of the last move made, where `san` is the SAN of the move and
* `url` is the url of the move.
* @returns {null|{url: string, san: string}}: url that represents the move and SAN that describes the move
*/
getLastMove() {
return this.lastMove;
}
/**
* This method returns {san, url} of the last move made by the user, where `san` is the SAN of the move and
* `url` is the url of the move.
* @returns {null|{url: string, san: string}}: url that represents the move and SAN that describes the move
*/
getLastUserMove() {
return this.lastUserMove;
}
/**
* This method returns the color of the user, where 'w' is white and 'b' is black.
* @returns {string}: 'w' (white) or 'b' (black)
*/
getUserColor() {
return this.colorOfUser;
}
/**
* This method returns the color of the opponent, where 'w' is white and 'b' is black.
* @returns {string}: 'w' (white) or 'b' (black)
*/
getOpponentColor() {
return this.colorOfOpponent;
}
/**
* This method returns chess.js game that is used.
* @returns {Chess}: Chess from chess.js
*/
getChess() {
return this.chess;
}
/**
* This method return the WebId of the opponent.
* @returns {string}: WebId of the opponent
*/
getOpponentWebId() {
return this.opponentWebId;
}
/**
* This method returns the URL of the game.
* @returns {string}: URL of the game
*/
getUrl() {
return this.url;
}
/**
* This method returns the function that generates the base url for a new move.
* @returns {function(): string}: function that generates the base url for a new move
*/
geMoveBaseUrl() {
return this.moveBaseUrl;
}
/**
* This method returns the name of the game.
* @returns {string|null}: name of the game
*/
getName() {
return this.name;
}
/**
* This method returns the start position (using FEN) of the game.
* @returns {string|null}: starting position of the game
*/
getStartPosition() {
return this.startPosition;
}
/**
* This method returns true if the game is played real time.
* @returns {boolean}: true if the game is played real time, else false
*/
isRealTime() {
return this.realTime;
}
/**
* This method adds a new move for the user and generates the corresponding SPARQL update and notification.
* @param {string} san: SAN of the new move
* @returns {{sparqlUpdate: string, notification: string}}: sparqlUpdate is the SPARQL update representing this move
* and the corresponding inbox notification that should be send to the opponent
* @private
*/
_addUserMove(san) {
// generate URL for move
const moveURL = this.moveBaseUrl() + `#` + this.uniqid();
let sparqlUpdate = 'INSERT DATA {\n';
let notification = null;
sparqlUpdate +=
`<${this.url}> <${namespaces.chess}hasHalfMove> <${moveURL}>.
<${moveURL}> <${namespaces.rdf}type> <${namespaces.chess}HalfMove>;
<${namespaces.schema}subEvent> <${this.url}>;
<${namespaces.chess}hasSANRecord> "${san}"^^<${namespaces.xsd}string>;
<${namespaces.chess}resultingPosition> "${this.chess.fen()}".\n`;
if (this.lastMove) {
sparqlUpdate += `<${this.lastMove.url}> <${namespaces.chess}nextHalfMove> <${moveURL}>.\n`;
notification = `<${this.lastMove.url}> <${namespaces.chess}nextHalfMove> <${moveURL}>.`;
} else {
sparqlUpdate += `<${this.url}> <${namespaces.chess}hasFirstHalfMove> <${moveURL}>.\n`;
notification = `<${this.url}> <${namespaces.chess}hasFirstHalfMove> <${moveURL}>.`;
}
// if we have a checkmate we also say that this move is the last move
if (this.chess.in_checkmate()) {
sparqlUpdate += `<${this.url}> <${namespaces.chess}hasLastHalfMove> <${moveURL}>.\n`;
notification += `<${this.url}> <${namespaces.chess}hasLastHalfMove> <${moveURL}>.`;
}
sparqlUpdate += `}`;
this.lastMove = {
url: moveURL,
san
};
// because this method is only called when a move is done by the user, so we can set the lastUserMove
this.lastUserMove = this.lastMove;
return {
sparqlUpdate,
notification
}
}
}
module.exports = SemanticChess;