add move validation and visual hints
This commit is contained in:
parent
46fd7de203
commit
a7528c859a
|
|
@ -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;
|
||||
|
|
|
|||
16
index.html
16
index.html
|
|
@ -141,20 +141,26 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<form id="cb_control_form">
|
||||
<div id="cb_controls">
|
||||
<button id="cb_undo" disabled="true">Undo</button>
|
||||
<button id="cb_redo" disabled="true">Redo</button>
|
||||
<button id="cb_reset">Reset</button>
|
||||
<button id="cb_pass">Pass</button>
|
||||
<span id="cb_light_move" style="display: hidden">Light player's move</span>
|
||||
<span id="cb_dark_move" style="display: hidden">Dark player's move</span>
|
||||
<form id="cb_names">
|
||||
<input id="cb_light_name" placeholder="Light"> vs. <input id="cb_dark_name" placeholder="Dark"><br>
|
||||
<span id="cb_message"></span><br>
|
||||
</div>
|
||||
<div id="cb_names">
|
||||
<label>Players:</label>
|
||||
<input id="cb_light_name" placeholder="Light"> vs. <input id="cb_dark_name" placeholder="Dark">
|
||||
</div>
|
||||
<div id='cb_game'>
|
||||
<label>Select game:</label>
|
||||
<select id="cb_select_game">
|
||||
<option id="cb_new_game">— New Game —</option>
|
||||
</select>
|
||||
<p id="cb_history"></p>
|
||||
</div>
|
||||
</form>
|
||||
<p id="cb_history"></p>
|
||||
</div>
|
||||
|
||||
<div id="cb_hidden" style="display: none">
|
||||
|
|
|
|||
304
js/chess.js
304
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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue