paco_sako/js/chess.js

830 lines
25 KiB
JavaScript

'use strict';
var gun = Gun({
peers: ['https://jessemcdonald.info/gun'],
/* workaround for persistent errors accumulating which break synchronization */
localStorage: false,
});
let initialBoard = (function (){
var init = JSON.stringify({
'light': [
'rnbqkbnr',
'pppppppp',
' ',
' ',
' ',
' ',
' ',
' ',
],
'dark': [
' ',
' ',
' ',
' ',
' ',
' ',
'pppppppp',
'rnbqkbnr',
],
'player': 'light',
'timestamp': new Date().getTime(),
});
return function (){ return JSON.parse(init); }
})();
function cloneJSON(obj){
return JSON.parse(JSON.stringify(obj));
}
function isLight(side){
return side[0] === 'l';
}
function isDark(side){
return side[0] === 'd';
}
function normalizeSide(side){
return isDark(side) ? 'dark' : 'light';
}
function otherSide(side){
return isDark(side) ? 'light' : 'dark';
}
function boardGet(board, where, side){
side = normalizeSide(side);
if (where === 'phantom') {
if (!board['phantom'] || board['phantom']['type'][1] !== side[0]) {
return ' ';
} else {
return board['phantom']['type'][0];
}
} else {
var column = 'abcdefgh'.indexOf(where[0]);
var row = Number(where[1]) - 1;
return board[side][row][column];
}
}
function boardPut(board, where, side, piece){
side = isDark(side) ? 'dark' : 'light';
var column = 'abcdefgh'.indexOf(where[0]);
var row = Number(where[1]) - 1;
var data = board[side][row];
var prefix = data.substring(0, column);
var suffix = data.substring(column + 1, 8);
board[side][row] = prefix + piece + suffix;
}
function countMoves(board) {
var n = 0;
while (board && board['prior']) {
if (board['prior']['player'] !== board['player']) {
++n;
}
board = board['prior'];
}
return n;
}
function offsetSquare(from, columnsRight, rowsUp){
var column = 'abcdefgh'.indexOf(from[0]);
var row = Number(from[1]) - 1;
if (column < 0 || column > 7 || row < 0 || row > 7) {
return null;
}
column += columnsRight;
row += rowsUp;
if (column < 0 || column > 7 || row < 0 || row > 7) {
return null;
}
return 'abcdefgh'[column] + String(row + 1);
}
function validDestination(board, side, where, canCapture){
var ours = boardGet(board, where, side);
var theirs = boardGet(board, where, otherSide(side));
return ((theirs === ' ') ? (ours === ' ') : canCapture);
}
function scanPath(accum, board, side, from, canCapture, columnsLeft, rowsUp, remainder){
while (true) {
var there = offsetSquare(from, columnsLeft, rowsUp);
if (!there || !validDestination(board, side, there, canCapture)) { return; }
accum.push(there);
if (boardGet(board, there, otherSide(side)) !== ' ' || remainder < 1) { return; }
from = there;
remainder -= 1;
}
}
function hasMoved(board, side, where){
side = normalizeSide(side);
while (true) {
if (!board) {
return false;
}
const move = board['move'];
if (move && move['side'] === side && move['to'] === where) {
return true;
}
board = board['prior'];
}
}
function legalMoves(board, side, type, from, canCapture){
if (board['move'] && board['move']['took'] === 'k') {
return [];
}
const ortho = [[-1, 0], [1, 0], [0, -1], [0, 1]];
const diag = [[-1, -1], [-1, 1], [1, -1], [1, 1]];
var legals = [];
if (from === 'phantom') {
from = board['phantom']['from'];
}
if (type === 'k') {
for (const dir of ortho.concat(diag)) {
scanPath(legals, board, side, from, false, dir[0], dir[1], 0);
}
if (from[0] === 'e' && from[1] === (isDark(side) ? '8' : '1')) {
/* check for castling conditions */
if (!hasMoved(board, side, from)) {
if (boardGet(board, 'f' + from[1], side) === ' ' &&
boardGet(board, 'g' + from[1], side) === ' ' &&
boardGet(board, 'h' + from[1], side) === 'r') {
if (!hasMoved(board, side, 'h' + from[1])) {
legals.push('g' + from[1]);
}
}
if (boardGet(board, 'd' + from[1], side) === ' ' &&
boardGet(board, 'c' + from[1], side) === ' ' &&
boardGet(board, 'b' + from[1], side) === ' ' &&
boardGet(board, 'a' + from[1], side) === 'r') {
if (!hasMoved(board, side, 'a' + from[1])) {
legals.push('c' + from[1]);
}
}
}
}
} else if (type === 'q') {
for (const dir of ortho.concat(diag)) {
scanPath(legals, board, side, from, canCapture, dir[0], dir[1], 8);
}
} else if (type === 'b') {
for (const dir of diag) {
scanPath(legals, board, side, from, canCapture, dir[0], dir[1], 8);
}
} else if (type === 'n') {
for (const there of [offsetSquare(from, -1, 2), offsetSquare(from, 1, 2),
offsetSquare(from, -2, 1), offsetSquare(from, 2, 1),
offsetSquare(from, -2, -1), offsetSquare(from, 2, -1),
offsetSquare(from, -1, -2), offsetSquare(from, 1, -2)]) {
if (there && validDestination(board, side, there, canCapture)) {
legals.push(there);
}
}
} else if (type === 'r') {
for (const dir of ortho) {
scanPath(legals, board, side, from, canCapture, dir[0], dir[1], 8);
}
} else if (type === 'p') {
const dark = isDark(side);
const forward = offsetSquare(from, 0, dark ? -1 : 1);
const forward2 = offsetSquare(from, 0, dark ? -2 : 2);
const diagL = offsetSquare(from, -1, dark ? -1 : 1);
const diagR = offsetSquare(from, 1, dark ? -1 : 1);
if (forward && validDestination(board, side, forward, false)) {
legals.push(forward);
if (dark ? (from[1] >= '7') : (from[1] <= '2')) {
if (forward2 && validDestination(board, side, forward2, false)) {
legals.push(forward2);
}
}
}
if (canCapture) {
for (const there of [diagL, diagR]) {
if (there) {
if (boardGet(board, there, otherSide(side)) !== ' ') {
legals.push(there);
} else {
var otherBoard = board;
while (otherBoard && otherBoard['move'] && otherBoard['move']['side'][0] === side[0]) {
otherBoard = otherBoard['prior'];
}
if (otherBoard && otherBoard['move']) {
const move = otherBoard['move'];
if (move['side'][0] !== side[0] && move['type'] === 'p') {
const from =
(move['from'] === 'phantom') ?
otherBoard['prior']['phantom']['from'] :
move['from'];
if (from[0] === there[0] && from[1] === forward2[1]) {
/* en passant */
legals.push(there);
}
}
}
}
}
}
}
}
return legals;
}
function movePiece(priorBoard, side, from, to){
var other = otherSide(side);
var type = boardGet(priorBoard, from, side);
var took = boardGet(priorBoard, to, other);
var replaced = boardGet(priorBoard, to, side);
var alongside = boardGet(priorBoard, from, other);
var actuallyFrom = (from === 'phantom') ? priorBoard['phantom']['from'] : from;
const legals = legalMoves(priorBoard, side, type, from, alongside === ' ');
if (!legals.includes(to)) {
return priorBoard;
}
var undoBoard = priorBoard;
if (undoBoard['subsequent']) {
undoBoard = cloneJSON(undoBoard);
delete undoBoard['subsequent'];
}
var board = cloneJSON(undoBoard);
board['prior'] = undoBoard;
board['timestamp'] = new Date().getTime();
board['move'] = {
'side': normalizeSide(side),
'type': type,
'from': from,
'to': to
};
if (took !== ' ') {
board['move']['took'] = took;
}
if (replaced !== ' ') {
board['move']['replaced'] = replaced;
}
if (alongside !== ' ') {
board['move']['alongside'] = alongside;
}
if (type === 'k' && actuallyFrom[0] === 'e' && to[0] === 'g') {
board['move']['castle'] = true;
boardPut(board, 'h' + actuallyFrom[1], side, ' ');
boardPut(board, 'f' + actuallyFrom[1], side, 'r');
}
if (type === 'k' && actuallyFrom[0] === 'e' && to[0] === 'c') {
board['move']['queen_castle'] = true;
boardPut(board, 'a' + actuallyFrom[1], side, ' ');
boardPut(board, 'd' + actuallyFrom[1], side, 'r');
}
if (type === 'p' && (isDark(side) ? (to[1] === '1') : (to[1] === '8'))) {
board['move']['promotion'] = 'q'; /* TODO: allow other choices */
type = 'q';
}
if (alongside === 'p' && (isDark(other) ? (to[1] === '1') : (to[1] === '8'))) {
board['move']['promotion'] = 'q'; /* TODO: allow other choices */
alongside = 'q';
}
if (type === 'p' && alongside === ' ' && to[0] !== actuallyFrom[0]) {
var otherBoard = priorBoard;
while (otherBoard && otherBoard['move'] && otherBoard['move']['side'][0] === side[0]) {
otherBoard = otherBoard['prior'];
}
if (otherBoard && otherBoard['move']) {
const move = otherBoard['move'];
if (move['type'] === 'p' && move['to'][0] === to[0] && move['to'][1] === actuallyFrom[1]) {
const moveFrom = (move['from'] === 'phantom') ? otherBoard['prior']['phantom']['from'] : move['from'];
if (move['side'][0] === other[0] && moveFrom[1] != to[1]) {
board['move']['en_passant'] = true;
alongside = 'p';
boardPut(board, move['to'], other, ' ');
}
}
}
}
if (from === 'phantom') {
delete board['phantom'];
} else {
boardPut(board, from, side, ' ');
boardPut(board, from, other, ' ');
}
boardPut(board, to, side, type);
if (alongside !== ' ') {
boardPut(board, to, other, alongside);
}
if (replaced === ' ') {
board['player'] = otherSide(board['player']);
} else {
board['phantom'] = { 'from': to, 'type': replaced + side[0] };
}
return board;
}
function renderHistory(board) {
var list = [];
while (board && board['move']) {
list.push(board['move']);
board = board['prior'];
}
var result = '';
var n = 0;
while (list.length > 0) {
const move = list.pop();
if (move['from'] === 'phantom') {
const took = move['took'] ? 'x' : '';
result += '*' + move['type'].toUpperCase() + took + move['to'];
} else {
if (n > 0 || move['side'] === 'dark') {
result += ' ';
}
if (move['side'] === 'light') {
++n;
result += String(n) + '. ';
}
if (move['pass']) {
result += '...';
} else if (move['alongside']) {
if (move['side'] === 'light') {
result += move['type'].toUpperCase() + move['alongside'].toUpperCase() + move['from'] + move['to'];
} else {
result += move['alongside'].toUpperCase() + move['type'].toUpperCase() + move['from'] + move['to'];
}
} else if (move['castle']) {
result += 'O-O';
} else if (move['queen_castle']) {
result += 'O-O-O';
} else {
const took = move['took'] ? 'x' : '';
result += move['type'].toUpperCase() + move['from'] + took + move['to'];
}
}
if (move['en_passant']) {
result += 'e.p.';
}
if (move['promotion']) {
result += '(' + move['promotion'][0].toUpperCase() + ')';
}
if (move['took'] === 'k') {
result += '#';
}
}
return result;
}
function pieceStartDrag(ev, ui){
const board = $('#cb_board').data('board');
const dragged = $(this);
const type = dragged.data('type');
const from = dragged.data('location');
const where = (from === 'phantom') ? board['phantom']['from'] : from;
const canCapture = boardGet(board, from, otherSide(type[1])) === ' ';
const legals = legalMoves(board, type[1], type[0], where, canCapture);
for (const there of legals) {
$('#cb_' + there).addClass('cb-legal').droppable('enable');
}
}
function pieceStopDrag(ev, ui){
$('#cb_board .cb-legal').removeClass('cb-legal').droppable('disable');
}
function placePiece(where, type, count){
var piece_id = 'cb_piece_' + type + '_' + count;
var piece = $($('#' + piece_id)[0] || $('#cb_piece_' + type + ' img').clone());
piece.attr('style', '');
piece.attr('id', piece_id);
piece.data({ 'type': type, 'location': where });
piece.appendTo('#cb_' + where);
piece.draggable({
disabled: true,
containment: '#cb_inner',
revert: 'invalid',
zIndex: 100,
start: pieceStartDrag,
stop: pieceStopDrag,
});
return piece;
}
function renderBoard(board){
$('#cb_board .cb-piece .ui-draggable').draggable('destroy');
$('#cb_board .cb-piece').attr('style', '').appendTo('#cb_hidden');
$('#cb_board .cb-start').removeClass('cb-start');
$('#cb_board .cb-end').removeClass('cb-end');
$('#cb_phantom').appendTo('#cb_hidden');
for (const side of ['light', 'dark']) {
var counters = {};
for (var row = 0; row < 8; ++row) {
for (var column = 0; column < 8; ++column) {
var here = 'abcdefgh'[column] + String(row+1);
var type = board[side][row][column];
if (type !== ' ') {
if (!counters[type]) {
counters[type] = 0;
}
var count = ++counters[type];
placePiece(here, type + side[0], count);
}
}
}
}
var clss = board['player'] === 'light' ? '.cb-lt-piece' : '.cb-dk-piece';
if (board['phantom']) {
var where = board['phantom']['from'];
placePiece('phantom', board['phantom']['type'], 'ph');
$('#cb_phantom').appendTo('#cb_' + where);
$('#cb_board .ui-draggable').draggable('disable');
$('#cb_phantom .ui-draggable-disabled').filter(clss).draggable('enable');
} else {
$('#cb_board .ui-draggable').draggable('disable');
if (!board['move'] || board['move']['took'] !== 'k') {
$('#cb_board .ui-draggable-disabled').filter(clss).draggable('enable');
}
}
if (board['move']) {
if (board['move']['from'] === 'phantom') {
$('#cb_' + board['prior']['move']['to']).addClass('cb-start');
} else {
$('#cb_' + board['move']['from']).addClass('cb-start');
}
$('#cb_' + board['move']['to']).addClass('cb-end');
}
var msg = '';
if (board['move'] && board['move']['took'] === 'k') {
msg = (board['move']['side'][0] === 'd' ? 'Dark' : 'Light') + ' player won!';
} else {
msg = (board['player'][0] === 'd' ? 'Dark' : 'Light') + " player's move";
}
$('#cb_message').text(msg);
$('#cb_history').text(renderHistory(board));
$('#cb_undo').attr('disabled', board['prior'] ? false : true);
$('#cb_redo').attr('disabled', board['subsequent'] ? false : true);
$('#cb_pass').attr('disabled', board['phantom'] ? true : false);
}
function randomId(){
var res = '';
for (var i = 0; i < 4; ++i) {
var part = Math.floor(Math.random() * 65536).toString(16);
res = res + ('0000'.substring(part.length, 4) + part);
}
return res;
}
var PacoSakoUUID = '7c38edd4-c931-49c8-9f1a-84de560815db';
function putState(board){
var boardElem = $('#cb_board');
var gameId = boardElem.data('gameId');
var game = gun.get(PacoSakoUUID).get('games').get(gameId);
var state = JSON.stringify(board);
$('#cb_board').data('skip_notify', state);
game.put({ 'board': state });
putMeta();
}
function putMeta(){
var board = $('#cb_board').data('board');
var gameId = $('#cb_board').data('gameId');
var lightName = $('#cb_light_name').val();
var darkName = $('#cb_dark_name').val();
var meta = gun.get(PacoSakoUUID).get('meta').get(gameId);
var stat = (board['move'] && board['move']['took'] === 'k') ? 'mate' : null;
meta.put({
'gameId': gameId,
'lightName': lightName,
'darkName': darkName,
'moves': countMoves(board),
'timestamp': board['timestamp'] || new Date().getTime(),
'status': stat,
});
}
function switchGameId(newId){
var boardElem = $('#cb_board');
var gameId = boardElem.data('gameId');
if (newId === gameId) {
return;
}
if (gameId) {
//gun.get(PacoSakoUUID).get('games').get(gameId).off();
//gun.get(PacoSakoUUID).get('meta').get(gameId).off();
}
boardElem.data('gameId', newId);
location.hash = '#/' + newId;
/* this will be the starting state if no data is received from peers */
var newBoard = initialBoard();
boardElem.data('board', newBoard);
renderBoard(newBoard);
$('#cb_notify')[0].checked = false;
$('#cb_light_name').val('');
$('#cb_dark_name').val('');
$('#cb_board').removeData('skip_notify');
gun.get(PacoSakoUUID).get('games').get(newId).on(function(d){
if (d && d['board'] && $('#cb_board').data('gameId') === newId) {
const board = JSON.parse(d['board']);
const cb_board = $('#cb_board').first();
if ($('#cb_notify')[0].checked && cb_board.data('skip_notify') !== d['board']) {
if (board['move'] && board['move']['took'] === 'k') {
notify((board['move']['side'][0] === 'd' ? 'Dark' : 'Light') + ' player won!');
} else {
notify((board['player'][0] === 'd' ? 'Dark' : 'Light') + ' player\'s turn');
}
}
$('#cb_board').data('skip_notify', d['board']);
$('#cb_board').data('board', board);
renderBoard(board);
}
});
gun.get(PacoSakoUUID).get('meta').get(newId).on(function(d){
if (d && $('#cb_board').data('gameId') === newId) {
$('#cb_light_name').val(d['lightName'] || '');
$('#cb_dark_name').val(d['darkName'] || '');
}
});
var selOpt = $('#cb_game_' + newId);
if (selOpt.length === 1) {
$('#cb_select_game')[0].selectedIndex = selOpt.index();
} else {
$('#cb_select_game')[0].selectedIndex = -1;
}
}
function disableNotify(){
$('#cb_notify')[0].checked = false;
$('#cb_notify').attr('disabled', true);
}
function requestNotify(){
try {
Notification.requestPermission(function (permission){
if (permission === 'denied') {
disableNotify();
}
});
} catch (err) {
disableNotify();
}
}
function notify(body) {
try {
Notification.requestPermission(function(permission){
if (permission === 'granted') {
navigator.serviceWorker.ready.then(function(registration){
registration.showNotification('Paco Ŝako', {
badge: 'svg/Chess_klt45.svg',
icon: 'svg/Chess_klt45.svg',
body: body,
tag: 'notice',
});
});
} else if (permission === 'denied') {
disableNotify();
}
});
} catch (err) {
disableNotify();
}
}
$(function (){
try {
if (Notification.permission === 'denied') {
disableNotify();
} else {
navigator.serviceWorker.register('sw.js').catch(disableNotify);
}
} catch (err) {
disableNotify();
}
$('.cb-square').droppable({
accept: '.cb-piece',
disabled: true,
deactivate: function(ev, ui){
$(this).droppable('disable');
},
drop: function(ev, ui) {
var dragged = ui.draggable;
var type = dragged.data('type');
var from = dragged.data('location');
var to = this.id.replace(/^cb_/, '');
dragged.appendTo('#cb_hidden');
var newBoard = movePiece($('#cb_board').data('board'), type[1], from, to);
putState(newBoard);
},
});
$('#cb_undo').on('click', function(){
var board = $('#cb_board').data('board');
if (board['prior']) {
var newBoard = cloneJSON(board['prior']);
var redoBoard = cloneJSON(board);
delete redoBoard['prior'];
newBoard['subsequent'] = redoBoard;
putState(newBoard);
}
});
$('#cb_redo').on('click', function(){
var board = $('#cb_board').data('board');
if (board['subsequent']) {
var newBoard = cloneJSON(board['subsequent']);
var undoBoard = cloneJSON(board);
delete undoBoard['subsequent'];
newBoard['prior'] = undoBoard;
newBoard['timestamp'] = new Date().getTime();
putState(newBoard);
}
});
$('#cb_reset').on('click', function(){
putState(initialBoard());
});
$('#cb_pass').on('click', function(){
var board = $('#cb_board').data('board');
if (!board['phantom']) {
var newBoard = cloneJSON(board);
newBoard['prior'] = board;
newBoard['move'] = { 'side': board['player'], 'pass': true };
newBoard['player'] = otherSide(board['player']);
newBoard['timestamp'] = new Date().getTime();
putState(newBoard);
}
});
$('#cb_select_game').on('change', function(){
var optIndex = $('#cb_select_game')[0].selectedIndex;
if (optIndex === 0) {
switchGameId(randomId());
} else if (optIndex >= 1) {
var opt = $('#cb_select_game option')[optIndex];
if (opt) {
switchGameId(opt.id.replace(/^cb_game_/, ''));
}
}
});
$('#cb_notify').on('change', function(){
if (this.checked) {
requestNotify();
}
});
let updateMeta = function() { putMeta(); }
$('#cb_light_name').on('input', updateMeta);
$('#cb_dark_name').on('input', updateMeta);
var gameId = location.hash.replace(/^#\//, '');
if (gameId.length !== 16) {
gameId = randomId();
}
switchGameId(gameId);
function updateTitle(opt){
opt = $(opt);
const then = opt.data('then');
const now = new Date().getTime();
var age_str = '';
if (then > now) {
age_str = ' (future)';
} else if ((now - then) < 60*60*1000) {
age_str = ' (' + Math.floor((now - then) / (60*1000)) + 'm)';
} else if ((now - then) < 24*60*60*1000) {
age_str = ' (' + Math.floor((now - then) / (60*60*1000)) + 'h)';
} else if ((now - then) < 14*24*60*60*1000) {
age_str = ' (' + Math.floor((now - then) / (24*60*60*1000)) + 'd)';
} else if (opt.data('gameId') !== $('#cb_board').data('gameId')) {
opt.remove();
return;
}
opt.text(opt.data('title') + age_str);
}
gun.get(PacoSakoUUID).get('meta').map().on(function(d){
if (d && d['gameId']) {
const lightName = d['lightName'] ? d['lightName'] : 'Light';
const darkName = d['darkName'] ? d['darkName'] : 'Dark';
const moves = !d['moves'] ? '' :
(', ' + d['moves'] + (d['moves'] === 1 ? ' move' : ' moves'));
var opt = $('#cb_game_' + d['gameId']);
if (!(d['lightName'] || d['darkName']) && !d['moves'] && d['gameId'] !== $('#cb_board').data('gameId')) {
if (opt.length >= 1) {
opt.remove();
}
} else {
if (opt.length === 0) {
opt = $('<option></option>');
opt.attr('id', 'cb_game_' + d['gameId']);
}
var stat = '';
if (d['status']) {
stat = ', ' + d['status'];
}
opt.data('gameId', d['gameId']);
opt.data('title', lightName + ' vs. ' + darkName + moves + stat);
opt.data('then', d['timestamp'] || new Date().getTime());
opt.addClass('cb-game-option');
opt.appendTo('#cb_select_game');
updateTitle(opt);
var select = $('#cb_select_game');
var list = select.children('.cb-game-option').get();
list.sort(function(a,b) {
const then_a = $(a).data('then');
const then_b = $(b).data('then');
return (then_a < then_b) ? 1 : (then_a === then_b) ? 0 : -1;
});
for (const e of list) {
$(e).appendTo(select);
}
}
var selOpt = $('#cb_game_' + $('#cb_board').data('gameId'));
if (selOpt.length === 1) {
$('#cb_select_game')[0].selectedIndex = selOpt.index();
} else {
$('#cb_select_game')[0].selectedIndex = -1;
}
}
});
window.setInterval(function(){
$('#cb_select_game').first().children('.cb-game-option').each(function(idx,opt){
updateTitle(opt);
});
}, 15000);
window.onpopstate = function(event){
var gameId = location.hash.replace(/^#\//, '');
if (gameId.length === 16) {
switchGameId(gameId);
}
};
/* force page reload after four hours to keep client code up to date */
window.setTimeout(function(){ location.reload(); }, 4*3600*1000);
});
/* vim:set expandtab sw=3 ts=8: */