מדיה ויקי:Gadget-pgnviewer.js: הבדלים בין גרסאות בדף

גרסה מעודכנת
מ 85 גרסאות של הדף wikipedia:he:מדיה_ויקי:Gadget-pgnviewer.js יובאו
 
(82 גרסאות ביניים של 3 משתמשים אינן מוצגות)
שורה 11: שורה 11:
*/
*/
"use strict";
"use strict";
$(function( $ ) {
window.mw.hook( 'wikipage.content' ).add( function ( $content ) {
var
var // const, really, but linter...
mw = window.mw,
pieceImageUrl = {},
boardImageUrl,
WHITE = 'l',
WHITE = 'l',
BLACK = 'd',
BLACK = 'd',
acode = 'a'.charCodeAt(0),
acode = 'a'.charCodeAt(0),
moveBucket = [], // this is a scratch thing, but since we access it from different objects, it's convenient to have it global
anim = 1000,
maxDelay = 8000,  // max delay between moves for autoplay, in ms
sides = ['n', 'e', 's', 'w'], // used for legends
defaultBlockSize = 45,
minBlockSize = 20,
minBlockSize = 20,
maxBlockSize = 60,
maxBlockSize = 60,
rtlregex = /[א-ת]/,
boardPadding = 20,
wrapperSelector = 'div.pgn-source-wrapper',
wrapperSelector = 'div.pgn-source-wrapper',
gameChangeEvent = 'pgn-geme-changed';
defaultBlockSize = 36,
sides = ['n', 'e', 's', 'w'], // used for legends
rowClassPrefix = 'pgn-prow-',
fileClassPrefix = 'pgn-pfile-',
allFilesAndColumnClasses = "01234567"
.split("")
.map( function( i ) { return rowClassPrefix + i + ' ' + fileClassPrefix + i; } )
.join(" "),
hiddenPiece = 'pgn-piece-hidden',
chessboardClass = 'pgn-board-img',
resetGameButtonClass = 'pgn-button-tostart',
ffButtonClass = 'pgn-button-toend',
advanceButtonClass = 'pgn-button-advance',
retreatButtonClass = 'pgn-button-retreat',
playButtonClass = 'pgn-button-play',
fasterButtonClass = 'pgn-button-faster',
slowerButtonClass = 'pgn-button-slower',
flipBoardButtonClass = 'pgn-button-flip',
ccButtonClass = 'pgn-button-cc',


mw = window.mw,
$ = window.$,
domParser = new DOMParser(),
mobile = mw.config.get('skin') === 'minerva';
// some global, utility functions.
// some global, utility functions.
function bindex(file, row) { return 8 * file + row; }
    function bindex(file, row) { return row == undefined ? file : 8 * file + row; }
function file(ind) { return Math.floor(ind / 8);}
function file(ind) { return Math.floor(ind / 8);}
function row(ind) { return ind % 8; }
function row(ind) { return ind % 8; }
שורה 37: שורה 52:
function fileOfStr(file) { return file && file.charCodeAt(0) - acode;}
function fileOfStr(file) { return file && file.charCodeAt(0) - acode;}
function rowOfStr(row) { return row && (row - 1);}
function rowOfStr(row) { return row && (row - 1);}
function indexOfMoveNotation(notation) {
var match = notation.match( /(\d+)([ld])/ );
if (match) return ( parseInt( match[1] ) - 1 ) * 2 + ( match[2] === 'l' ? 1 : 2 );
return 0;
}
function boardToFen(board) {
function boardToFen(board) {
var res = [],
var res = [],
שורה 49: שורה 69:
return res.reverse().join('/'); // fen begins with row 8 file a - go figure...
return res.reverse().join('/'); // fen begins with row 8 file a - go figure...
}
}
// Classes
function Button( className, action, stateful ) {
var button = this;
$.extend( button, {
state: 0,
setState: function( state ) {
var oldState = this.state;
state = 0 + !!state;
this.state = state;
if ( stateful )
this.elem
.toggleClass( 'pgn-image-button-on', !!state )
.toggleClass( 'pgn-image-button-off', !state );
if ( ( !stateful || state != oldState ) && typeof( action ) == 'function' )
action( state );
},
setVisible: function( visible ) {
this.elem.toggle( !! visible );
},
elem: $( '<div>' )
.addClass( 'pgn-image-button pgn-image-button-off ' + className )
.click( function() {
button.setState( ! button.state );
} )
} );
}


function linkMoveClick(e) {
function Gameset( wrapperDiv ) { // set of functions and features that depend on blocksize, and currentGame.
var
var gameSet = this;
$this = $(this),
game = $this.data('game'),
index = $this.data('index'),
noAnim = $this.data('noAnim');
game.gs.stopAutoPlay();
game.showMoveTo(index, noAnim);
}


function Gameset() { // set of functions and features that depend on blocksize, and currentGame.
$.extend(gameSet, {
$.extend(this, {
wrapperDiv: wrapperDiv,
tabberDiv: null,
blockSize: defaultBlockSize,
blockSize: defaultBlockSize,
needRefresh: false,
allGames: [],
allGames: [],
currentGame: null,
currentGame: null,
showDetails:false,
showDetails:false,
flip: false,
timer: null,
timer: null,
isAutoplay: 0,
autoPlayDelay: 750,
autoPlayRate: 0,
// actions
top: function(row, l) { return (((this.flip ? row : (7 - row)) + (l ? 0.3 : 0)) * this.blockSize + 20) + 'px'; },
faster: function() {
left: function(file, l) { return (((this.flip ? 7 - file : file) + (l ? 0.5 : 0)) * this.blockSize + 20) + 'px'; },
gameSet.autoPlayDelay -= 500;
if (gameSet.autoPlayDelay < 500) gameSet.autoPlayDelay = 500;
gameSet.reportDelay();
},
slower: function() {
gameSet.autoPlayDelay += 500;
gameSet.reportDelay();
},
reportDelay: function() {
gameSet.toggleAutoPlay(); // no param means keep state, but use new delay
var message = gameSet.config.delay_msg
? gameSet.config.delay_msg.replace('$sec$', 0.001 * gameSet.autoPlayDelay)
: 0.001 * gameSet.autoPlayDelay;
mw.notify( message, { tag: 'delay' } );
// eventurally use config and format better message.
},
// 6 is half letter size (assuming font-size 0.875em)
top: function(row, l) { return (((this.isFlipped ? row : (7 - row)) + (l ? 0.3 : 0)) * this.blockSize + (l? boardPadding-6: boardPadding)) + 'px'; },
left: function(file, l) { return (((this.isFlipped ? 7 - file : file) + (l ? 0.5 : 0)) * this.blockSize + (l? boardPadding-6: boardPadding)) + 'px'; },
legendLocation: function(side, num) {
legendLocation: function(side, num) {
var n = 0.5 + num;
switch (side) {  
switch (side) {
case 'n': return {top: 0, left: this.left(num, true)};
case 'n':
case 'e': return {top: this.top(num, true), left: this.blockSize * 8 + boardPadding + 5};
return {top: 0, left: this.left(num, true)};
case 's': return {top: this.blockSize * 8 + 20, left: this.left(num, true)};
case 'e':
case 'w': return {top: this.top(num, true), left: 5};
return {top: this.top(num, true), left: this.blockSize * 8 + 20};
case 's':
return {top: this.blockSize * 8 + 20, left: this.left(num, true)};
case 'w':
return {top: this.top(num, true), left: 10};
}
}
},
},
שורה 97: שורה 148:
game.analyzePgn();
game.analyzePgn();
this.currentGame = game;
this.currentGame = game;
this.ccButton.setVisible( game.hasComments() );
game.show();
game.show();
$( '.pgn-comment-toggler' ).trigger( gameChangeEvent );
}
}
},
},
שורה 105: שורה 156:
this.fenDiv.text( boardToFen(board) );
this.fenDiv.text( boardToFen(board) );
},
},
drawIfNeedRefresh: function(again) {
drawIfNeedRefresh: function() {
this.refreshFEN();
if (this.currentGame)
if (this.needRefresh && this.currentGame)
this.currentGame.drawBoard();
this.currentGame.drawBoard();
this.needRefresh = false;
},
},
changeAppearance: function() {
changeAppearance: function() {
this.needRefresh = true;
this.currentGame.drawBoard();
this.currentGame.drawBoard();
this.relocateLegends();
this.relocateLegends();
שורה 120: שורה 168:
var
var
widthPx = width * 8,
widthPx = width * 8,
widthPxPlus = widthPx + 40,
widthPxPlus = widthPx + 40;
widthPxPlusPlus = widthPx + 80;
this.tabberDiv // disgusting, but i could not get heightStyle of jquery tabs to do what i need.
.css( { height: widthPxPlus } )
.find( 'div' ).css( { height: mobile ? widthPxPlus : widthPx - 20 } );
this.blockSize = width;
this.blockSize = width;
this.currentGame.tds.tabberDiv.css( { maxWidth: widthPxPlus } );
this.piecesDiv.css({width: widthPx, height: widthPx});
this.boardImg.css({width: widthPx, height: widthPx});
this.boardDiv.css({width: widthPxPlus, height: widthPxPlus});
this.currentGame.tds.boardDiv.css({width: widthPxPlus, height: widthPxPlus});
this.changeAppearance();
this.changeAppearance();
this.currentGame.showCurrentMoveLink();
},
},
doFlip: function() {
hideComments: function( state ) {
this.flip ^= 1;
this.wrapperDiv.toggleClass( 'pgn-comments-hidden', state );
},
isFlipped: false,
doFlip: function( state ) {
this.isFlipped = state;
this.changeAppearance();
this.changeAppearance();
return this.flip;
},
},
stopAutoPlay: function() {
playing: false,
clearTimeout(this.timer);
toggleAutoPlay: function( state ) {
this.isAutoplay = false;
clearInterval(this.timer);
this.currentGame.tds.playButton.attr('value', '\u25BA');
if (state == undefined) state = this.playing;
},
this.playing = state;
sceduleAutoPlay: function() {
if ( state ) {
clearTimeout( this.timer );
this.currentGame.wrapAround();
if ( this.isAutoplay ) {
this.timer = setInterval(function() { gameSet.currentGame.advance() }, gameSet.autoPlayDelay);
var gs = this;
this.timer = setTimeout( function() { gs.autoPlayCallback(); }, this.autoPlayRate );
}
}
},
},
autoPlayCallback: function() {
stopAutoPlay: function() {
var cg = this.currentGame;
gameSet.autoPlayButton.setState( false );
cg.wrapAround();
cg.advance();
},
toggleAutoPlay: function() {
if ( this.isAutoplay ^= 1 )
{
this.currentGame.tds.playButton.attr('value', '\u275A\u275A');
this.autoPlayCallback();
} else
this.stopAutoPlay();
}
}
});
});
שורה 166: שורה 205:
this.type = type;
this.type = type;
this.color = color;
this.color = color;
this.img = $('<img>', {src: pieceImageUrl[type + color], 'class': 'pgn-chessPiece'})
this.avatar = $('<div>')
.toggle(false);
.addClass('pgn-chessPiece pgn-ptype-color-' + type + color)
.on('transitionstart', function(){ $(this).addClass('moving'); } ) //supposedly elevates z-index
.on('transitionend', function(){ $(this).removeClass('moving'); } );
var piece = this;
$.extend( piece, {
appear: function(file, row) {
if (game.gs.isFlipped) {
file = 7 - file;
row = 7 - row;
}
this.avatar
.removeClass(hiddenPiece)
.removeClass(allFilesAndColumnClasses)  // remove them all
.addClass(rowClassPrefix + row) //
.addClass(fileClassPrefix + file);
},
disappear: function() { return this.avatar.addClass( hiddenPiece ); },
setSquare: function( file, row ) {
this.file = file;
this.row = row;
this.onBoard = true;
},
capture: function( file, row ) {
if (this.type == 'p' && !this.game.pieceAt(file, row))  // en passant
this.game.clearPieceAt(file, this.row);
else
this.game.clearPieceAt(file, row);
this.move(file, row);
},
move: function(file, row) {
// with chess960 castling, we sometimes have to test.
if ( this.game.pieceAt( this.file, this.row ) == this )
this.game.clearSquare(this.file, this.row);
this.game.pieceAt(file, row, this); // place it on the board)
},
pawnDirection: function() { return this.color == WHITE ? 1 : -1; },
toString: function() { return this.type + this.color; },
fen: function() { return this.color == WHITE ? this.type.toUpperCase() : this.type; },
pawnStart: function() { return this.color == WHITE ? 1 : 6; },
remove: function() { this.onBoard = false; },
canMoveTo: function(file, row, capture) {
if (!this.onBoard)
return false;
var rd = Math.abs(this.row - row), fd = Math.abs(this.file - file);
switch(this.type) {
case 'n':
return rd * fd == 2; // how nice that 2 is prime: its only factors are 2 and 1....
case 'p':
var dir = this.pawnDirection();
return (
((this.row == this.pawnStart() && row ==  this.row + dir * 2 && !fd && this.game.roadIsClear(this.file, file, this.row, row) && !capture)
|| (this.row + dir == row && fd == !!capture))); // advance 1, and either stay in file and no capture, or move exactly one
case 'k':
return (rd | fd) == 1; // we'll accept 1 and 1 or 1 and 0.
case 'q':
return (rd - fd) * rd * fd == 0 && this.game.roadIsClear(this.file, file, this.row, row); // same row, same file or same diagonal.
case 'r':
return rd * fd == 0 && this.game.roadIsClear(this.file, file, this.row, row);
case 'b':
return rd == fd && this.game.roadIsClear(this.file, file, this.row, row);
}
}, // function canMoveTo
matches: function(oldFile, oldRow, isCapture, file, row) {
if (typeof oldFile == 'number' && oldFile != this.file)
return false;
if (typeof oldRow  == 'number' && oldRow != this.row)
return false;
return this.canMoveTo(file, row, isCapture);
}
} ); // extend
}
}
   
   
ChessPiece.prototype.appear = function(file, row) {
this.img.css({top: this.game.gs.top(row), left: this.game.gs.left(file), width: this.game.gs.blockSize})
.fadeIn(anim);
};
ChessPiece.prototype.showMove = function(file, row) {
var gameSet = this.game.gs;
gameSet.refreshFEN();
this.img.animate( {top: gameSet.top(row), left: gameSet.left(file)},
anim,
'swing',
function() {
gameSet.sceduleAutoPlay();
}
);
};
ChessPiece.prototype.disappear = function() { this.img.fadeOut(anim); };
   
   
ChessPiece.prototype.setSquare = function(file, row) {
function Game(gameSet) {
this.file = file;
this.row = row;
this.onBoard = true;
};
ChessPiece.prototype.capture = function(file, row) {
if (this.type == 'p' && !this.game.pieceAt(file, row))  // en passant
this.game.clearPieceAt(file, this.row);
else
this.game.clearPieceAt(file, row);
this.move(file, row);
};
ChessPiece.prototype.move = function(file, row) {
this.game.clearSquare(this.file, this.row);
this.game.pieceAt(file, row, this); // place it on the board)
this.game.registerMove({what:'m', piece: this, file: file, row: row});
};
ChessPiece.prototype.pawnDirection = function() { return this.color == WHITE ? 1 : -1; };
ChessPiece.prototype.toString = function(fen) { return this.type + this.color; };
ChessPiece.prototype.fen = function() { return this.color == WHITE ? this.type.toUpperCase() : this.type; };
ChessPiece.prototype.pawnStart = function() { return this.color == WHITE ? 1 : 6; };
ChessPiece.prototype.remove = function() { this.onBoard = false; };
ChessPiece.prototype.canMoveTo = function(file, row, capture) {
if (!this.onBoard)
return false;
var rd = Math.abs(this.row - row), fd = Math.abs(this.file - file);
switch(this.type) {
case 'n':
return rd * fd == 2; // how nice that 2 is prime: its only factors are 2 and 1....
case 'p':
var dir = this.pawnDirection();
return (
((this.row == this.pawnStart() && row ==  this.row + dir * 2 && !fd && this.game.roadIsClear(this.file, file, this.row, row) && !capture)
|| (this.row + dir == row && fd == !!capture))); // advance 1, and either stay in file and no capture, or move exactly one
case 'k':
return (rd | fd) == 1; // we'll accept 1 and 1 or 1 and 0.
case 'q':
return (rd - fd) * rd * fd == 0 && this.game.roadIsClear(this.file, file, this.row, row); // same row, same file or same diagonal.
case 'r':
return rd * fd == 0 && this.game.roadIsClear(this.file, file, this.row, row);
case 'b':
return rd == fd && this.game.roadIsClear(this.file, file, this.row, row);
}
};
ChessPiece.prototype.matches = function(oldFile, oldRow, isCapture, file, row) {
if (typeof oldFile == 'number' && oldFile != this.file)
return false;
if (typeof oldRow  == 'number' && oldRow != this.row)
return false;
return this.canMoveTo(file, row, isCapture);
};
ChessPiece.prototype.showAction = function(move) {
switch (move.what) {
case 'a':
this.appear(move.file, move.row);
break;
case 'm':
this.showMove(move.file, move.row);
break;
case 'r':
this.disappear();
break;
}
};
function Game(tds, gameSet) {
$.extend(this, {
$.extend(this, {
board: [],
board: [],
boards: [],
boards: [],
pieces: [],
pieces: [],
notations: [],
moves: [],
moves: [],
linkOfIndex: [],
index: 0,
index: 0,
piecesByTypeCol: {},
piecesByTypeCol: {},
שורה 276: שורה 291:
comments: [],
comments: [],
analyzed: false,
analyzed: false,
tds: tds,
gs: gameSet});
gs: gameSet});
}
Game.prototype.moveLinkText = function(notation, color) {
var config = this.gs.config,
tpiece = config && config.translate && config.translate.piece,
tfile = config && config.translate && config.translate.file,
trow = config && config.translate && config.translate.row;
if (tpiece) {
tpiece =  color == WHITE && tpiece.white
|| color == BLACK && tpiece.black
|| tpiece;
try {
var regex = new RegExp('(' + Object.keys(tpiece).join("|") + ')', 'g');
notation = notation.replace(regex, function(c) { return tpiece[c] || c; } );
} catch (e) {
mw.log('bad config.translate.pieces');
throw e;
}
}
if (tfile) {
try {
notation = notation.replace(/[abcdefgh]/g, function(c) { return tfile[c] || c; } );
} catch (e) {
mw.log('bad config.translate.file');
throw e;
}
}
if (trow) {
try {
notation = notation.replace(/[12345678]/g, function(c) { return trow[c] || c; } );
} catch (e) {
mw.log('bad config.translate.row');
throw e;
}
}
return $.trim(notation.replace(/-/g, '\u2011')) + ' ';
}
}
   
   
שורה 283: שורה 335:
var desc = $.extend({}, this.descriptions),
var desc = $.extend({}, this.descriptions),
rtl = desc['Direction'] == 'rtl',
rtl = desc['Direction'] == 'rtl',
num = '',
gs = this.gs;
tds = this.tds;
   
   
// cleanup from previous game.
// cleanup from previous game.
this.gs.stopAutoPlay();
gs.stopAutoPlay();
tds.descriptionsDiv.empty();
 
tds.pgnDiv.empty();
tds.boardDiv.find('img.pgn-chessPiece').toggle(false);
// setup descriptions
// setup descriptions
delete desc['Direction'];
delete desc['Direction'];
tds.descriptionsDiv.css({ direction: rtl ? 'rtl' : 'ltr', textAlign: rtl ? 'right' : 'left' });
gs.descriptionsDiv
$.each(desc, function(key, val) { tds.descriptionsDiv.append(key + ': ' + val + '<br />'); });
.empty()
.css( { direction: rtl ? 'rtl' : 'ltr', textAlign: rtl ? 'right' : 'left' } );
$.each(desc, function(key, val) {  
var line = mw.html.escape(key + ': ' + val)  + '<br />';
gs.descriptionsDiv.append(line);  
} );
   
   
// setup pgn section
// setup pgn section
this.linkOfIndex = [];
gs.pgnDiv.empty().append( this.notations );
for (var i = 0; i < Math.max(this.moves.length, this.comments.length); i++) {
var move = this.moves[i],
comment = this.comments[i];
if (move && move.s) {
if (move.a)
num = move.s;
if (move.turn == 'd')
num = num.replace(/\.*$/, '...');
var link = $('<span>', {'class': (move.a ? 'pgn-steplink' : 'pgn-movelink')})
.text(move.s.replace(/-/g, '\u2011')) // replace hyphens with non-breakable hyphens, to avoid linebreak within O-O or 1-0
.data({game: this, index: i, noAnim: move.a, moveNum: num})
.click(linkMoveClick);
tds.pgnDiv.append(link);
this.linkOfIndex[i] = link;
}
if (comment)
$('<p>', { 'class': 'pgn-comment', text: comment })
.toggleClass( 'pgn-rtl-comment' , rtlregex.test(comment) )
.appendTo( tds.pgnDiv );
}
   
   
// set the board.
// set the board.  
$(this.pieces).each(function(i, piece){piece.img.appendTo(tds.boardDiv);});
var hiddenAvatars = this.pieces.map( function(piece) { return piece.disappear(); } );
this.showMoveTo(this.index, true);
this.gs.piecesDiv.empty().append( hiddenAvatars );
this.drawBoard();
}
}
   
   
Game.prototype.copyBoard = function() { return this.board.slice(); }
    Game.prototype.done = function() { return this.boards.length - 1 <= this.index; }
   
   
Game.prototype.pieceAt = function(file, row, piece) {
Game.prototype.pieceAt = function(file, row, piece) {
שורה 341: שורה 376:
   
   
Game.prototype.clearPieceAt = function(file, row) {
Game.prototype.clearPieceAt = function(file, row) {
var
var piece = this.pieceAt(file, row);
piece = this.pieceAt(file, row);
if (piece)
if (piece)
piece.remove();
piece.remove();
this.clearSquare(file, row);
this.clearSquare(file, row);
this.registerMove({what:'r', piece: piece, file: file, row: row})
}
}
   
   
שורה 352: שורה 386:
var file = file1,  
var file = file1,  
row = row1,  
row = row1,  
moves = 0,
steps = 0,
dfile = sign(file1, file2),
dfile = sign(file1, file2),
drow = sign(row1, row2);
drow = sign(row1, row2);
שורה 362: שורה 396:
if (this.pieceAt(file, row))
if (this.pieceAt(file, row))
return false;
return false;
if (moves++ > 10)
if (steps++ > 10)
throw 'something is wrong in function roadIsClear.' +
throw 'something is wrong in function roadIsClear.' +
' file=' + file + ' file1=' + file1 + ' file2=' + file2 +
' file=' + file + ' file1=' + file1 + ' file2=' + file2 +
שורה 381: שורה 415:
byTypeCol.push(piece);
byTypeCol.push(piece);
};
};
Game.prototype.registerMove = function(move) {
function act() { this.piece.showAction(this) };
moveBucket.push($.extend(move, {act: act}));
}
Game.prototype.gotoBoard = function(index) {
this.index = index;
this.drawBoard();
}
   
   
Game.prototype.advance = function(delta) {
Game.prototype.advance = function(delta) {
var m = this.index + (delta || 1); // no param means 1 forward.
var m = this.index + (delta || 1); // no param means 1 forward.
if (0 <= m && m < this.moves.length) {
if (0 <= m && m < this.boards.length) {
this.showMoveTo(m);
this.drawBoard(m);
if (this.moves[this.index].a)
} else this.gs.autoPlayButton.setState(false);
this.advance(delta);
}
if (this.index == this.moves.length - 1)
this.gs.stopAutoPlay();
}
}
   
   
Game.prototype.showCurrentMoveLink = function() {
Game.prototype.showCurrentMoveLink = function() {
var moveLink = this.linkOfIndex[this.index];
var moveLink = this.moves[this.index];
if (moveLink) {
if (moveLink) {
moveLink.addClass('pgn-current-move').siblings().removeClass('pgn-current-move');
moveLink.addClass('pgn-current-move').siblings().removeClass('pgn-current-move');
שורה 414: שורה 434:
}
}
   
   
Game.prototype.showMoveTo = function(index, noAnim) {
Game.prototype.drawBoard = function(index) {
var dif = index - this.index;
if ( index == undefined) index = this.index;
if (noAnim || dif < 1 || 2 < dif)
if ( index < 0 ) index += this.boards.length;
this.gotoBoard(index);
 
else
this.index = index;
while (this.index < index)
$.each(this.moves[++this.index].bucket, function(index, drop) {drop.act()});
var board = this.boards[index];
for (var i in this.pieces) this.pieces[i].disappear();
for (var i in board) if (board[i]) board[i].appear(file(i), row(i));
this.showCurrentMoveLink();
this.showCurrentMoveLink();
}
this.gs.refreshFEN();
Game.prototype.drawBoard = function() {
var
saveAnim = anim,
board = this.boards[this.index];
anim = 0;
for (var i in this.pieces)
this.pieces[i].disappear();
for (var i in board)
board[i].appear(file(i), row(i));
anim = saveAnim;
}
}
   
   
Game.prototype.wrapAround = function() {
Game.prototype.wrapAround = function() {
if (this.index >= this.boards.length - 1)
if (this.index >= this.boards.length - 1)
this.gotoBoard(0);
this.drawBoard(0);
}
}
 
Game.prototype.kingSideCastle = function(color) {
    Game.prototype.castle = function( color, side, kingTargetFile, rookTargetFile ) {
var king = this.piecesByTypeCol['k'][color][0];
var king = this.piecesByTypeCol['k'][color][0],
var rook = this.pieceAt(7, (color == WHITE ? 0 : 7));
            rook = this.piecesByTypeCol['r'][color][side];
if (!rook || rook.type != 'r')
if (!rook || rook.type != 'r')
throw 'attempt to castle without rook on appropriate square';
throw 'attempt to castle without rook on appropriate square';
king.move(fileOfStr('g'), king.row);
king.move(fileOfStr(kingTargetFile), king.row);
rook.move(fileOfStr('f'), rook.row);
rook.move(fileOfStr(rookTargetFile), rook.row);
       
    }
   
Game.prototype.kingSideCastle = function(color) {
        this.castle( color, 1, 'g', 'f' );
}
}
   
   
Game.prototype.queenSideCastle = function(color) {
Game.prototype.queenSideCastle = function(color) {
var king = this.piecesByTypeCol['k'][color][0];
        this.castle( color, 0, 'c', 'd' );
var rook = this.pieceAt(0, (color == WHITE ? 0 : 7));
if (!rook || rook.type != 'r')
throw 'attempt to castle without rook on appropriate square';
king.move(fileOfStr('c'), king.row);
rook.move(fileOfStr('d'), rook.row);
}
}
   
   
שורה 464: שורה 475:
this.clearPieceAt(file, row);
this.clearPieceAt(file, row);
var newPiece = this.createPiece(type, piece.color, file, row);
var newPiece = this.createPiece(type, piece.color, file, row);
this.registerMove({what:'a', piece: newPiece, file: file, row: row});
}
}
   
   
שורה 485: שורה 495:
return moveStr; // end of game - white wins, black wins, draw, game halted/abandoned/unknown.
return moveStr; // end of game - white wins, black wins, draw, game halted/abandoned/unknown.
var match = moveStr.match(/([RNBKQ])?([a-h])?([1-8])?(x)?([a-h])([1-8])(=[RNBKQ])?/);
var match = moveStr.match(/([RNBKQ])?([a-h])?([1-8])?(x)?([a-h])([1-8])(=[RNBKQ])?/);
if (!match) {
return false;
if (!match) return false;
}
   
   
var type = match[1] ? match[1].toLowerCase() : 'p',
var type = match[1] ? match[1].toLowerCase() : 'p',
שורה 505: שורה 514:
for (var i = 0; i < 2; i++) {
for (var i = 0; i < 2; i++) {
var piece = thePiece[i];
var piece = thePiece[i];
// lift the piece, check if the king is under threat
delete this.board[bindex(piece.file, piece.row)];
delete this.board[bindex(piece.file, piece.row)];
for (var j in this.board) {
for (var j in this.board) {
var threat = this.board[j];
var threat = this.board[j];
if (threat && threat.color != color && threat.canMoveTo(king.file, king.row, true)) {
if (threat && threat.color != color && threat.canMoveTo(king.file, king.row, true)) { // found that this piece can't move, so it's the other one...
ok = true;
ok = true;
thePiece = thePiece[1-i];
thePiece = thePiece[1-i];
שורה 514: שורה 524:
}
}
}
}
this.pieceAt(piece.file, piece.row, piece);
// put the piece back in place
this.board[bindex( piece.file, piece.row )] = piece;
if (ok)
if (ok)
break;
break;
שורה 533: שורה 544:
return moveStr;
return moveStr;
}
}
Game.prototype.addComment = function(str) {
if ( str )
Game.prototype.addComment = function(str) {
this.comments[this.moves.length] = str.replace( /[{}()]/g, '' );
if ( ! str ) return;
this.notations.push( $( '<p>' )
.addClass( 'pgn-comment' )
.text( str.replace( /[{}()]/g, '' ) )
);
}
}
 
Game.prototype.addDescription = function(description) {
Game.prototype.addDescription = function(description) {
description = $.trim(description);
description = $.trim(description);
שורה 545: שורה 560:
}
}
   
   
Game.prototype.description = function(pgn) {
Game.prototype.description = function() {
var d = this.descriptions,
var d = this.descriptions,
round = d['Round'] ? ' (' + d['Round'] + ')' : '',
event = d['Event'] || d['אירוע'],
s = d['Name']  
round =  d['Round'] ||  d['סיבוב'],
|| d['שם']  
white = d['White'] || d['לבן'],
|| ( (d['Event'] || d['אירוע'] || '') + ': ' + (d['White'] || d['לבן'] || '') + ' - ' + (d['Black'] || d['שחור'] || '') + round);
black = d['Black'] || d['שחור'],
return s;
name = d['Name'] || d['שם'];
}
return name
|| ( event ? event + ': ' : '')
Game.prototype.addMoveLink = function(str, noAnim, turn) {
+ (white ? white : '')
this.boards.push(this.board.slice());
+ (white || black ? ' – ' : '')
this.moves.push({bucket: moveBucket, s: str, a: noAnim, turn: turn});
+ (black ? black : '')
moveBucket = [];
+ (round ? ' (' + round + ')' : '');
}
}
 
 
Game.prototype.preAnalyzePgn = function(pgn) {
Game.prototype.preAnalyzePgn = function(pgn) {
var
match,
turn,
indexOfMove = {},
moveNum = '';
function removeHead(match) {
var ind = pgn.indexOf(match) + match.length;
pgn = pgn.substring(ind);
return match;
}
function tryMatch(regex) {
function tryMatch(regex) {
var rmatch = pgn.match(regex);
var match = pgn.match(regex);
if (rmatch) {
            if (match)
removeHead(rmatch[0]);
                pgn = pgn.replace( match, '' );
moveNum = rmatch[1] || moveNum;
return match && match[0];
}
return rmatch && rmatch[0];
}
}
       
while (match = tryMatch(/^\s*\[[^\]]*\]/))
        var match;
while (match = tryMatch(/^\s*\[[^\]]*\]/))
this.addDescription(match);
this.addDescription(match);
this.pgn = pgn;  
this.pgn = pgn;  
}
}
שורה 594: שורה 595:
match,
match,
turn,
turn,
indexOfMove = {},
moveNum = '',
moveNum = '',
game = this,
pgn = this.pgn;
pgn = this.pgn;
   
   
שורה 608: שורה 609:
if (rmatch) {
if (rmatch) {
removeHead(rmatch[0]);
removeHead(rmatch[0]);
moveNum = rmatch[1] || moveNum;;
moveNum = rmatch[1] || moveNum;
}
}
return rmatch && rmatch[0];
return rmatch && rmatch[0];
}
}
function addMoveLink(str, isMove, color) {
var notation = $( '<span>' )
.addClass( isMove ? 'pgn-movelink' : 'pgn-steplink' )
.text( game.moveLinkText( str, color ) )
game.notations.push( notation );
if ( isMove ) {
game.boards.push( game.board.slice() );
game.moves.push( notation );
} else if ( game.moves.length == 0 )
game.moves.push( notation );
var index = game.boards.length - 1;
notation.on( 'click', function() {
game.gs.stopAutoPlay();
game.drawBoard(index);
} );
}
       
pgn = pgn.replace(/;(.*)\n/g, ' {$1} ').replace(/\s+/g, ' '); // replace to-end-of-line comments with block comments, remove newlines and noramlize spaces to 1
pgn = pgn.replace(/;(.*)\n/g, ' {$1} ').replace(/\s+/g, ' '); // replace to-end-of-line comments with block comments, remove newlines and noramlize spaces to 1
this.populateBoard(this.descriptions.FEN || 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR');
this.populateBoard( this.descriptions.FEN || 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR' );
this.boards.push(this.board.slice());
var prevLen = -1;
var prevLen = -1;
this.addMoveLink();
while (pgn.length) {
while (pgn.length) {
if (prevLen == pgn.length)
if (prevLen == pgn.length)
שורה 624: שורה 645:
this.addComment( tryMatch( /^\s*\([^\)]*\)\s*/ ) );
this.addComment( tryMatch( /^\s*\([^\)]*\)\s*/ ) );
if (match = tryMatch(/^\s*(\d+)\.+/)) {
if (match = tryMatch(/^\s*(\d+)\.+/)) {
turn = /\.\.\./.test(match) ? BLACK : WHITE;
turn = /\.{3}/.test(match) ? BLACK : WHITE;
this.addMoveLink(match, true);
addMoveLink(match, false, turn);
continue;
continue;
}
}
if (match = tryMatch(/^\s*[^ ]+ ?/)) {
if (match = tryMatch(/^\s*[^ ]+ ?/)) {
this.createMove(turn, match);
this.createMove(turn, match);
this.addMoveLink(match, false, turn);
addMoveLink(match, true, turn);
indexOfMove[moveNum + turn] = this.moves.length - 1;
turn = BLACK;
turn = BLACK;
}
}
}
}
var showFirst = this.descriptions['FirstMove'];
this.index = this.descriptions.FirstMove
this.index = (showFirst && indexOfMove[showFirst]) || this.moves.length - 1;
? indexOfMoveNotation(this.descriptions.FirstMove)
: this.descriptions.FEN ? 0 : this.boards.length - 1;
}
}
   
   
שורה 643: שורה 664:
var fenar = fen.split(/[\/\s]/);
var fenar = fen.split(/[\/\s]/);
if (fenar.length < 8)
if (fenar.length < 8)
throw 'illegal fen: "' + fen + '"';
            throw 'illegal fen: "' + fen + '"';
for (var row = 0; row < 8; row++) {
for (var row = 0; row < 8; row++) {
var file = 0;
var file = 0;
שורה 658: שורה 679:
}
}
}
}
function selectGame() {
Game.prototype.hasComments = function() {
var gameSet = $(this).data('gameSet');
var hasComment = function( n ){ return n.hasClass( 'pgn-comment' ); };
gameSet.selectGame(this.value);
return !! this.notations.filter( hasComment ).length;
}
function createFlipper( gameSet ) {
return $( '<input>', { type: 'button', 'class': 'pgn-button', value: '\u262F' } ) // yin-yeng character
.click(function() { $(this).toggleClass( 'pgn-upside-down', gameSet.doFlip() ); } );
}
}


function advanceButton(gameSet, forward, allTheway) {
function buildBoardDiv(container, selector, gameSet, ind) {
var legend =
allTheway
? (forward ? '|\u25B7' : '\u25C1|')
: (forward ? '<' : '>')
return $('<input>', {'class': 'pgn-button', type: 'button', rel: 'f', value: legend})
.click(function() {
gameSet.stopAutoPlay();
if (allTheway) {
var links = gameSet.currentGame.linkOfIndex;
links[forward ? links.length - 1 : 1].click();
}
else
gameSet.currentGame.advance(forward * 2 - 1);
gameSet.refreshFEN();
});
}
function autoPlayButton(gameSet) {
return $('<input>', {type: 'button', 'class': 'pgn-button', value: '\u25BA'})
.click(function() { gameSet.toggleAutoPlay(); });
}
function toggleCommentButton(gameSet) {
return $( '<input>', { type: 'button', 'class': 'pgn-button', value: '{..}' } )
.addClass( 'pgn-comment-toggler' )
.click(function() { $( '.pgn-comment' ).toggle() })
.on( gameChangeEvent, function(e) {
$( this ).toggle( gameSet.currentGame.comments.length > 0 )
});
}
function buildBoardDiv(container, selector, gameSet) {
var
var
id = container.attr( 'id' ) || '',
id = container.attr( 'id' ) || 'pgn-viewer-' + ind,
config = container.data( 'config' ) || {},
config = gameSet.config = container.data( 'config' ) || {},
notationId = 'pgn-notation-' + id,
notationId = 'pgn-notation-' + id,
infoId = 'pgn-info-' + id,
infoId = 'pgn-info-' + id,
fenId = 'pgn-fen-' + id,
fenId = 'pgn-fen-' + id,
pgnDiv = $('<div>', {'class': 'pgn-pgndiv', id: notationId }),
descriptionsDiv = $('<div>', {'class': 'pgn-descriptions', id: infoId }),
gameSetDiv,
controlsDiv,
controlsDiv,
tabberDiv,
createBotton = function( className, param, todraw ) {
flipper = createFlipper(gameSet),
return new Button( className,
gotoend = advanceButton(gameSet, true, true),
function() {
forward = advanceButton(gameSet, true),
gameSet.stopAutoPlay();
backstep = advanceButton(gameSet, false),
if ( todraw ) gameSet.currentGame.drawBoard( param )
gotostart = advanceButton(gameSet, false, true),
else gameSet.currentGame.advance( param );
autoPlay = autoPlayButton(gameSet),
} )
slider = $('<div>', {'class': 'pgn-slider'})
},
.slider({
gotoend = createBotton( ffButtonClass, -1, true ),
min: 0,
forward = createBotton( advanceButtonClass, 1 ),
max: maxDelay,
backstep = createBotton( retreatButtonClass, -1 ),
orientation: 'horizontal',
gotostart = createBotton( resetGameButtonClass, 0, true ),
value: maxDelay,
flip = new Button( flipBoardButtonClass, function( state ){ gameSet.doFlip( state ); }, true ),
stop: function() { gameSet.autoPlayRate = maxDelay - slider.slider( 'value' ); }
slower = new Button( slowerButtonClass, function(){ gameSet.slower(); } ),
}),
faster = new Button( fasterButtonClass, function(){ gameSet.faster(); } ),
collapsibleDiv = $( '<div>' ),
autoplay = new Button( playButtonClass, function( state ){ gameSet.toggleAutoPlay( state ); }, true ),
collapseText = mw.messages.get( 'collapsible-collapse' ),
commentsToggle = new Button( ccButtonClass, function( state ){ gameSet.hideComments( state ); }, true ),
expandText = mw.messages.get( 'collapsible-expand' ),
tabnames = $.extend( {
collapseState,
notation: 'Game Notation',
collapseSpeed = 0,
metadata: 'Information',
collapser = $( '<input>', { type: 'button', value: collapseText } )
fen: 'FEN' },
.click( function(e) {
config.tab_names );
collapseState ^= true;
gameSet.autoPlayButton = autoplay;
$( this ).val( collapseState ? expandText : collapseText );
gameSet.ccButton = commentsToggle
collapsibleDiv[ collapseState ? 'slideUp' : 'slideDown' ]( collapseSpeed );
gameSet.descriptionsDiv = $('<div>', {'class': 'pgn-descriptions', id: infoId })
} ),
gameSet.fixedDelay = 'delay' in config;
commentsToggler = toggleCommentButton(gameSet);
if ( gameSet.fixedDelay ) gameSet.autoPlayDelay = Math.max(500, config.delay);
 
if ( $( '#' + notationId ) ) notationId += '_1';
gameSet.pgnDiv = $('<div>', {'class': 'pgn-pgndiv', id: notationId } );
  gameSet.blockSize = Math.max( minBlockSize, Math.min( maxBlockSize, config.squareSize || defaultBlockSize ) );
  gameSet.blockSize = Math.max( minBlockSize, Math.min( maxBlockSize, config.squareSize || defaultBlockSize ) );
gameSetDiv = $('<div>', {'class': 'pgn-gameset-div'})
.css({width: 40 + 8 * gameSet.blockSize});
gameSet.fenDiv = $('<div>', { id: fenId } )
gameSet.fenDiv = $('<div>', { id: fenId } )
.css( { 'word-wrap': 'break-word' } );  
.css( { 'word-wrap': 'break-word' } );  
tabberDiv = $( '<div>' )
gameSet.tabberDiv = $( '<div>', { 'class': 'pgn-tabber' } );
.css( { margin: 'auto' } )
if ( mobile ) {
.append($( '<ul>' )
gameSet.tabberDiv
.append( $( '<li>' ).append( $( '<a>', { href: '#' + notationId } ).text('מסעים') ) )  
.append( gameSet.pgnDiv );
.append($('<li>').append($('<a>', { href: '#' + infoId }).text('מידע נוסף') ) )
} else {
.append($('<li>').append($('<a>', { href:'#' + fenId }).text('FEN') ) )
gameSet.tabberDiv
)
.append($( '<ul>' )
.append( pgnDiv )
.append( $( '<li>' ).append( $( '<a>', { href: '#' + notationId } ).text( tabnames.notation ) ) )  
.append( descriptionsDiv )
.append($('<li>').append($('<a>', { href: '#' + infoId }).text( tabnames.metadata ) ) )
.append( gameSet.fenDiv )
.append($('<li>').append($('<a>', { href:'#' + fenId }).text( tabnames.fen ) ) )
.tabs();
)
.append( gameSet.pgnDiv )
.append( gameSet.descriptionsDiv )
.append( gameSet.fenDiv )
.tabs();
}
var buttons = gameSet.fixedDelay
? [gotostart, backstep, autoplay, forward, gotoend, flip, commentsToggle]
: [gotostart, backstep, slower, autoplay, faster, forward, gotoend, flip, commentsToggle]
controlsDiv = $('<div>', {'class': 'pgn-controls'})
controlsDiv = $('<div>', {'class': 'pgn-controls'})
.css({clear: 'both', textAlign: 'center'})
.css( { textAlign: 'center' } ) // todo: move to css
.append(gotostart)
.append( buttons.map( function(x){ return x.elem; } ) );
.append(backstep)
.append(autoPlay)
.append(forward)
.append(gotoend)
.append(flipper)
.append(commentsToggler)
.append(slider)
;


gameSet.boardDiv = $('<div>', {'class': 'pgn-board-div'});
gameSet.boardDiv = $('<div>')
gameSet.boardImg = $('<img>', {'class': 'pgn-board-img', src: boardImageUrl } )
.addClass('pgn-board-div');
.css({padding: 20})
 
gameSet.piecesDiv = $('<div>').css({position:'absolute', left: '20px', top: '20px' })
.addClass( chessboardClass )
.appendTo(gameSet.boardDiv);
.appendTo(gameSet.boardDiv);
var fl = 'abcdefgh'.split('');
var fl = 'abcdefgh'.split('');
   
var fileCaption = config && config.translate && config.translate.file;
if (fileCaption) {
fl = fl.map(function(c) { return fileCaption[c] || '' ; } );
}
var rl = '12345678'.split('');
var rowCaption = config && config.translate && config.translate.row;
if (rowCaption) {
rl = rl.map(function(c) { return rowCaption[c] || '' ; } );
}
for (var side in sides) {
for (var side in sides) {
var
var
שורה 781: שורה 775:
for (var i = 0; i < 8; i++) {
for (var i = 0; i < 8; i++) {
var sp = $('<span>', {'class': isFile ? 'pgn-file-legend' : 'pgn-row-legend'})
var sp = $('<span>', {'class': isFile ? 'pgn-file-legend' : 'pgn-row-legend'})
.text(isFile ? fl[i] : (i + 1))
.text(isFile ? fl[i] : rl[i])
.appendTo(gameSet.boardDiv)
.appendTo(gameSet.boardDiv)
.css(gameSet.legendLocation(s, i));
.css(gameSet.legendLocation(s, i));
שורה 789: שורה 783:
container
container
.append( config.collapsible ? collapser : '' )
.append( selector || '' )
.append( selector || '' )
.append( collapsibleDiv
.append($('<div>')
.append(gameSet.boardDiv)
.append(gameSet.boardDiv)
.append( gameSet.tabberDiv )
.append(controlsDiv)
.append(controlsDiv)
.append(tabberDiv)
);
);
if ( config.collapsible && config.collapsed )
collapser.click();
collapseSpeed = 1500;
return {boardDiv: gameSet.boardDiv, pgnDiv: pgnDiv, descriptionsDiv: descriptionsDiv, tabberDiv: tabberDiv, playButton: autoPlay};
}
}
   
   
function doIt() {
function doIt() {
$(wrapperSelector).each(function() {
$(wrapperSelector).each(function( ind ) {
var
var
wrapperDiv = $(this),
wrapperDiv = $(this),
initial = wrapperDiv.text(),
pgnSource = $('div.pgn-sourcegame', wrapperDiv),
pgnSource = $('div.pgn-sourcegame', wrapperDiv),
boardDiv,
selector,
selector,
gameSet = new Gameset();
gameSet = new Gameset( wrapperDiv );
try {
if (pgnSource.length > 1)
if (pgnSource.length > 1)
selector = $('<select>', { 'class': 'pgn-selector' } ).data({gameSet: gameSet}).change(selectGame);
selector = $('<select>', { 'class': 'pgn-selector' } )
.change( function() { gameSet.selectGame(this.value); } );
var tds = buildBoardDiv(wrapperDiv, selector, gameSet);
 
var game, ind = 0;
buildBoardDiv(wrapperDiv, selector, gameSet, ind);
pgnSource.each(function() {
var game, ind = 0;
try {
pgnSource.each(function() {
game = new Game(tds, gameSet);
try {
game.preAnalyzePgn($(this).text());
game = new Game(gameSet);
wrapperDiv.data({currentGame: game});
game.preAnalyzePgn($(this).text());
ind++;
wrapperDiv.data({currentGame: game});
gameSet.allGames.push(game);
ind++;
if (selector)
gameSet.allGames.push(game);
selector.append($('<option>', {value: gameSet.allGames.length - 1, text: game.description()})
if (selector)
.css('direction', game.descriptions['Direction'] || 'ltr')
selector.append($('<option>', {value: gameSet.allGames.length - 1, text: game.description()})
);
.css('direction', game.descriptions['Direction'] || 'ltr')
} catch (e) {
);
mw.log('exception in game ' + ind + ' problem is: "' + e + '"');
} catch (e) {
if (game && game.descriptions)
mw.log('exception in game ' + ind + ' problem is: "' + e + '"');
for (var d in game.descriptions)
if (game && game.descriptions)
mw.log(d + ':' + game.descriptions[d]);
for (var d in game.descriptions)
}
mw.log(d + ':' + game.descriptions[d]);
});
}
gameSet.selectGame(0);
});
gameSet.setWidth();
gameSet.selectGame(0);
gameSet.setWidth();
} catch(e) {
console.error(e);
console.error( 'exception analyzing game :', initial );
wrapperDiv.empty();
}
})
})
}
}
   
   
function populateImages() {
if ( $(wrapperSelector, $content).length) {
var
if ( mobile )
colors = [WHITE, BLACK],
doIt();
allPieces = [],
else
types = ['p', 'r', 'n', 'b', 'q', 'k'];
mw.loader.using( 'jquery.ui' ).done( doIt );
for (var c in colors)
for (var t in types)
allPieces.push('File:Chess ' + types[t] + colors[c] + 't45.svg');
allPieces.push('File:Chessboard480.png');
var params = {titles: allPieces.join('|'), prop: 'imageinfo', iiprop: 'url'};
var api = new mw.Api();
api.get(params)
.done(function(data) {
if (data && data.query) {
$.each(data.query.pages, function(index, page) {
var
url = page.imageinfo[0]['url'],
match = url.match(/Chess_([prnbqk][dl])t45\.svg/); // piece
if (match)
pieceImageUrl[match[1]] = url;
else if (/Chessboard/.test(url))
boardImageUrl = url;
});
doIt();
}
});
}
}
if ( $(wrapperSelector).length)
mw.loader.using( ['mediawiki.api', 'jquery.ui.tabs', 'jquery.ui.slider'] ).done( populateImages );
});
});