1022 lines
48 KiB
TypeScript
1022 lines
48 KiB
TypeScript
import { Chess } from 'chessops/chess';
|
|
import { INITIAL_FEN, makeFen, parseFen } from 'chessops/fen';
|
|
import { makeSan, parseSan } from 'chessops/san';
|
|
import { NormalMove } from 'chessops/types';
|
|
import { board } from 'chessops/debug';
|
|
import { defaultSetup, makeUci, parseUci } from 'chessops';
|
|
|
|
export default function (token: string) {
|
|
|
|
const root = document.getElementById('dgt-play-zone') as HTMLDivElement;
|
|
const consoleOutput = document.getElementById('dgt-play-zone-log') as HTMLPreElement;
|
|
|
|
console.log(parseFen('rnbqkbnr/pppp1ppp/8/8/3pP3/2P5/PP3PPP/RNBQKBNR b KQkq - 1 3'));
|
|
|
|
// and your code in here.
|
|
|
|
/**
|
|
* CONFIGURATION VALUES
|
|
*/
|
|
const liveChessURL = localStorage.getItem('dgt-livechess-url');
|
|
const announceAllMoves = (localStorage.getItem('dgt-speech-announce-all-moves') == "true");
|
|
const verbose = (localStorage.getItem('dgt-verbose') == "true");
|
|
const announceMoveFormat = localStorage.getItem('dgt-speech-announce-move-format') ? localStorage.getItem('dgt-speech-announce-move-format') : "san";
|
|
const speechSynthesisOn = (localStorage.getItem('dgt-speech-synthesis') == "true");
|
|
const voice = localStorage.getItem('dgt-speech-voice');
|
|
var keywords = {
|
|
"K": "King",
|
|
"Q": "Queen",
|
|
"R": "Rook",
|
|
"B": "Bishop",
|
|
"N": "Knight",
|
|
"P": "Pawn",
|
|
"x": "Takes",
|
|
"+": "Check",
|
|
"#": "Checkmate",
|
|
"(=)": "Game ends in draw",
|
|
"O-O-O": "Castles queenside",
|
|
"O-O": "Castles kingside",
|
|
"white": "White",
|
|
"black": "Black",
|
|
"wins by": "wins by",
|
|
"timeout": "timeout",
|
|
"resignation": "resignation",
|
|
"illegal": "illegal",
|
|
"move": "move"
|
|
}
|
|
try {
|
|
var tempString = localStorage.getItem('dgt-speech-keywords');
|
|
keywords = tempString && JSON.parse(tempString);
|
|
} catch (error) {
|
|
console.error("Invalid JSON Object for Speech Keywords. Using English default.");
|
|
}
|
|
|
|
//Lichess Integration with Board API
|
|
|
|
/**
|
|
* GLOBAL VATIABLES - Lichess Connectivity
|
|
*/
|
|
var time = new Date(); //A Global time object
|
|
var currentGameId = ''; //Track which is the current Game, in case there are several open games
|
|
var currentGameColor = ''; //Track which color is being currently played by the player. 'white' or 'black'
|
|
var me: { id: string; username: string }; //Track my information
|
|
var gameInfoMap = new Map(); //A collection of key values to store game inmutable information of all open games
|
|
var gameStateMap = new Map(); //A collection of key values to store the changing state of all open games
|
|
var gameConnectionMap = new Map<string, { connected: boolean, lastEvent: number }>(); //A collection of key values to store the network status of a game
|
|
var gameChessBoardMap = new Map<string, Chess>(); //A collection of chessops Boads representing the current board of the games
|
|
var eventSteamStatus = { connected: false, lastEvent: time.getTime() }; //An object to store network status of the main eventStream
|
|
const keywordsBase = ["K", "Q", "R", "B", "N", "P", "x", "+", "#", "(=)", "O-O-O", "O-O", "white", "black", "wins by", "timeout", "resignation"]
|
|
var lastSanMove: { player: string, move: string, by: string }; //Track last move in SAN format . This is because there is no easy way to keep history of san moves
|
|
/**
|
|
* Global Variables for DGT Board Connection (JACM)
|
|
*/
|
|
var localBoard: Chess = startingPosition(); //Board with valid moves played on Lichess and DGT Board. May be half move behind Lichess or half move in advance
|
|
var DGTgameId = ''; //Used to track if DGT board was setup already with the lichess currentGameId
|
|
var boards = Array<{ serialnr: string, state: string }>(); //An array to store all the board recognized by DGT LiveChess
|
|
var liveChessConnection: WebSocket; //Connection Object to LiveChess through websocket
|
|
var isLiveChessConnected = false; //Used to track if a board there is a connection to DGT Live Chess
|
|
var currentSerialnr = '0'; //Public property to store the current serial number of the DGT Board in case there is more than one
|
|
//subscription stores the information about the board being connected, most importantly the serialnr
|
|
var subscription = { "id": 2, "call": "subscribe", "param": { "feed": "eboardevent", "id": 1, "param": { "serialnr": "" } } };
|
|
/***
|
|
* Bind console output to HTML pre Element
|
|
*/
|
|
rewireLoggingToElement(consoleOutput, root, true);
|
|
function rewireLoggingToElement(eleLocator: HTMLPreElement, eleOverflowLocator: HTMLDivElement, autoScroll: boolean) {
|
|
//Clear the console
|
|
eleLocator.innerHTML = "";
|
|
//Bind to all types of console messages
|
|
fixLoggingFunc('log');
|
|
fixLoggingFunc('debug');
|
|
fixLoggingFunc('warn');
|
|
fixLoggingFunc('error');
|
|
fixLoggingFunc('info');
|
|
fixLoggingFunc('table');
|
|
|
|
function fixLoggingFunc(name: string) {
|
|
console['old' + name] = console[name];
|
|
//Rewire function
|
|
console[name] = function () {
|
|
var output = "";
|
|
for (let i = 0; i < arguments.length; i++) {
|
|
let arg = arguments[i];
|
|
output += "<span class=\"log-" + (typeof arg) + " log-" + name + "\">";
|
|
if (
|
|
typeof arg === "object" &&
|
|
typeof JSON === "object" &&
|
|
typeof JSON.stringify === "function"
|
|
) {
|
|
output += JSON.stringify(arg);
|
|
} else {
|
|
output += arg;
|
|
}
|
|
output += "</span> ";
|
|
}
|
|
if (output != "*" && output != ":")
|
|
output += "<br>";
|
|
if (autoScroll) {
|
|
const isScrolledToBottom = eleOverflowLocator.scrollHeight - eleOverflowLocator.clientHeight <= eleOverflowLocator.scrollTop + 1;
|
|
eleLocator.innerHTML += output;
|
|
if (isScrolledToBottom) {
|
|
eleOverflowLocator.scrollTop = eleOverflowLocator.scrollHeight - eleOverflowLocator.clientHeight;
|
|
}
|
|
} else {
|
|
eleLocator.innerHTML += output;
|
|
}
|
|
//Call original function
|
|
try {
|
|
console['old' + name].apply(undefined, arguments);
|
|
} catch {
|
|
console['olderror'].apply(undefined, ['Error when loggin']);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wait some time without blocking other code
|
|
*
|
|
* @param {number} ms - The number of milliseconds to sleep
|
|
*/
|
|
function sleep(ms: number = 0) {
|
|
return new Promise(r => setTimeout(r, ms));
|
|
}
|
|
|
|
|
|
/**
|
|
* GET /api/account
|
|
*
|
|
* Get my profile
|
|
*
|
|
* Shows Public informations about the logged in user.
|
|
*
|
|
* Example
|
|
* {"id":"andrescavallin","username":"andrescavallin","online":true,"perfs":{"blitz":{"games":0,"rating":1500,"rd":350,"prog":0,"prov":true},"bullet":{"games":0,"rating":1500,"rd":350,"prog":0,"prov":true},"correspondence":{"games":0,"rating":1500,"rd":350,"prog":0,"prov":true},"classical":{"games":0,"rating":1500,"rd":350,"prog":0,"prov":true},"rapid":{"games":0,"rating":1500,"rd":350,"prog":0,"prov":true}},"createdAt":1599930231644,"seenAt":1599932744930,"playTime":{"total":0,"tv":0},"language":"en-US","url":"http://localhost:9663/@/andrescavallin","nbFollowing":0,"nbFollowers":0,"count":{"all":0,"rated":0,"ai":0,"draw":0,"drawH":0,"loss":0,"lossH":0,"win":0,"winH":0,"bookmark":0,"playing":0,"import":0,"me":0},"followable":true,"following":false,"blocking":false,"followsYou":false}
|
|
* */
|
|
function getProfile() {
|
|
//Log intention
|
|
if (verbose) console.log('getProfile - About to call /api/account');
|
|
fetch('/api/account', {
|
|
headers: { 'Authorization': 'Bearer ' + token }
|
|
}).then(r => r.json()).then(
|
|
data => {
|
|
//Log raw data received
|
|
if (verbose) console.log('/api/account Response:' + JSON.stringify(data));
|
|
//Diplay Title + UserName . Title may be undefined
|
|
console.log("\n");
|
|
console.log("┌─────────────────────────────────────────────────────┐");
|
|
console.log("│ " + (typeof (data.title) == "undefined" ? '' : data.title) + ' ' + data.username);
|
|
//Display performance ratings
|
|
console.table(data.perfs);
|
|
//Store my profile
|
|
me = data;
|
|
})
|
|
.catch(
|
|
err => { console.error('getProfile - Error. ' + err.message) }
|
|
);
|
|
}
|
|
|
|
/**
|
|
GET /api/stream/event
|
|
Stream incoming events
|
|
|
|
Stream the events reaching a lichess user in real time as ndjson.
|
|
|
|
Each line is a JSON object containing a type field. Possible values are:
|
|
|
|
challenge Incoming challenge
|
|
gameStart Start of a game
|
|
When the stream opens, all current challenges and games are sent.
|
|
|
|
Examples:
|
|
{"type":"gameStart","game":{"id":"kjKzl2MO"}}
|
|
{"type":"challenge","challenge":{"id":"WTr3JNcm","status":"created","challenger":{"id":"andrescavallin","name":"andrescavallin","title":null,"rating":1362,"provisional":true,"online":true,"lag":3},"destUser":{"id":"godking666","name":"Godking666","title":null,"rating":1910,"online":true,"lag":3},"variant":{"key":"standard","name":"Standard","short":"Std"},"rated":false,"speed":"rapid","timeControl":{"type":"clock","limit":900,"increment":10,"show":"15+10"},"color":"white","perf":{"icon":"#","name":"Rapid"}}}
|
|
*/
|
|
async function connectToEventStream() {
|
|
//Log intention
|
|
if (verbose) console.log('connectToEventStream - About to call /api/stream/event');
|
|
const response = await fetch('/api/stream/event', {
|
|
headers: { 'Authorization': 'Bearer ' + token }
|
|
});
|
|
const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader();
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
if (verbose) console.log('connectToEventStream - Chunk received', value);
|
|
//Update connection status
|
|
eventSteamStatus = { connected: true, lastEvent: time.getTime() };
|
|
//Response may contain several JSON objects on the same chunk separated by \n . This may create an empty element at the end.
|
|
var jsonArray = value.split('\n');
|
|
for (let i = 0; i < jsonArray.length; i++) {
|
|
//Skip empty elements that may have happened witht the .split('\n')
|
|
if (jsonArray[i].length > 2) {
|
|
try {
|
|
var data = JSON.parse(jsonArray[i]);
|
|
//JSON data found, let's check if this is a game that started. field type is mandatory except on http 4xx
|
|
if (data.type == "gameStart") {
|
|
if (verbose) console.log('connectToEventStream - gameStart event arrived. GameId: ' + data.game.id);
|
|
try {
|
|
//Connect to that game's stream
|
|
connectToGameStream(data.game.id);
|
|
}
|
|
catch (error) {
|
|
//This will trigger if connectToGameStream fails
|
|
console.error('connectToEventStream - Failed to connect to game stream. ' + Error(error).message);
|
|
}
|
|
}
|
|
else if (data.type == "challenge") {
|
|
//Challenge received
|
|
//TODO
|
|
}
|
|
else if (response.status >= 400) {
|
|
console.warn('connectToEventStream - ' + data.error);
|
|
}
|
|
}
|
|
catch (error) {
|
|
console.error('connectToEventStream - Unable to parse JSON or Unexpected error. ' + Error(error).message);
|
|
}
|
|
}
|
|
else {
|
|
//Signal that some empty message arrived. This is normal to keep the connection alive.
|
|
if (verbose) console.log("*"); //process.stdout.write("*"); Replace to support browser
|
|
}
|
|
}
|
|
}
|
|
|
|
console.warn('connectToEventStream - Event Stream ended by server');
|
|
//Update connection status
|
|
eventSteamStatus = { connected: false, lastEvent: time.getTime() };
|
|
}
|
|
|
|
/**
|
|
Stream Board game state
|
|
|
|
GET /api/board/game/stream/{gameId}
|
|
|
|
Stream the state of a game being played with the Board API, as ndjson.
|
|
Use this endpoint to get updates about the game in real-time, with a single request.
|
|
Each line is a JSON object containing a type field. Possible values are:
|
|
|
|
gameFull Full game data. All values are immutable, except for the state field.
|
|
gameState Current state of the game. Immutable values not included. Sent when a move is played, a draw is offered, or when the game ends.
|
|
chatLine Chat message sent by a user in the room "player" or "spectator".
|
|
The first line is always of type gameFull.
|
|
|
|
Examples:
|
|
|
|
New Game
|
|
{"id":"972RKuuq","variant":{"key":"standard","name":"Standard","short":"Std"},"clock":{"initial":900000,"increment":10000},"speed":"rapid","perf":{"name":"Rapid"},"rated":false,"createdAt":1586647003562,"white":{"id":"godking666","name":"Godking666","title":null,"rating":1761},"black":{"id":"andrescavallin","name":"andrescavallin","title":null,"rating":1362,"provisional":true},"initialFen":"startpos","type":"gameFull","state":{"type":"gameState","moves":"e2e4","wtime":900000,"btime":900000,"winc":10000,"binc":10000,"wdraw":false,"bdraw":false,"status":"started"}}
|
|
First Move
|
|
{"type":"gameState","moves":"e2e4","wtime":900000,"btime":900000,"winc":10000,"binc":10000,"wdraw":false,"bdraw":false,"status":"started"}
|
|
Middle Game
|
|
{"type":"gameState","moves":"e2e4 c7c6 g1f3 d7d5 e4e5 c8f5 d2d4 e7e6 h2h3 f5e4 b1d2 f8b4 c2c3 b4a5 d2e4 d5e4 f3d2 d8h4 g2g3 h4e7 d2e4 e7d7 e4d6 e8f8 d1f3 g8h6 c1h6 h8g8 h6g5 a5c7 e1c1 c7d6 e5d6 d7d6 g5f4 d6d5 f3d5 c6d5 f4d6 f8e8 d6b8 a8b8 f1b5 e8f8 h1e1 f8e7 d1d3 a7a6 b5a4 g8c8 a4b3 b7b5 b3d5 e7f8","wtime":903960,"btime":847860,"winc":10000,"binc":10000,"wdraw":false,"bdraw":false,"status":"started"}
|
|
After reconnect
|
|
{"id":"ZQDjy4sa","variant":{"key":"standard","name":"Standard","short":"Std"},"clock":{"initial":900000,"increment":10000},"speed":"rapid","perf":{"name":"Rapid"},"rated":true,"createdAt":1586643869056,"white":{"id":"gg60","name":"gg60","title":null,"rating":1509},"black":{"id":"andrescavallin","name":"andrescavallin","title":null,"rating":1433,"provisional":true},"initialFen":"startpos","type":"gameFull","state":{"type":"gameState","moves":"e2e4 c7c6 g1f3 d7d5 e4e5 c8f5 d2d4 e7e6 h2h3 f5e4 b1d2 f8b4 c2c3 b4a5 d2e4 d5e4 f3d2 d8h4 g2g3 h4e7 d2e4 e7d7 e4d6 e8f8 d1f3 g8h6 c1h6 h8g8 h6g5 a5c7 e1c1 c7d6 e5d6 d7d6 g5f4 d6d5 f3d5 c6d5 f4d6 f8e8 d6b8 a8b8 f1b5 e8f8 h1e1 f8e7 d1d3 a7a6 b5a4 g8c8 a4b3 b7b5 b3d5 e7f8 d5b3 a6a5 a2a3 a5a4 b3a2 f7f6 e1e6 f8f7 e6b6","wtime":912940,"btime":821720,"winc":10000,"binc":10000,"wdraw":false,"bdraw":false,"status":"resign","winner":"white"}}
|
|
Draw Offered
|
|
{"type":"gameState","moves":"e2e4 c7c6","wtime":880580,"btime":900000,"winc":10000,"binc":10000,"wdraw":false,"bdraw":true,"status":"started"}
|
|
After draw accepted
|
|
{"type":"gameState","moves":"e2e4 c7c6","wtime":865460,"btime":900000,"winc":10000,"binc":10000,"wdraw":false,"bdraw":false,"status":"draw"}
|
|
Out of Time
|
|
{"type":"gameState","moves":"e2e3 e7e5","wtime":0,"btime":900000,"winc":10000,"binc":10000,"wdraw":false,"bdraw":false,"status":"outoftime","winner":"black"}
|
|
Mate
|
|
{"type":"gameState","moves":"e2e4 e7e5 f1c4 d7d6 d1f3 b8c6 f3f7","wtime":900480,"btime":907720,"winc":10000,"binc":10000,"wdraw":false,"bdraw":false,"status":"mate"}
|
|
Promotion
|
|
{"type":"gameState","moves":"e2e4 b8c6 g1f3 c6d4 f1c4 e7e5 d2d3 d7d5 f3d4 f7f6 c4d5 f6f5 f2f3 g7g6 e1g1 c7c6 d5b3 d8d5 e4d5 a8b8 d4e6 f8b4 e6c7 e8e7 d5d6 e7f6 d6d7 b4f8 d7d8q","wtime":2147483647,"btime":2147483647,"winc":0,"binc":0,"wdraw":false,"bdraw":false,"status":"started"}
|
|
@param {string} gameId - The alphanumeric identifier of the game to be tracked
|
|
*/
|
|
|
|
async function connectToGameStream(gameId: string) {
|
|
//Log intention
|
|
if (verbose) console.log('connectToGameStream - About to call /api/board/game/stream/' + gameId);
|
|
const response = await fetch('/api/board/game/stream/' + gameId, {
|
|
headers: { 'Authorization': 'Bearer ' + token }
|
|
});
|
|
const reader = response.body?.pipeThrough(new TextDecoderStream()).getReader();
|
|
while (reader) { //while (true)
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
//Log raw data received
|
|
if (verbose) console.log('connectToGameStream - board game stream recevied:', value);
|
|
//Update connection status
|
|
gameConnectionMap.set(gameId, { connected: true, lastEvent: time.getTime() });
|
|
//Response may contain several JSON objects on the same chunk separated by \n . This may create an empty element at the end.
|
|
var jsonArray = value!.split('\n');
|
|
for (let i = 0; i < jsonArray.length; i++) {
|
|
//Skip empty elements that may have happened witht the .split('\n')
|
|
if (jsonArray[i].length > 2) {
|
|
try {
|
|
var data = JSON.parse(jsonArray[i]);
|
|
//The first line is always of type gameFull.
|
|
if (data.type == "gameFull") {
|
|
if (!verbose) console.clear();
|
|
//Log game Summary
|
|
//logGameSummary(data);
|
|
//Store game inmutable information on the gameInfoMap dictionary collection
|
|
gameInfoMap.set(gameId, data);
|
|
//Store game state on the gameStateMap dictionary collection
|
|
gameStateMap.set(gameId, data.state);
|
|
//Update the ChessBoard to the ChessBoard Map
|
|
initializeChessBoard(gameId, data);
|
|
//Log the state. Note that we are doing this after storing the state and initializing the chessops board
|
|
logGameState(gameId);
|
|
//Call chooseCurrentGame to determine if this stream will be the new current game
|
|
chooseCurrentGame();
|
|
}
|
|
else if (data.type == "gameState") {
|
|
if (!verbose) console.clear();
|
|
//Update the ChessBoard Map
|
|
updateChessBoard(gameId, gameStateMap.get(gameId), data);
|
|
//Update game state with most recent state
|
|
gameStateMap.set(gameId, data);
|
|
//Log the state. Note that we are doing this after storing the state and updating the chessops board
|
|
logGameState(gameId);
|
|
}
|
|
else if (data.type == "chatLine") {
|
|
//Received chat line
|
|
//TODO
|
|
}
|
|
else if (response.status >= 400) {
|
|
console.log('connectToGameStream - ' + data.error);
|
|
}
|
|
}
|
|
catch (error) {
|
|
console.error('connectToGameStream - No valid game data or Unexpected error. ' + Error(error).message);
|
|
}
|
|
}
|
|
else {
|
|
//Signal that some empty message arrived
|
|
if (verbose) console.log(":"); //process.stdout.write(":"); Changed to support browser
|
|
}
|
|
}
|
|
}
|
|
//End Stream output.end();
|
|
console.warn('connectToGameStream - Game ' + gameId + ' Stream ended.');
|
|
//Update connection state
|
|
gameConnectionMap.set(gameId, { connected: false, lastEvent: time.getTime() });
|
|
}
|
|
|
|
/**
|
|
* Return a string representation of the remaining time on the clock
|
|
*
|
|
* @param {number} timer - Numeric representation of remaining time
|
|
*
|
|
* @returns {String} - String representation of numeric time
|
|
*/
|
|
function formattedTimer(timer: number): string {
|
|
// Pad function to pad with 0 to 2 or 3 digits, default is 2
|
|
var pad = (n: number, z = 2) => (`00${n}`).slice(-z);
|
|
return pad(timer / 3.6e6 | 0) + ':' + pad((timer % 3.6e6) / 6e4 | 0) + ':' + pad((timer % 6e4) / 1000 | 0) //+ '.' + pad(timer % 1000, 3);
|
|
}
|
|
|
|
/**
|
|
* mainLoop() is a function that tries to keep the streams connected at all times, up to a maximum of 20 retries
|
|
*/
|
|
async function lichessConnectionLoop() {
|
|
//Program ends after 20 re-connection attempts
|
|
for (let attempts = 0; attempts < 20; attempts++) {
|
|
//Connect to main event stream
|
|
connectToEventStream();
|
|
//On the first time, if there are no games, it may take several seconds to receive data so lets wait a bit. Also give some time to connect to started games
|
|
await sleep(5000);
|
|
//Now enter a loop to monitor the connection
|
|
do {
|
|
//sleep 5 seconds and just listen to events
|
|
await sleep(5000);
|
|
//Check if any started games are disconnected
|
|
for (let [gameId, networkState] of gameConnectionMap) {
|
|
if (!networkState.connected && gameStateMap.get(gameId).status == "started") {
|
|
//Game is not conencted and has not finished, reconnect
|
|
if (verbose) console.log(`Started game is disconnected. Attempting reconnection for gameId: ${gameId}`);
|
|
connectToGameStream(gameId);
|
|
}
|
|
}
|
|
}
|
|
while (eventSteamStatus.connected)
|
|
//This means event stream is not connected
|
|
console.warn("No connection to event stream. Attempting re-connection. Attempt: " + attempts);
|
|
}
|
|
console.error("No connection to event stream after maximum number of attempts 20. Reload page to start again.");
|
|
|
|
}
|
|
|
|
/**
|
|
* This function will update the currentGameId with a valid active game
|
|
* and then will attach this game to the DGT Board
|
|
* It requires that all maps are up to date: gameInfoMap, gameStateMap, gameConnectionMap and gameChessBoardMap
|
|
*/
|
|
async function chooseCurrentGame() {
|
|
//Determine new value for currentGameId. First create an array with only the started games
|
|
//So then there is none or more than one started game
|
|
var playableGames = playableGamesArray();
|
|
//If there is only one started game, then its easy
|
|
if (playableGames.length == 1) {
|
|
currentGameId = playableGames[0].gameId;
|
|
attachCurrentGameIdToDGTBoard(); //Let the board know which color the player is actually playing and setup the position
|
|
console.log('Active game updated. currentGameId: ' + currentGameId);
|
|
}
|
|
else if (playableGames.length == 0) {
|
|
console.log('No started playable games, challenges or games are disconnected. Please start a new game or fix connection.');
|
|
//TODO What happens if the games reconnect and this move is not sent?
|
|
}
|
|
else {
|
|
console.table(playableGames);
|
|
//TODO
|
|
//If more than one game is playable player must pick one.
|
|
//for now alert user
|
|
console.warn('Currently only one game is supported. Ability to select game coming soon. Defaulting to latest.')
|
|
let index = playableGames.length - 1;
|
|
currentGameId = playableGames[Number(index)].gameId;
|
|
//Command looks good for currentGameId
|
|
attachCurrentGameIdToDGTBoard(); //Let the board know which color the player is actually playing and setup the position
|
|
console.log('Active game updated. currentGameId: ' + currentGameId);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Initialize a ChessBoard when connecting or re-connecting to a game
|
|
*
|
|
* @param {string} gameId - The gameId of the game to store on the board
|
|
* @param {Object} data - The gameFull event from lichess.org
|
|
*/
|
|
function initializeChessBoard(gameId: string, data: { initialFen: string; state: { moves: string; }; }) {
|
|
try {
|
|
var initialFen: string = INITIAL_FEN;
|
|
if (data.initialFen != "startpos")
|
|
initialFen = data.initialFen;
|
|
var setup = parseFen(initialFen).unwrap();
|
|
var chess: Chess = Chess.fromSetup(setup).unwrap();
|
|
var moves = data.state.moves.split(' ');
|
|
for (let i = 0; i < moves.length; i++) {
|
|
if (moves[i] != '') {
|
|
//Make any move that may have been already played on the ChessBoard. Useful when reconnecting
|
|
var uciMove = parseUci(moves[i]);
|
|
if (uciMove && chess.isLegal(uciMove))
|
|
chess.play(uciMove);
|
|
}
|
|
}
|
|
//Store the ChessBoard on the ChessBoardMap
|
|
gameChessBoardMap.set(gameId, chess);
|
|
if (verbose) console.log(`initializeChessBoard - New Board for gameId: ${gameId}`);
|
|
if (verbose) console.log(board(chess.board));
|
|
if (verbose) console.log(chess.turn + "'s turn");
|
|
}
|
|
catch (error) {
|
|
console.error(`initializeChessBoard - Error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the ChessBoard for the specified gameId with the new moves on newState since the last stored state
|
|
*
|
|
* @param {string} gameId - The gameId of the game to store on the board
|
|
* @param {Object} currentState - The state stored on the gameStateMap
|
|
* @param {Object} newState - The new state not yet stored
|
|
*/
|
|
function updateChessBoard(gameId: string, currentState: { moves: string }, newState: { moves: string; }) {
|
|
try {
|
|
var chess = gameChessBoardMap.get(gameId);
|
|
if (chess) {
|
|
var pendingMoves: string
|
|
if (!currentState.moves) {
|
|
//No prior moves. Use the new moves
|
|
pendingMoves = newState.moves;
|
|
}
|
|
else {
|
|
//Get all the moves on the newState that are not present on the currentState
|
|
pendingMoves = newState.moves.substring(currentState.moves.length, newState.moves.length);
|
|
}
|
|
var moves = pendingMoves.split(' ');
|
|
for (let i = 0; i < moves.length; i++) {
|
|
if (moves[i] != '') {
|
|
//Make the new move
|
|
var uciMove = parseUci(moves[i]);
|
|
if (uciMove && chess.isLegal(uciMove)) {
|
|
//This is a good chance to get the move in SAN format
|
|
if (chess.turn == 'black')
|
|
lastSanMove = { player: 'black', move: makeSan(chess, uciMove), by: gameInfoMap.get(currentGameId).black.id };
|
|
else
|
|
lastSanMove = { player: 'white', move: makeSan(chess, uciMove), by: gameInfoMap.get(currentGameId).white.id };
|
|
chess.play(uciMove);
|
|
}
|
|
}
|
|
}
|
|
//Store the ChessBoard on the ChessBoardMap
|
|
if (verbose) console.log(`updateChessBoard - Updated Board for gameId: ${gameId}`);
|
|
if (verbose) console.log(board(chess.board));
|
|
if (verbose) console.log(chess.turn + "'s turn");
|
|
}
|
|
}
|
|
catch (error) {
|
|
console.error(`updateChessBoard - Error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Utility function to update which color is being played with the board
|
|
*/
|
|
function attachCurrentGameIdToDGTBoard() {
|
|
//Every times a new game is connected clear the console
|
|
consoleOutput.innerHTML = "";
|
|
//
|
|
if (me.id == gameInfoMap.get(currentGameId).white.id)
|
|
currentGameColor = 'white';
|
|
else
|
|
currentGameColor = 'black';
|
|
//Send the position to LiveChess for synchronization
|
|
sendBoardToLiveChess(gameChessBoardMap.get(currentGameId)!);
|
|
}
|
|
|
|
/**
|
|
* Iterate the gameConnectionMap dictionary and return an arrays containing only the games that can be played with the board
|
|
* @returns {Array} - Array containing a summary of playable games
|
|
*/
|
|
function playableGamesArray(): Array<{
|
|
'gameId': string,
|
|
'versus': string,
|
|
'vs rating': string,
|
|
'game rating': string,
|
|
'Timer': string,
|
|
'Last Move': string
|
|
}> {
|
|
var playableGames: Array<{
|
|
'gameId': string,
|
|
'versus': string,
|
|
'vs rating': string,
|
|
'game rating': string,
|
|
'Timer': string,
|
|
'Last Move': string
|
|
}> = [];
|
|
var keys = Array.from(gameConnectionMap.keys());
|
|
//The for each iterator is not used since we don't want to continue execution. We want a syncrhonous result
|
|
//for (let [gameId, networkState] of gameConnectionMap) {
|
|
// if (gameConnectionMap.get(gameId).connected && gameStateMap.get(gameId).status == "started") {
|
|
for (var i = 0; i < keys.length; i++) {
|
|
if (gameConnectionMap.get(keys[i])?.connected && gameStateMap.get(keys[i])?.status == "started") {
|
|
//Game is good for commands
|
|
var gameInfo = gameInfoMap.get(keys[i]);
|
|
//var gameState = gameStateMap.get(keys[i]);
|
|
var lastMove = getLastUCIMove(keys[i]);
|
|
var versus = (gameInfo.black.id == me.id) ? ((gameInfo.white.title !== null) ? gameInfo.white.title : '@') + ' ' + gameInfo.white.name : ((gameInfo.black.title !== null) ? gameInfo.black.title : '@') + ' ' + gameInfo.black.name;
|
|
playableGames.push({
|
|
'gameId': gameInfo.id,
|
|
'versus': versus,
|
|
'vs rating': (gameInfo.black.id == me.id) ? gameInfo.white.rating : gameInfo.black.rating,
|
|
'game rating': gameInfo.variant.short + ' ' + (gameInfo.rated ? 'rated' : 'unrated'),
|
|
'Timer': gameInfo.speed + ' ' + ((gameInfo.clock !== null) ? (String(gameInfo.clock.initial / 60000) + "'+" + String(gameInfo.clock.increment / 1000) + "''") : '∞'),
|
|
'Last Move': lastMove.player + ' ' + lastMove.move + ' by ' + lastMove.by
|
|
})
|
|
}
|
|
}
|
|
return playableGames;
|
|
}
|
|
|
|
/**
|
|
* Display the state as stored in the Dictionary collection
|
|
*
|
|
* @param {string} gameId - The alphanumeric identifier of the game for which state is going to be shown
|
|
*/
|
|
function logGameState(gameId: string) {
|
|
if (gameStateMap.has(gameId) && gameInfoMap.has(gameId)) {
|
|
var gameInfo = gameInfoMap.get(gameId);
|
|
var gameState = gameStateMap.get(gameId);
|
|
var lastMove = getLastUCIMove(gameId);
|
|
console.log(""); //process.stdout.write("\n"); Changed to support browser
|
|
console.table({
|
|
'Title': { white: ((gameInfo.white.title !== null) ? gameInfo.white.title : '@'), black: ((gameInfo.black.title !== null) ? gameInfo.black.title : '@'), game: 'Id: ' + gameInfo.id },
|
|
'Username': { white: gameInfo.white.name, black: gameInfo.black.name, game: 'Status: ' + gameState.status },
|
|
'Rating': { white: gameInfo.white.rating, black: gameInfo.black.rating, game: gameInfo.variant.short + ' ' + (gameInfo.rated ? 'rated' : 'unrated') },
|
|
'Timer': { white: formattedTimer(gameState.wtime), black: formattedTimer(gameState.btime), game: gameInfo.speed + ' ' + ((gameInfo.clock !== null) ? (String(gameInfo.clock.initial / 60000) + "'+" + String(gameInfo.clock.increment / 1000) + "''") : '∞') },
|
|
'Last Move': { white: (lastMove.player == 'white' ? lastMove.move : '?'), black: (lastMove.player == 'black' ? lastMove.move : '?'), game: lastMove.player },
|
|
});
|
|
switch (gameState.status) {
|
|
case "started":
|
|
//Announce the last move
|
|
if (me.id !== lastMove.by || announceAllMoves) {
|
|
announcePlay(lastMove, gameState.wtime, gameState.btime);
|
|
}
|
|
break;
|
|
case "outoftime":
|
|
announceWinner(keywords[gameState.winner], 'flag', keywords[gameState.winner] + ' ' + keywords['wins by'] + ' ' + keywords['timeout']);
|
|
break;
|
|
case "resign":
|
|
announceWinner(keywords[gameState.winner], 'resign', keywords[gameState.winner] + ' ' + keywords['wins by'] + ' ' + keywords['resignation']);
|
|
break;
|
|
case "mate":
|
|
announceWinner(keywords[lastMove.player], 'mate', keywords[lastMove.player] + ' ' + keywords['wins by'] + ' ' + keywords['#']);
|
|
break;
|
|
case "draw":
|
|
announceWinner('draw', 'draw', keywords['(=)']);
|
|
break;
|
|
default:
|
|
console.log(`Unknown status received: ${gameState.status}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Peeks a game state and calculates who played the last move and what move it was
|
|
*
|
|
* @param {string} gameId - The alphanumeric identifier of the game where the last move is going to be calculated
|
|
*
|
|
* @return {Object} - The move in JSON
|
|
*/
|
|
function getLastUCIMove(gameId: string): { player: string, move: string, by: string } {
|
|
if (gameStateMap.has(gameId) && gameInfoMap.has(gameId)) {
|
|
var gameInfo = gameInfoMap.get(gameId);
|
|
var gameState = gameStateMap.get(gameId);
|
|
//This is the original code that does not used chessops objects and can be used to get the UCI move but not SAN.
|
|
if (String(gameState.moves).length > 1) {
|
|
var moves = gameState.moves.split(' ');
|
|
if (verbose) console.log(`getLastUCIMove - ${moves.length} moves detected. Last one: ${moves[moves.length - 1]}`);
|
|
if (moves.length % 2 == 0)
|
|
return { player: 'black', move: moves[moves.length - 1], by: gameInfo.black.id };
|
|
else
|
|
return { player: 'white', move: moves[moves.length - 1], by: gameInfo.white.id };
|
|
}
|
|
}
|
|
if (verbose) console.log('getLastUCIMove - No moves.');
|
|
return { player: 'none', move: 'none', by: 'none' };
|
|
}
|
|
|
|
/**
|
|
* Fedback the user about the detected moved
|
|
*
|
|
* @param lastMove JSON object with the move information
|
|
* @param wtime Remaining time for white
|
|
* @param btime Remaining time for black
|
|
*/
|
|
function announcePlay(lastMove: { player: string, move: string, by: string }) {
|
|
//ttsSay(lastMove.player);
|
|
//Now play it using text to speech library
|
|
var moveText: string;
|
|
if (announceMoveFormat && announceMoveFormat.toLowerCase() == "san" && lastSanMove) {
|
|
moveText = lastSanMove.move;
|
|
ttsSay(raplaceKeywords(lastSanMove.move));
|
|
}
|
|
else {
|
|
moveText = lastMove.move;
|
|
ttsSay(lastMove.move);
|
|
}
|
|
if (lastMove.player == 'white') {
|
|
console.log('<span class="dgt-white-move">' + moveText + ' by White' + '</span>');
|
|
}
|
|
else {
|
|
console.log('<span class="dgt-black-move">' + moveText + ' by Black' + '</span>');
|
|
}
|
|
//TODO
|
|
//Give feedback on running out of time
|
|
}
|
|
|
|
function announceWinner(winner: string, status: string, message: string) {
|
|
if (winner == 'white') {
|
|
console.log(' ' + status + ' - ' + message);
|
|
}
|
|
else {
|
|
console.log(' ' + status + ' - ' + message);
|
|
}
|
|
//Now play message using text to speech library
|
|
ttsSay(message);
|
|
}
|
|
|
|
function announceInvalidMove() {
|
|
if (currentGameColor == 'white') {
|
|
console.warn(' [ X X ] - Illegal move by white.')
|
|
}
|
|
else {
|
|
console.warn(' [ X X ] - Illegal move by black.')
|
|
}
|
|
//Now play it using text to speech library
|
|
ttsSay('Illegal Move');
|
|
}
|
|
|
|
|
|
async function connectToLiveChess() {
|
|
var SANMove: string; //a move in san format returned by liveChess
|
|
//Open the WebSocket
|
|
liveChessConnection = new WebSocket((liveChessURL) ? liveChessURL : "ws://localhost:1982/api/v1.0");
|
|
|
|
//Attach Events
|
|
liveChessConnection.onopen = () => {
|
|
isLiveChessConnected = true;
|
|
if (verbose) console.info("Websocket onopen: Connection to LiveChess was sucessful")
|
|
liveChessConnection.send('{"id":1,"call":"eboards"}');
|
|
}
|
|
|
|
liveChessConnection.onerror = () => {
|
|
console.error("Websocket ERROR: ");
|
|
}
|
|
|
|
liveChessConnection.onclose = () => {
|
|
console.error('Websocket to LiveChess disconnected');
|
|
//Clear the value of current serial number this serves as a diconnected status
|
|
currentSerialnr = '0';
|
|
//Set connection state to false
|
|
isLiveChessConnected = false;
|
|
};
|
|
|
|
liveChessConnection.onmessage = async e => {
|
|
if (verbose) console.info('Websocket onmessage with data:' + e.data);
|
|
var message = JSON.parse(e.data);
|
|
if (message.response == 'call' && message.id == '1') {
|
|
//Get the list of availble boards on LiveChess
|
|
boards = message.param;
|
|
console.table(boards)
|
|
if (verbose) console.info(boards[0].serialnr)
|
|
//TODO
|
|
//we need to be able to handle more than one board
|
|
//for now using the first board found
|
|
//Update the base subscription message with the serial number
|
|
currentSerialnr = boards[0].serialnr;
|
|
subscription.param.param.serialnr = currentSerialnr;
|
|
if (verbose) console.info('Websocket onmessage[call]: board serial number updated to: ' + currentSerialnr);
|
|
if (verbose) console.info('Webscoket - about to send the following message \n' + JSON.stringify(subscription));
|
|
liveChessConnection.send(JSON.stringify(subscription))
|
|
//Check if the board is properly connected
|
|
if (boards[0].state != "ACTIVE" && boards[0].state != "INACTIVE") // "NOTRESPONDING" || "DELAYED"
|
|
console.error(`Board with serial ${currentSerialnr} is not properly connected. Please fix`);
|
|
//Send setup with stating position
|
|
|
|
if (gameStateMap.has(currentGameId) && gameConnectionMap.get(currentGameId).connected && gameStateMap.get(currentGameId).status == "started") {
|
|
//There is a game in progress, setup the board as per lichess board
|
|
if (currentGameId != DGTgameId) {
|
|
//We know we have not synchronized yet
|
|
if (verbose) console.info('There is a game in progress, calling liveChessBoardSetUp...');
|
|
sendBoardToLiveChess(gameChessBoardMap.get(currentGameId));
|
|
}
|
|
}
|
|
}
|
|
else if (message.response == 'feed' && !(!message.param.san)) {
|
|
//Received move from board
|
|
if (verbose) console.info('onmessage - san: ' + message.param.san)
|
|
//get last move known to lichess and avoid calling multiple times this function
|
|
var lastMove = getLastUCIMove(currentGameId);
|
|
if (message.param.san.length == 0) {
|
|
if (verbose) console.info('onmessage - san is empty')
|
|
}
|
|
else {
|
|
//A move was received
|
|
SANMove = String(message.param.san[message.param.san.length - 1]).trim();
|
|
if (verbose) console.info('onmessage - SANMove = ' + SANMove);
|
|
var moveObject: NormalMove; //a move in chessops format
|
|
moveObject = parseSan(localBoard, SANMove); //get move from DGT LiveChess
|
|
//if valid move on local chessops
|
|
if (localBoard.isLegal(moveObject)) {
|
|
if (verbose) console.info('onmessage - Move is legal');
|
|
//if received move.color == this.currentGameColor
|
|
if (localBoard.turn == currentGameColor) {
|
|
//This is a valid new move send it to lichess
|
|
if (verbose) console.info('onmessage - Valid Move played: ' + SANMove)
|
|
await validateAndSendBoardMove(moveObject);
|
|
//Update the lastSanMove
|
|
lastSanMove = { player: localBoard.turn , move: SANMove, by: me.id }
|
|
//Play the move on local board to keep it in sync
|
|
localBoard.play(moveObject);
|
|
}
|
|
else if (lastMove.move == makeUci(moveObject)) {
|
|
//This is a valid adjustment - Just making the move from Lichess
|
|
if (verbose) console.info('onmessage - Valid Adjustment: ' + SANMove);
|
|
//no need to send anything to Lichess moveObject required
|
|
//Play the move on local board to keep it in sync
|
|
localBoard.play(moveObject);
|
|
}
|
|
else {
|
|
//Invalid Adjustment. Move was legal but does not match last move received from Lichess
|
|
console.error('onmessage - Invalid Adjustment was made');
|
|
if (lastMove.move == makeUci(moveObject)) {
|
|
console.error('onmessage - Played move has not been received by Lichess.');
|
|
} else {
|
|
console.error('onmessage - Expected:' + lastMove.move + ' by ' + lastMove.player);
|
|
console.error('onmessage - Detected:' + makeUci(moveObject) + ' by ' + localBoard.turn);
|
|
}
|
|
announceInvalidMove();
|
|
await sleep(1000);
|
|
//Repeat last game state announcement
|
|
var gameState = gameStateMap.get(currentGameId);
|
|
announcePlay(lastMove, gameState.wtime, gameState.btime);
|
|
}
|
|
}
|
|
else {
|
|
//Move was valid on DGT Board but not ilegal on localBoard
|
|
if (verbose) console.info('onmessage - Move is NOT legal');
|
|
if (lastMove.move == makeUci(moveObject)) {
|
|
//This is fine, the same last move was received again and seems ilegal
|
|
if (verbose) console.warn('onmessage - Move received is the same as the last moved played: ' + SANMove)
|
|
}
|
|
else {
|
|
//Receiving a legal move on DGT Board but invalid move on localBoard signals a de-sycnhronization
|
|
if (verbose) console.error('onmessage - invalidMove - Position Mismatch between DGT Board and internal in memory Board . SAN: ' + SANMove);
|
|
announceInvalidMove();
|
|
console.info(board(localBoard.board));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (message.response == 'feed') {
|
|
//feed received but not san
|
|
//No moves received, this may be an out of snc problem or just the starting position
|
|
if (verbose) console.info('onmessage - No move received on feed event.');
|
|
//TODO THIS MAY REQUIRE RE-SYNCHRONIZATION BETWEEN LICHESS AND DGT BOARD
|
|
}
|
|
}
|
|
}
|
|
|
|
async function DGTliveChessConnectionLoop() {
|
|
//Attempt connection right away
|
|
connectToLiveChess();
|
|
//Program ends after 20 re-connection attempts
|
|
for (let attempts = 0; attempts < 20; attempts++) {
|
|
do {
|
|
//Just sleep five seconds while there is a valid currentSerialnr
|
|
await sleep(5000);
|
|
}
|
|
while (currentSerialnr != '0' && isLiveChessConnected)
|
|
//currentSerialnr is 0 so still no connection to board. Retry
|
|
if (!isLiveChessConnected) {
|
|
console.warn("No connection to DGT Live Chess. Attempting re-connection. Attempt: " + attempts);
|
|
connectToLiveChess();
|
|
}
|
|
else {
|
|
//Websocket is fine but still no board detected
|
|
console.warn("Connection to DGT Live Chess is Fine but no board is detected. Attempting re-connection. Attempt: " + attempts);
|
|
liveChessConnection.send('{"id":1,"call":"eboards"}');
|
|
}
|
|
}
|
|
console.error("No connection to DGT Live Chess after maximum number of attempts 20. Reload page to start again.");
|
|
}
|
|
|
|
/**
|
|
* Syncrhonizes the position on Lichess with the position on the board
|
|
* If the position does not match, no moves will be received from LiveChess
|
|
* @param chess - The chessops Chess object with the position on Lichess
|
|
*/
|
|
async function sendBoardToLiveChess(chess: Chess) {
|
|
var fen = makeFen(chess.toSetup());
|
|
var setupMessage = {
|
|
"id": 3,
|
|
"call": "call",
|
|
"param": {
|
|
"id": 1,
|
|
"method": "setup",
|
|
"param": {
|
|
"fen": fen
|
|
}
|
|
}
|
|
}
|
|
if (verbose) console.log("setUp -: " + JSON.stringify(setupMessage))
|
|
if (isLiveChessConnected) {
|
|
liveChessConnection.send(JSON.stringify(setupMessage))
|
|
//Store the gameId so we now we already synchronized
|
|
DGTgameId = currentGameId;
|
|
//Initialize localBoard too so it matched what was sent to LiveChess
|
|
localBoard = gameChessBoardMap.get(currentGameId).clone();
|
|
}
|
|
else {
|
|
console.error("WebSocket is not open - cannot send setup command.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This functions hanldes the sending the move to the right lichess game.
|
|
* If more than one game is being played, it will ask which game to connect to,
|
|
* thus waiting for user input and so, it becomes async
|
|
*
|
|
* @param {Object} boardMove - The move in chessops format or string if in lichess format
|
|
*/
|
|
async function validateAndSendBoardMove(boardMove: NormalMove) {
|
|
//While there is not and active game, keep trying to find one so the move is not lost
|
|
while (!(gameStateMap.has(currentGameId) && gameConnectionMap.get(currentGameId).connected && gameStateMap.get(currentGameId).status == "started")) {
|
|
//Wait a few seconds to see if the games reconnects or starts and give some space to other code to run
|
|
console.warn('validateAndSendBoardMove - Cannot send move while disconnected. Re-Trying in 2 seconds...')
|
|
await sleep(2000);
|
|
//Now attempt to select for which game is this command intented
|
|
await chooseCurrentGame();
|
|
}
|
|
//Now send the move
|
|
var command = makeUci(boardMove);
|
|
sendMove(currentGameId, command);
|
|
}
|
|
|
|
/**
|
|
* Make a Board move
|
|
*
|
|
* /api/board/game/{gameId}/move/{move}
|
|
*
|
|
* Make a move in a game being played with the Board API.
|
|
* The move can also contain a draw offer/agreement.
|
|
*
|
|
* @param {string} gameId - The gameId for the active game
|
|
* @param {string} uciMove - The move un UCI format
|
|
*/
|
|
function sendMove(gameId: string, uciMove: string) {
|
|
//prevent sending empty moves
|
|
if (uciMove.length > 1) {
|
|
//Log intention
|
|
//Automatically decline draws when making a move
|
|
var url = `/api/board/game/${gameId}/move/${uciMove}?offeringDraw=false`;
|
|
if (verbose) console.log('sendMove - About to call ' + url);
|
|
fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Authorization': 'Bearer ' + token }
|
|
}).then(response => {
|
|
try {
|
|
if (response.status == 200 || response.status == 201) {
|
|
//Move sucessfully sent
|
|
if (verbose) console.log('sendMove - Move sucessfully sent.');
|
|
}
|
|
else {
|
|
response.json().then((errorJson) => {
|
|
console.error('sendMove - Failed to send move. ' + errorJson.error);
|
|
})
|
|
}
|
|
}
|
|
catch (error) {
|
|
console.error('sendMove - Unexpected error. ' + error);
|
|
}
|
|
|
|
})
|
|
.catch(
|
|
err => { console.error('sendMove - Error. ' + err.message) }
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Replaces letters with full name of the pieces or move name
|
|
* @param sanMove The move in san format
|
|
*/
|
|
function raplaceKeywords(sanMove) {
|
|
var extendedSanMove = sanMove;
|
|
for (let i = 0; i < keywordsBase.length; i++) {
|
|
try {
|
|
extendedSanMove = extendedSanMove.replace(keywordsBase[i], ' ' + keywords[keywordsBase[i]] + ' ');
|
|
} catch (error) {
|
|
console.error(`raplaceKeywords - Error replacing keyword. ${keywordsBase[i]} . ${Error(error).message}`);
|
|
}
|
|
}
|
|
return extendedSanMove;
|
|
}
|
|
|
|
|
|
/**
|
|
* GLOBAL VATIABLES
|
|
*/
|
|
async function ttsSay(text: string) {
|
|
//Check Voice is disables
|
|
if (!speechSynthesisOn) return;
|
|
if (verbose) console.log('TTS - for text: ' + text);
|
|
var utterThis = new SpeechSynthesisUtterance(text);
|
|
var selectedOption = voice;
|
|
var availableVoices = speechSynthesis.getVoices();
|
|
for (let i = 0; i < availableVoices.length; i++) {
|
|
if (availableVoices[i].name === selectedOption) {
|
|
utterThis.voice = availableVoices[i];
|
|
break;
|
|
}
|
|
}
|
|
//utterThis.pitch = pitch.value;
|
|
utterThis.rate = .60;
|
|
speechSynthesis.speak(utterThis);
|
|
}
|
|
|
|
function start() {
|
|
console.log("");
|
|
console.log(" ,...., ▄████▄ ██░ ██ ▓█████ ██████ ██████ ");
|
|
console.log(" ,::::::< ▒██▀ ▀█ ▓██░ ██▒▓█ ▀ ▒██ ▒ ▒██ ▒ ");
|
|
console.log(" ,::/^\\\"``. ▒▓█ ▄ ▒██▀▀██░▒███ ░ ▓██▄ ░ ▓██▄ ");
|
|
console.log(" ,::/, ` e`. ▒▓▓▄ ▄██▒░▓█ ░██ ▒▓█ ▄ ▒ ██▒ ▒ ██▒ ");
|
|
console.log(" ,::; | '. ▒ ▓███▀ ░░▓█▒░██▓░▒████▒▒██████▒▒▒██████▒▒ ");
|
|
console.log(" ,::| \___,-. c) ░ ░▒ ▒ ░ ▒ ░░▒░▒░░ ▒░ ░▒ ▒▓▒ ▒ ░▒ ▒▓▒ ▒ ░ ");
|
|
console.log(" ;::| \\ '-' ░ ▒ ▒ ░▒░ ░ ░ ░ ░░ ░▒ ░ ░░ ░▒ ░ ░ ");
|
|
console.log(" ;::| \\ ░ ░ ░░ ░ ░ ░ ░ ░ ░ ░ ░ ");
|
|
console.log(" ;::| _.=`\\ ░ ░ ░ ░ ░ ░ ░ ░ ░ ");
|
|
console.log(" `;:|.=` _.=`\\ ░ ");
|
|
console.log(" '|_.=` __\\ ");
|
|
console.log(" `\\_..==`` / Lichess.org - DGT Electronic Board Connector ");
|
|
console.log(" .'.___.-'. Developed by Andres Cavallin and Juan Cavallin ");
|
|
console.log(" / \\ ");
|
|
console.log("jgs('--......--') ");
|
|
console.log(" /'--......--'\\ ");
|
|
console.log(" `\"--......--\"` ");
|
|
}
|
|
|
|
/**
|
|
* Show the profile and then
|
|
* Start the Main Loop
|
|
*/
|
|
start();
|
|
getProfile();
|
|
lichessConnectionLoop();
|
|
DGTliveChessConnectionLoop();
|
|
|
|
|
|
function startingPosition(): Chess {
|
|
return Chess.fromSetup(defaultSetup()).unwrap();
|
|
//const setup = parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1").unwrap();
|
|
//return Chess.fromSetup(setup).unwrap();
|
|
}
|
|
|
|
}
|