diff --git a/css/chess.css b/css/chess.css
index 645bee7..1166b58 100644
--- a/css/chess.css
+++ b/css/chess.css
@@ -1,5 +1,5 @@
#cb_outer2 {
- max-width: 80vmin;
+ max-width: 66.7vh;
}
#cb_outer {
@@ -33,6 +33,18 @@
position: relative;
}
+#cb_light_name, #cb_dark_name {
+ width: 8em;
+}
+
+#cb_controls, #cb_names, #cb_game {
+ margin-top: 0.5em;
+}
+
+#cb_reset, #cb_pass {
+ display: none;
+}
+
.cb-square {
position: relative;
width: calc((100% - 16pt) / 8);
@@ -88,6 +100,10 @@
background-color: #CCFFC0;
}
+.cb-lt-bg.cb-legal {
+ background-color: #E6E6F2;
+}
+
.cb-dk-bg {
background-color: #F5DEB3;
}
@@ -96,6 +112,10 @@
background-color: #D0E398;
}
+.cb-dk-bg.cb-legal {
+ background-color: #C4B2C2;
+}
+
.cb-phantom {
position: absolute;
top: 0;
diff --git a/index.html b/index.html
index 18ca1ea..d59d14a 100644
--- a/index.html
+++ b/index.html
@@ -141,20 +141,26 @@
-
-
-
-
-
Light player's move
-
Dark player's move
-
+
diff --git a/js/chess.js b/js/chess.js
index 7f358c4..df9d965 100644
--- a/js/chess.js
+++ b/js/chess.js
@@ -24,6 +24,7 @@ let initialBoard = (function (){
'rnbkqbnr',
],
'player': 'light',
+ 'moves': 0,
});
return function (){ return JSON.parse(init); }
})();
@@ -65,12 +66,183 @@ function boardPut(board, where, side, piece){
board[side][row] = prefix + piece + suffix;
}
+function countMoves(board) {
+ var n = 0;
+ while (board && board['prior']) {
+ if (board['player'] === 'light' && board['prior']['player'] !== 'light') {
+ ++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, from){
+ while (true) {
+ if (!board || !board['move']) {
+ return false;
+ }
+
+ if (!board['move']['pass'] && board['move']['side'][0] === side[0]) {
+ if (board['move']['to'] === from) {
+ return true;
+ }
+ }
+
+ board = board['prior'];
+ }
+}
+
+function legalMoves(board, side, type, from, canCapture){
+ if (board['move'] && board['move']['took'] && board['move']['took'][0] === '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] === 'd' && from[1] === (side[0] === 'd' ? '8' : '1')) {
+ /* check for castling conditions */
+ if (!hasMoved(board, side, from)) {
+ if (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('b' + from[1]);
+ }
+ }
+ if (boardGet(board, 'e' + from[1], side) === ' ' &&
+ 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('f' + 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') {
+ var forward = offsetSquare(from, 0, (side[0] === 'd') ? -1 : 1);
+ var forward2 = offsetSquare(from, 0, (side[0] === 'd') ? -2 : 2);
+ var diagL = offsetSquare(from, -1, (side[0] === 'd') ? -1 : 1);
+ var diagR = offsetSquare(from, 1, (side[0] === 'd') ? -1 : 1);
+
+ if (forward && validDestination(board, side, forward, false)) {
+ legals.push(forward);
+ if ((side[0] === 'd') ? (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 = (from === 'phantom') ? ' ' : boardGet(priorBoard, from, other);
+ 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']) {
@@ -96,6 +268,41 @@ function movePiece(priorBoard, side, from, to){
if (alongside !== ' ') {
board['move']['alongside'] = alongside;
}
+ if (type === 'k' && actuallyFrom[0] === 'd' && to[0] === 'b') {
+ board['move']['castle'] = true;
+ boardPut(board, 'a' + actuallyFrom[1], side, ' ');
+ boardPut(board, 'c' + actuallyFrom[1], side, 'r');
+ }
+ if (type === 'k' && actuallyFrom[0] === 'd' && to[0] === 'f') {
+ board['move']['queen_castle'] = true;
+ boardPut(board, 'h' + actuallyFrom[1], side, ' ');
+ boardPut(board, 'e' + actuallyFrom[1], side, 'r');
+ }
+ if (type === 'p' && ((side[0] === 'd') ? (to[1] === '1') : (to[1] === '8'))) {
+ board['move']['promotion'] = 'queen'; /* TODO: allow other choices */
+ type = 'q';
+ }
+ if (alongside === 'p' && ((other[0] === 'd') ? (to[1] === '1') : (to[1] === '8'))) {
+ board['move']['promotion'] = 'queen'; /* 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'];
@@ -131,9 +338,9 @@ function renderHistory(board) {
var n = 0;
while (list.length > 0) {
- var move = list.pop();
+ const move = list.pop();
if (move['from'] === 'phantom') {
- var took = move['took'] ? 'x' : '';
+ const took = move['took'] ? 'x' : '';
result += '*' + move['type'].toUpperCase() + took + move['to'];
} else {
if (n > 0 || move['side'] === 'dark') {
@@ -153,31 +360,48 @@ function renderHistory(board) {
} 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 {
- var took = move['took'] ? 'x' : '';
+ 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'] && move['took'][0] === 'k') {
+ result += '#';
+ }
}
return result;
}
function pieceStartDrag(ev, ui){
- var board = $('#cb_board').data('board');
- var dragged = $(this);
- var type = dragged.data('type');
- var from = dragged.data('location');
- if (from === 'phantom' || boardGet(board, from, otherSide(type[1])) === ' ') {
- var clss = (type[1] === 'd') ? '.cb-dk-piece' : '.cb-lt-piece';
- var other = (type[1] === 'd') ? '.cb-lt-piece' : '.cb-dk-piece';
- $('.cb-square').not(':has('+clss+')').droppable('enable');
- $('.cb-square').filter(':has('+other+')').droppable('enable');
- } else {
- /* moving together, must go to an empty square */
- $('.cb-square').not(':has(.cb-piece)').droppable('enable');
+ 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;
@@ -192,6 +416,7 @@ function placePiece(where, type, count){
revert: 'invalid',
zIndex: 100,
start: pieceStartDrag,
+ stop: pieceStopDrag,
});
return piece;
}
@@ -241,8 +466,13 @@ function renderBoard(board){
$('#cb_' + board['move']['to']).addClass('cb-end');
}
- $('#cb_' + otherSide(board['player']) + '_move').hide();
- $('#cb_' + normalizeSide(board['player']) + '_move').show();
+ var msg = '';
+ if (board['move'] && board['move']['took'] && board['move']['took'][0] === '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));
@@ -255,7 +485,7 @@ 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);
+ res = res + ('0000'.substring(part.length, 4) + part);
}
return res;
}
@@ -271,18 +501,25 @@ function putState(board){
}
function putMeta(){
+ var board = $('#cb_board').data('board');
var gameId = $('#cb_board').data('gameId');
- var lightName = $('#cb_light_name').val() || 'Light';
- var darkName = $('#cb_dark_name').val() || 'Dark';
+ var lightName = $('#cb_light_name').val();
+ var darkName = $('#cb_dark_name').val();
var meta = gun.get(PacoSakoUUID).get('meta').get(gameId);
- meta.put({ 'gameId': gameId, 'lightName': lightName, 'darkName': darkName, 'timestamp': new Date().getTime() });
+ meta.put({
+ 'gameId': gameId,
+ 'lightName': lightName,
+ 'darkName': darkName,
+ 'moves': countMoves(board),
+ 'timestamp': new Date().getTime()
+ });
}
function switchGameId(newId){
var boardElem = $('#cb_board');
var gameId = boardElem.data('gameId');
- if (newId == gameId) {
+ if (newId === gameId) {
return;
}
@@ -312,12 +549,8 @@ function switchGameId(newId){
gun.get(PacoSakoUUID).get('meta').get(newId).on(function(d){
if (d && $('#cb_board').data('gameId') === newId) {
- if (d['lightName']) {
- $('#cb_light_name').val(d['lightName']);
- }
- if (d['darkName']) {
- $('#cb_dark_name').val(d['darkName']);
- }
+ $('#cb_light_name').val(d['lightName'] || '');
+ $('#cb_dark_name').val(d['darkName'] || '');
}
});
@@ -402,7 +635,7 @@ $(function (){
$('#cb_dark_name').on('input', updateMeta);
gun.get(PacoSakoUUID).get('meta').map().on(function(d){
- if (d && d['gameId'] && d['lightName'] && d['darkName']) {
+ if (d && d['gameId']) {
function updateTitle(opt){
const then = opt.data('then');
const now = new Date().getTime();
@@ -435,7 +668,12 @@ $(function (){
window.setTimeout(refreshTitle, 0);
}
- opt.data('title', d['lightName'] + ' vs. ' + d['darkName']);
+ 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'));
+
+ opt.data('title', lightName + ' vs. ' + darkName + moves);
opt.data('then', d['timestamp'] || new Date().getTime());
opt.addClass('cb-game-option');
opt.appendTo('#cb_select_game');
@@ -446,7 +684,7 @@ $(function (){
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;
+ return (then_a < then_b) ? 1 : (then_a === then_b) ? 0 : -1;
});
for (const e of list) {