add move validation and visual hints

This commit is contained in:
Jesse D. McDonald 2020-03-11 02:56:54 -05:00
parent 46fd7de203
commit a7528c859a
3 changed files with 311 additions and 47 deletions

View File

@ -1,5 +1,5 @@
#cb_outer2 { #cb_outer2 {
max-width: 80vmin; max-width: 66.7vh;
} }
#cb_outer { #cb_outer {
@ -33,6 +33,18 @@
position: relative; 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 { .cb-square {
position: relative; position: relative;
width: calc((100% - 16pt) / 8); width: calc((100% - 16pt) / 8);
@ -88,6 +100,10 @@
background-color: #CCFFC0; background-color: #CCFFC0;
} }
.cb-lt-bg.cb-legal {
background-color: #E6E6F2;
}
.cb-dk-bg { .cb-dk-bg {
background-color: #F5DEB3; background-color: #F5DEB3;
} }
@ -96,6 +112,10 @@
background-color: #D0E398; background-color: #D0E398;
} }
.cb-dk-bg.cb-legal {
background-color: #C4B2C2;
}
.cb-phantom { .cb-phantom {
position: absolute; position: absolute;
top: 0; top: 0;

View File

@ -141,20 +141,26 @@
</div> </div>
<div> <div>
<form id="cb_control_form">
<div id="cb_controls">
<button id="cb_undo" disabled="true">Undo</button> <button id="cb_undo" disabled="true">Undo</button>
<button id="cb_redo" disabled="true">Redo</button> <button id="cb_redo" disabled="true">Redo</button>
<button id="cb_reset">Reset</button> <button id="cb_reset">Reset</button>
<button id="cb_pass">Pass</button> <button id="cb_pass">Pass</button>
<span id="cb_light_move" style="display: hidden">Light player's move</span> <span id="cb_message"></span><br>
<span id="cb_dark_move" style="display: hidden">Dark player's move</span> </div>
<form id="cb_names"> <div id="cb_names">
<input id="cb_light_name" placeholder="Light"> vs. <input id="cb_dark_name" placeholder="Dark"><br> <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> <label>Select game:</label>
<select id="cb_select_game"> <select id="cb_select_game">
<option id="cb_new_game">&mdash; New Game &mdash;</option> <option id="cb_new_game">&mdash; New Game &mdash;</option>
</select> </select>
<p id="cb_history"></p> </div>
</form> </form>
<p id="cb_history"></p>
</div> </div>
<div id="cb_hidden" style="display: none"> <div id="cb_hidden" style="display: none">

View File

@ -24,6 +24,7 @@ let initialBoard = (function (){
'rnbkqbnr', 'rnbkqbnr',
], ],
'player': 'light', 'player': 'light',
'moves': 0,
}); });
return function (){ return JSON.parse(init); } return function (){ return JSON.parse(init); }
})(); })();
@ -65,12 +66,183 @@ function boardPut(board, where, side, piece){
board[side][row] = prefix + piece + suffix; 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){ function movePiece(priorBoard, side, from, to){
var other = otherSide(side); var other = otherSide(side);
var type = boardGet(priorBoard, from, side); var type = boardGet(priorBoard, from, side);
var took = boardGet(priorBoard, to, other); var took = boardGet(priorBoard, to, other);
var replaced = boardGet(priorBoard, to, side); 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; var undoBoard = priorBoard;
if (undoBoard['subsequent']) { if (undoBoard['subsequent']) {
@ -96,6 +268,41 @@ function movePiece(priorBoard, side, from, to){
if (alongside !== ' ') { if (alongside !== ' ') {
board['move']['alongside'] = 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') { if (from === 'phantom') {
delete board['phantom']; delete board['phantom'];
@ -131,9 +338,9 @@ function renderHistory(board) {
var n = 0; var n = 0;
while (list.length > 0) { while (list.length > 0) {
var move = list.pop(); const move = list.pop();
if (move['from'] === 'phantom') { if (move['from'] === 'phantom') {
var took = move['took'] ? 'x' : ''; const took = move['took'] ? 'x' : '';
result += '*' + move['type'].toUpperCase() + took + move['to']; result += '*' + move['type'].toUpperCase() + took + move['to'];
} else { } else {
if (n > 0 || move['side'] === 'dark') { if (n > 0 || move['side'] === 'dark') {
@ -153,31 +360,48 @@ function renderHistory(board) {
} else { } else {
result += move['alongside'].toUpperCase() + move['type'].toUpperCase() + move['from'] + move['to']; 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 { } else {
var took = move['took'] ? 'x' : ''; const took = move['took'] ? 'x' : '';
result += move['type'].toUpperCase() + move['from'] + took + move['to']; 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; return result;
} }
function pieceStartDrag(ev, ui){ function pieceStartDrag(ev, ui){
var board = $('#cb_board').data('board'); const board = $('#cb_board').data('board');
var dragged = $(this); const dragged = $(this);
var type = dragged.data('type'); const type = dragged.data('type');
var from = dragged.data('location'); const from = dragged.data('location');
if (from === 'phantom' || boardGet(board, from, otherSide(type[1])) === ' ') { const where = (from === 'phantom') ? board['phantom']['from'] : from;
var clss = (type[1] === 'd') ? '.cb-dk-piece' : '.cb-lt-piece'; const canCapture = boardGet(board, from, otherSide(type[1])) === ' ';
var other = (type[1] === 'd') ? '.cb-lt-piece' : '.cb-dk-piece'; const legals = legalMoves(board, type[1], type[0], where, canCapture);
$('.cb-square').not(':has('+clss+')').droppable('enable'); for (const there of legals) {
$('.cb-square').filter(':has('+other+')').droppable('enable'); $('#cb_' + there).addClass('cb-legal').droppable('enable');
} else {
/* moving together, must go to an empty square */
$('.cb-square').not(':has(.cb-piece)').droppable('enable');
} }
}; }
function pieceStopDrag(ev, ui){
$('#cb_board .cb-legal').removeClass('cb-legal').droppable('disable');
}
function placePiece(where, type, count){ function placePiece(where, type, count){
var piece_id = 'cb_piece_' + type + '_' + count; var piece_id = 'cb_piece_' + type + '_' + count;
@ -192,6 +416,7 @@ function placePiece(where, type, count){
revert: 'invalid', revert: 'invalid',
zIndex: 100, zIndex: 100,
start: pieceStartDrag, start: pieceStartDrag,
stop: pieceStopDrag,
}); });
return piece; return piece;
} }
@ -241,8 +466,13 @@ function renderBoard(board){
$('#cb_' + board['move']['to']).addClass('cb-end'); $('#cb_' + board['move']['to']).addClass('cb-end');
} }
$('#cb_' + otherSide(board['player']) + '_move').hide(); var msg = '';
$('#cb_' + normalizeSide(board['player']) + '_move').show(); 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)); $('#cb_history').text(renderHistory(board));
@ -255,7 +485,7 @@ function randomId(){
var res = ''; var res = '';
for (var i = 0; i < 4; ++i) { for (var i = 0; i < 4; ++i) {
var part = Math.floor(Math.random() * 65536).toString(16); 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; return res;
} }
@ -271,18 +501,25 @@ function putState(board){
} }
function putMeta(){ function putMeta(){
var board = $('#cb_board').data('board');
var gameId = $('#cb_board').data('gameId'); var gameId = $('#cb_board').data('gameId');
var lightName = $('#cb_light_name').val() || 'Light'; var lightName = $('#cb_light_name').val();
var darkName = $('#cb_dark_name').val() || 'Dark'; var darkName = $('#cb_dark_name').val();
var meta = gun.get(PacoSakoUUID).get('meta').get(gameId); 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){ function switchGameId(newId){
var boardElem = $('#cb_board'); var boardElem = $('#cb_board');
var gameId = boardElem.data('gameId'); var gameId = boardElem.data('gameId');
if (newId == gameId) { if (newId === gameId) {
return; return;
} }
@ -312,12 +549,8 @@ function switchGameId(newId){
gun.get(PacoSakoUUID).get('meta').get(newId).on(function(d){ gun.get(PacoSakoUUID).get('meta').get(newId).on(function(d){
if (d && $('#cb_board').data('gameId') === newId) { if (d && $('#cb_board').data('gameId') === newId) {
if (d['lightName']) { $('#cb_light_name').val(d['lightName'] || '');
$('#cb_light_name').val(d['lightName']); $('#cb_dark_name').val(d['darkName'] || '');
}
if (d['darkName']) {
$('#cb_dark_name').val(d['darkName']);
}
} }
}); });
@ -402,7 +635,7 @@ $(function (){
$('#cb_dark_name').on('input', updateMeta); $('#cb_dark_name').on('input', updateMeta);
gun.get(PacoSakoUUID).get('meta').map().on(function(d){ gun.get(PacoSakoUUID).get('meta').map().on(function(d){
if (d && d['gameId'] && d['lightName'] && d['darkName']) { if (d && d['gameId']) {
function updateTitle(opt){ function updateTitle(opt){
const then = opt.data('then'); const then = opt.data('then');
const now = new Date().getTime(); const now = new Date().getTime();
@ -435,7 +668,12 @@ $(function (){
window.setTimeout(refreshTitle, 0); 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.data('then', d['timestamp'] || new Date().getTime());
opt.addClass('cb-game-option'); opt.addClass('cb-game-option');
opt.appendTo('#cb_select_game'); opt.appendTo('#cb_select_game');
@ -446,7 +684,7 @@ $(function (){
list.sort(function(a,b) { list.sort(function(a,b) {
const then_a = $(a).data('then'); const then_a = $(a).data('then');
const then_b = $(b).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) { for (const e of list) {