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

מתוך ויקיפדיה, האנציקלופדיה החופשית
תוכן שנמחק תוכן שנוסף
יותר מדויק
מ legend placement minor fix
שורה 29: שורה 29:
minBlockSize = 20,
minBlockSize = 20,
maxBlockSize = 60,
maxBlockSize = 60,
boardPadding = 20,
rtlregex = /[א-ת]/,
rtlregex = /[א-ת]/,
wrapperSelector = 'div.pgn-source-wrapper',
wrapperSelector = 'div.pgn-source-wrapper',
שורה 124: שורה 125:
mw.notify( gameSet.autoPlayDelay, { tag: 'delay' } ); // eventurally use mw.messages.get and format better message. named notification so it replaces previous
mw.notify( gameSet.autoPlayDelay, { tag: 'delay' } ); // eventurally use mw.messages.get and format better message. named notification so it replaces previous
},
},
// 6 is half letter size (assuming font-size 0.875em)
top: function(row, l) { return (((this.flipButton.state ? row : (7 - row)) + (l ? 0.3 : 0)) * this.blockSize + 20) + 'px'; },
left: function(file, l) { return (((this.flipButton.state ? 7 - file : file) + (l ? 0.5 : 0)) * this.blockSize + 20) + 'px'; },
top: function(row, l) { return (((this.flipButton.state ? row : (7 - row)) + (l ? 0.3 : 0)) * this.blockSize + (l? boardPadding-6: boardPadding)) + 'px'; },
left: function(file, l) { return (((this.flipButton.state ? 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;
var n = 0.5 + num;
שורה 132: שורה 134:
return {top: 0, left: this.left(num, true)};
return {top: 0, left: this.left(num, true)};
case 'e':
case 'e':
return {top: this.top(num, true), left: this.blockSize * 8 + 20};
return {top: this.top(num, true), left: this.blockSize * 8 + boardPadding + 5};
case 's':
case 's':
return {top: this.blockSize * 8 + 20, left: this.left(num, true)};
return {top: this.blockSize * 8 + 20, left: this.left(num, true)};
case 'w':
case 'w':
return {top: this.top(num, true), left: 10};
return {top: this.top(num, true), left: 5};
}
}
},
},
שורה 828: שורה 830:
gameSet.boardDiv = $('<div>', {'class': 'pgn-board-div'});
gameSet.boardDiv = $('<div>', {'class': 'pgn-board-div'});
gameSet.boardImg = $('<img>', {'class': 'pgn-board-img', src: images.board.url } )
gameSet.boardImg = $('<img>', {'class': 'pgn-board-img', src: images.board.url } )
.css({padding: 20})
.css({padding: boardPadding})
.appendTo(gameSet.boardDiv);
.appendTo(gameSet.boardDiv);

גרסה מ־18:53, 13 באפריל 2019

/*
this work is placed by its authors in the public domain.
it was created from scratch, and no part of it was copied from elsewhere.
it can be used, copied, modified, redistributed, as-is or modified,
	whole or in part, without restrictions.
it can be embedded in a copyright protected work, as long as it's clear
	that the copyright does not apply to the embedded parts themselves.
please do not claim for yourself copyrights for this work or parts of it.
the work comes with no warranty or guarantee, stated or implied, including
	fitness for a particular purpose.
*/
"use strict";
window.mw.hook( 'wikipage.content' ).add( function ( $content ) {
		var
		mw = window.mw,
		brainDamage =  /MSIE|Trident/.test( window.navigator.userAgent ),
		mobile = mw.config.get('skin') === 'minerva',
		images = {},
		pieceImageUrl = {},
		boardImageUrl,
		WHITE = 'l',
		BLACK = 'd',
		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
		defaultAnimation = 1000,
		maxDelay = 8000,  // max delay between moves for autoplay, in ms
		sides = ['n', 'e', 's', 'w'], // used for legends
		defaultBlockSize = 45,
		minBlockSize = 20,
		maxBlockSize = 60,
		boardPadding = 20,
		rtlregex = /[א-ת]/,
		wrapperSelector = 'div.pgn-source-wrapper',
		gameChangeEvent = 'pgn-geme-changed';

// some global, utility functions.
    function bindex(file, row) { return 8 * file + row; }
	function file(ind) { return Math.floor(ind / 8);}
	function row(ind) { return ind % 8; }
	function sign(a, b) { return a == b ? 0 : (a < b ? 1 : -1); }
	function fileOfStr(file) { return file && file.charCodeAt(0) - acode;}
	function rowOfStr(row) { return row && (row - 1);}
	function boardToFen(board) {
		var res = [],
		len = function(s) { return s.length; };
 
		for (var r = 0; r < 8; r++) {
			var row = '';
			for (var f = 0; f < 8; f++)  
				row += board[bindex(f, r)] ? board[bindex(f, r)].fen() : ' ';
			res.push(row.replace(/(\s+)/g, len));
		}
		return res.reverse().join('/'); // fen begins with row 8 file a - go figure...
	}

	function linkMoveClick(e) {
		var
			$this = $(this),
			game = $this.data('game'),
			index = $this.data('index'),
			noAnim = $this.data('noAnim');
		game.gs.toggleAutoPlay( false );
		game.showMoveTo(index, noAnim);
	}
	
	function Button( iname, action, stateful, tooltip ) {
		var button = this,
			changer = $.isArray( iname ),
			url = $.isArray( iname ) 
				? [ images[iname[0]].url, images[iname[1]].url ]
				: [ images[iname].url, images[iname].url ],
			tooltip = $.isArray( tooltip )
				? tooltip
				: [ tooltip, tooltip ];

		$.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 )
						.attr( { src: url[state], title: tooltip[state] || '' } );
				if ( ( !stateful || state != oldState ) && typeof( action ) == 'function' )
					action( state );
			},
			elem: $( '<img>', { src: url[0], 'class': 'pgn-image-button pgn-image-button-off', title: tooltip[0] } )
				.click( function() {
					button.setState( ! button.state );
				} )
		} );
	}	

	function Gameset( wrapperDiv ) { // set of functions and features that depend on blocksize, and currentGame.
		var gameSet = this;

		$.extend(gameSet, {
			wrapperDiv: wrapperDiv,
			tabberDiv: null,
			blockSize: defaultBlockSize,
			needRefresh: false,
			allGames: [],
			currentGame: null,
			showDetails:false,
			timer: null,
			faster: function() { 
				gameSet.autoPlayDelay -= 500; 
				if (gameSet.autoPlayDelay < 0) gameSet.autoPlayDelay = 0;
				gameSet.reportDelay();
			},
			slower: function() { 
				gameSet.autoPlayDelay += 500; 
				gameSet.reportDelay();
			},
			flipButton: new Button( 'flip', function( state ) { gameSet.doFlip( state ); }, true ),
			slowerButton:  new Button( 'slower', function() { gameSet.slower(); } ),
			autoPlayButton: new Button( [ 'play', 'pause' ], function( state ) { gameSet.toggleAutoPlay( state ); }, true ),
			fasterButton:  new Button( 'faster', function() { gameSet.faster(); } ),
			commentsToggleButton: new Button( 'comments', function( state ) { gameSet.hideComments( state ) }, true ),
			autoPlayDelay: 0,
			reportDelay: function() {
				mw.notify( gameSet.autoPlayDelay, { tag: 'delay' } ); // eventurally use mw.messages.get and format better message. named notification so it replaces previous
			},
			// 6 is half letter size (assuming font-size 0.875em)
			top: function(row, l) { return (((this.flipButton.state ? row : (7 - row)) + (l ? 0.3 : 0)) * this.blockSize + (l? boardPadding-6: boardPadding)) + 'px'; },
			left: function(file, l) { return (((this.flipButton.state ? 7 - file : file) + (l ? 0.5 : 0)) * this.blockSize + (l? boardPadding-6: boardPadding)) + 'px'; },
			legendLocation: function(side, num) {
				var n = 0.5 + num;
				switch (side) {
					case 'n':
						return {top: 0, left: this.left(num, true)};
					case 'e':
						return {top: this.top(num, true), left: this.blockSize * 8 + boardPadding + 5};
					case 's':
						return {top: this.blockSize * 8 + 20, left: this.left(num, true)};
					case 'w':
						return {top: this.top(num, true), left: 5};
				}
			},
			relocateLegends: function() {
				for (var si in sides)
					for (var n = 0; n < 8; n++)
						this[sides[si]][n].css(this.legendLocation(sides[si], n));
			},
			selectGame: function(val) {
				var game = this.allGames[val];
				if (game) {
					game.analyzePgn();
					this.currentGame = game;
					game.show();
					$( '.pgn-comment-toggler' ).trigger( gameChangeEvent );
				}
			},
			refreshFEN: function() {
				var board = this.currentGame.boards[this.currentGame.index];
				this.fenDiv.text( boardToFen(board) );
			},
			drawIfNeedRefresh: function(again) {
				this.refreshFEN();
				if (this.needRefresh && this.currentGame)
					this.currentGame.drawBoard();
				this.needRefresh = false;
			},
			changeAppearance: function() {
				this.needRefresh = true;
				this.currentGame.drawBoard();
				this.relocateLegends();
			},
			setWidth: function(width) {
				width = width || this.blockSize;
				var
					widthPx = width * 8,
					widthPxPlus = widthPx + 40;
				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.boardImg.css({width: widthPx, height: widthPx});
				this.currentGame.tds.boardDiv.css({width: widthPxPlus, height: widthPxPlus});
				this.changeAppearance();
				this.currentGame.showCurrentMoveLink();
			},
			hideComments: function( state ) {
				this.wrapperDiv.toggleClass( 'pgn-comments-hidden', state );
			},
			doFlip: function( state ) {
				this.changeAppearance();
			},
			scheduleAutoPlay: function() {
				clearTimeout( this.timer );
				if ( this.autoPlayButton.state ) {
                    if ( this.currentGame.done() )
                        this.toggleAutoPlay();
                    else {
                        var gs = this;
                        this.timer = setTimeout( function() { gs.autoPlayCallback(); }, this.autoPlayDelay );
                    }
				}
			},
			autoPlayCallback: function() {
					var cg = this.currentGame;
					cg.wrapAround();
					cg.advance();
			},
			toggleAutoPlay: function( state ) {
				clearTimeout(this.timer);
				this.wrapperDiv.find( '.pgn-chessPiece' ).finish();
				this.autoPlayButton.setState( state );
				if ( state )
					this.autoPlayCallback();
			}
		});
	}
 
	function ChessPiece(type, color, game) {
		this.game = game;
		this.type = type;
		this.color = color;
		this.img = $('<img>', { src: images[type + color].url, 'class': 'pgn-chessPiece' } )
			.toggle(false);
	}
 
	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(this.game.gs.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)}, 
			gameSet.anim, 
			'swing',
			function() { gameSet.scheduleAutoPlay(); }
		);
	};
 
	ChessPiece.prototype.disappear = function() { this.img.fadeOut(this.game.gs.anim); };
 
	ChessPiece.prototype.setSquare = function(file, row) {
		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) {
        if ( this.game.pieceAt( this.file, this.row ) == this ) // with chess960 castling, we sometimes have to test.
		  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, {
			board: [],
			boards: [],
			pieces: [],
			moves: [],
			linkOfIndex: [],
			index: 0,
			piecesByTypeCol: {},
			descriptions: {},
			comments: [],
			analyzed: false,
			tds: tds,
			gs: gameSet});
	}
	
	Game.prototype.moveLinkText = function(afterCommentNum, move) {
		var config = this.gs.config,
			notation = move.s,
			turn = move.turn,
			piece = config && config.translate && config.translate.piece,
			file = config && config.translate && config.translate.file,
			row = config && config.translate && config.translate.row;
		if (piece) {
			piece =   piece.white && turn == 'l'
					|| piece.black && turn == 'd'
					|| piece;
			try {
				var regex = new RegExp('(' + Object.keys(piece).join("|") + ')');
				notation = notation.replace(regex, function(c) { return piece[c] || c; } );
			} catch (e) {
				mw.log('bad config.translate.pieces');
				throw e;
			}
		}
		
		if (file) {
			try {
				notation = notation.replace(/[abcdefgh]/g, function(c) { return file[c] || c; } );
			} catch (e) {
				mw.log('bad config.translate.file');
				throw e;
			}
		}
		if (row) {
			try {
				notation = notation.replace(/[12345678]/g, function(c) { return row[c] || c; } );
			} catch (e) {
				mw.log('bad config.translate.row');
				throw e;
			}
		}

		return afterCommentNum + notation.replace(/-/g, '\u2011'); // replace hyphen with unbreakable
	}
 
	Game.prototype.show = function() {
		var desc = $.extend({}, this.descriptions),
			rtl = desc['Direction'] == 'rtl',
			num = '',
			tds = this.tds;
 
		// cleanup from previous game.
		this.gs.toggleAutoPlay( false );
		tds.descriptionsDiv.empty();
		tds.pgnDiv.empty();
		tds.boardDiv.find('img.pgn-chessPiece').toggle(false);
 
		// setup descriptions
		delete desc['Direction'];
		tds.descriptionsDiv.css({ direction: rtl ? 'rtl' : 'ltr', textAlign: rtl ? 'right' : 'left' });
		$.each(desc, function(key, val) { tds.descriptionsDiv.append(key + ': ' + val + '<br />'); });
 
		// setup pgn section
		this.linkOfIndex = [];
		for (var i = 0; i < Math.max(this.moves.length, this.comments.length); i++) {
			var move = this.moves[i],
			afterCommentNum = '',
			comment = this.comments[i];
			if (comment) 
				$('<p>', { 'class': 'pgn-comment', text: comment })
					.toggleClass( 'pgn-rtl-comment' , rtlregex.test(comment) )
					.appendTo( tds.pgnDiv );
			if (move && move.s) {
				if (move.a)
					num = move.s;
				if (move.turn == 'd') {
					num = num.replace(/\.*$/, '...');
					if (comment)
						afterCommentNum	= Math.floor(i / 3) + '... ';
				}
				var link = $('<span>', {'class': (move.a ? 'pgn-steplink' : 'pgn-movelink')})
					.text(this.moveLinkText(afterCommentNum, move)) // 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;
			}
		}
 
		// set the board.
		$(this.pieces).each(function(i, piece){piece.img.appendTo(tds.boardDiv);});
		this.showMoveTo(this.index, true);
		this.gs.refreshFEN();
	}
 
    Game.prototype.done = function() { return this.moves.length - 1 <= this.index; }
 
	Game.prototype.pieceAt = function(file, row, piece) {
		var i = bindex(file, row);
		if (piece) {
			this.board[i] = piece;
			piece.setSquare(file, row);
		}
		return this.board[i];
	}
 
	Game.prototype.clearSquare = function(file, row) {
		delete this.board[bindex(file, row)];
	}
 
	Game.prototype.clearPieceAt = function(file, row) {
		var piece = this.pieceAt(file, row);
		
		if (piece)
			piece.remove();
		this.clearSquare(file, row);
		this.registerMove({what:'r', piece: piece, file: file, row: row})
	}
 
	Game.prototype.roadIsClear = function(file1, file2, row1, row2) {
		var file = file1, 
			row = row1, 
			moves = 0,
			dfile = sign(file1, file2),
			drow = sign(row1, row2);
		while (true) {
			file += dfile;
			row += drow;
			if (file == file2 && row == row2)
				return true;
			if (this.pieceAt(file, row))
				return false;
			if (moves++ > 10)
				throw 'something is wrong in function roadIsClear.' +
					' file=' + file + ' file1=' + file1 + ' file2=' + file2 +
					' row=' + row + ' row1=' + row1 + ' row2=' + row2 +
					' dfile=' + dfile + ' drow=' + drow;
		}
	};
 
	Game.prototype.addPieceToDicts = function(piece) {
		this.pieces.push(piece);
		var type = piece.type, color = piece.color;
		var byType = this.piecesByTypeCol[type];
		if (! byType)
			byType = this.piecesByTypeCol[type] = {};
		var byTypeCol = byType[color];
		if (!byTypeCol)
			byTypeCol = byType[color] = [];
		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) {
		var m = this.index + (delta || 1); // no param means 1 forward.
		if (0 <= m && m < this.moves.length) {
			this.showMoveTo(m);
			if (this.moves[this.index].a)
				this.advance(delta);
		}
	}
 
	Game.prototype.showCurrentMoveLink = function() {
		var moveLink = this.linkOfIndex[this.index];
		if (moveLink) {
			moveLink.addClass('pgn-current-move').siblings().removeClass('pgn-current-move');
			var wannabe = moveLink.parent().height() / 2,
				isNow = moveLink.position().top,
				newScrolltop = moveLink.parent()[0].scrollTop + isNow - wannabe;
			moveLink.parent().stop().animate({scrollTop: newScrolltop}, 500);
		}
	}
 
	Game.prototype.showMoveTo = function(index, noAnim) {
		var dif = index - this.index;
		if (noAnim || dif < 1 || 2 < dif)
			this.gotoBoard(index);
		else
			while (this.index < index)
				$.each(this.moves[++this.index].bucket, function(index, drop) {drop.act()});
		this.showCurrentMoveLink();
	}
 
	Game.prototype.drawBoard = function() {
		var
			board = this.boards[this.index],
			saveAnim = this.gs.anim;
		this.gs.refreshFEN();
		this.gs.anim = 0;
		for (var i in this.pieces)
			this.pieces[i].disappear();
		for (var i in board)
			board[i].appear(file(i), row(i));
		this.gs.anim = saveAnim;
	}
 
	Game.prototype.wrapAround = function() {
		if (this.index >= this.boards.length - 1)
			this.gotoBoard(0);
	}

    Game.prototype.castle = function( color, side, kingTargetFile, rookTargetFile ) {
		var king = this.piecesByTypeCol['k'][color][0],
            rook = this.piecesByTypeCol['r'][color][side];
		if (!rook || rook.type != 'r')
			throw 'attempt to castle without rook on appropriate square';
		king.move(fileOfStr(kingTargetFile), king.row);
		rook.move(fileOfStr(rookTargetFile), rook.row);
        
    }
    
	Game.prototype.kingSideCastle = function(color) {
        this.castle( color, 1, 'g', 'f' );
	}
 
	Game.prototype.queenSideCastle = function(color) {
        this.castle( color, 0, 'c', 'd' );
	}
 
	Game.prototype.promote = function(piece, type, file, row, capture) {
		piece[capture ? 'capture' : 'move'](file, row);
		this.clearPieceAt(file, row);
		var newPiece = this.createPiece(type, piece.color, file, row);
		this.registerMove({what:'a', piece: newPiece, file: file, row: row});
	}
 
	Game.prototype.createPiece = function(type, color, file, row) {
		var piece = new ChessPiece(type, color, this);
		this.pieceAt(file, row, piece);
		this.addPieceToDicts(piece);
		return piece;
	}
 
	Game.prototype.createMove = function(color, moveStr) {
		moveStr = moveStr.replace(/^\s+|[!?+# ]*(\$\d{1,3})?$/g, ''); // check, mate, comments, glyphs.
		if (!moveStr.length)
			return false;
		if ($.inArray(moveStr, ['O-O', '0-0']) + 1)
			return this.kingSideCastle(color);
		if ($.inArray(moveStr, ['O-O-O', '0-0-0']) + 1)
			return this.queenSideCastle(color);
		if ($.inArray(moveStr, ['1-0', '0-1', '1/2-1/2', '*']) + 1)
			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])?/);
		if (!match) {
			return false;
		}
 
		var type = match[1] ? match[1].toLowerCase() : 'p',
			oldFile = fileOfStr(match[2]),
			oldRow = rowOfStr(match[3]),
			isCapture = !!match[4],
			file = fileOfStr(match[5]),
			row = rowOfStr(match[6]),
			promotion = match[7],
			thePiece = $(this.piecesByTypeCol[type][color]).filter(function() {
					return this.matches(oldFile, oldRow, isCapture, file, row);
				});
		if (thePiece.length != 1) {
			var ok = false;
			if (thePiece.length == 2) { // maybe one of them can't move because it protects the king?
				var king = this.piecesByTypeCol['k'][color][0];
				for (var i = 0; i < 2; i++) {
					var piece = thePiece[i];
					delete this.board[bindex(piece.file, piece.row)];
					for (var j in this.board) {
						var threat = this.board[j];
						if (threat && threat.color != color && threat.canMoveTo(king.file, king.row, true)) {
							ok = true;
							thePiece = thePiece[1-i];
							break;
						}
					}
					this.pieceAt(piece.file, piece.row, piece);
					if (ok)
						break;
				}
 
			}
			if (!ok)
				throw 'could not find matching pieces. type="' + type + ' color=' + color + ' moveAGN="' + moveStr + '". found ' + thePiece.length + ' matching pieces';
		}
		else
			thePiece = thePiece[0];
		if (promotion)
			this.promote(thePiece, promotion.toLowerCase().charAt(1), file, row, isCapture);
		else if (isCapture)
			thePiece.capture(file, row);
		else
			thePiece.move(file, row);
		return moveStr;
	}
 	Game.prototype.addComment = function(str) {
		if ( str )
			this.comments[this.moves.length] = str.replace( /[{}()]/g, '' );
	}
 
	Game.prototype.addDescription = function(description) {
		description = $.trim(description);
		var match = description.match(/\[([^"]+)"(.*)"\]/);
		if (match)
			this.descriptions[$.trim(match[1])] = match[2];
	}
 
	Game.prototype.description = function(pgn) {
		var d = this.descriptions,
			round = d['Round'] ? ' (' + d['Round'] + ')' : '',
			s = d['Name'] 
			|| d['שם'] 
			|| ( (d['Event'] || d['אירוע'] || '') + ': ' + (d['White'] || d['לבן'] || '') + ' - ' + (d['Black'] || d['שחור'] || '') + round);
		return s;
	}
 
	Game.prototype.addMoveLink = function(str, noAnim, turn) {
		this.boards.push(this.board.slice());
		this.moves.push({bucket: moveBucket, s: str, a: noAnim, turn: turn});
		moveBucket = [];
	}

	Game.prototype.preAnalyzePgn = function(pgn) {
		function tryMatch(regex) {
			var match = pgn.match(regex);
            if (match)
                pgn = pgn.replace( match, '' );
			return match && match[0];
		}
        
        var match;
 		while (match = tryMatch(/^\s*\[[^\]]*\]/))
			this.addDescription(match);
		this.pgn = pgn; 
	}

	Game.prototype.analyzePgn = function() {
		if (this.analyzed) return;
		this.analyzed = true;
		var
			match,
			turn,
			indexOfMove = {},
			moveNum = '',
			pgn = this.pgn;
 
		function removeHead(match) {
			var ind = pgn.indexOf(match) + match.length;
			pgn = pgn.substring(ind);
			return match;
		}
 
		function tryMatch(regex) {
			var rmatch = pgn.match(regex);
			if (rmatch) {
				removeHead(rmatch[0]);
				moveNum = rmatch[1] || moveNum;;
			}
			return rmatch && rmatch[0];
		}
        
		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' );
		var prevLen = -1;
		this.addMoveLink();
		while (pgn.length) {
			if (prevLen == pgn.length)
				throw "analysePgn encountered a problem. pgn is: " + pgn;
			prevLen = pgn.length;
			this.addComment( tryMatch( /^\s*\{[^\}]*\}\s*/ ) );
			this.addComment( tryMatch( /^\s*\([^\)]*\)\s*/ ) );
			if (match = tryMatch(/^\s*(\d+)\.+/)) {
				turn = /\.\.\./.test(match) ? BLACK : WHITE;
				this.addMoveLink(match, true);
				continue;
			}
			if (match = tryMatch(/^\s*[^ ]+ ?/)) {
				this.createMove(turn, match);
				this.addMoveLink(match, false, turn);
				indexOfMove[moveNum + turn] = this.moves.length - 1;
				turn = BLACK;
			}
		}
 		this.index = indexOfMove[this.descriptions.FirstMove || 'nofirstnmove'] || 
            ( this.descriptions.FEN || this.descriptions.FirstMove === '0' ? 0 : this.moves.length - 1 );
	}
 
	Game.prototype.populateBoard = function(fen) {
		var fenar = fen.split(/[\/\s]/);
		if (fenar.length < 8)
            throw 'illegal fen: "' + fen + '"';
		for (var row = 0; row < 8; row++) {
			var file = 0;
			var filear = fenar[row].split('');
			for (var i in filear) {
				var p = filear[i], lp = p.toLowerCase();
				if (/[1-8]/.test(p))
					file += parseInt(p, 10);
				else if (/[prnbkq]/.test(lp))
					this.createPiece(lp, (p == lp ? BLACK : WHITE), file++, 7-row)
				else
					throw 'illegal fen: "' + fen + '"';
			}
		}
	} 
	function selectGame() {
		var gameSet = $(this).data('gameSet');
		gameSet.selectGame(this.value);
	}

	function tooltip( which ) {
		return ( which || '' ) + '';
	}
 
	function advanceButton(gameSet, which) {
		return new Button( which, function() {
				gameSet.toggleAutoPlay( false );
				switch ( which ) {
					case 'forward': gameSet.currentGame.advance( 1 );
						break; 
					case 'back': gameSet.currentGame.advance( -1 );
						break; 
					case 'to_start': gameSet.currentGame.linkOfIndex[1].click();
						break; 
					case 'to_end': gameSet.currentGame.linkOfIndex.slice(-1).pop().click();
						break; 
				}
				gameSet.refreshFEN();
			},
			false,
			tooltip( which )
		);
	}
 
	function buildBoardDiv(container, selector, gameSet, ind) {
		var
			id = container.attr( 'id' ) || 'pgn-viewer-' + ind,
			config = gameSet.config = container.data( 'config' ) || {},
			notationId = 'pgn-notation-' + id,
			infoId = 'pgn-info-' + id,
			fenId = 'pgn-fen-' + id,
			descriptionsDiv = $('<div>', {'class': 'pgn-descriptions', id: infoId }),
			controlsDiv,
			gotoend = advanceButton( gameSet, 'to_end' ),
			forward = advanceButton( gameSet, 'forward' ),
			backstep = advanceButton( gameSet, 'back' ),
			gotostart = advanceButton( gameSet, 'to_start' ),
			collapsibleDiv = $( '<div>' ),  // residue. to be renamed or remove
			tabnames = $.extend( {
				notation: 'Game Notation',
				metadata: 'Information',
				fen: 'FEN' }, 
				config.tab_names );
				
		gameSet.anim = config.anim || defaultAnimation;
		
		gameSet.fixedDelay = 'delay' in config;
		if ( gameSet.fixedDelay ) gameSet.autoPlayDelay = 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.fenDiv = $('<div>', { id: fenId } )
			.css( { 'word-wrap': 'break-word' } ); 
		gameSet.tabberDiv = $( '<div>', { 'class': 'pgn-tabber' } );
		if ( mobile ) {
			gameSet.tabberDiv
				.append( gameSet.pgnDiv );
		} else {
			gameSet.tabberDiv
				.append($( '<ul>' )
					.append( $( '<li>' ).append( $( '<a>', { href: '#' + notationId } ).text( tabnames.notation ) ) ) 
					.append($('<li>').append($('<a>', { href: '#' + infoId }).text(  tabnames.metadata ) ) )
					.append($('<li>').append($('<a>', { href:'#' + fenId }).text( tabnames.fen ) ) )
				)
				.append( gameSet.pgnDiv )
				.append( descriptionsDiv )
				.append( gameSet.fenDiv )
				.tabs();
		}
		controlsDiv = $('<div>', {'class': 'pgn-controls'})
			.css({clear: 'both', textAlign: 'center'})
			.append(gotostart.elem)
			.append(backstep.elem);
		if ( ! gameSet.fixedDelay )	controlsDiv.append( gameSet.slowerButton.elem );
		controlsDiv.append(gameSet.autoPlayButton.elem);
		if ( ! gameSet.fixedDelay )	controlsDiv.append( gameSet.fasterButton.elem );
		controlsDiv
			.append(forward.elem)
			.append(gotoend.elem)
			.append(gameSet.flipButton.elem)
			.append(gameSet.commentsToggleButton.elem)
			;

		gameSet.boardDiv = $('<div>', {'class': 'pgn-board-div'});
		gameSet.boardImg = $('<img>', {'class': 'pgn-board-img', src: images.board.url } )
			.css({padding: boardPadding})
			.appendTo(gameSet.boardDiv);
		
		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) {
			var
				s = sides[side],
				isFile = /n|s/.test(s);
			gameSet[s] = [];
			for (var i = 0; i < 8; i++) {
				var sp = $('<span>', {'class': isFile ? 'pgn-file-legend' : 'pgn-row-legend'})
					.text(isFile ? fl[i] : rl[i])
					.appendTo(gameSet.boardDiv)
					.css(gameSet.legendLocation(s, i));
				gameSet[s][i] = sp;
			}
		}
		
		container
			.append( selector || '' )
			.append( collapsibleDiv
				.append(gameSet.boardDiv)
	 			.append( gameSet.tabberDiv )
				.append(controlsDiv)
				);
		return { boardDiv: gameSet.boardDiv, pgnDiv: gameSet.pgnDiv, descriptionsDiv: descriptionsDiv };
	}
 
	function doIt() {
		$(wrapperSelector, $content).each(function( ind ) {
			var
				wrapperDiv = $(this),
				pgnSource = $('div.pgn-sourcegame', wrapperDiv),
				boardDiv,
				selector,
				gameSet = new Gameset( wrapperDiv );
 
			if (pgnSource.length > 1)
				selector = $('<select>', { 'class': 'pgn-selector' } ).data({gameSet: gameSet}).change(selectGame);
 
			var tds = buildBoardDiv(wrapperDiv, selector, gameSet, ind);
			var game, ind = 0;
			pgnSource.each(function() {
				try {
					game = new Game(tds, gameSet);
					game.preAnalyzePgn($(this).text());
					wrapperDiv.data({currentGame: game});
					ind++;
					gameSet.allGames.push(game);
					if (selector)
						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 + '"');
					if (game && game.descriptions)
						for (var d in game.descriptions)
							mw.log(d + ':' + game.descriptions[d]);
				}
			});
			gameSet.selectGame(0);
			gameSet.setWidth();
		})
	}
 
	function populateImages() {
		var
			flist = [];

		images = { 
			to_start: { fn: 'Ic skip previous 48px.svg' }, 
			to_end:   { fn: 'Ic skip next 48px.svg' }, 
			back:     { fn: 'Ic keyboard arrow left 48px.svg' }, 
			forward:  { fn: 'Ic keyboard arrow right 48px.svg' }, 
			flip:     { fn: 'Ic sync 48px.svg' }, 
			play:     { fn: 'Ic play arrow 48px.svg' }, 
			pause:    { fn: 'Ic pause 48px.svg' },
			comments: { fn: 'Ic closed caption 48px.svg' },
			board:    { fn: 'Chessboard480.png' },
			slower:   { fn: 'Ic directions walk 48px.svg'},
			faster:   { fn: 'Ic directions bike 48px.svg' }
		};

		$( [WHITE, BLACK] ).each( function( _, c ) {
			$( ['p', 'r', 'n', 'b', 'q', 'k'] ).each( function( _, t ) {
                var tc = t + c;
				images[tc] = { fn: 'Chess ' + tc + 't45.svg' };
            } );
        } );
        
		for ( var i in images ) flist.push( 'File:' + images[i].fn ); 
		var params = { titles: flist.join( '|' ), prop: 'imageinfo', iiprop: 'url' };
		if ( brainDamage )
			params.iiurlwidth = 48;

		new mw.Api().get(params)
			.done( function( data ) {
				if (data && data.query) 
					$.each(data.query.pages, function(_, page) {
						var
							ia = page.imageinfo[0],
							title = page.title,
							url = ( brainDamage && /\.svg$/.test( title ) ) ? ia.thumburl : ia.url;
                        for ( var key in images ) 
                            if ( ~ title.indexOf( images[key].fn ) ) 
                                images[key].url = url;
					} );
					doIt();
			} );
	}
 
	if ( $(wrapperSelector, $content).length)
		mw.loader.using( 'mediawiki.api').done( function() { 
			if ( ! mobile )
				mw.loader.using( 'jquery.ui.tabs' ).done( populateImages );
			else 
				populateImages();
		} );
});